Термометр для теплицы я вылизывал два вечера, а на третий сын глянул на экран и спросил, почему тут голые цифры, как в калькуляторе. Придраться вроде не к чему: 21.4 C читается. Но когда рядом с числом стоит настоящий градус, справа — стрелка тренда, а в углу — значок дома главной страницы, прибор сразу выглядит как прибор, а не как отладочная печать. Всё это HD44780 умеет рисовать сам: у контроллера есть восемь ячеек памяти символов, куда вы кладёте свои картинки побайтово. Как устроена эта память и где в постоянной таблице лежит готовый градус под кодом 223, я разбирал в уроке про знакогенератор — там теория и анатомия байта. Здесь практика: соберём комплект иконок для прибора и встроим его в живой экран.
Комплект, а не разрозненные значки, — принципиальная вещь. Слотов всего восемь, и их раскладку лучше решить заранее, чем выгребать потом место под седьмой глиф из-под шестого. Тот же принцип экономии работает и на этапе рисунка: каждый глиф — это восемь чисел в массиве, для памяти платы копейки, а вот дизайн по чужим скриншотам наугад — верная трата вечера. Я рисую на бумаге в клетку: пять столбцов, восемь рядов, потом переписываю закрашенные клетки в биты. Пять минут бумаги экономят час перепрошивок — это не образность, а мой средний счёт по трём последним проектам с экранами.
План такой: перевод рисунка в массив byte[8] на примере стрелки и домика, полный скетч комплекта, классическая ловушка с нулевым слотом, динамик температуры со стрелками тренда и полоса загрузки из своих блоков. Симулятор поможет наполовину: движок не рисует глифы из CGRAM, поэтому стрелки в нём будут ASCII-двойниками, а настоящий вид вы увидите на плате.
Пять клеток ширины: собираем комплект
Механика перевода рисунка в байты простая до бездушия. Каждый глиф — восемь байтов; байт — одна строка решётки, порядок сверху вниз; в каждом байте значимы пять младших битов: единица закрашивает точку, ноль оставляет фон. Нарисовали стрелку на клетчатой бумаге — переписали каждую строку двоичной константой вида B00100. Компилятор съедает и обычные числа, но буква B с нулями читается в редакторе без калькулятора: сразу видно, где нарисован столбик стрелки, а где пусто. Шестнадцатеричный вариант 0x04 компактнее, но править его потом мучительно — биты глазом не видны, я после пары промахов вернулся к двоичной записи навсегда.
Два приёма, которые экономят и слоты, и время. Первый: стрелка вниз — это стрелка вверх с переписанным в обратном порядке массивом, отдельный рисунок не нужен, зеркалим за десять секунд. Второй: сплошные горизонтальные ряды читаются с двух метров, изрезанные диагонали — только вблизи. Домик я специально собираю из крыши-треугольника, пары стен и линии фундамента: на пять пикселей ширины окна и двери вырождаются в кашу, поэтому фасад только намекается. И не пытайтесь рисовать буквы: пять пикселей не хватает даже на читаемую М — подписи в комплекте делает обычный print, а глифы только обозначают.
Раскладку слотов закрепите до кода. Мой стандарт для прибора с одной метрикой: слот 0 — домик, метка главного экрана; слоты 1 и 2 — стрелки тренда; слот 3 — свой градус, если встроенный не нравится; 4-7 — блоки полосы загрузки. Восемь ячеек кончаются быстрее, чем кажется: добавите спиннер — и комплект уже не лезет, приходится что-то выселять.
#include <Wire.h>#include <LiquidCrystal_I2C.h> LiquidCrystal_I2C lcd(0x27, 16, 2);const int btn = 2; // Глифы: байты — строки рисунка 5x8, порядок сверху внизbyte domik[8] = { B00100, B01110, B11111, B10001, B10001, B10101, B10101, B11111};byte strelkaVverh[8] = { B00100, B01110, B10101, B00100, B00100, B00100, B00100, B00000};byte strelkaVniz[8] = { // та же стрелка, строки в обратном порядке B00000, B00100, B00100, B00100, B00100, B10101, B01110, B00100};byte gradus[8] = { B01100, B10010, B10010, B01100, B00000, B00000, B00000, B00000}; int trend = 1; // 1 - вверх, 2 - вниз: листаем кнопкойbool lastBtn = LOW; void setup() { pinMode(btn, INPUT); lcd.init(); lcd.backlight(); // слоты 0-3 занимаем один раз, дальше печатаем по номерам lcd.createChar(0, domik); lcd.createChar(1, strelkaVverh); lcd.createChar(2, strelkaVniz); lcd.createChar(3, gradus); lcd.setCursor(0, 0); lcd.write(byte(0)); // домик: только через byte! lcd.print(" TEPLICA"); lcd.setCursor(0, 1); lcd.print("24.6"); lcd.write(byte(3)); // свой градус из слота 3 lcd.print("C "); lcd.write(byte(223)); // ROM-градус: альтернатива без слота lcd.print("C "); lcd.setCursor(13, 1); lcd.write(byte(trend)); // стрелка тренда} void loop() { bool b = digitalRead(btn); if (b == HIGH && lastBtn == LOW) { if (trend == 1) trend = 2; else trend = 1; lcd.setCursor(13, 1); lcd.write(byte(trend)); } lastBtn = b; delay(20);}Глифы — полдела; вторая половина — вписать их в живой кадр так, чтобы они работали, а не стояли нарядом. Домик живёт в левом верхнем углу и служит меткой страницы: увидели его — значит, вы на главном экране прибора, а не в настройках. Градус ставится сразу после числа: запись 24.6C со слитным значком читается лучше, чем с пробелом, а заодно экономит драгоценный столбец. Стрелке тренда — фиксированное место справа от значения: одна и та же ячейка, перерисовка только при смене тренда, никакого переписывания на каждом проходе цикла.
С градусом определитесь один раз. В постоянном знакогенераторе под кодом 223 уже лежит готовый значок — lcd.write(byte(223)) достаёт его без всяких слотов, так работали все мои термометры до этого года. Свой слот оправдан в двух случаях: хочется другой пропорции либо ваш клон панели встречается без градуса в ROM — я такое видел на двух ноунейм-модулях из дешёвых наборов. Дисциплина обновления ячейки при этом ничем не отменяется: переписывать стрелку на каждом проходе loop так же расточительно, как перерисовывать весь кадр, — в уроке про термометр без мерцания это отточено на цифрах шины.
Динамик: мёртвая зона и полоса стадий
Динамик температуры — три строки кода, если не считать одну скользкую деталь. Сравниваем текущий замер с предыдущим: выше — стрелка вверх, ниже — вниз. Но датчик дрожит на десятых долях градуса сам по себе: NTC-термистор на делителе живёт в шуме плюс-минус две десятых, и без мёртвой зоны стрелка мигает, врёт, будто температура пляшет каждую секунду. Рабочая зона — 0,3-0,5 градуса: внутри неё тренд не трогается, стрелка двигается только на честные ходы. Значение держите в целых десятых — int t10, где 246 означает 24,6 градуса: float тут лишний, а сравнение десятых не страдает от округлений и мусорной точности.
Полоса загрузки — тот же бюджет слотов, но другая работа. Классический бар роста я собирал в уроке про прогресс-бар, спиннер с процентами — в уроке про загрузку. Здесь полоса стадий старта: пять блоков — экран, шина, датчик, часы, готово. Каждый пройденный этап закрашивает клетку, зависшая стадия видна мгновенно — прибор сам показывает, где встал, без Serial и мультиметра. Цены копеечные: пять стадий — пять обновлений за всю загрузку, по одной ячейке каждое, против перерисовки кадра после каждого шага. Блоки для неё — те же byte[8] с частичной заливкой: сколько колонок закрашено в байте, такая и ступень клетки.
Отладку комплекта ведите в редакторе пикселей, а не в скетче: онлайн-генераторы глифов для HD44780 позволяют рисовать мышкой по сетке пять на восемь и сразу выдают готовый массив — копируете его в код целиком. Но бумагу я не оставил и после обзаведения редактором: карандаш честнее показывает бюджет пяти пикселей, а редактор развращает деталями — хочется дорисовать ещё окошко, ещё ступеньку, и глиф превращается в кашу, которую не видно с метрового расстояния. Для тонкой работы по битам — выверить, какой бит за какую колонку отвечает, — полезен урок про иконки побитово, там же приёмы вёрстки сложных рисунков.
На живом приборе заведите контрольную страницу: в setup печатайте одним заходом все восемь слотов — от нулевого до седьмого — в одну строку. Она показывает сразу и то, что нарисовано, и то, что съехало: перекос в один бит виден на экране мгновенно, а в массиве байтов его ищешь глазами минуту. Такую страницу я печатаю на каждой новой панели перед сборкой прибора — заодно выясняется партия чипа: на китайских клонах постоянная таблица с катаканой местами отличается от привычной, и пара значков уезжает по кодам.
И последнее из практики: глиф, нарисованный впритык по краям, на экране выглядит жирнее и грязнее, чем на бумаге. Оставляйте пустую колонку справа и пустую строку снизу — значок дышит и не сливается с соседями. Мой домик после пары правок получил пустую правую колонку и читается лучше, хотя на сетке казался недорисованным.
Перед тем как рисовать свои стрелки, пролистайте постоянную таблицу знакогенератора целиком: в стандартной кодировке A00 уже лежат стрелки-указатели, ноты, галочка и пара значков, которые достаются одним write без всяких слотов. Перерисовывать их своими руками стоит только когда встроенный рисунок не нравится или нужен другой размер. Я один раз целую стрелку нарисовал заново, а потом нашёл почти такую же в ROM — нарисованная осталась только потому, что была шире и читалась с дальше.
| Слот | Глиф | Роль в приборе | Грабля |
|---|---|---|---|
| 0 | домик | метка главного экрана в левом углу | печатается только write(byte(0)), print(0) даёт цифру |
| 1, 2 | стрелки вверх и вниз | тренд температуры справа от числа | вниз — копия вверх с обратным порядком строк |
| 3 | градус | после значения температуры | в ROM уже есть 223: свой слот — только под другой рисунок |
| 4-7 | блоки заливки | полоса стадий при старте | освобождаются, если полоса не нужна — не занимайте впрок |
- 1. Сколько собственных символов одновременно живёт в CGRAM типичного HD44780?
- 2. Как правильно напечатать глиф из нулевого слота?
- 3. Куда рисует байт с индексом 0 в массиве глифа?
- 4. Как из массива стрелки вверх дешевле всего получить стрелку вниз?
- 5. Готовый значок градуса достаётся из постоянного знакогенератора командой…
- Глиф — восемь байтов по строкам 5x8 сверху вниз; рисуйте на бумаге в клетку, переписывайте двоичными константами B00100
- Слот 0 печатается только lcd.write(byte(0)): print(0) даёт цифру ноль, без приведения — пустота
- Градус уже лежит в ROM под кодом 223: свой слот берите только под другой рисунок или кривой клон
- Стрелка вниз — массив стрелки вверх в обратном порядке: экономия времени и слотов
- Динамик тренда живёт на мёртвой зоне 0,3-0,5 градуса и целых десятых — иначе стрелка дрожит от шума датчика
Частые вопросы по теме урока
- Сколько собственных символов одновременно живёт в CGRAM типичного HD44780?
- Восемь ячеек с номерами 0-7, каждая — восемь байтов по строкам 5x8, всего 64 байта оперативной памяти под глифы. Восемь
- Как правильно напечатать глиф из нулевого слота?
- Только lcd.write(byte(0)): print(0) печатает цифру, а без приведения к byte компилятор может выбрать не ту перегрузку. lcd.write(byte(0))
- Куда рисует байт с индексом 0 в массиве глифа?
- Строки укладываются сверху вниз: байт 0 — самая верхняя строка из пяти пикселей, байт 7 — нижняя. В верхнюю строку решётки
- Как из массива стрелки вверх дешевле всего получить стрелку вниз?
- Стрелка вниз — зеркальное отражение по вертикали: те же восемь байтов, прочитанные снизу вверх. Отдельный рисунок и слот не нужны. Переписать строки массива в обратном порядке
- Готовый значок градуса достаётся из постоянного знакогенератора командой…
- В ROM кодировки A00 под кодом 223 лежит градус: lcd.write(byte(223)) печатает его, не тратя ни одного слота CGRAM. lcd.write(byte(223))