Термометр на LCD я собрал за вечер, а значок градуса доводил ещё неделю. Классический HD44780 пишет латиницей и цифрами, но «C» без градуса рядом выглядит как обезличенная единица — хотелось «25°». Первая попытка была наивной: я нашёл в сети совет про lcd.write(223), поставил — и получил пустое место там, где обещали градус. Вторая — скопировала кусок чужого кода с lcd.print((char)0): вместо значка — белый прямоугольник. Только даташит на 40 страниц расставил всё: у контроллера есть постоянный знакогенератор с сотней готовых букв и отдельно — восемь ячеек, куда символы можно положить свои, побайтово, как рисунок.
В этом уроке разберём обе половины знакогенератора: что уже лежит в ROM и достаётся одним write, и как рисовать в CGRAM свои глифы через createChar — сердце, стрелку, свой градус. Заодно поймаем две классические ловушки с нулевым кодом и узнаем, почему весь этот трюк не живёт в симуляторе и что он там замещает.
Восемь слотов по 8 байтов: как устроен CGRAM
Каждое знакоместо на экране — сетка 5 на 8 пикселей. Знакогенератор просто хранит, какие из этих точек закрашены: восемь строк по пять бит. Постоянная память (ROM) содержит готовую таблицу — латиницу, цифры, японскую катакану и набор значков; ваша плата почти наверняка говорит на кодировке A00, где под номером 223 (в шестнадцатеричном 0xDF) лежит градус. А ещё у контроллера есть оперативная CGRAM на восемь ячеек с номерами от 0 до 7: в них вы можете записать собственные восмёрки байтов, и номера слотов начнут вести себя как обычные символы — их можно печатать, вставлять в строки, двигать setCursor-ом.
Библиотека прячет грубую работу за двумя командами. lcd.createChar(номер, массив) копирует ваш массив из восьми байтов в CGRAM — это делается один раз в setup. lcd.write(byte(номер)) выводит слот на экран как символ. Каждый байт массива — одна строка глифа: байт с индексом 0 рисуется самым верхним, порядок сверху вниз, единицы в младших пяти битах закрашивают точки слева направо. Вся ваша оперативная память под глифы — 64 байта: меньше, чем одна строка журнала в ОЗУ Arduino, поэтому значки и хранят побайтово, а не картинками. Удобно проектировать на бумаге в клетку или в онлайн-редакторе пикселей: пять столбцов, восемь рядов, нолики и единицы. Массивы байтов для глифов — тот же инструмент, что мы изучали в уроке про массивы и журналы, просто здесь каждый бит на вес золота.
| Что печатаем | Как это делаем | Откуда берётся |
|---|---|---|
| Готовый градус | lcd.write(byte(223)) или lcd.write(0xDF) | ROM знакогенератора, кодировка A00 |
| Своё сердце, стрелка, звонок | lcd.createChar(1, массив); lcd.write(byte(1)) | CGRAM, слоты 0–7 |
| Слоты 1–7 в print | lcd.print(массив) нельзя — только write или запись байта в буфер | Ограничение типов Arduino |
| Слот 0 | lcd.write(byte(0)) — обязательно с приведением | CGRAM, ячейка 0 |
| Стрелка вправо без CGRAM | lcd.write(126) — символ тильды выглядит как стрелка | ROM |
#include <Wire.h>#include <LiquidCrystal_I2C.h> LiquidCrystal_I2C lcd(0x27, 16, 2); // сердце: 8 строк по 5 пикселей, 1 = закрашенная точкаbyte heart[8] = { 0b00000, 0b01010, 0b11111, 0b11111, 0b01110, 0b00100, 0b00000, 0b00000}; // стрелка вниз — маркер выбранного пунктаbyte arrow[8] = { 0b00000, 0b00100, 0b00110, 0b11111, 0b00110, 0b00100, 0b00000, 0b00000}; void setup() { lcd.init(); lcd.backlight(); lcd.createChar(1, heart); // копируем в CGRAM один раз lcd.createChar(2, arrow); lcd.setCursor(0, 0); lcd.print("Reyting: "); lcd.write(byte(1)); // рисуем слот 1 lcd.setCursor(0, 1); lcd.write(byte(2)); // слот 2 перед текстом пункта lcd.print(" Dalshe");} void loop() {}Две тонкости, на которых спотыкаются. Первая — нулевой слот: запись lcd.print((char)0) в Arduino часто печатает не глиф, а пустоту, потому что ноль внутри строковых функций означает конец строки. Правильный путь — lcd.write(byte(0)) с явным приведением типа, как в таблице выше; по той же причине многие сознательно начинают нумерацию слотов с единицы. Вторая — время жизни: CGRAM живёт, пока на дисплей подано питание, и lcd.clear() её не трогает, зато глубокая просадка питания или перетыкание шлейфа стирает содержимое. Мой порядок в setup поэтому такой: сначала lcd.init()/begin(), потом все createChar — и при странностях после «мигания света» значки просто перезаливаются той же процедурой.
Слоты — валюта, и её всего восемь. Я трачу их по правилу «один смысл — один слот»: градус и единицы измерения почти всегда доступны из ROM, значит слоты остаются для интерфейса — стрелка выбранного пункта, замочек, антенна, батарейка. В меню с курсором стрелка из CGRAM смотрится взросло по сравнению с текстовой «>», а на термостате из урока про гистерезис пара слотов уходит на огонёк нагрева и снежинку охлаждения.
Градус из ROM и честная оговорка про симулятор
Прежде чем отдавать слоты под градус, посмотрите в ROM: в стандартной кодировке A00 под номером 223 уже лежит готовый значок, и lcd.write(byte(223)) печатает его без всяких createChar — тот самый совет, который у меня сначала «не сработал», оказался верным, я просто ошибся платой: мой экземпляр был из партии с другой таблицей, и я это выяснил, напечатав подряд символы с 128 по 255 — своего рода таблицу кодов. Такой дамп стоит делать на любом новом дисплее: две строки по 64 символа через write в цикле, и вы точно знаете, что у вас в ROM, вместо предположений.
Теперь оговорка, которую я обещал: интерпретатор симулятора команды createChar не поддерживает — слоты и пиксельные глифы он не воспроизводит. Зато символ градуса он печатает честно, если отдать его как обычный текст: в скетче ниже значок идёт прямо в lcd.print строкой. Это текстовая имитация: на реальной плате то же место должно занимать lcd.write(byte(223)) либо собственный глиф из CGRAM, если ROM вашей партии оказался без градуса. Схема при этом настоящая: потенциометр на A0 задаёт температуру, map переводит показания в диапазон, как мы делали с термистором.
Если значок в симуляции не отрисовался на вашей раскладке клавиатуры, ничего не сломалось — это ограничение шрифта браузера, в коде символ всё равно один байт текста. На железе же правило одно: сначала дамп ROM, потом слоты. Не отдавайте CGRAM за то, что уже лежит в постоянной памяти, — восемь ячеек кончаются быстрее, чем кажется, особенно когда в интерфейсе появляются стрелки и иконки состояний. И выносите печать единиц в отдельную функцию: когда перейдёте на настоящую плату, заменить печать значка градуса на lcd.write(byte(223)) с тем же суффиксом можно будет в одном месте.
Своя иконка в знакогенераторе — это уже наполовину интерфейс, а не просто вывод. Дальше по маршруту два направления: углубляться в меню с курсором, где глифы работают как маркеры, или уходить в графическую лигу — в уроке про первый OLED SSD1306 пиксели настоящие, и ограничений в восемь слотов там нет. Для символьных дисплеев же запас прочности такой: латиница и цифры из ROM, интерфейсные значки из CGRAM, всё остальное — смыслом, а не пикселями.
Тройка про дисплеи на этом замыкается: контраст мы доверили ручке на V0, четырёхстрочный экран — адресной карте DDRAM, а знакогенератор — восьми слотам CGRAM и дампу ROM. С этим набором символический LCD перестаёт быть коробкой сюрпризов: любой странности находится имя — электрика, адресация или знакогенератор. Дальше решайте сами, что ближе: интерфейсы с курсором на том же железе или переход на графический OLED, где пиксель наконец становится по-настоящему вашим.
- Нарисуйте на бумаге в клетку свой градус (шарик и палочка умещаются в 5×3), переведите в восемь байтов и залейте в слот 3 — сравните с write(byte(223)) из ROM.
- Соберите дамп ROM: в двух вложенных циклах печатайте write с кодами 128–255 и запишите, что лежит в памяти вашей панели — это займёт пять минут и пригодится на каждом следующем дисплее.
- Сделайте индикатор заряда из трёх слотов: пустая батарейка, половина, полная — и печатайте нужный в зависимости от показания потенциометра.
- Вернитесь к скетчу меню и замените текстовый маркер «>» на стрелку из CGRAM; проверьте, что write(byte(0)) печатает слот 0, а print((char)0) — нет.
- Знакоместо HD44780 — сетка 5×8; свои глифы живут в CGRAM: 8 слотов с номерами 0–7, каждый — 8 байтов по 5 значащих бит
- createChar(номер, массив) — один раз в setup после begin/init; вывод — lcd.write(byte(номер))
- Нулевой слот печатайте только через write(byte(0)): внутри print ноль обрывается как конец строки
- Градус обычно уже есть в ROM под кодом 223 (0xDF) — дамп кодов 128–255 покажет таблицу вашей партии
- CGRAM стирается только по питанию; clear() значки сохраняет, а после просадки — перезаливайте createChar заново
- Симулятор createChar не поддерживает: значок там — текстовая имитация, глифы проверяйте на реальной плате
- 1. Сколько своих символов помещается в CGRAM классического HD44780?
- 2. Как правильно вывести на экран глиф из слота 0?
- 3. Где обычно берут значок градуса для термометра на LCD?
- 4. Сколько байтов описывает один глиф 5×8 в массиве для createChar?
- 5. Что произойдёт с залитыми в CGRAM значками после lcd.clear()?
Частые вопросы по теме урока
- Сколько своих символов помещается в CGRAM классического HD44780?
- Восемь ячеек с номерами 0–7, каждая хранит глиф 5×8 как восемь байтов. ROM при этом отдельная и содержит готовую таблицу. 8
- Как правильно вывести на экран глиф из слота 0?
- Нулевой байт внутри print обрабатывается как конец строки, поэтому пишут write(byte(0)). createChar лишь заливает слот, но не печатает его. lcd.write(byte(0)) — с явным приведением типа
- Где обычно берут значок градуса для термометра на LCD?
- В стандартной кодировке A00 градус лежит под кодом 223 (0xDF). Сначала проверьте дампом ROM своей панели — и только потом тратьте слот CGRAM. Достают из ROM командой lcd.write(byte(223)) на большинстве панелей
- Сколько байтов описывает один глиф 5×8 в массиве для createChar?
- Восемь байтов — по одному на строку пикселей; в каждом байте значащи младшие пять бит, они и закрашивают пять столбцов. 8
- Что произойдёт с залитыми в CGRAM значками после lcd.clear()?
- clear() затирает экранную память DDRAM. CGRAM живёт до снятия питания, поэтому иконки перезаливают createChar лишь после перетыкания дисплея. Ничего — clear() чистит только DDRAM с текстом