Динамическая индикация: семисегментный индикатор на Arduino

Как вывести четыре разряда на 12 выводах, убрать паразитную засветку и свести нагрузку на контроллер к долям процента.
Зачем нужна динамическая индикация
Одноразрядный семисегментный индикатор состоит из восьми светодиодов: семь образуют цифру, восьмой - точку. К Arduino он подключается как обычный набор светодиодов, на что уходит 8 выводов и 8 токоограничивающих резисторов. С каждым новым разрядом число выводов растёт: четырёхразрядному индикатору при таком подходе понадобилось бы 8 x 4 = 32 вывода, что по силам разве что Arduino Mega.
Между тем у четырёхразрядного индикатора всего 12 ножек. Одноимённые сегменты всех разрядов внутри соединены параллельно, а вторые выводы светодиодов каждого разряда собраны в отдельный общий контакт D1-D4. Индикаторы выпускаются с общим катодом и с общим анодом; разница только в направлении тока. Подав ток между выводом сегмента и общим выводом разряда, можно зажечь любой отдельный светодиод.
Проблема в том, что показать разные цифры в разных разрядах одновременно не получится: если включить сегмент A в первом разряде и сегмент B во втором, загорятся оба сегмента в обоих разрядах. Выручает инертность зрения. Разряды зажигают по очереди, и при достаточной скорости глаз видит их светящимися одновременно.

Опытным путём подобрано время около 5 мс на разряд, то есть 20 мс на четыре, или 50 кадров в секунду. При меньшей частоте изображение дрожит, большая лишь напрасно нагружает контроллер. Такой способ и называется динамической индикацией. Он экономит выводы, провода и резисторы ценой небольшой вычислительной нагрузки.
Схема и первый скетч
В опыте сегменты подключены к выводам 0-7, общие контакты разрядов - к выводам 8-11. Резисторы ставятся со стороны сегментов, на каждый светодиод свой. Так как разряды никогда не горят одновременно, резистор рассчитывают на один светодиод: при 5 В это 220-240 Ом.
Простейшая программа хранит таблицу сегментов для цифр 0-9 и в цикле для каждого разряда вычисляет цифру, выставляет сегменты, включает нужный разряд и ждёт через delay(). Обнаруживаются два недостатка. Во-первых, заметна засветка лишних сегментов: новые сегменты выставляются на несколько десятков микросекунд раньше, чем переключается разряд. Во-вторых, контроллер занят индикатором целиком, и любой добавленный код вызывает мерцание.
Вывод по таймеру millis()
Во второй версии перед сменой разряда все сегменты принудительно гасятся, а delay() заменён сравнением с millis(). Вывод оформлен отдельной функцией, вызываемой из loop(); если 5 мс ещё не прошли, она сразу возвращает управление. Засветка пропадает полностью.
Измерение логическим анализатором при отключённом таймере показало 342 мкс на полный цикл из четырёх цифр, то есть около 85 мкс на цифру. Из 5 мс на индикацию уходит 1,7% времени, остальные 98,3% свободны для других задач.
Прямая запись в порты и прерывание
Третья версия сохраняет логику, но меняет реализацию. Таблица сегментов стала битовой: один байт на цифру вместо семи. Вместо digitalWrite() значения пишутся прямо в порты - сегменты в PORTD, разряды в PORTB. Функция вызывается по прерыванию таймера 1 в режиме CTC с делителем 64: TCCR1B = 0b00001011, OCR1A = 0x04E2 соответствует периоду 5 мс, обработчик - ISR(TIMER1_COMPA_vect).
Основной программе остаётся записать число в переменную, и оно появится на индикаторе в течение 20 мс. Индикации не мешает даже delay() в loop(). Изображение стало немного ярче, поскольку гасить сегменты больше не нужно: порт переключает их одновременно. На все четыре цифры уходит меньше 11 мкс, меньше трёх на одну, что составляет около 0,05% времени контроллера. Сократился и код: 642 байта памяти программ и 23 байта ОЗУ против 1122 и 88 у предыдущего варианта. Это важно для контроллеров с малой памятью вроде ATtiny2313 с двумя килобайтами или ATtiny13 с одним.
Модуль на сдвиговых регистрах 74HC595
Существует готовый модуль: четырёхразрядный индикатор с резисторами и двумя регистрами 74HC595. Ему хватает трёх выводов вместо двенадцати, а несколько модулей соединяются каскадом. Первый регистр, дальний от входа, управляет сегментами, четыре старших бита второго - разрядами; байт сегментов отправляется первым.

В примере защёлка подключена к A0, данные к A1, тактовый сигнал к A2. Вариант на shiftOut() с таймером millis() тратит 182 мкс на разряд, менее 4% времени. Версия с прямой записью в PORTC и вызовом по прерыванию таймера укладывается в неполные 42 мкс, или 0,85%. Это медленнее прямого подключения индикатора, зато свободны как минимум 8 выводов.
Дополнительные замечания
- Для 8 разрядов нужно 16 выводов, что близко к пределу ATmega328; период на разряд уменьшают до 2,5 мс, чтобы сохранить 50 Гц.
- При каскаде модулей на 74HC595 на каждый индикатор отправляют по 2 байта, начиная с самого дальнего регистра.
- Модуль на TM1637 внешне похож, но формирует изображение сам и удерживает его до следующей команды; в каскад не соединяется.
- Отдельные разряды можно выделять яркостью, увеличивая время их свечения и сокращая его у остальных.
- Способ применим к любым светодиодам: панелям, бегущим огням, гирляндам.
