Управление LCD и OLED дисплеями на AVR-ассемблере

Сразу предупреждаю, что не собираюсь разводить холивары насчет преимуществ AVR-ассемблера перед С/Arduino, или даже перед BASCOM-AVR и MikroPascal for AVR — каждый инструмент уместен в своей области. У ассемблерного подхода в ряде случаев имеются свои преимущества — в основном это небольшие проекты, а также системы, в которых требуется точный расчет времени. Немаловажное достоинство этого подхода — простота необходимого инструментария. Но один из крупнейших недостатков в сравнении с языками высокого уровня — отсутствие готовых библиотек хотя бы для базовых задач. Для того же Arduino они имеются на все случаи жизни, но, к сожалению, совмещать их с ассемблером оказывается сложно и потому нецелесообразно — проще уж все и сделать с помощью самого Arduino. Поэтому некоторое время назад я задался целью создать более-менее законченную экосистему для проектов на основе AVR-контроллеров с программированием на чистом ассемблере.

Основные результаты по созданию такой экосистемы изложены в книжке под названием «Программирование микроконтроллеров AVR: от Arduino к ассемблеру». Там же вы найдете подробное изложение целесообразности и границ применимости изложенного подхода. Руководствуясь приведенными в книге примерами, можно строить вполне законченные проекты с минимальной затратой сил и средств, и получить в результате девайс, ласкающий взор своей компактностью, экономичностью и скоростью работы. В этой статье я привожу один из примеров обращения с современными периферийными устройствами с помощью ассемблера, который работает лучше, быстрее и стабильнее, чем его аналог на Arduino.
Примеры тестовых программ, приведенные в этой статье далее, показывают, как на ассемблере работать со строчными дисплеями (алфавитно-цифровыми, текстовыми, знакосинтезирующими — все это разные называния одного и того же типа) на базе HD44780-совместимых контроллеров, взамен LiqudCrystal — одной наиболее широко применяемых Arduino-библиотек. В этих программах вы, кроме того, встретите примеры использования на ассемблере портов UART и I2C (TWI).

Пара предварительных замечаний
Один неглупый человек некоторое время назад заметил, что современное программирование напоминает ему ремонт автомобиля через выхлопную трубу,»которая становится с каждым годом все длиннее и длиннее». Не будучи специалистом, не могу судить о собственно программировании, хотя со стороны такое сравнение кажется вполне адекватным современным реалиям. Но я немного о другом: как я заметил в своей области, программисты все больше подменяют электронщиков. Апофеоз такого подхода представлен вот в этой публикации: типичный ход программистского ума, когда для изготовления садовой скамейки сначала строится технико-экономическое обоснование и приобретается лесообрабатывающий комбинат. Но, бог весть, я-то ни разу не программист: я готов изучать какие-то необходимые для практики инструменты, но меня совершенно не интересуют подробности обращения с оптимизирующими компиляторами и не импонируют гадания над смыслом фразы »встроенная в MS Visual Studio поддержка комитов в локальном репозитории». Я убежден, что для дела гораздо полезнее потратить время на изучение нюансов встроенного АЦП или таймеров.

Когда очередная версия монстра, в который превратился когда-то компактный и быстрый инструмент, ныне известный под названием 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):
e27222fd85cb45c047c30f2993c1aed2.png

Тогда программа будет такой:

Текст скетча OLED_Liquid_Crystal_1602_test
#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) делить ничего не потребуется, но организация отсчета времени в программе может быть самой разнообразной, так что это знание будет не лишним.

Результат работы программы показан на фото:

8d2f7ce52b024357e81e6d9374a83d16.jpg

Условное число (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. В результате получим такую схему:
87c6f1969a500fcf3953f4623d129894.png

На схеме к контроллеру подключен кварцевый резонатор 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 имеется нормальный значок градуса):

0006998341c4cc5097e709a54f2da13d.png

Размещение массива в самом начале программы имеет смысл, заключающийся в том, чтобы он занял место в пределах одного байтового сегмента. Извлекать данные мы будем через двухбайтовый указатель 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 вполне прилично держит время как минимум в течение полугода-года. Так что перегружать основную программу редко используемой функциональностью я не стал — здесь она приводится все-таки в иллюстративных целях. А при желании объединить установку с основной программой можно и самостоятельно.
Полная схема подключения часов:
bffdbba63330d476c51aa2bd71f37d0c.png
В схеме можно применять как готовые модули RTC (выводы обозначены красным цветом), так и самостоятельно изготовленные. Программа далее рассчитана на два типа RTC&nb

© Habrahabr.ru