- Назначение и принцип работы,
- Примеры использования,
- Выводы,
- FAQ.
Назначение и принцип работы
Простой и понятный семисегментный одноразрядный индикатор. Состоит из восьми светодиодов, семь из них формируют цифру (сегменты), один точку. Подключаются к Ардуино как обычный набор светодиодов, используя 8 пинов и 8 токоограничивающих резисторов.Включая-выключая отдельные сегменты, рисуем любую цифру, а также символы, которые возможно изобразить таким расположением светящихся черточек. Если разрядов два и более, потребуется в два и более раза больше пинов. А сколько пинов потребуется для этого не самого большого в природе монстра?
А для светодиодной матрицы 8х8, да еще в двух или трехцветном исполнении? О таком даже подумать страшно, поэтому начнём с чего попроще, возьмем, например, весьма популярный четырехразрядный семисегментный индикатор. Подключая его классическим способом, потребуется 8*4=32 пина, что уже по силам лишь Ардуино Мега, да и на для нее это слишком расточительно. Но почему на картинке можем насчитать всего 12 ножек (включая невидимые под таким углом)? Можно ли при помощи 12 пинов управлять 32 светодиодами? Оказывается, можно, и довольно легко.
Весь фокус заключается в схеме соединения светодиодов в индикаторе и программе, которая их обслуживает. Посмотрим на схему подключения. Если с сегментами a-f и точкой dp все понятно, то что такое D1-D4? Чтобы понять это, достаточно окинуть взглядом то, что происходит внутри индикатора, а именно его принципиальную схему. Это одна схема, но в двух вариантах исполнения индикаторов: с общим катодом и общим анодом, отличие только в направлении подачи тока. На схеме мы видим, что сегменты всех разрядов с одной стороны подключены параллельно, а обратные их стороны соединяются у каждого разряда в свой отдельный проводок. Что это дает? Каждый отдельный разряд может быть включен и выключен независимо от остальных, благодаря чему есть возможность зажечь любой светодиод во всей этой матрице при минимуме контактов.
Например, нам нужно включить сегмент F в разряде 2. Для этого подаем ток между пинами 10 и 9, не забывая про токоограничивающий резистор и выбранную полярность, разумеется. Сегмент D в разряде 4 - пины 4 и 6. И так далее. Сформировать любую цифру или символ не сложно, достаточно просто зажечь нужные сегменты и погасить ненужные.
Вроде бы все просто, но возникает одна важная проблема. Например, есть потребность включить одновременно сегмент A разряде 1 и сегмент B в разряде 2. Подаем ток на пины 11-12 и 7-9 и с огорчением наблюдаем, что зажглись все сегменты A и B в первом и втором разряде. Четыре вместо нужных двух! То есть, одновременно изобразить разные цифры в разных разрядах таким способом, к сожалению, не получится, только одинаковые.
Но как же заставить работать индикатор правильно? На помощь, как это часто бывает в технике, приходит человеческая физиология. В данном случае, инертность нашего зрения. Принцип прост, зажигаем цифры на разрядах поочередно. Показываем цифру в первом разряде, гасим ее, показываем на втором, гасим и так далее, если делать это достаточно быстро, глаз будет видеть все четыре разряда светящимися одновременно.
Опытным путем подобрано, что для комфортного восприятия требуется порядка 5 мс на один разряд, то есть 20 на все четыре, что составляет знакомые по ламповому телевидению 50 кадров в секунду. К слову, и в телевизоре, и в мониторе эксплуатируется тот же эффект инертности глаза. 50 к/с (по-английски 50 fps) для излучаемого индикатора - оптимальная частота, при меньшей изображение неприятно дрожит, а слишком большая лишь без нужды нагружает контроллер.
Такой метод отображения называется “динамическая индикация”. Она позволяет значительно экономить пины, провода и резисторы, за счет небольшой нагрузки на вычислительные возможности контроллера, о чем мы подробнее поговорим дальше.Примеры использования
Проведем несколько опытов с описанным выше четырехразрядным индикатором, для чего соберем такую нехитрую схему.Синими проводами присоединяются общие контакты для разрядов, катоды или аноды, в зависимости от исполнения конкретного индикатора зелеными - сегменты. Резистор должен быть обязательно подключен к каждому светодиоду, поэтому устанавливаем их со стороны сегментов (по приведенной ранее принципиальной схеме снизу). Так как разряды загораются поочередно и никогда не должны включаться одновременно, рассчитываем резистор на один светодиод. Для 5В это порядка 220-240 Ом.
Пишем и заливаем простую программу, что первое пришло в голову.
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; }
Картинка получилась отличная, никаких намеков на засветы лишних сегментов. Это видно даже не фотографии, но поверьте на слово, в реальности выглядит еще сочнее. А что по второму вопросу? Теперь вывод изображения заключен в отдельную функцию, внутри которой есть таймер, пропускающий внутрь не раньше, чем положено. Все остальное время свободно под другие задачи. Давайте вычислим, сколько мы выиграли машинного времени за счет такой несложной манипуляции. Между итерациями функции задана пауза в 5 мс, отнимем от нее время на вывод цифры. Засечь это время поможет анализатор уровней, для чего подключаем его к любому пину общего электрода, временно удаляем из функции таймер, чтобы она молотила на максимальных оборотах и смотрим сколько длится полный период вывода всех цифр, что происходит между одноименными точками на графике. 342 микросекунды делим на четыре, получаем порядка 85 мкс, в переводе в миллисекунды 0.085. Из 5 мс на вывод цифры тратится 0.085 мс, что составляет 1.7% времени. Ничего себе разница! Из контроллера-придатка индикатора, который обслуживал его на 100% и не мог толком делать ничего другого, мы высвободили 98.3% времени и превратили индикатор в придаток контроллера, как и должно быть.Казалось бы, о чем еще мечтать? Но и теперь осталась возможность для совершенствования.
Логику оставим прежнюю, но переделаем пару мест: заменим матрицу кодирования сегментов с байтовой на битовую, то есть один сегмент - один бит, а не байт как раньше, заменим вывод в пины со стандартных 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; // крутим активный разряд }
Изображение получилось даже немного ярче, чем раньше, вероятно, сказывается отказ от периода, когда все сегменты погашены. Теперь это не нужно, так как сегменты теперь включаются абсолютно одновременно и не могут пересекаться.Посмотрим на время, которое тратится на вывод цифр по описанной выше методике: крутим функцию максимально быстро, измеряем. Меньше одиннадцати микросекунд! И это на все четыре цифры, на одну приходится меньше трех. Три микросекунды из пяти миллисекунд, это порядка 0.05% рабочего времени тратит теперь контроллер на исключительно ровную и яркую динамическую индикацию. Пять сотых процента, Карл! Основной программе достаточно лишь отправить нужную цифру в переменную digital, и она мгновенно отобразится на экране. Хотя нет, не мгновенно, конечно, а постепенно аж в течение 20 миллисекунд, за 1/50 секунды, иначе говоря.
Посмотрим, сэкономили ли мы на размере кода. Прошлый вариант занимал 1122 байта памяти устройства и 88 байт динамической памяти. Новый 642 и 23 соответственно, более чем ощутимая экономия. В каких-то случаях это может спасти проект, особенно если используется контроллер с небольшой памятью, например Attiny2313 с двумя килобайтами на борту или Attiny13 с одним, куда первый вариант не влезет даже в таком виде.
Для разнообразия, приведу еще один пример динамической индикации на готовом модуле индикатора. Это собранная схема из четырехразрядного индикатора, резисторов и двух сдвиговых регистров 74HC595. Модель доступна в продаже. Имеет ряд плюсов: компактна, не требует доработки, для индикации достаточно трех пинов, вместо двенадцати. А еще, как и все что работает на сдвиговых регистрах, ее можно подключать каскадом, несколько штук в цепочку. Вдруг число будет больше 9999?
Значения пинов на самом индикаторе формируются при помощи регистров, для чего достаточно отправить им правильную комбинацию нулей и единиц. Регистра два: первый, дальний от входа, подключен к сегментам, второй, точнее его четыре старших бита, к разрядам. Отправляем данные в том же порядке, сперва сегменты, потом разряды. Описанием работы с регистрами здесь заниматься не будем, об этом есть отдельная статья на нашем сайте, в остальном же программа очень похожа на предыдущие.
Подключаем модуль согласно таблице: Заливаем программу, запускаем.
#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); // "закрываем защелку", выходные ножки регистра установлены }
Отлично работает. Очень ярко и без засветки ненужных сегментов, регистры ведь тоже включают все пины одновременно. Измеряем время на отправку данных одного разряда по ножке “защелки” LATCH. 182 микросекунды из 5 миллисекунд, это менее 4%. В принципе неплохо, но можно и лучше. Для этого опять обратимся к портам, а заодно переведем вызов функции в прерывание по таймеру, что имеет одни только преимущества, как мы убедились выше.Улучшаем программу, запускаем.
#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); }
Внешне ничего не изменилось, потому что и раньше работало хорошо. Посмотрим что со временем исполнения. Неполные 42 микросекунды на цифру, 0.85% загрузки контроллера. Хоть и ожидаемо не дотягивает до скорости работы “голого” индикатора, но тоже очень хорошо, нагрузка почти невесома, при этом сэкономили 8 пинов, а может и больше.Выводы
Динамическая индикация - удобное и полезное изобретение, позволяющее экономить пины контроллера, время и силы на сборку, ресурсы контроллера и радиодетали. Это простой способ работы с индикаторами и светодиодными матрицами, которым, несомненно, должен владеть любой DIY-мастер.FAQ
1. Сколько разрядов можно подключать на контроллер максимально?Зависит от количества свободных пинов, памяти и производительности контроллера. Для подключения 8 разрядов потребуется 16 пинов, что уже на грани для Atmega328 и Ардуин на ее базе. Кроме того, понадобится пропорционально уменьшить период индикации для сохранения оптимальной частоты 50 Гц. Для 8 разрядов это будет 2.5 мс, что легко задать в таймере прерывания.
2. Как соединить несколько индикаторов на регистрах 595?
У каждого индикатора есть вход и выход, соединяются они последовательно “паровозиком”. Разумеется, в программу тоже следует внести изменения, отправляя по 2 байта для каждого индикатора. Первым улетает байт для самого дальнего регистра.
3. Что за модуль индикатора на чипе TM1637?
Внешне он очень похож на модуль на регистрах, даже индикатор используется тот же самый, но принципиально от него отличается. Формированием изображения занимается сам, достаточно отправить на него информацию по интерфейсу i2c, что именно высвечивать в разрядах, и он будет поддерживать изображение, пока не придет новая команда. В каскад не соединяется.
4. Можно ли выделять яркостью некоторые разряды?
Можно, иногда это удобнее, чем мигающий разряд и выглядит стильно. Для этого нужно немного усилить нужный разряд (или нужные) и притушить остальные, при помощи увеличения и, соответственно, уменьшения времени экспонирования. Например, так получается при периоде 10 мс во втором разряде и 1 мс в нулевом, втором и третьем.Довольно часто этот прием используется в промышленных устройствах.5. Можно ли использовать динамическую индикацию для работы с обычными светодиодами?
Конечно! Светодиоды в индикаторах ничем не отличаются от любых других. Подключайте динамическим способом лампочки на приборной панели, бегущие огоньки, живое освещение лестниц и коридоров, да хоть елочную гирлянду, все будет работать красиво и надежно.