Управление LCD и OLED дисплеями на AVR-ассемблере
Основные результаты по созданию такой экосистемы изложены в книжке под названием «Программирование микроконтроллеров AVR: от Arduino к ассемблеру». Там же вы найдете подробное изложение целесообразности и границ применимости изложенного подхода. Руководствуясь приведенными в книге примерами, можно строить вполне законченные проекты с минимальной затратой сил и средств, и получить в результате девайс, ласкающий взор своей компактностью, экономичностью и скоростью работы. В этой статье я привожу один из примеров обращения с современными периферийными устройствами с помощью ассемблера, который работает лучше, быстрее и стабильнее, чем его аналог на Arduino.
Примеры тестовых программ, приведенные в этой статье далее, показывают, как на ассемблере работать со строчными дисплеями (алфавитно-цифровыми, текстовыми, знакосинтезирующими — все это разные называния одного и того же типа) на базе HD44780-совместимых контроллеров, взамен LiqudCrystal — одной наиболее широко применяемых Arduino-библиотек. В этих программах вы, кроме того, встретите примеры использования на ассемблере портов UART и I2C (TWI).
Когда очередная версия монстра, в который превратился когда-то компактный и быстрый инструмент, ныне известный под названием Atmel Studio, отказалась устанавливаться на то железо, которым я располагаю и потребовала более современный комп, я окончательно решил попробовать вернуться к истокам, когда никаких AVR GCC еще не существовало. Обдумав ситуацию, я понял, что в ряде случаев отказ от высокоуровневых инструментов позволит сэкономить деньги и время, позволяя при этом получать при этом более надежные и эргономичные девайсы. Остальные подробности по этому поводу см. в упомянутой книге. И еще раз хочу повторить: это не развязывание холивара, это напоминание о том, что инструмент обязан быть адекватен задаче.
Замечание по поводу практического применения AVR-ассемблера. Для того, чтобы создавать на нем программы, достаточен минимальный набор инструментов: собственно ассемблер (программы далее рассчитаны на avrasm2), набор файлов макроопределений для имеющихся у вас контроллеров (inc-файлов) соответствующей ассемблеру версии, текстовый редактор а-ля Блокнот и AVR-программатор. Ассемблер avrasm2.exe и inc-файлы извлекаются из любой версии AVR Studio с номером 5 и выше. Все это прелестно заработает хоть на самом древнем 386-м десктопе с экраном 640×480 и Windows 98. Но если вы в упор не можете обойтись без продвинутых средств отладки, то никто, конечно, не мешает вам ваять свои программы в самой AVR Studio или всяческих Протеусах, если вам это по душе — примеры ниже не содержат ничего такого специфического. Для старых контроллеров (в том числе ATmega8, ATmega16 и ATtiny2313, на которые я в основном ориентируюсь, можно употреблять и версию AVR Studio 4.х (с ассемблером avrasm32), куда более компактную и дружелюбную к пользователю. Подробнее обо всех этих особенностях также см. упомянутую книгу.
И еще замечание о терминологии: в ассемблере никаких, разумеется «функций» не существует и все подпрограммы оформляются одинаково (несколько отличаются от обычных подпрограмм только обработчики прерываний). Я их по привычке все скопом называю процедурами, хотя, строго говоря, это тоже не совсем верно — чаще всего они что-то где-то все-таки возвращают, как функции.
Описанные далее программы рассчитаны на работу в принципе с любыми строчными дисплеями LCD или OLED, имеющими HD44780-совместимый контроллер. Проверялись они на продукции Winstar (OLED, LCD), BLAZE (LCD BCB1602), российской фирмы МЭЛТ (с отечественными же контроллерами внутри) и др. Демо-примеры ориентированы на наиболее часто применяющийся тип 16 символов в 2 строки (1602), но базовые ассемблерные процедуры без изменений или с минимальной коррекцией могут применяться для любой конфигурации без специального указания типа: однострочные дисплеи (с числом символов больше 8) или 4-строчные все равно логически разбиты на две строки.
Демо-программа на Arduino
Начнем с того, что создадим простенькую демо-программу вывода на экран часов с термометром — для иллюстрации того, как это выглядит на Arduino. Для этого примера возьмем OLED-дисплей, как наиболее сложный случай для библиотеки LiqudCrystal — как я неоднократно писал (например, здесь), для OLED ее приходится несколько допиливать. Доработанную версию русифицированной библиотеки LiquidCrystalRus_OLED с примером применения можно скачать отсюда. Несколько видоизмененный текст этого примера (назовем его OLED_Liquid_Crystal_1602_test) мы и возьмем здесь за основу. На рис. ниже приведена схема подключения дисплея к отечественному клону Arduino mini (Iskra mini):
Тогда программа будет такой:
#include
// RS, E, DB4, DB5, DB6, DB7
LiquidCrystalRus OLED1(3, 4, 5, 6, 7, 8);
volatile byte month=0; //месяцы 0-11
//год постояный 2020, число 01
volatile word time=0; //минуты 0-1440 (23:59)
//названия месяцев по-русски, дополненные до 8 символов пробелами:
const char *m_name[] = {" января ","февраля "," марта "," апреля "," мая "," июня "," июля ","августа ","сентября","октября "," ноября ","декабря "};
byte degree[8] = { //значок градуса
0b00001100,
0b00010010,
0b00010010,
0b00001100,
0b00000000,
0b00000000,
0b00000000,
0b00000000
};
void setup() {
delay (1000);
OLED1.begin(16,2); //16 символов 2 строки
OLED1.clear();
OLED1.createChar(0, degree);//создаем значок градуса
OLED1.setCursor(9,0); //верхняя строка, 9 позиция
OLED1.print("-22,3"); //-22.3
OLED1.write(byte(0));//градусов
OLED1.print('C'); //Цельсия
OLED1.setCursor(0,0); //верхняя строка, 0 позиция
OLED1.print("00:00"); //время
OLED1.setCursor(0,1); //нижняя строка нулевая позиция
OLED1.print("01 января 2020"); //дата
OLED1.setCursor(2,0); //верхняя строка, 2 позиция ":"
OLED1.blink(); //двоеточие мигает
delay(1000);
}
void loop() {
time++; if (time==1440) time=0; //1440 число минут в сутках
byte hours=time/60; //число условных часов
OLED1.setCursor(0,0); //верхняя строка, 0 позиция
if (hours>=10) OLED1.print(hours);
else
{OLED1.print('0'); OLED1.print(hours);} //с ведущим нулем
OLED1.print(':');
byte minits=time%60; //минуты - остаток от часов
if (minits>=10) OLED1.print(minits);
else
{OLED1.print('0'); OLED1.print(minits);} //с ведущим нулем
OLED1.setCursor(3,1); //нижняя строка 3 позиция
//выводим месяцы:
OLED1.print(m_name[month]); //выводим месяц на место января
OLED1.setCursor(2,0); //верхняя строка, 2 позиция ":"
OLED1.blink(); //двоеточие мигает
delay(1000);
month++;
if (month==12) month=0;
}//конец loop
Этот скетч, вместе с приводимыми дальше ассемблерными примерами, вы сможете найти в архиве, ссылка на который размещена в конце статьи. Программа каждую секунду (что задается с помощью функции
delay()
) меняет название месяца и добавляет единицу к значению минут. При достижении числа 23:59 отсчет времени сбрасывается в нули и начинается заново. Эти операции демонстрируют, как обращаться со строками и делить длинное число времени на минуты и часы. При прямом получении значений часов, минут и секунд из часов реального времени (RTC) делить ничего не потребуется, но организация отсчета времени в программе может быть самой разнообразной, так что это знание будет не лишним.Результат работы программы показан на фото:
Условное число (01), год (2020) и значение температуры остаются в этой демо-программе неизменными, а примеры подключения к Arduino различных градусников вы найдете и без меня. В верхнюю строку вы можете также запихнуть и значение влажности со значком процентов, особенно если удалить последнюю букву «C» и сдвинуть температуру еще на один знак вправо. Я намеренно не конкретизирую этот вопрос, так как в подключении фирменных датчиков при ассемблерном подходе имеются свои особенности (подробности см. в упомянутой книге).
Здесь мы создаем значок градуса с помощью функции createChar
. Увы, нормальный значок градуса я встречал только в знакогенераторе отечественных дисплеев фирмы МЭЛТ, для остальных требуется его отрисовка. Ранее (в том числе и в библиотеке LiquidCrystalRus_OLED) я заменял значок градуса на верхний квадратик (символ 0xdf, восьмеричный код \337), но это некрасиво. Отметим, что функция createChar
библиотеки LiquidCrystal в отношении OLED-дисплеев Winstar работает не очень надежно (иногда значок просто пропадает при первом включении), и причины мне установить не удалось. Соответствующая ассемблерная процедура (см. далее) не сбоила ни разу.
Напомню также, что в доработанной библиотеке LiquidCrystalRus_OLED введена замена кода нуля (0×30) на код буквы «O» (0×4f). Желающие могут вернуть перечеркнутый ноль обратно, просто удалив или закомментировав строку замены (строка 308 измененного файла LiquidCrystalRus_OLED.cpp).
Отметим еще, что теоретически к одному контроллеру можно подключать сколько угодно подобных дисплеев, если вывод Enable (E) каждого подключать к отдельному порту, остальные линии подключаются просто параллельно. И необязательно это должны быть дисплеи одного типа (может быть один текстовый, другой графический, один LCD, другой OLED и т.д.), лишь бы интерфейсы управления у них совпадали. На практике, так как контроллер при этом еще что-то делает, одновременно лучше подключать не более 2–3 дисплеев, чтобы сильно не тормозить все остальное — процедуры управления дисплеем в Arduino-версии достаточно долгие.
По поводу мигания двоеточия. Здесь мы в конце каждого цикла устанавливаем курсор на соответствующую позицию и включаем аппаратное мигание функцией blink()
. Это отлично работает как раз на OLED-вариантах, в вот во всех прошедших через мои руки LCD аппаратный «блинк» реализован совершенно безобразно. Поэтому в них приходится мигание реализовывать программно и мы предусмотрим в ассемблерной «библиотеке» возможность отключения-включения функции аппаратного мигания.
Нужно добавить про вывод русских символов на дисплей, с чем в Arduino традиционно творится жуткая путаница. У меня несколько лет назад была надежда, что ситуация как-то выправится, но судя по обсуждениям, вплоть до последних версий IDE этого не произошло. В программе я просто указал русские символы, так как LiquidCrystalRus_OLED работает с кодировкой UTF-8, и в большинстве случаев это проходит безболезненно. Если же у вас будут выводиться «кракозябры», можно попробовать рецепты, приведенные в статье на iarduino, где попытались вывести некоторую закономерность. Загляните также в официальную статью (довольно мутную) на arduino.cc. В ассемблерных программах мы будем работать с таблицей знакогенератора напрямую, и такой проблемы не возникнет.
Ассемблерная демо-программа
Подключение к ATmega8
Все примеры далее рассчитаны на ATmega8, но ничто не может вам помешать адаптировать их для любого подходящего AVR. Для этого придется сменить ссылку на inc-файл (у нас тут это будет m8def.inc), а также, возможно, заменить номера и названия портов, к которым подключается дисплей. Учтите, что при выводе через параллельный интерфейс на ассемблере удобно работать с выводами, идущими подряд и относящимися к одному порту, о чем подробнее далее. Не стоит применять какой-нибудь Tiny, у которого четырех подряд идущих выводов портов не имеется: конечно, это вполне возможно, но приведет к необходимости слишком существенных переделок программ и схем.
Как указано в программе и на схеме выше, дисплей WEH1602 подключается к выводам 3–8 Arduino. Для Arduino это удобно — выводы идут подряд, причем важные для применения в качестве часов-термометра выводы I2C и АЦП остаются свободными. А вот для ассемблера такое подключение не очень хорошо: если посмотрите соответствие выводов Arduino-AVR, то увидите, что выводы портов идут вразбивку; последний вывод данных оказывается подключенным к порту PB0, тогда как первые три — к старшим битам порта D. Можно просто сдвинуть эту тетраду на младшие биты порта B, но тогда мы «наедем» на выводы программирования (PB3 = MISO), а без нужды этого делать не следует. Да и просто неудобно в отладке: придется все время отключать-подключать программатор.
В ATmega8 удобно подключиться к четырем младшим битам порта C, которые идут подряд в том числе и в разводке выводов (и не только для DIP-корпуса). При этом мы теряем младшие входы АЦП (ADC0-ADC3), но у нас остаются еще два (ADC4-ADC5), а в планарном 32-выводном корпусе TQFR еще имеются и дополнительные ADC6-ADC7. Выводы PC4-PC5 (ADC4-ADC5), кроме того, заняты аппаратным I2C (SDA, SCL), но на ассемблере удобней пользоваться программным, которому можно назначить любые два вывода любых портов, и программа при этом получается как минимум не сложнее официального способа. Ее применение мы увидим ближе к концу этой статьи на примере чтения часов реального времени (RTC).
Выводы RS и E мы оставим подключенными к портам PD3 и PD4. В результате получим такую схему:
На схеме к контроллеру подключен кварцевый резонатор 4 МГц, все программы подогнаны под эту частоту тактирования. Однако, для управления дисплеями источник частоты неважен — можно работать и от встроенного генератора (для ATmega8 частоту 4 МГц дает установка фьюзов CKSEL3:0=0011
) и от внешнего кварца (для ATmega8 фьюзы CKSEL3:0=1111
). Источник тактирования выбирается в зависимости от требований остальной части схемы: так, например, для нормальной работы UART встроенный генератор применять не рекомендуется (у нас далее последовательный порт применяется для начальной установки часов). Для другой частоты тактирования нужно будет пересчитать постоянные для подсчета задержек, о чем подробнее далее.
Я традиционно пользуюсь 10-контактным разъемом программирования (под него заточены крайне удобные программаторы AS-2/¾), и именно он приведен на схеме. Для подключения к более распространенному и компактному 6-контактному потребуется элементарный переходник или просто установка другого разъема (см. конфигурации). Если у вас контроллер в DIP-корпусе, то его удобно программировать отдельно (можно просто на макетке), а потом установить в схему на панельку.
Программа
Обычный ATmega8 существует в нескольких разновидностях, соответственно в последних версиях Atmel Studio имеются две модификации файла макроопределений: m8def.inc просто и m8adef.inc. Разницы между ними я не нашел никакой, потому можно применять любой из них для любой модификации контроллера.
Общие части программы управления (инициализацию дисплея, вывод команд и данных) я вынес в отдельный файл, назвав его LCD1602.prg. Такое расширение файла — неофициальная придумка, с целью отличия его от законченной программы, можно оставить и официальное .asm, если хочется. Таким образом мы получаем некий аналог библиотеки, его добавляют в конечную программу через обычную директиву .include
. Учтите, что никакой оптимизации тут нет, и .include
просто тупо копирует исходный текст указанного в ней файла в конечную программу, учитываются только директивы условной компиляции, если они есть.
Для начала нам потребуются процедуры задержек, они необходимы для формирования правильного обмена контроллера с дисплеем — контроллер ведь работает гораздо быстрее дисплея. Кроме самой первой задержки на установление питания перед инициализацией, в принципе без них можно обойтись, если сформировать полностью корректный протокол с проверкой флага занятости (busy flag — см. процедуры инициализации и загрузки в даташитах на дисплеи), но этого никто не делает, так как проверка сильно загромождает программу. Проще просто немного задержать подачу следующей команды.
Задержки будем формировать программным путем (подобно функции delayMicroseconds()
в Arduino), последовательным уменьшением на единицу некоего числа:
Delay:
subi Razr0,1
sbci Razr1,0
;sbci Razr2,0 – если потребуется 3-й регистр
brcc Delay
Длительность такой процедуры — по одному такту на каждое вычитание (команды
subi
или sbci
), плюс два такта на переход обратно к началу цикла (brcc
). В общем случае число N, соответствующее нужному интервалу времени T © при тактовой частоте fтакт (Гц) можно получить по формуле N = T∙fтакт/(r+2), где r — число регистров. Соответственно, один задействованный регистр при частоте 4 МГц даст максимальную задержку (N = $FF = 255) примерно 200 мкс, два (N = $FFFF = 65535) — 65,5 мс, три (N = $FFFFFF = 16777215) — около 21 сек. Нам понадобятся задержки 150 мкс, 5 мс и 500 мс, они определены опытным путем, и подходят для любых типов дисплеев.Можно сделать одну универсальную процедуру Delay
(сэкономив количество команд в коде), но для удобства программирования сделаем три отдельных процедуры задержек:
Del_150mks: ;процедура задержки на 150 мкс
cli ;запрещаем прерывания
push Razr0
ldi Razr0,200 ;
Del_50: dec Razr0
brne Del_50
pop Razr0
sei ;разрешаем прерывания
ret
;N = TF/4 5 ms N= 5000 при 4 МГц
Del_5ms:
cli ;запрещаем прерывания
push Razr0
push Razr1
ldi Razr1,high(5000) ;старший байт N
ldi Razr0,low(5000) ;младший байт N
R5_sub:
subi Razr0,1
sbci Razr1,0
brcc R5_sub
pop Razr1
pop Razr0
sei ;разрешаем прерывания
ret
;N = TF/5 500 ms N= 400000 при 4 МГц
Del_500ms:
cli ;запрещаем прерывания
push Razr0
push Razr1
push Razr2
ldi Razr2,byte3(400000) ;старший байт N
ldi Razr1,high(400000) ;средний байт N
ldi Razr0,low(400000) ;младший байт N
R200_sub:
subi Razr0,1
sbci Razr1,0
sbci Razr2,0
brcc R200_sub
pop Razr2
pop Razr1
pop Razr0
sei ;разрешаем прерывания
ret
Итого нам потребуется три регистра. Чтобы их можно было задействовать в основной программе для каких-то других целей, прерывания на время задержек запрещаются (команды
cli/sei
), а регистры помещаются в стек в начале и извлекаются в конце (команды push/pop
). Конечно, если регистров хватает, то лучше для других целей использовать свободные, как мы и будем поступать далее. Ассемблер avrasm2 (в отличие от старого avrasm32) не любит переименований, и будет на них выдавать предупреждения (warnings, см. программу реальных часов далее). Кроме этих трех регистров, нам в этом «библиотечном» файле еще потребуется всего одна рабочая переменная, которую назовем традиционно temp
. Итого инициализация регистров в начале программы будет выглядеть так:
. . . . .
.def temp = r16 ; рабочий регистр
;регистры r17-r19 помещаются в стек:
.def Razr0 = r17 ;счетчик задержки
.def Razr1= r18 ;счетчик задержки
.def Razr2 = r19 ;счетчик задержки
. . . . .
Кроме этого, мы для удобства присвоим имена битам, управляющим выводами RS и E дисплея, а также битам установки адреса знакогенератора и установки номера строки для соответствующих команд:
.equ E = 4 ;PD4 (вывод 6 контроллера)
.equ RS = 3 ;PD3 (вывод 5 контроллера)
.equ Addr = 7 ;бит7=1 команда установки адреса в RAM
.equ Str = 6 ;бит6=0 - строка 1, бит6=1 - строка 2
Теперь можно приступать к процедурам. Сначала придется оформить две процедуры для вывода команд по 4-битовому интерфейсу:
LCD_command: ;выводим тетраду команды из младших бит temp
cbi PortD,RS ;RS=0
out PortC,temp ;выводим младшую PC3..0
sbi PortD,E ;E=1 - строб 1 mks
nop ;1 mks при 4 МГц
nop
nop
nop
cbi PortD,E ;E=0
ret
LCD_command_4bit: ;выводим байт команды из temp в два приема
cbi PortD,RS ;RS=0
swap temp ;
out PortC,temp ;выводим старший PC0..3
sbi PortD,E ;E=1 - строб 1 mks
nop ;1 mks
nop
nop
nop
cbi PortD,E ;E=0
nop ;1 mks
nop
nop
nop
swap temp ;
out PortC,temp ;выводим младший PC0..3
sbi PortD,E ;E=1 - строб 1 mks
nop ;1 mks
nop
nop
nop
cbi PortD,E ;E=0
ret
Почему две? Первая выводит в порт данных дисплея только четыре бита (младших), вторая осуществляет вывод полного байта по 4-битовому интерфейсу в один прием. Иными словами, две процедуры
LCD_command
заменяют одну LCD_command_4bit
. То есть в принципе достаточно одной (второй), но я слизнул эту идею из кода LiquidCrystal — хотя в даташитах на дисплеи процедура указана обычно иначе, но это соответствует оригинальному описанию HD44780 от фирмы Hitachi, и все работает отлично («работает — не трогай!»). Первая процедура понадобится только в начале инициализации, которая здесь выглядит следующим образом: LCD_ini: ;все почти как в LiqidCrystal
ldi temp,0b00011000 ;PB3,PB4 на выход
out DDRD,temp
cbi PortD,E ;E=0
ldi temp,0b00001111 ;PC0-PC3 на выход
out DDRC,temp
rcall Del_500ms ;ждем 0,5 с - установление питания
ldi temp,0b00000011
rcall LCD_command
rcall Del_5ms
ldi temp,0b00000011
rcall LCD_command
rcall Del_5ms
ldi temp,0b00000011
rcall LCD_command
rcall Del_5ms
#ifdef Rus_table
;для Wistar OLED
ldi temp,0b00101010 ;DL=1-4 bit N=1–2 строки, FT=10-рус/англ таблица
#else
;для остальных рус/англ дисплеев
ldi temp,0b00101000 ;DL=1 - 4 bit N=1 – 2 строки,
#endif
rcall LCD_command_4bit
rcall Del_5ms
ldi temp,0b00001000
rcall LCD_command_4bit ;дисплей Off
rcall Del_5ms
ldi temp,0b00000001
rcall LCD_command_4bit ;дисплей clear
rcall Del_5ms
ldi temp,0b00000110
rcall LCD_command_4bit ;I/D=1 - инкремент S=0 - сдвиг курсора
rcall Del_5ms
#ifdef Blink
;включение с миганием
ldi temp,0b00001101 ;D=1-дисплей On B=1-мигает символ в позиции курсора
#else
;просто включение
ldi temp,0b00001100 ;D=1- дисплей On
#endif
rcall LCD_command_4bit
rcall Del_5ms
ldi temp,0b10000000 ;курсор в позицию 0,0
rcall LCD_command_4bit ;
rcall Del_5ms
ret
Задержки подобраны, как уже говорилось, опытным путем и процедура работает безупречно на всех проверенных мной типах дисплеев, причем запуск проходит быстрее и совершено без сбоев, в отличие от LiquidCrystal в Arduino. В отдельных случаях может потребоваться включение-выключение питания после первой загрузки программы.
Здесь применена условная компиляция в двух местах. Во-первых, это опция мигания в позиции курсора, о которой мы говорили ранее. Для включения этой опции нужно в основной программе где-то перед строкой .include "LCD1602.prg"
вставить строку #define Blink
. Во-вторых, опция включения русской таблицы в OLED-дисплеях Winstar (она имеет номер 0×02), она включается строкой #define Rus_table
.
Правкой значений двух младших бит в этой опции можно также включать и другие кодировочные таблицы. У русифицированных LCD-дисплеев по умолчанию (то есть номер 0×00) стоит таблица, аналогичная таблице 0×02 Winstar, но часто встречаются и другие случаи. Например, у дисплеев МЭЛТ вторая таблица (номер 0×01) содержит кириллицу в кодировке 1251 (ANSI), что позволяет вводить русский текст в ассемблерной программе непосредственно с клавиатуры (при условии, что именно такая кодировка установлена у вас в редакторе кода).
Далее нам понадобится процедура вывода данных (она отличается от вывода команды сочетанием уровней на RS и E):
LCD_data: ;выводим код сисмвола из temp в 2 приема
#ifdef Zerosymb
cpi temp,$30 ;код цифры ноль
brne Z_ok
ldi temp,'O' ;подменяем ноль на букву О
Z_ok:
#endif
sbi PortD,RS ;RS=1
swap temp ;
out PortC,temp ;выводим старший PC0..3
sbi PortD,E ;E=1 - строб 1 mks
nop ;1 mks
nop
nop
nop
cbi PortD,E ;E=0
nop ;1 mks
nop
nop
nop
swap temp ;
out PortC,temp ;выводим младший PC0..3
sbi PortD,E ;E=1 - строб 1 mks
nop ;1 mks
nop
nop
nop
cbi PortD,E ;E=0
rcall Del_150mks
ret
Все процедуры вывода данных в дисплей, как видите, обременены некоторым количеством команд nop
— таким образом реализована пауза в 1 мкс для надежной установки уровней на выводах контроллера дисплея (частота 4 МГц все-таки для него высоковата). Здесь также применена условная компиляция — для замены ненавидимого мной перечеркнутого нуля на букву «О» (все иллюстрации в этой статье сделаны с такой заменой). Чтобы включить эту опцию, в основной программе должно стоять #define Zerosymb
.
Осталось две необходимых процедуры. Одна из них — установка курсора на нужное место строка: позиция (в обратном порядке, чем в LiquidCrystal, что кажется мне более естественным). Эту процедуру оформляем в виде макроса для удобства указания параметров:
.macro Set_cursor
push temp ;чтобы не думать о сохранности temp
ldi temp,(1<
Сохранение рабочей переменной temp в стеке здесь применено для того, чтобы можно было помещать вызов этого макроса в любое место основной программы, в которой рабочая переменная также будет широко использоваться.
И последняя процедура рисует значок градуса и помещает его на место символа номер 1. Здесь, в отличие от LiquidCrystal, она оформлена в единую подпрограмму вместе с данными:
Symbol_degree: ;рисуем символ градуса
ldi temp,0b01001000 ;CGRAM адрес 0х01
rcall LCD_command_4bit ;
rcall Del_5ms
ldi temp,0b00001100
rcall LCD_data
ldi temp,0b00010010
rcall LCD_data
ldi temp,0b00010010
rcall LCD_data
ldi temp,0b00001100
rcall LCD_data
ldi temp,0b00000000
rcall LCD_data
ldi temp,0b00000000
rcall LCD_data
ldi temp,0b00000000
rcall LCD_data
ldi temp,0b00000000
rcall LCD_data
ldi temp,0b10000000 ;курсор в позицию 0,0
rcall LCD_command_4bit ;
rcall Del_5ms
ret
Демо-программа на ассемблере
Демо-программа будет работать аналогично показанной ранее на примере Arduino. Нам надо решить несколько проблем, которые в Arduino решаются как бы автоматически. Во-первых, это размещение строк-констант с названиями месяцев: их можно размещать в программной памяти, в SRAM (как в Arduino) или в EEPROM. Последний способ плох тем, что он не очень надежен (EEPROM может испортиться при невнимательном отношении к включению-выключению питания), зато строки можно загрузить заранее и отдельно от программы. Мы здесь для демонстрации применим два других способа: в демо-программе разместим массив строк в программной памяти, а в реальной программе часов (см. далее) загрузим их в SRAM в начале программы.
Вторая проблема заключается в том, что мы можем выводить информацию на дисплей только посимвольно, команд для выгрузки целой строки или числа (подобно тому, как это сделано в функции print()
), разумеется, не существует. Поэтому нам придется любое число, содержащее больше одного десятичного разряда, преобразовывать в десятичное (BCD) и выводить разряды по отдельности. Повторяю, что в RTC все величины (часы-минуты-секунды-дни-месяцы-годы) хранятся по отдельности и уже в BCD-форме, потому там у нас встанет обратная задача по формированию номера месяца из BCD-кода. Но проблема эта в реальных программах все равно встает при выводе данных с различных датчиков. Поэтому мы в демо-программе сразу покажем, как решается и эта задача, облегчив себе ее тем, что будем считать отдельно часы и минуты — чтобы не возиться с преобразованиями больших чисел.
Создадим новый файл OLED1602_proba.asm, в той же папке, где размещена «библиотека» LCD1602.prg. Объявим регистры-переменные и включим опции (для OLED-дисплея я их включаю все). Начало программы будет таким:
.include "m8def.inc"
#define Blink ;включено мигание в позиции курсора
#define Rus_table ;включена поддержка русской таблицы (для OLED)
#define Zerosymb ;вкл подмена 0 на букву О
;.def temp = r16 ; рабочий регистр - определен в LCD1602.prg
;.def Razr0 = r17 ;счетчик задержки - определен в LCD1602.prg
;.def Razr1= r18 ;счетчик задержки - определен в LCD1602.prg
;.def Razr2 = r19 ;счетчик задержки - определен в LCD1602.prg
.def temp1 = r20 ;вспомогательный регистр
.def month = r21 ;номер месяца
.def hour = r22 ;число часов
.def min = r23 ;число минут
rjmp RESET ;Reset Handler
Закомментированные определения — для памяти, чтобы все время не вспоминать, какие регистры уже заняты в «библиотечном» файле .prg и подо что.
Здесь из всех векторов прерывания задействован только самый первый
Reset
, по нулевому адресу в памяти. Сразу после него можно уже размещать массив названий месяцев: m_name:
.db $20,$C7,$BD,$B3,'a','p',$C7,$20, \
$E4,'e',$B3,'p','a',$BB,$C7,$20, \
$20,$BC,'a','p',$BF,'a',$20,$20, \
$20,'a',$BE,'p','e',$BB,$C7,$20, \
$20,$20,$BC,'a',$C7,$20,$20,$20, \
$20,$20,$B8,$C6,$BD,$C7,$20,$20, \
$20,$20,$B8,$C6,$BB,$C7,$20,$20, \
'a',$B3,$B4,'y','c',$BF,'a',$20, \
'c','e',$BD,$BF,$C7,$B2,'p',$C7, \
'o',$BA,$BF,$C7,$B2,'p',$C7,$20, \
$20,$BD,'o',$C7,$B2,'p',$C7,$20, \
$E3,'e',$BA,'a',$B2,'p',$C7,$20
Обратный слеш, если кто не знает, позволяет в AVR-ассемблере разбивать длинные строки. Как и в случае Arduino, название каждого месяца дополнено пробелами до 8 символов. Русские буквы обозначаются HEX-кодами, в соответствии с таблицей знакогенератора. Для удобства я ее привожу здесь в максимально обезличенном виде (на пустых местах в разных дисплеях размещаются разные символы; например, для МЭЛТ в позиции $99 имеется нормальный значок градуса):
Размещение массива в самом начале программы имеет смысл, заключающийся в том, чтобы он занял место в пределах одного байтового сегмента. Извлекать данные мы будем через двухбайтовый указатель ZH:ZL
, и чтобы не возиться с добавлением смещения конкретного месяца к двухбайтовому числу, мы будем добавлять только его только к младшему регистру ZL
.
Далее нам надо не забыть добавить нашу библиотеку:
.include "LCD1602.prg"
Мы могли бы добавить массив и после нее — код библиотеки занимает 300 байт (с учетом первой команды rjmp — 302 байта), а объем массива 96 байт, так что он оказался бы все равно в пределах одного байтового сегмента (второго, а не самого первого). Но это нужно все считать, и несложно промахнуться при изменениях программы, так что надежнее либо размещать в самом начале, либо уж делать по правилам: добавлять смещение к двухбайтовому указателю.
Далее нам понадобится одна-единственная вспомогательная процедура bin2bcd8 — преобразование 8-битового HEX-числа в BCD-код:
;преобразование 8-разрядного hex в неупакованный BCD
;вход hex = temp, выход BCD temp1 — старший, temp — младший
bin2bcd8: ;вход hex= temp, выход BCD temp1-старш; temp - младш
clr temp1 ;clear result MSD
bBCD8_1:
subi temp,10 ;input = input - 10
brcs bBCD8_2 ;abort if carry set
inc temp1 ;inc MSD
rjmp bBCD8_1 ;loop again
bBCD8_2:
subi temp,-10 ;compensate extra subtraction
ret
Англоязычные комменты перекочевали сюда из старой атмеловской аппноты 204, откуда заимствована эта процедура.
Теперь можно писать собственно программу. Она, согласно традиции, начинается с необходимых установок по метке Reset
(аналог функции setup()
):
RESET:
ldi temp,low(RAMEND)
out SPL,temp
ldi temp,high(RAMEND) ;указатель стека
out SPH,temp
clr hour ;
clr min ;обнулили минуты и часы
rcall LCD_ini ;инициализация дисплея
rcall Symbol_degree ;рисуем символ градуса
. . . . .
Далее идет начальное заполнение дисплея символами, которые больше не будут меняться:
;=== начальный вывод
Set_cursor 0,0 ;курсор строка 0 позиция 0
ldi temp,'0' ;0
rcall LCD_data
ldi temp,'0' ;0
rcall LCD_data
ldi temp,':' ;:
rcall LCD_data
ldi temp,'0' ;0
rcall LCD_data
ldi temp,'0' ;0
rcall LCD_data
Set_cursor 0,9 ;курсор строка 0 позиция 9
ldi temp,'-' ;минус
rcall LCD_data
ldi temp,'1' ;1
rcall LCD_data
ldi temp,'2' ;2
rcall LCD_data
ldi temp,',' ;запятая
rcall LCD_data
ldi temp,'3' ;3
rcall LCD_data
ldi temp,$01 ;рисованный значок градуса
rcall LCD_data
ldi temp,'C' ;C
rcall LCD_data
. . . . .
< и так далее – вторая строка>
. . . . .
;в конце обязательно ставим курсор в позицию двоеточия:
Set_cursor 0,2 ;курсор строка 0 позиция 2 - мигает аппаратно
. . . . .
Далее программа в цикле посекундно меняет названия месяцев, а также после каждого такого 12-секундного цикла увеличивает значение минут и часов (в отличие от Arduino-программы, которая обновляла часы вместе с месяцами каждую секунду). Обратите внимание, что здесь месяцы нумеруются с нуля, то есть в реальности при получении их с часов нужно вычитать единицу:
Gcykle:
;=== перебираем месяцы по названию
ldi month,0 ;нулевой месяц - январь
mon_num: ;перебираем месяцы от 0 до 11
Set_cursor 1,3 ;курсор строка 1 позиция 3 (0-15)
mov temp,month
lsl temp
lsl temp
lsl temp ;умножили на 8
ldi ZH,high ((m_name)*2) ;адрес начала массива названий
ldi ZL,low ((m_name)*2)
add ZL,temp ;прибавляем адрес названия
ldi temp1,8 ;повторяем для 8 символов месяца
mon_view: ;загружаем строку 8 символов
lpm ;очередной байт массива в r0
mov temp,r0
rcall LCD_data
adiw ZL,1
dec temp1
brne mon_view
Set_cursor 0,2 ;курсор строка 0 позиция 2 - мигает аппаратно
rcall Del_500ms
rcall Del_500ms ;пауза 1 с
inc month
cpi month,12
brlo mon_num
;=== добавляем минуты и часы
inc min ;увеличиваем минуты
cpi min,60
brlo out_time ;если меньше 60, то на вывод времени
clr min ;иначе обнуляем минуты
inc hour ;увеличиваем часы
cpi hour,24
brlo out_time ;если меньше 24, то на вывод времени
clr hour ;иначе обнуляем минуты
;==== выводим минуты и часы
out_time:
mov temp,hour
rcall bin2bcd8 ;преобразуем в BCD, temp1 — старший, temp — младший
subi temp,-$30 ;код младшей цифры часов = значение +$30
Set_cursor 0,1 ;курсор строка 0 позиция 1
rcall LCD_data ;выводим младший
mov temp,temp1
subi temp,-$30 ;код старшей цифры часов = значение +$30
Set_cursor 0,0 ;курсор строка 0 позиция 0
rcall LCD_data ;выводим старший
mov temp,min
rcall bin2bcd8 ;преобразуем в BCD, temp1 — старший, temp — младший
subi temp,-$30 ;код младшей цифры минут = значение +$30
Set_cursor 0,4 ;курсор строка 0 позиция 4
rcall LCD_data ;выводим младший
mov temp,temp1
subi temp,-$30 ;код старшей цифры часов = значение +$30
Set_cursor 0,3 ;курсор строка 0 позиция 3
rcall LCD_data ;выводим старший
rjmp Gcykle ;к самому началу
Развернуто комментировать тут особо нечего, все основное сказано ранее. Программа OLED1602_proba займет в памяти 702 байта (Arduino-аналог занимает почти 4 кбайта, притом, что данные у него размещаются в ОЗУ, а не вместе с программой). Полностью архив со всем приведенным здесь программами можно скачать по ссылке в конце статьи.
Рабочая программа часов
Тут необходима даже не одна, а две программы. Для начала нам понадобится начальная установка часов, и я ее выделил в отдельную программу, так как коррекция требуется нечасто — популярный модуль на основе DS3231 вполне прилично держит время как минимум в течение полугода-года. Так что перегружать основную программу редко используемой функциональностью я не стал — здесь она приводится все-таки в иллюстративных целях. А при желании объединить установку с основной программой можно и самостоятельно.
Полная схема подключения часов:
В схеме можно применять как готовые модули RTC (выводы обозначены красным цветом), так и самостоятельно изготовленные. Программа далее рассчитана на два типа RTC&nb