Прибор включается полторы секунды: экран показывает название прошивки и колонку, заполняющуюся снизу вверх. Восемь стадий заливки — и восемь слотов CGRAM, из которых шесть уже заняты значками интерфейса: градус, стрелка меню, огонёк нагрева. Моё первое решение — по слоту на каждый кадр колонки — проработало до первого же запуска: createChar в setup перезаписал байты чужих слотов, и вместо градуса на экране красовалась половинка колонки. Слоты я раздал дважды, а раскладку не сохранил — классический промах после урока про свои символы и значок градуса, где бюджет из восьми ячеек только начинался.
Ответ прячется в слове оперативная. CGRAM — не альбом картинок, а шестьдесят четыре байта памяти, которые вы перезаливаете когда хотите; слот — ячейка, а не портрет. У колокольчика из урока про байтовые иконки два кадра жили в двух слотах, и это честная цена за мгновенное переключение. Восемь стадий колонки в восемь слотов вместе с интерфейсом не влезут, зато в один слот влезают по очереди: одна перезаливка — девять байтов по шине, порядка миллисекунды при 100 кГц.
Разбор начну с синтаксиса: половина затыков с глифами случается на этапе объявления массива, до всякого экрана. Дальше — битовая бухгалтерия сетки 5×8, приём слот-хамелеон на живом сплэше и граница применимости: когда перезаливать слот выгодно, а когда честнее завести второй.
Что примет createChar: типы массивов
Сигнатура библиотеки жёсткая: createChar(uint8_t nomer, uint8_t massiv[]). Второй аргумент — указатель на массив байтов, и компилятор следит за типом не хуже контроллера. Массив int, объявленный по привычке, код не пройдёт: указатель на двухбайтовые ячейки не превращается в указатель на байт молча, IDE выпишет cannot convert int* to uint8_t* ещё до прошивки. Чинится одним словом — byte stolb[8] вместо int stolb[8]. Значения от этого не меняются, меняется честность описания: каждый байт здесь — восемь точек, а не число. Про массивы байтов вообще я писал в уроке про массивы и журналы, там же — про то, как их держать в порядке.
С char история тоньше. По битам char и byte — одно и то же восемь раз, и после явного приведения слот получит ровно те же биты. Но байты выше 127 в знаковом типе читаются как минус: сплошная колонка 0b11111111 превращается в -1. Я потратил на это вечер, отлаживая рамку панели: монитор порта сыпал отрицательными числами, хотя экран рисовал правильные столбики. Вывод тот же, что и с int: глифы — это byte, без вариантов.
Отдельная ловушка — PROGMEM. Соблазн понятен: на проекте с жирным Serial-журналом экономить даже шестьдесят четыре байта — святое дело. Но createChar читает указатель в RAM, а PROGMEM-массив лежит во flash, и без memcpy_P во временный буфер в слот уедет мусор из случайных байтов — значок соберётся, но не тот, что вы рисовали. Контекст для масштаба: вся SRAM у ATmega328P — два килобайта, и CGRAM-бюджет в 64 байта — это три процента всей памяти платы. Цена одной длинной строки журнала. Я в итоге перестал заморачиваться: держу глифы в обычных массивах, а если проект упирается в RAM всерьёз, копирую кадр в буфер перед заливкой — две строки кода.
И последнее по типам: размер массива. Восемь байтов — обязательная норма; библиотека скопирует в слот ровно восемь штук, лишние игнорирует, недостающие прочтёт из соседней памяти. Массив из семи байтов собирается без предупреждения и рисует мусор в нижней строке — поэтому я ставлю индекс [8] в объявление машинально, как скобки.
| Запись байта | Где уместна | Подводный камень |
|---|---|---|
| 0b00100 | Рисование глифов: каждый бит — точка на бумажке | Легко перепутать сторону: бит 4 — левая колонка |
| 0x04 | Таблицы из даташита, дампы ROM знакогенератора | Не читается как картинка |
| 4 | Арифметика стадий: r >= 8 - k | В объявлении глифа — слепая запись |
| 00100 без префикса | Нигде | Ведущий ноль — восьмеричная запись: в слот уедет 64 |
Пять колонок и три бита в никуда
Байт — восемь бит, глиф 5×8 съедает пять. Живут они в младшей половине: бит 4 — левая колонка, бит 0 — правая, а биты 5, 6 и 7 контроллер читает и молча игнорирует — механика такого общения разобрана в уроке про устройство HD44780 и продолжается в разборе команд без библиотеки. Я рисовал первый значок на сетке восемь на восемь, честно заполняя правый край, и не мог понять, почему у глифа обрублена сторона. Проверка занимает минуту: залейте байт 0b11100000 — экран останется пустым; байт 0b00010000 — даст один столбик слева.
Отсюда привычка, которую я тащу через все проекты: нижнюю строку значка в текстовой строке держать пустой. Курсор ввода рисуется в той же строке, что и символ, и глиф без воздуха снизу слипается с подчёркиванием. У клеток бара из урока про прогресс рамка снизу закрывает вопрос; у одинокой колонки загрузки — я просто оставляю восьмую строку свободной, если значок стоит рядом с текстом.
Битовая запись 0b экономит не память, а минуты: кривой значок ловится на бумажке до прошивки, потому что нули и единицы видны глазами. Шестнадцатеричный 0x04 — язык даташита, удобный для дампы ROM; десятичная четвёрка — язык арифметики стадий. А вот 00100 без префикса — ловушка языка C: ведущий ноль означает восьмеричную запись, и в слот уедет 64, не 4. Я наступил на это, копируя раскладку из статьи, где биты печатались строкой без 0b, — потом час искал пропавшую колонку, пока не напечатал массив в Serial и не увидел чужие числа.
Дешёвая страховка — дамп перед заливкой: цикл из восьми строк, каждая печатает байт и его 0b-версию через Serial. Пять секунд на написание, полминуты на прогон — и раскладка глифов подтверждена не глазами на экране, а числами в мониторе. К строкам с ведущими нулями в статьях я с тех пор отношусь как к чужому коду без тестов.
Колонка загрузки: один слот на восемь кадров
Механика стадий проще, чем кажется. Колонка стоит в средней колонке глифа — это бит 2, байт 0b00100. Стадия k должна залить k строк снизу: строка с номером r закрашена, если r >= 8 - k. Ни массивов на восемь кадров, ни отдельных значений — один буфер из восьми байтов, который перед каждой перезаливкой пересчитывается циклом из восьми шагов. Заодно исчезает соблазн хранить восемь одинаковых почти массивов: код короткий, и ошибиться в одной строке из восьми труднее, чем в восьми строках.
Цена вопроса — девять байтов по шине за перезаливку: номер слота плюс восемь байтов раскладки. При 100 кГц это порядка миллисекунды; сплэш с кадром раз в 100-300 миллисекунд тратит на глиф меньше процента времени. Альтернатива — держать все кадры в слотах сразу — стоит восемь ячеек бюджета и оправдана только для коротких циклов из двух-трёх кадров, где переключение должно быть мгновенным и бесплатным. Рамка применимости, на которой я настаиваю: перезаливка — для редких кадров (меньше десятка в секунду); живой анимации на 20-30 кадрах в секунду через createChar не делайте — упрётесь в шину раньше, чем в глаза зрителя.
Второй бонус перезаписи — смена глифа не трогает DDRAM: ячейка экрана, где напечатан слот, остаётся на месте и обновляет содержимое сама. Не надо ни setCursor, ни повторной печати — экран живёт своей жизнью, а буфер меняется под ним. Это дешевле и по шине, и по коду.
#include <Wire.h>#include <LiquidCrystal_I2C.h> LiquidCrystal_I2C lcd(0x27, 16, 2); const int pot = A0;const int btn = 2;const int led = 12; byte kadry[8]; // буфер одного кадра колонки void zalivka(int k) { // k - сколько строк залито снизу, 0-8 for (int r = 0; r < 8; r++) { if (r >= 8 - k) { kadry[r] = 0b00100; } // средняя колонка: бит 2 else { kadry[r] = 0b00000; } } lcd.createChar(1, kadry); // перезапись слота 1 на лету: 9 байтов по шине} int stage = 0;int mode = 0; // 0 - сплэш, 1 - готовоunsigned long tick = 0;int prevBtn = HIGH; void setup() { Serial.begin(9600); pinMode(btn, INPUT_PULLUP); pinMode(led, OUTPUT); lcd.init(); lcd.backlight(); zalivka(0); lcd.setCursor(0, 0); lcd.print("BOILER v2"); lcd.setCursor(0, 1); lcd.print("ZAGRUZKA "); lcd.write(byte(1)); // клетка колонки: слот 1 в ячейке (9,1) Serial.println("SPLASH START"); tick = millis();} void loop() { int iv = map(analogRead(pot), 0, 1023, 80, 300); // темп загрузки с ручки unsigned long now = millis(); if (mode == 0 && now - tick >= iv) { tick = now; stage = stage + 1; zalivka(stage); Serial.print("STADIA "); Serial.println(stage); if (stage >= 8) { mode = 1; digitalWrite(led, HIGH); lcd.setCursor(0, 0); lcd.print("GOTOVO "); lcd.setCursor(0, 1); lcd.print("KADRA 8 IZ 8 "); Serial.println("SPLASH DONE"); } } int nowBtn = digitalRead(btn); if (prevBtn == HIGH && nowBtn == LOW && mode == 1) { mode = 0; stage = 0; digitalWrite(led, LOW); lcd.clear(); // смена экрана - здесь clear() уместен lcd.setCursor(0, 0); lcd.print("BOILER v2"); lcd.setCursor(0, 1); lcd.print("ZAGRUZKA "); lcd.write(byte(1)); Serial.println("SPLASH RESTART"); tick = millis(); } prevBtn = nowBtn;}- 1. Сколько бит байта глифа реально рисует HD44780 в шрифте 5×8?
- 2. Что сделает компилятор с createChar(1, massiv), где massiv объявлен как int[8]?
- 3. Почему глиф из PROGMEM-массива заливается мусором?
- 4. Что окажется в слоте после записи байта 00100 без префикса?
- 5. Зачем у значка в текстовой строке нижнюю строку глифа держат пустой?
- CGRAM перезаливается на лету: одна команда — девять байтов по шине, порядка миллисекунды при 100 кГц
- createChar ждёт byte[8]: int не пройдёт компиляцию, char читается в минусах, PROGMEM без memcpy_P кладёт мусор
- Глиф живёт в младших пяти битах: бит 4 — левая колонка, биты 5-7 контроллер игнорирует
- 00100 без 0b — восьмеричная ловушка: в слот уедет 64, а не 4
- Восемь стадий колонки — один слот и восемь перезаливок, когда слоты интерфейса дороже миллисекунд шины
Частые вопросы по теме урока
- Сколько бит байта глифа реально рисует HD44780 в шрифте 5×8?
- Пять колонок живут в младших битах, бит 4 — левая колонка. Старшие три бита контроллер читает, но молча игнорирует. Пять — биты 0-4
- Что сделает компилятор с createChar(1, massiv), где massiv объявлен как int[8]?
- Библиотека ждёт массив байтов, и компилятор не приводит int* к uint8_t* молча. Чинится заменой типа массива на byte, а не приведением в вызове. Откажется: указатель на int не превращается в uint8_t*
- Почему глиф из PROGMEM-массива заливается мусором?
- Библиотека копирует из RAM по указателю. PROGMEM-массив во flash: нужен memcpy_P во временный буфер или обычный массив — 64 байта RAM дешевле отладки. createChar читает указатель в RAM, а массив лежит во flash
- Что окажется в слоте после записи байта 00100 без префикса?
- В C ведущий ноль означает восьмеричную запись: 00100 — это 64. Именно поэтому раскладки глифов копируют только с префиксом 0b или 0x. 64 — ведущий ноль читается как восьмеричная
- Зачем у значка в текстовой строке нижнюю строку глифа держат пустой?
- Курсор рисуется в той же строке, что и символ: без воздуха снизу значок сливается с подчёркиванием. У одиночных виджетов вроде колонки загрузки такой заботы нет. Чтобы курсор ввода не слипался с глифом