Самый обидный кракозябрный вечер я провёл на вывеске для офисной двери: строка «Привет, мир!» в IDE была набрана красиво, скетч залился без единой жалобы — а на экране выросла знаменитая каша из Р, С и лишних клеток, ровно та, что потом я встречал на каждом третьем форуме. Хорошая новость: это не поломка и не брак экрана. Плохая: лечится не перепайкой, а пониманием того, что Arduino IDE и экран говорят на разных языках — и переводить придётся вам.
В этом уроке разберём, почему русская буква едет на дисплей двумя байтами и что видит знакогенератор, получив такую пару. Пройдём три лечения — транслит, библиотеки-перекодировщики и свои глифы createChar — с ценой каждого. И соберём гибридную надпись, которая не зависит ни от прошивки, ни от версии библиотеки. Схема по-прежнему I2C на четырёх проводах из урока про PCF8574; адрес 0x27 против 0x3F и прочие прелюдии уже решены в первом уроке про текст.
Почему UTF-8 ломает экран
IDE хранит исходник в кодировке UTF-8. Латиница в ней занимает один байт, русские буквы — два. Заглавная «П» — это пара байтов D0 и 9F, строчная «р» — D1 и 80: первый байт служит флагом «дальше русская буква», второй — номером буквы внутри блока. Функция print() честно шлёт эти байты в дисплей по одному, а экран каждый байт понимает как номер ячейки своей однобайтовой таблицы из прошлого урока: пара D0 9F превращается в два чужих знакоместа. Строка из шестнадцати русских букв — это 32 байта и 32 знакоместа мусора, хвост вылезает на вторую строку, и выглядит всё это как порча, хотя каждый байт дошёл по адресу.
Латиница в UTF-8 ломается первым: один байт — один символ, поэтому «Privet, mir!» печатается верно на любом экране без единого движения. Отсюда и нулевое лечение — транслит: скучно, зато работает всегда, на любой прошивке, без библиотек. Когда транслита мало, включаются три способа посерьёзнее — их цена и есть предмет выбора.
| Лечение | Как работает | Цена |
|---|---|---|
| Транслит | Пишете «Privet, mir!» прямо в скетче | Работает всегда и везде; минус один: кириллицы на экране нет |
| Библиотека-перекодировщик: LCD_1602_RUS_ALL, LiquidCrystalRus, utf8rus | Перехватывает вывод и на лету переводит UTF-8 в коды знакогенератора | Русский пишется прямо в коде; не все версии дружат с I2C, таблица всё равно своя |
| Свои глифы createChar + байты | Восемь ячеек CGRAM под недостающие буквы и значки, печать write-ом | Полный контроль независимо от прошивки; ручная работа, ячеек всего восемь |
Перекодировщики — самый приятный путь, когда он доступен: кладёте в скетч строку «Привет» как есть, библиотека на лету переводит её в коды экрана. Проблема у них одна, но регулярная: версии для параллельной шины ходят где угодно, а дружелюбный к I2C-вариант нужно проверять под свою плату — я однажды полвечера нёс «работающую» библиотеку в проект, где уже стоял PCF8574-переходник. Второе условие: перекодировщик всё равно опирается на раскладку конкретной прошивки, поэтому карта из сканера нужна и здесь.
Глифы — лечение от полной безнадёжности и одновременно инструмент сверх неё: если в прошивке не нашлось буквы «ё» или «Ё» — в моём экземпляре их не было ни разу, классическая жертва, — букву рисуют сами. Ячейка глифа — восемь байтов, младшие пять бит каждого задают строку из пяти точек. Рисуется по клеточкам, печатается write-ом, и тот же приём работает для стрелок, часов и баров из урока про прогресс-бар. Устройство слотов и грабля нулевого байта разобраны в уроке про createChar.
#include <Wire.h>#include <LiquidCrystal_I2C.h> LiquidCrystal_I2C lcd(0x27, 16, 2); // глиф буквы "ё": восемь строк, младшие пять бит каждой - точкиbyte yo[8] = { 0b00100, // две точки над буквой 0b01010, 0b00000, // пустая строка между точками и буквой 0b01110, 0b10001, 0b10001, 0b10011, 0b00101}; void setup() { lcd.init(); lcd.backlight(); lcd.createChar(1, yo); // слот 1 - наша буква lcd.setCursor(0, 0); lcd.print("Test: "); lcd.write(byte(1)); // глиф из слота 1 lcd.setCursor(0, 1); lcd.print("Yozhik zhiv!"); // остальное - транслитом} void loop() { // экран уже нарисован и никуда не денется}Массив yo — восемь строк по пять бит: старшие биты дисплею не нужны, клетки шире пяти точек не бывает. Точки над буквой я поднял в первые две строки, а между ними оставил пустую: глиф, нарисованный от края до края, читается слитно с соседями. createChar(1, yo) кладёт рисунок в слот 1 — нулевой я не трогаю без нужды: его печать требует write(byte(0)), и грабля обрыва на нуле уже была описана. Дальше смешанная строка: print ведёт латиницу, write вставляет глиф в нужное место — так собирается любая надпись, в которой кириллицы в ROM не хватило на пару букв.
Мой рецепт первой заливки на новом экране: строка-эталон. Транслит-подпись, пара кодов из карты и глиф «ё» — одна загрузка, и видно всё: подсветка, контраст, раскладка, слоты. Хранится такой тест в папке проекта рядом с основной прошивкой и переезжает от дисплея к дисплею.
Симулятор: честные кракозябры
Движок тренажёра не перекодирует UTF-8 и не рисует глифы createChar — обе эти истории он оставляет живой плате. Зато он умеет показывать само последствие: кнопка на D2 переключает два режима. В первом — честный транслит, LED горит. Во втором та же фраза разложена на байты UTF-8, по два на букву, и отправлена write-ом: вторая строка заполняется чужими клетками и пустотами — ровно та картина, что на живом экране с однобайтовой прошивкой, когда ей скармливают UTF-8.
Отдельно о том, почему тексты в тренажёре набраны латиницей и транслитом, хотя движок согласен на любые строки: литерал «Привет» в симуляторе выглядел бы нормально, а на живой плате дал бы кашу — тренажёр молча солгал бы про результат. Чтобы картинка не расходилась с железом, в симе строки только латиницей, а русские буквы живут данными — байтами в массиве, как и приедется на настоящей плате. Отладка байтов через порт — та же, что в уроке про отладку: печатай коды, а не надежду. После прогона в тренажёре тот же скетч с байтовым массивом удобен и на живой плате как диагноз: если вторая строка сложилась в буквы — у вас прошивка с раскладкой, похожей на CP1251, и перекодировщик подойдёт почти любой; если каша — сначала карта из прошлого урока, потом лечение.
Как выбирать лечение под задачу. Вывеска на один вечер и лично себе — транслит, он дешевле любого спора. Проект, который уедет к другому человеку, — библиотека-перекодировщик плюс эталонная строка в комплекте: чужая прошивка всё равно станет сюрпризом, и проверка за тридцать секунд снимет вопрос. Значки, стрелки, недостающие буквы — глифы, и они же скрашивают меню с курсором: иконка перед пунктом читается быстрее текста. Числа для ориентира: глиф — это восемь байтов массива и одна строка createChar, транслит — ноль накладных расходов, перекодировщик — порядка килобайта машинного кода и зависимость от чужой библиотеки. На Uno весь выбор помещается без натяжек; на крошечной ATtiny13 с килобайтом флеша и 64 байтами оперативки он становится принудительным: транслит и максимум пара глифов.
- 1. Сколько байтов UTF-8 занимает одна русская буква в исходнике Arduino IDE?
- 2. Что оказывается на экране после lcd.print("Привет") с обычной библиотекой?
- 3. Что делает библиотека-перекодировщик вроде LCD_1602_RUS_ALL?
- 4. Сколько своих глифов влезает в CGRAM одновременно?
- 5. Почему глиф нулевого слота печатают lcd.write(byte(0)), а не print?
- Русская буква в UTF-8 — два байта: экран рисует каждое знакоместо отдельно, отсюда каша на живом дисплее.
- Латиница не ломается никогда: один байт — один символ, поэтому транслит — запасной выход, который работает всегда.
- Библиотеки-перекодировщики (LCD_1602_RUS_ALL, LiquidCrystalRus, utf8rus) пишут русский за вас — проверяйте поддержку I2C под свой переходник.
- Недостающие буквы рисуйте глифами: восемь слотов CGRAM, печать write-ом, нулевой — только write(byte(0)).
- Эталонная строка из латиницы, кодов и глифа «ё» — первое, что заливается на новом экране: тридцать секунд вместо вечера гаданий.
Частые вопросы по теме урока
- Сколько байтов UTF-8 занимает одна русская буква в исходнике Arduino IDE?
- Русские буквы в UTF-8 кодируются парой байтов, например «П» — это D0 9F. Латиница занимает один байт, поэтому она и не ломается. Два: байт-флаг и номер буквы
- Что оказывается на экране после lcd.print("Привет") с обычной библиотекой?
- Каждый из двух байтов UTF-8 экран трактует как номер ячейки своей однобайтовой таблицы, и на букву приходится два знакоместа чужих символов. Два знакоместа мусора на каждую букву — пары байтов рисуются по отдельности
- Что делает библиотека-перекодировщик вроде LCD_1602_RUS_ALL?
- Библиотека перехватывает вывод и перекодирует строку в коды знакогенератора. Важно проверять, есть ли у её версии поддержка I2C-переходника. Переводит UTF-8 в коды знакогенератора на лету при выводе
- Сколько своих глифов влезает в CGRAM одновременно?
- CGRAM вмещает восемь ячеек со слотами 0–7. Их хватает на недостающие буквы и пару значков, но планировать слоты надо заранее. Восемь
- Почему глиф нулевого слота печатают lcd.write(byte(0)), а не print?
- Внутри строковых функций ноль означает конец строки, и печать обрывается. write шлёт сырой байт, поэтому нулевой слот печатается только им. Нулевой байт внутри print обрывается как конец строки