В этой статье я расскажу о подключении к микроконтроллеру ATtiny13 семисегментного LED-индикатора, об управлении им (включая программный код на AVR ассемблере) и о возможностях использования полученной печатной платы.
Принципы, оговариваемые в статье универсальны, и подойдут, разумеется, и для других микроконтроллеров.
Семисегментные индикаторы используются повсеместно для индикации какой-то простой информации (чаще всего, разумеется, счётной). Например, на материнских платах большинства производителей ПК можно встретить индикатор состояния системы (односимвольные коды). Ну и очень часто студентам дают различные задания, связанные с этим простым прибором. Поэтому я, когда начинал осваивать микроконтроллеры, ну никак не мог обойти его вниманием :-)
Сам по себе семисегментный индикатор обычно представляет собой набор светодиодов, «упакованных» в корпус. Светящиеся прозрачные полоски, передающие свет от диодов наблюдателю, называются сегментами. Сегменты принято называть буквами латинского алфавита A, B, C, D, E, F, G. На многих семисегментных индикаторах присутствует также точка, которую обозначают dp. Вот принятое соответствие сегментов буквам:
Обычно используются либо красные, либо зелёные светодиоды. Подключаются семисегментные индикаторы посредством семи (если с точкой — восьми) контактных выводов для управления каждым отдельным сегментом, а также общим выводом — тут очень важно обратить внимание, что является общим выводом индикатора — общий анод или катод. В первом случае общий анод подключается на напряжение питания, а сегменты зажигаются на низком логическом уровне «0»; во втором — общий катод вешается на землю, а сегменты светятся, если подать на них логическую «1»:
Не забываем также, что каждому светодиоду необходим токоограничивающий резистор. Я использую здесь резисторы номиналом 1К (хотя вполне подойдут и резисторы в пределах 250-1250 Ом). Нетрудно догадаться, что резисторная обвязка для устройств с большим количеством семисегментных индикаторов будет довольно тяжёлой.
Для экспериментов прежде всего необходима тестовая плата. Учитывая, что в начале статьи я упоминал ATtiny13, возможно, кто-то уже хочет задать вопрос: как можно управлять семисегментным индикатором при помощи микроконтроллера, у которого всего лишь 5 (максимум 6) программируемых линий ввода-вывода? Ответ: используя дополнительный компонент в виде товарища по имени сдвиговый регистр. Восьмибитный товарищ 74HC164 типа SIPO (serial in parallel out, т.е. последовательный ввод, параллельный вывод) может управляться посредством всего двух сигнальных линий — одна синхролиния (часы), и одна информационная (сэмпловая) линия. На выходе его имеем 8 сигналов, которые сохраняются с течением времени, если не трогать вход синхролинии.
Пользуясь далее общими принципами подключения микроконтроллеров, я сделал плату ATtiny13 xBoard v2. Использовал SMD-версию 74HC164, а в качестве индикатора — довольно большой Kingbright SA08-11EWA с общим анодом. Если кому-то понадобится разводка соответствующей печатной платы (напоминаю, прошивочный интерфейс у меня особенный и заточено всё под SMD+оговоренный выше толстый индикатор), то её можно похитить отсюда, в формате Sprint Layout 5.0. А вот как она выглядит:
Дополнительно поставил на плату оранжевый светодиод и кнопку. Питается плата от стабилизированного напряжения +5В от гнезда 2.1/5.5мм. У меня, кстати, в качестве источника питания используется адаптер от зарядки мобильного телефона, прямо в провод которого я встроил стабилизатор 7805.
Важные замечания:
1) Если вы захотите использовать код программ ассемблера без изменений, то вам нужен семисегментный индикатор с общим анодом, а также необходимо будет выполнить подключения точь-в-точь согласно вышеприведённой схеме, нетрудно заметить, что у меня нет строгого соответствия выходов сдвигового регистра сегментам, то есть Y0 != A, Y1 != B и так далее, что обсуловлено в первую очередь требованиями разводки платы. В общем-то, если вы поймёте программный код, то изменить закодированные символы индикатора по таблице соответствий выходов регистра сегментам не составит никакой проблемы[1]. Поэтому советую разводить плату как вам удобно будет.
2) В схеме указан индикатор LA-401AD, и приходится констатировать, что его распиновка и близко не стояла с SA08-11EWA, который использовал я, поэтому будьте внимательны — обязательно почитайте в даташите, какой пин чему соответствует.
3) Индикатор управляется через сдвиговый регистр только по двум проводам, поэтому при проталкивании значений через регистр сегменты индикатора будут неизбежно слабо мерцать. Чтобы избежать этого, можно добавить в схему дополнительный транзистор, который будет управляться ещё одной сигнальной линией с микроконтроллера и запирать/открывать напряжение питания для индикатора (иными словами, на время загрузки сегментов весь индикатор можно разом погасить). Однако в этом случае опять же нужно будет дополнять мой код.
Итак, предположим плата готова. Дело теперь за программированием микроконтроллера. Для этой простой платы, как ни странно, можно придумать много применений. Я рассмотрю четыре из них.
Однако первым делом рассмотрим фрагмент кода, который используется для достижения нашей главной цели — управления индикатором.
; Проект для ATtiny13 .INCLUDE "tn13def.inc" .DEF TMP = R17 .DEF SYM = R18 .DEF CNT = R19 ; OUTP порт, значение .MACRO OUTP LDI R16,@1 OUT @0,R16 .ENDM .CSEG .ORG 0x0000 RJMP init sym_table: ; Таблица символов 7SEG дисплея ; для платы ATtiny13 xBoard v2 ; Q0 = C, Q1 = D, Q2 = dot, Q3 = E, ; Q4 = F, Q5 = A, Q6 = B, Q7 = G ; Для ОБЩЕГО АНОДА: ; 0 = сегмент горит, 1 = сегмент погашен ; (для ОБЩЕГО КАТОДА инвертировать значения) ; qqqqqqqq ; 01234567 .DB 0b00100001, 0b01111101 ; 0, 1 .DB 0b10101000, 0b00111000 ; 2, 3 .DB 0b01110100, 0b00110010 ; 4, 5 .DB 0b00100010, 0b01111001 ; 6, 7 .DB 0b00100000, 0b00110000 ; 8, 9 .DB 0b01100000, 0b00100110 ; A, B .DB 0b10100011, 0b00101100 ; C, D .DB 0b10100010, 0b11100010 ; E, F .DB 0b11111111, 0b00000000 ; all_clr, all_set ; setdisplay рисует указанный символ ; SYM на 7-мисегментном дисплее ; NB! До вызова нужно указать SYM setdisplay: ; Установить контрольные биты порта OUTP DDRB, (1<<PB3) | (1<<PB4) ; Загрузить адрес таблицы символов LDI ZL, LOW (2*sym_table) LDI ZH, HIGH(2*sym_table) ; Найти нужный символ ADD ZL, SYM ; Загрузить данные символа в R0 LPM ; Начало итерации LDI TMP, 8 back1: SBRC R0, 0 RJMP bitset ; Установить сегмент в 0 (горит) OUTP PORTB,(0<<PB3)|(0<<PB4) OUTP PORTB,(1<<PB3)|(0<<PB4) RJMP bitunset bitset: ; Установить сегмент в 1 (погашен) OUTP PORTB,(0<<PB3)|(1<<PB4) OUTP PORTB,(1<<PB3)|(1<<PB4) bitunset: LSR R0 DEC TMP BRNE back1 ; Выход из процедуры RET init: ; Инициализация ...
Как видно, в начале кода задаются символьные имена для регистров R17, R18, R19, макро для выдачи в порт какого-то значения, а программный код начинается с адреса во флеш-памяти 0x0000 (то есть, таблица переходов по прерываниям в данном случае отсутствует, однако мы ещё о ней поговорим). Далее идёт безусловный переход на процедуру инициализации, она располагается после метки init.
Что нас интересует, так это сама процедура вывода желаемого символа на индикатор. Очевидно, где-то в памяти нужно держать таблицу всех символов. Эта таблица содержится после метки sym_table и в неё посредством директивы .DB занесены символы 0x0…F (0…15 в десятичном представлении). Кроме того, есть два дополнительных символа в конце, для полной очистки индикатора и для полного заполнения.
Далее смотрим процедуру setdisplay. Первым делом в процедуре устанавливаются пины порта B для управления индикатором: у меня пин PB3 — это синхролиния, а пин PB4 — сэмпловый вход регистра.
Затем в регистр Z загружается адрес таблицы символов и прибавляется номер нашего символа SYM, который должен быть предварительно задан[2]. Дальше байт — 8 бит — символа загружаются в регистр R0. Если определённый бит этого байта «0» — то соответствующий сегмент (см. таблицу) будет по оканчании процедуры гореть, если «1» — то он будет погашен.
И тут начинается итерация. Смысл такой: проверяется нулевой бит байта символа. Если он «0», то то инструкция перехода RJMP bitset будет пропущена, и на регистр будет подано значение «0» = «сегмент горит»[3]:
OUTP PORTB,(0<<PB3)|(0<<PB4) ; Сэмпловое значение «0» OUTP PORTB,(1<<PB3)|(0<<PB4) ; Протолкнуть значение
Ну а если нулевой бит установлен в «1», то переход происходит и в регистр проталкивается «1» = «сегмент погашен»:
OUTP PORTB,(0<<PB3)|(1<<PB4) ; Сэмпловое значение «1» OUTP PORTB,(1<<PB3)|(1<<PB4) ; Протолкнуть значение
При подаче сигнала 0→1 на синхролинию значение на сэмпловой линии проталкивается в сдвиговый регистр так: Y7=Y6, Y6=Y5, Y5=Y4, Y4=Y3, Y3=Y2, Y2=Y1, Y1=Y0, Y0=sample. Примечание: в даташите может использоваться буква Q вместо Y.
После этого байт в R0 побитово сдвигается вправо, и проверяется следующий бит. И так всего 8 раз, пока все значения не окажутся в сдвиговом регистре аналогично байту символа. После последней итерации в регистре R0 окажется ряд нулей.
Приведу конкретный пример. Пусть нам нужно отобразить на дисплее цифру 7. Смотрим по таблице: код этого символа также будет 7 (байт символа — 0b01111001). Вызываем откуда-то из нашей программы процедуру следующим образом:
; Занесём в регистр SYM значение 7 LDI SYM, 7 ; Вызовем процедуру RCALL setdiplay
Предположим, что все светодиоды индикатора погашены (на выходах сдвигового регистра 0b11111111). Тогда после вызова процедуры произойдёт следующее:
При реальной работе микроконтроллера это происходит очень быстро, поэтому практически мгновенно на индикаторе загорается нужная цифра или символ. Однако, как видите, значения пробегают по сегментам — отсюда их слабое мерцание. Особенно это заметно, когда необходима быстрая смена символов на индикаторе.
Итак, с управлением индикатором разобрались. Посмотрим теперь, какие применения можно найти для данной платы.
1. Счетчик
В самом простом случае счётчик будет считать нажатия кнопки. Это и сделаем. Процедура для управления индикатором уже готова, так что продолжим код начиная с инициализации:
; Инициализация init: ; Установить кнопку как ввод, LED как вывод OUTP DDRB, (1<<PB0) | (0<<PB1) | (0<<PB2) main: ; Установить счётчик в ноль и вывести на индикатор LDI CNT, 0 MOV SYM, CNT RCALL setdisplay RJMP back2 ; Ждать пока кнопка нажата back3: ; Зажечь светодиод OUTP PORTB, (1<<PB0) back31: IN R0, PINB SBRS R0, 1 RJMP back31 ; Ждать пока кнопка не нажата back2: ; Отключить светодиод OUTP PORTB, (0<<PB0) back21: IN R0, PINB SBRC R0, 1 RJMP back21 ; Инкрементация счётчика INC CNT ; Проверить условие CNT>15 CPI CNT, 16 BRNE next1 ; CNT=0 LDI CNT, 0; next1: ; Отобразить новое значение MOV SYM, CNT RCALL setdisplay RJMP back3 RJMP main
Работает это следующим образом: сперва на индикаторе высвечивается «0», а потом программа будет крутиться в цикле back21 до тех пор, пока в первом бите порта B (PB1), который настроен у нас на ввод с кнопки, не окажется значения «0», что значит собой нажатую кнопку.
Тогда произойдёт инкремент счётчика и на обратном пути загорится светодиод, который будет гореть, пока кнопка нажата — ещё один цикл. И дальше всё повторяется. А при достижении значения 0xF следующим будет 0x0.
У этого программного кода есть небольшой недостаток — не учитывается «дребезг» контактов кнопки, но для сохранения простоты кода мы пока этим будем пренебрегать.
Демонстрация работы:
Полный код (комментарии на английском).
Для чего можно использовать подобный счётчик? Ну, например, подключить к микроконтроллеру сенсор, прицепив его на АЦП, и считать что-нибудь, а при достижении какого-нибудь значения выдавать сигнал на выводной порт. Так можно сделать копилку, которая показывает, сколько монет в неё было положено. Конечно, для этого было бы неплохо добавить пару индикаторов — учитывая, что по двум проводам можно управлять каким угодно их количеством, это не кажется большой проблемой.
2. Метроном
Давайте теперь сделаем что-нибудь посложнее и поинтереснее. Например, метроном, тикающий в темпе 120bpm, в метре 4/4.
Для этого задействуем такую железную фичу ATtiny13, как таймер. Я не буду сильно вдаваться в технические детали настройки таймера в работу (это материала на отдельную статью, наподобие этой), плюс они весьма подробно описаны в даташите.
Таймер в железе у нас восьмибитный, это значит, что в соответствующем регистре будет пробегать значение в пределах 0x00…0xFF (0…255). При переполнении таймера (когда он сбрасывается 0xFF→0x00) будет возникать прерывание, и управление будет передаваться обработчику прерывания. Таблицу прерываний необходимо будет добавить в код программы, соответственно, начало непосредственно кода будет смещено. Изменим предыдущий код:
; Проект для ATtiny13 .INCLUDE "tn13def.inc" .DEF TMP = R17 .DEF SYM = R18 .DEF CNT = R19 ; OUTP порт, значение .MACRO OUTP LDI R16,@1 OUT @0,R16 .ENDM .CSEG ; Таблица прерываний .ORG 0x0000 RJMP init ; Обработчик сброса RETI ; Обработчик IRQ0 RETI ; PCINT0 Handler RJMP TIM0_OVF ; Обработчик переполнения Timer0 RETI ; Обработчик готовности EEPROM RETI ; Обработчик аналогового компаратора RETI ; Обработчик Timer0 CompareA RETI ; Обработчик Timer0 CompareB RETI ; Обработчик Watchdog прерывания RETI ; Обработчик АЦП-преобразования ; Код основной программы .ORG 0x000A sym_table: ...
Здесь нас интересует инструкция RJMP TIM0_OVF: при переполнении таймера произойдёт прерывание и будет вызвана процедура, расположенная после метки TIM0_OVF. После окончания процедуры, выполнение программы продолжится с места прерывания[4].
Теперь подумаем, как реализовать метроном. Немного элементарной математики. Нам нужен темп 120 bpm — то есть 120 ударов (четвертных нот) в минуту или 120/60 = 2 удара/сек.
Прошивать фьюзы микроконтроллера будем так, чтобы он работал от внутреннего частотогенератора с частотой 128000 Гц (см. даташит). В качестве делителя частоты таймера используем 1024. Значит, за одну секунду таймер будет нащёлкивать 128000/1024 = 125 тиков.
Поскольку нам нужно два удара на секунду, посчитаем 125/2 = 62.5, округлённо 63. Прерывание генерируется на переполнении 255→0, поэтому количество тиков, устанавливаемое в таймер, будет 255-63 = 192.
К сожалению, от внутреннего частотогенератора, да ещё с погрешностью вычислений при восьмибитном таймере будет несколько сложно получить 100% точность метронома. Однако, играя с количеством тиков у меня при разных условиях получалась неплохая точность (проверял относительно точного метронома) при значениях 192…195. Экспериментируйте. Однако так или иначе, для хорошего точного дигитального метронома лучше подойдёт микроконтроллер подороже с кварцем в обвязке.
Исходя из всего вышесказанного получаем код программы:
; Инициализация init: ; Кнопка — ввод, светодиод — вывод OUTP DDRB, (1<<PB0) | (0<<PB1) | (0<<PB2) main: ; Отобразить точку LDI SYM, 18 RCALL setdisplay LDI CNT, 0 ; Включить прерывание по переполнению таймера OUTP TIMSK0, (1<<TOIE0) ; Ждать нажатия кнопки wait: SBIC PINB, 1 RJMP wait ; Выключить флаг прерываний CLI ; Установить число тиков OUTP TCNT0, 192 ; Установить делитель частоты = 1024 OUTP TCCR0B, 5 ; Включить прерывания SEI ; Установить пустой цикл loop: NOP RJMP loop ; Обработчик прерываний TIM0_OVF: ; Установить число тиков OUTP TCNT0, 192 ; Зажечь светодиод OUTP PORTB, (1<<PB0) ; Увеличить счётчик удара INC CNT ; Проверить условие CNT>4 CPI CNT, 5 BRNE next1 ; CNT=0 LDI CNT, 1 next1: MOV SYM, CNT RCALL setdisplay ; Выключить светодиод OUTP PORTB, (0<<PB0) ;Вернуться из прерывания RETI
Пара комментариев по приведённому выше коду. Во-первых, я слегка расширил таблицу символов, добавив туда точку, которая отображается при включении микроконтроллера до нажатия кнопки (посмотреть расширенную таблицу можно в полном коде). Во-вторых, обращаю ваше внимание: для того, чтобы метроном вообще заработал нужно выполнить два условия: выставить требуемый делитель частоты, а также разрешить прерывания. Это правило распространяется на все ситуации, где нужно использовать прерывание по переполнению таймера. Ну и в-третьих, метроном будет после нажатия кнопки щёлкать до бесконечности (пока на него подано питание).
Вот как это работает:
3. Змейка
Практической пользы от змейки мало — это обыкновенный несложный визуальный эффект. Учитывая опыт, полученный при создании метронома, говорить я тут почти ничего не буду. Изменить код будет совсем не трудно — всего-то и нужно, что добавить символы, в которых горит только один сегмент, и запустить похожий счётный цикл.
Однако тут как раз будет заметно мерцание сегментов, напоминая нам, что лучше по возможности использовать управление сдвиговым регистром/семисегментным индикатором по трём проводам.
Вот что получается:
4. Вольтметр
Для создания простенького вольтметра применим одного из лучших друзей цифровой аппаратуры — аналого-цифровой преобразователь (АЦП), благо на борту ATtiny13 он наличиствует, и аж 4-х канальный. Опять же, вдаваться в мельчайшие технические детали не буду, однако покажу как это дело у меня функционирует.
Начальный код программы будет эквивалентен счётчику. А вот после инициализации последует такое:
; Инициализация init: ; Установить кнопку — ввод, светодиод — вывод OUTP DDRB, (1<<PB0) | (0<<PB1) | (0<<PB2) ; Запустить ADC1 на PB2 используя опорное ; напряжение +5V с выравниванием по левой стороне OUTP ADMUX, (0<<REFS0) | (1<<ADLAR) | (0<<MUX1) | (1<<MUX0) ; Загрузить 0 в SYM и отобразить это LDI SYM, 0 RCALL setdisplay ; Запустить АЦП OUTP ADCSRA, (1<<ADEN) | (1<<ADSC) main: ; Получить значение IN VOLT, ADCH ; Высчитать порог, вычитая по 25 единиц LDI TMP1, 0 LDI TMP2, 25 ; Начало итерации back: ; Прибавить значение (напряжения*2) INC TMP1 SBC VOLT, TMP2 BRCS next RJMP back next: ; Поделить значение напряжения на 2 чтобы ; получить округленное (погрешность +/- 0.5В) LSR TMP1 ; Сравнить текущее и предыдущее значения ; и не изменять отображение индикатора, если ; значения одинаковые CP SYM, TMP1 BREQ equal ; Иначе показать округленное значение напряжения MOV SYM, TMP1 RCALL setdisplay equal: ; Если А/Ц преобразование закончено, возобновить его SBIS ADCSRA, 6 OUTP ADCSRA, (1<<ADSC) | (1<<ADEN) RJMP main
Во-первых, здесь я использую в качестве опорного напряжения для АЦП на порте PB2 напряжение питания, то есть +5В. За его стабильность я не сильно переживаю, так как использую относительно надёжный источник. Понятно, что потолок измерения в таком случае будет эти самые 5В, больше на АЦП лучше не подавать, если нет большого желания пожечь вход микроконтроллера.
Выравнивание по левой стороне означает следующее. Наш АЦП 10-битный, значит его регистр состоит из двух байт. Однако в нашем случае нам хватит точности в 8 бит, да и в младших разрядах АЦП болтается по обыкновению всякий дигитальный мусор. Так что по идее, можно выкинуть эти два бита совсем. Разработчики AVR дали нам эту возможность — при помощи так называния выравнивания по левому краю — старшие 8 бит АЦП улетают в регистр ADCH.
А вот по какому принципу работает расчёт измеряемого напряжения. Учитывая, что в нашем распоряжении 8 бит, измеренное значение напряжения изменяется в пределах 0…255 (0…5В). У нас в распоряжении один индикатор, и точность выше +/- 0.5В нам ни к чему. Отсюда 5/0.5 = 10 — это число областей измерения. Дальше 255/10 = 25, в каждой области выходит по 25 значений оцифрованного сигнала.
Далее из полученного оцифрованного значения напряжения вычитается число 25 столько раз, пока результат вычисления больше/равен 0. Число вычитаний складывается, затем делится на 2 и в итоге мы получем измеренное число вольт с погрешностью +/- 0.5В. Если сразу не понятно как это работает, помедитируйте на код :-)
Вот, в принципе, и всё. В тесте я использовал потенциометр. Ползунок сидел на АЦП-канале на PB2, а крайние контакты я приладил на +5В и землю соответственно. Кстати, вот так напрямую подключать АЦП к измеряемому источнику не рекомендую — желательно добавить ещё токоограничивающие резисторы. Но моих для тестов и незащищенное подключение сойдёт:
Также я использовал этот вольтметр для измерения напряжения на двух 1.5В плоских батарейках, соединённых последовательно. Видео, впрочем, заснять не удосужился, так что вам придётся поверить мне на слово — маленький прибор с индикатором показал ровно 3В :-)
Вообще, применение АЦП ограничено только воображением конструктора. Конкретный аппарат можно использовать как, например, индикатор и/или переключатель режимов работы какого-нибудь другого устройства.
Надеюсь, данная информация поможет вам лучше разобраться с простыми семисегментными индикаторами, контроллерами и их совместной работой. Удачных экспериментов!
Примечания
- ↑Для существенного упрощения этой задачи вы можете воспользоваться специальной программой.
- ↑Здесь кроется потенциальная опасность: дело в том, что в угоду простоте я прибавлял значение порядкового номера символа напрямую к регистру ZL. Однако, что произойдёт, например, если первый символ находится в ячейке с адресом, заданным ZH=1, ZL=255? Прибавление к ZL значения при помощи ADD никак не влияет на старший регистр ZH, поэтому в итоге мы получим неверный адрес. Как же быть? У меня два варианта. Один из них выглядит так:
; Найти нужный символ ADD ZL, SYM ; Проверить адрес BRCC keepzh ; Если произошёл перенос, INC ZH ; увеличить значение регистра ZH keepzh: ; Продолжить выполнение программы ...
А ещё я часто встречал следующее. Тут нужен ещё один дополнительный регистр, например, можно взять R16:
CLR R16 ; Очистить регистр R16 ADD ZL, SYM ; Прибавить к младшему регистру адреса код символа ADC ZH, R16 ; Прибавить к старшему регистру ноль с переносом
Как работает: сперва очищаем регистр R16. Затем прибавляем код к ZL. В случае переноса следующая команда ADC прибавит к ZH единичку дополнительно к значению в R16 (а там у нас пусто). Получится в итоге то же самое.
- ↑На самом деле здесь везде имеется избыточность в задании операции через макрос
OUTP
. Следует использовать командыSBI
/CBI
, которые не меняют выходы порта глобально. - ↑Тут важно сделать следующее замечание. Поскольку прерывание может в принципе возникнуть в произвольном месте в программе, нужно позаботиться, чтобы возвращение из прерывания не убивало бы нормальное продолжение работы программного кода. Для этого, во-первых, нужно следить на использованием регистров, как служебных, так и системных. Во-вторых, в начале обработчика прерывания нужно сохранять значение регистра состояния процессора SREG и возвращать перед выходом из прерывания. Делать это можно так:
... ; Обработчик прерывания Interrupt_Handler: IN R16, SREG ; Перенести значение SREG в R16 PUSH R16 ; Занести значение в стек ; Здесь код обработчика прерывания ... POP R16 ; Вытащить значение SREG из стека OUT SREG, R16 ; Восстановить SREG RETI