- Назначение и принцип работы,
- Примеры использования,
- Выводы,
- FAQ.
Назначение и принцип работы
Простой и понятный семисегментный одноразрядный индикатор. Состоит из восьми светодиодов, семь из них формируют цифру (сегменты), один точку. Подключаются к Ардуино как обычный набор светодиодов, используя 8 пинов и 8 токоограничивающих резисторов.Весь фокус заключается в схеме соединения светодиодов в индикаторе и программе, которая их обслуживает. Посмотрим на схему подключения.
Например, нам нужно включить сегмент F в разряде 2. Для этого подаем ток между пинами 10 и 9, не забывая про токоограничивающий резистор и выбранную полярность, разумеется. Сегмент D в разряде 4 - пины 4 и 6. И так далее. Сформировать любую цифру или символ не сложно, достаточно просто зажечь нужные сегменты и погасить ненужные.
Вроде бы все просто, но возникает одна важная проблема. Например, есть потребность включить одновременно сегмент A разряде 1 и сегмент B в разряде 2. Подаем ток на пины 11-12 и 7-9 и с огорчением наблюдаем, что зажглись все сегменты A и B в первом и втором разряде. Четыре вместо нужных двух! То есть, одновременно изобразить разные цифры в разных разрядах таким способом, к сожалению, не получится, только одинаковые.
Но как же заставить работать индикатор правильно? На помощь, как это часто бывает в технике, приходит человеческая физиология. В данном случае, инертность нашего зрения. Принцип прост, зажигаем цифры на разрядах поочередно. Показываем цифру в первом разряде, гасим ее, показываем на втором, гасим и так далее, если делать это достаточно быстро, глаз будет видеть все четыре разряда светящимися одновременно.
Опытным путем подобрано, что для комфортного восприятия требуется порядка 5 мс на один разряд, то есть 20 на все четыре, что составляет знакомые по ламповому телевидению 50 кадров в секунду. К слову, и в телевизоре, и в мониторе эксплуатируется тот же эффект инертности глаза. 50 к/с (по-английски 50 fps) для излучаемого индикатора - оптимальная частота, при меньшей изображение неприятно дрожит, а слишком большая лишь без нужды нагружает контроллер.
Такой метод отображения называется “динамическая индикация”. Она позволяет значительно экономить пины, провода и резисторы, за счет небольшой нагрузки на вычислительные возможности контроллера, о чем мы подробнее поговорим дальше.Примеры использования
Проведем несколько опытов с описанным выше четырехразрядным индикатором, для чего соберем такую нехитрую схему.Пишем и заливаем простую программу, что первое пришло в голову.
byte seg[8] = {0, 1, 2, 3, 4, 5, 6, 7}; // пины сегментов byte raz[4] = {8, 9, 10, 11}; // пины общих обратных контактов int digital = 1234; // отображаемое на индикаторе число byte dig[10][7] = { // зажигаемые сегменты для цифр 0, 0, 0, 0, 0, 0, 1, //0 1, 0, 0, 1, 1, 1, 1, //1 0, 0, 1, 0, 0, 1, 0, //2 0, 0, 0, 0, 1, 1, 0, //3 1, 0, 0, 1, 1, 0, 0, //4 0, 1, 0, 0, 1, 0, 0, //5 0, 1, 0, 0, 0, 0, 0, //6 0, 0, 0, 1, 1, 1, 1, //7 0, 0, 0, 0, 0, 0, 0, //8 0, 0, 0, 0, 1, 0, 0, //9 }; void setup() { // настройка портов for (byte i = 0; i < 8; i++) { pinMode(seg[i], OUTPUT); } for (byte i = 0; i < 4; i++) { pinMode(raz[i], OUTPUT); } pinMode(13, OUTPUT); digitalWrite(seg[7], 1); } void loop() { static unsigned long timer; static byte r = 0; byte d; for (byte i = 0; i < 4; i++) { // вычисляем цифру активного разряда switch (i) { case 0: d = digital / 1000; break; case 1: d = (digital / 100) % 10; break; case 2: d = (digital / 10) % 10; break; case 3: d = 4;//digital % 10; break; } for (byte ii = 0; ii < 7; ii++) { // выставляем сегменты digitalWrite(seg[ii], dig[d][ii]); } for (byte ii = 0; ii < 4; ii++) { // выставляем разряд digitalWrite(raz[ii], (i == ii)); } delay(1); // задержка свечения одного разряда } }
Видим, что вроде бы работает, но как-то странно. Видны довольно отчетливо засветы тех сегментов, которые не нужны, у единицы выросла ручка как у четверки и так далее. Вторая проблема куда хуже. При такой программе контроллер занят индикатором полностью. Конечно, можно попытаться впихнуть код до или после циклов вывода на экран, но это будет заметно как мерцание, и чем больше кода, тем сильнее будет трясти картинку, вплоть до полной неразберихи и выпадения глаз из орбит. Даже обычный секундомер функционировать нормально не будет, надо же вычислять время, вставлять его в переменную, да еще кнопку вкл-выкл отслеживать, как минимум. Такое качество работы нас решительно не устроит.
Попробуем избавиться от обеих неприятностей разом. От засвета “соседей” добавим перед переключением регистра цикл, который принудительно погасит все сегменты. Теперь ничего лишнего на экране гореть не должно, даже слегка.
Для разгрузки контроллера и высвобождения его вычислительных мощностей под другие задачи, заменим мерзкий delay() на отслеживание времени между итерациями вывода цифр на экран. Выводим первую, засекаем время, занимаемся своими делами, пока она горит, поглядываем на часы. Когда время вышло, гасим ее, выводим вторую, засекаем время, ну, вы поняли.
Переписываем программу с учетом вышесказанного, заливаем ее.
byte seg[8] = {0, 1, 2, 3, 4, 5, 6, 7}; byte raz[4] = {8, 9, 10, 11}; int digital = 1235; byte dig[10][7] = { // сегменты цифр 0, 0, 0, 0, 0, 0, 1, //0 1, 0, 0, 1, 1, 1, 1, //1 0, 0, 1, 0, 0, 1, 0, //2 0, 0, 0, 0, 1, 1, 0, //3 1, 0, 0, 1, 1, 0, 0, //4 0, 1, 0, 0, 1, 0, 0, //5 0, 1, 0, 0, 0, 0, 0, //6 0, 0, 0, 1, 1, 1, 1, //7 0, 0, 0, 0, 0, 0, 0, //8 0, 0, 0, 0, 1, 0, 0, //9 }; void setup() { // настройка портов for (byte i = 0; i < 8; i++) { pinMode(seg[i], OUTPUT); } for (byte i = 0; i < 4; i++) { pinMode(raz[i], OUTPUT); } digitalWrite(seg[7], 1); // гасим точку } void loop() { digS_v(); // вызываем функцию // здесь пишем любой другой код } void digS_v() { static unsigned long timer; static byte r = 0; byte d; if (timer > millis()) return; // если не прошло 5 мс, ничего не делаем switch (r) { // вычисляем цифру активного разряда case 0: d = digital / 1000; break; case 1: d = (digital / 100) % 10; break; case 2: d = (digital / 10) % 10; break; case 3: d = 4;//digital % 10; break; } for (byte i = 0; i < 7; i++) { // принудительно гасим сегменты, борьба с засветом соседей digitalWrite(seg[i], 1); } for (byte i = 0; i < 4; i++) { // выставляем разряд digitalWrite(raz[i], (r == i)); } for (byte i = 0; i < 7; i++) { // выставляем сегменты digitalWrite(seg[i], dig[d][i]); } r = r == 3 ? 0 : r + 1; timer = millis() + 5; }
Картинка получилась отличная, никаких намеков на засветы лишних сегментов. Это видно даже не фотографии, но поверьте на слово, в реальности выглядит еще сочнее. Казалось бы, о чем еще мечтать? Но и теперь осталась возможность для совершенствования.
Логику оставим прежнюю, но переделаем пару мест: заменим матрицу кодирования сегментов с байтовой на битовую, то есть один сегмент - один бит, а не байт как раньше, заменим вывод в пины со стандартных digitalWrite() на прямые в порт. И добавим интересную плюшку, избавимся от контроля времени внутри функции, а заодно и от постоянного обращения к ней в основном цикле loop(). Вместо этого будем регулярно вызывать ее по прерыванию таймера.
Что нам дадут все эти обновления в теории? Во-первых, вывод изображения должен стать еще быстрее, во-вторых, код станет меньше, что иногда может иметь критическое значение, в-третьих, функция будет работать всегда, независимо от того, какой программой занят контроллер и в каком цикле крутится его программа, делать это плавно и ровно, без малейших мерцаний, даже если основная программа, не столь совершенная, намертво зависнет.
Пробуем.
unsigned int digital = 1234; // цифра для индикации byte dig[10] = { // сегменты цифр 0b11000000, //0 0b11111001, //1 0b10100100, //2 0b10110000, //3 0b10011001, //4 0b10010010, //5 0b10000010, //6 0b11111000, //7 0b10000000, //8 0b10010000, //9 }; void setup() { DDRD = 0xFF; // порт D на OUTPUT DDRB = 0xFF; // порт B на OUTPUT cli(); TCCR1A = 0b00000000; // OC1A Отключен TCCR1B = 0b00001011; // делитель 64, сброс при совпадении 1A (режим СТС) OCR1A = 0x04E2; // 5ms TIMSK1 = 0b0000010; // запусе прерывания А таймера 1 sei(); } void loop() { // в лупе можно выполнять любые задачи, по необходимости подставляя в digital нужное число // digital = millis() / 1000; // например считаая секунды // delay(1000); // индикации не мешает даже мерзкй delay() } ISR(TIMER1_COMPA_vect) { //обработчик прерывания по совпадению А таймера 1, счетчик сбрасывается в 0 static byte r = 0b00000001; // бегающий бит разряда static byte d; // цифра в активном разряде static byte s; // активный разряд // PORTB = 0x00; // гасим все разряды для борьбы с подсветкой (в данном случае необязаетльно) switch (s) { // вычисляем цифру активного разряда case 0: d = digital / 1000; break; case 1: d = (digital / 100) % 10; break; case 2: d = (digital / 10) % 10; break; case 3: d = digital % 10; break; } PORTD = dig[d]; // выставляем сегменты PORTB = r; // выставляем разряд r = r == 0b00001000 ? 0b00000001 : r << 1; // циклично смещаем бегунок регистра спарва налево s = s == 3 ? 0 : s + 1; // крутим активный разряд }
Изображение получилось даже немного ярче, чем раньше, вероятно, сказывается отказ от периода, когда все сегменты погашены. Теперь это не нужно, так как сегменты теперь включаются абсолютно одновременно и не могут пересекаться.Посмотрим на время, которое тратится на вывод цифр по описанной выше методике: крутим функцию максимально быстро, измеряем.
Посмотрим, сэкономили ли мы на размере кода. Прошлый вариант занимал 1122 байта памяти устройства и 88 байт динамической памяти. Новый 642 и 23 соответственно, более чем ощутимая экономия. В каких-то случаях это может спасти проект, особенно если используется контроллер с небольшой памятью, например Attiny2313 с двумя килобайтами на борту или Attiny13 с одним, куда первый вариант не влезет даже в таком виде.
Для разнообразия, приведу еще один пример динамической индикации на готовом модуле индикатора. Это собранная схема из четырехразрядного индикатора, резисторов и двух сдвиговых регистров 74HC595.
Значения пинов на самом индикаторе формируются при помощи регистров, для чего достаточно отправить им правильную комбинацию нулей и единиц. Регистра два: первый, дальний от входа, подключен к сегментам, второй, точнее его четыре старших бита, к разрядам. Отправляем данные в том же порядке, сперва сегменты, потом разряды. Описанием работы с регистрами здесь заниматься не будем, об этом есть отдельная статья на нашем сайте, в остальном же программа очень похожа на предыдущие.
Подключаем модуль согласно таблице:
#define DATA_PIN_OUT A1 // пин данных 595 #define LATCH_PIN_OUT A0 // пин защелки 595 #define CLOCK_PIN_OUT A2 // пин тактов синхронизации 595 int digital = 7235; byte dig[10] = { // сегменты цифр 0b00000011, //0 0b10011111, //1 0b00100101, //2 0b00001101, //3 0b10011001, //4 0b01001001, //5 0b01000001, //6 0b00011111, //7 0b00000001, //8 0b00001001, }; void setup() { pinMode(DATA_PIN_OUT, OUTPUT); // инициализация пинов 595 pinMode(CLOCK_PIN_OUT, OUTPUT); pinMode(LATCH_PIN_OUT, OUTPUT); digitalWrite(LATCH_PIN_OUT, HIGH); } void loop() { digS_v(); } void digS_v() { static unsigned long timer; static byte r = 0b10000000; static byte s; byte d; if (timer > millis()) return; // если не прошло 5 мс, ничего не делаем switch (s) { // вычисляем цифру активного разряда case 3: d = digital / 1000; break; case 2: d = (digital / 100) % 10; break; case 1: d = (digital / 10) % 10; break; case 0: d = digital % 10; break; } out_595_shift(dig[d], r); // отправляем данные сегментов и разрядов на регистры r = r == 0b00010000 ? 0b10000000 : r >> 1; // сдвигаем активный разряд s = s == 3 ? 0 : s + 1; // сдвигаем номер активной цифры timer = millis() + 5; } void out_595_shift(byte x1, byte x2) { digitalWrite(LATCH_PIN_OUT, LOW); // "открываем защелку" shiftOut(DATA_PIN_OUT, CLOCK_PIN_OUT, LSBFIRST, x1); // отправляем данные сегментов shiftOut(DATA_PIN_OUT, CLOCK_PIN_OUT, LSBFIRST, x2); // отправляем данные разрядов digitalWrite(LATCH_PIN_OUT, HIGH); // "закрываем защелку", выходные ножки регистра установлены }
Отлично работает. Очень ярко и без засветки ненужных сегментов, регистры ведь тоже включают все пины одновременно. Улучшаем программу, запускаем.
#define DATA_PIN 1 // пин данных 595 #define LATCH_PIN 0 // пин защелки 595 #define CLOCK_PIN 2 // пин тактов синхронизации 595 int digital = 7235; byte dig[10] = { 0b00000011, //0 0b10011111, //1 0b00100101, //2 0b00001101, //3 0b10011001, //4 0b01001001, //5 0b01000001, //6 0b00011111, //7 0b00000001, //8 0b00001001, }; void setup() { DDRC = 0b00000111; cli(); TCCR1A = 0b00000000; // OC1A Отключен TCCR1B = 0b00001011; // делитель 64, сброс при совпадении 1A (режим СТС) OCR1A = 0x04E2; // 5ms TIMSK1 = 0b0000010; // запусе прерывания А таймера 1 sei(); } void loop() { } ISR(TIMER1_COMPA_vect) { //обработчик прерывания по совпадению А таймера 1, счетчик сбрасывается в 0 static byte r = 0b10000000; static byte s; byte d; switch (s) { // вычисляем цифру активного разряда case 3: d = digital / 1000; break; case 2: d = (digital / 100) % 10; break; case 1: d = (digital / 10) % 10; break; case 0: d = digital % 10; break; } PCdigWL(LATCH_PIN); // "защелка" writeByteP(dig[d]); // сегменты writeByteP(r); // разряды PCdigWH(LATCH_PIN); // "защелка" r = r == 0b00010000 ? 0b10000000 : r >> 1;// сдвигаем активный разряд s = s == 3 ? 0 : s + 1; // сдвигаем номер активной цифры } inline void writeByteP(byte byteW) { // аналог shiftOut, работает намного быстрее for (int i = 0; i <= 7; i++) { if (bitRead(byteW, i)) { PCdigWH(DATA_PIN); } else { PCdigWL(DATA_PIN); } PCdigWH(CLOCK_PIN); PCdigWL(CLOCK_PIN); } } inline void PCdigWH(byte NB) { PORTC |= 1 << NB; } inline void PCdigWL(byte NB) { PORTC &= ~(1 << NB); }
Внешне ничего не изменилось, потому что и раньше работало хорошо. Выводы
Динамическая индикация - удобное и полезное изобретение, позволяющее экономить пины контроллера, время и силы на сборку, ресурсы контроллера и радиодетали. Это простой способ работы с индикаторами и светодиодными матрицами, которым, несомненно, должен владеть любой DIY-мастер.FAQ
1. Сколько разрядов можно подключать на контроллер максимально?Зависит от количества свободных пинов, памяти и производительности контроллера. Для подключения 8 разрядов потребуется 16 пинов, что уже на грани для Atmega328 и Ардуин на ее базе. Кроме того, понадобится пропорционально уменьшить период индикации для сохранения оптимальной частоты 50 Гц. Для 8 разрядов это будет 2.5 мс, что легко задать в таймере прерывания.
2. Как соединить несколько индикаторов на регистрах 595?
У каждого индикатора есть вход и выход, соединяются они последовательно “паровозиком”. Разумеется, в программу тоже следует внести изменения, отправляя по 2 байта для каждого индикатора. Первым улетает байт для самого дальнего регистра.
3. Что за модуль индикатора на чипе TM1637?
Внешне он очень похож на модуль на регистрах, даже индикатор используется тот же самый, но принципиально от него отличается. Формированием изображения занимается сам, достаточно отправить на него информацию по интерфейсу i2c, что именно высвечивать в разрядах, и он будет поддерживать изображение, пока не придет новая команда. В каскад не соединяется.
4. Можно ли выделять яркостью некоторые разряды?
Можно, иногда это удобнее, чем мигающий разряд и выглядит стильно. Для этого нужно немного усилить нужный разряд (или нужные) и притушить остальные, при помощи увеличения и, соответственно, уменьшения времени экспонирования. Например, так получается при периоде 10 мс во втором разряде и 1 мс в нулевом, втором и третьем.
Конечно! Светодиоды в индикаторах ничем не отличаются от любых других. Подключайте динамическим способом лампочки на приборной панели, бегущие огоньки, живое освещение лестниц и коридоров, да хоть елочную гирлянду, все будет работать красиво и надежно.