Мой первый экран погоды кончился драмой из-за одной строки. Я написал заголовок Temperatura za oknom sejchas — и хвост молча исчез: ни ошибки компиляции, ни предупреждения, просто чистое стекло там, где я ждал буквы. Неделей позже я поставил курсор setCursor(60, 20) под size(2), и цифра влезла в чужую зону, порезав соседнюю строку пополам. Экран ни в чём не виноват — виновата была арифметика, которую я не сделал на бумаге.
Текст на SSD1306 — это пиксельная сетка, а не текстовый процессор: у каждой строки шрифта своя клетка, у курсора — пиксельные координаты, а всё, что за краем, молча не рисуется. Понять эту сетку до кода — значит не переделывать интерфейс дважды.
В этом уроке посчитаем клетки базового шрифта и его увеличений, наведём порядок в print против println, поставим курсор в нужный пиксель и соберём трёхзонный экран прибора: шапка, крупное значение, подвал. Всё это — поверх модели буфера из прошлого урока про clearDisplay и display(): любые буквы сначала ложатся в ОЗУ платы и только display() показывает их на стекле.
Сетка символов: 6×8 и её увеличения
Базовый шрифт Adafruit GFX рисуется в клетке 6×8 пикселя: сам глиф занимает 5×7, последний столбец и строка — зазор, чтобы буквы не слипались. Плотная пятипиксельная ширина — стандарт из эпохи графических калькуляторов: узнаваемый, экономный по памяти и по скорости отрисовки. Отсюда вся арифметика экрана 128×64: по горизонтали 128 / 6 = 21 клетка (с округлением вниз, 126 пикселей заняты, 2 в запасе), по вертикали 64 / 8 = ровно 8 строк. Двадцать один символ и восемь строк — это весь лист бумаги, который у вас есть при size(1).
setTextSize(n) увеличивает клетку в n раз простым размножением пикселей — без сглаживания, поэтому крупный шрифт выглядит ступеньками; это не дефект, а плата за скорость на контроллере без плавающей точки. Size(2) даёт клетку 12×16: 10 символов и 4 строки. Size(3) — клетка 18×24: 7 символов (126 пикселей) и 2 строки (48 пикселей). Промежуточные размеры никто не запрещает: size(4) оставит 5 символов и 2 строки — это уже плакат, а не интерфейс. Заметьте, как быстро съедается лист: три шага размера — и от 21×8 не осталось и трети.
Курсор setCursor(x, y) ставится в пикселях, начало — левый верхний угол, ось y растёт вниз. print оставляет курсор сразу за последним напечатанным символом, println после печати уводит его в начало следующей строки той же высоты, что и шрифт. Курсор в знакоместах — родной режим у LCD на HD44780: там setCursor(0, 1) означает вторую строку; у OLED координаты пиксельные, и та же свобода компоновки стоит внимательности. Смешивание print и println без явного setCursor — верный способ случайно склеить два слова в одно; моё правило — перед каждой печатью ставить курсор явно, даже если он, кажется, уже там.
Две оговорки напоследок. Первая: в штатном шрифте только ASCII — кириллицы нет, пишем латиницей, это ограничение реального железа, а не симулятора. Вторая: по умолчанию перенос включён — setTextWrap(true), и строка длиннее 21 символа уползает на следующую строку сетки, ломая зоны; setTextWrap(false) превращает её в обрезанную. В обоих случаях ошибки нет — поэтому длины строк считайте заранее.
| setTextSize | Клетка, пиксели | Символов в строке | Строк | Типичное применение |
|---|---|---|---|---|
| 1 | 6×8 | 21 | 8 | шапки, подписи, строки журнала |
| 2 | 12×16 | 10 | 4 | главное значение, часы, счётчики |
| 3 | 18×24 | 7 | 2 | один показатель во весь экран |
Числа печатаются так же, как в Serial-мониторе: print(21) даст 21, а print(21.4) даст 21.40 — два знака после точки по умолчанию, что для температуры обычно лишнее. Десятые доли печатают целочисленным трюком: значение хранят умноженным на десять, целая часть — v / 10, десятые — остаток v % 10, между ними вручную печатают точку. Типы при этом остаются целыми — мы разбирали их в уроке про переменные и типы; полный разбор трюка на живом дашборде ждёт в уроке про графику.
Цвет текста задаёт setTextColor. Один аргумент, SSD1306_WHITE, просто включает пиксели букв. Два аргумента, setTextColor(SSD1306_WHITE, SSD1306_BLACK), рисуют ещё и фон каждой клетки — это ластик из прошлого урока, встроенный в шрифт: новая цифра сама стирает старую под собой, без ручного fillRect. Цвет живёт до следующего вызова setTextColor, а не до конца кадра — забытый чёрный фон потом удивляет артефактами в других зонах.
Для выделения пункта меню используют плашку: fillRect белым по зоне пункта, затем setTextColor(SSD1306_BLACK) — и текст печатается чёрным по белому полю. Классический выбранный пункт получается тремя вызовами. В симуляторе цвета игнорируются — он показывает текст без заливок, так что эффект двухцветной печати проверяйте на реальной плате.
И приём, который стоит заучить: правое выравнивание. Четыре символа size(1) занимают 24 пикселя, значит setCursor(128 - 24, 0) прижмёт статус к правому краю без всяких библиотек выравнивания. Для size(2) множитель не 6, а 12. Один такой вызов превращает лог в прибор: имя слева, состояние справа, как на заводской панели.
#include <Wire.h>#include <Adafruit_GFX.h>#include <Adafruit_SSD1306.h> Adafruit_SSD1306 display(128, 64, &Wire, -1); void setup() { display.begin(SSD1306_SWITCHCAPVCC, 0x3C); display.clearDisplay(); // шапка: мелкий шрифт, статус прижат к правому краю display.setTextSize(1); display.setCursor(0, 0); display.print("METEO"); display.setCursor(128 - 6 * 2, 0); // 2 символа по 6 px display.print("OK"); // данные: крупный шрифт во второй зоне display.setTextSize(3); display.setCursor(0, 16); display.print("21 C"); // подвал: последняя строка сетки, y = 56 display.setTextSize(1); display.setCursor(0, 56); display.print("DHT22 aktualno 1 s"); display.display();} void loop() {}Компоновка: шапка, данные, подвал
Читаемый экран проектируют по клеткам, а не на глаз. Раскладка моего дашборда из скетча выше: шапка занимает строку 0 сетки — пиксели y от 0 до 7; крупное значение size(3) стоит на y = 16 и занимает 24 пикселя высоты, до y = 40; подвал прижат к последней строке — setCursor(0, 56), потому что 64 − 8 = 56. Между зонами остаются пустые полосы — они и дают экрану воздух, не давая строкам слипнуться.
Ширину зон проверяю той же считалкой: заголовок из 5 символов — 30 пикселей, статус из 2 — 12, вместе с зазором меньше половины строки, конфликтов нет. Крупное значение 21 C — 4 символа по 18 пикселей = 72, влезает с запасом на рост до минуса: 5 символов дадут 90 пикселей, всё ещё в пределах строки из 7 клеток. Всё это — сложение в пределах таблицы умножения, но сделанное до кода, а не после первого запуска.
Управление цветом и зонами складывается в конвейер кадра из прошлого урока: clearDisplay, setTextSize для первой зоны, setCursor, печать; смена размера и курсора для следующей зоны; в конце — один display(). Смешивать зоны в одном проходе допустимо, но последовательность шапка-данные-подвал читается в коде как на экране — через полгода вы сами скажете себе спасибо. Подвал я всегда держу на последней строке: единицы измерения, возраст данных, версия прошивки — всё, что должно быть видно, но не должно мешать цифрам.
Раскладку отрабатывайте в симуляторе: текстовый API он повторяет честно, включая сетку 21×8 и поведение println. Отличия две — цвет игнорируется и графические примитивы не рисуются, их откладываем на урок про графику. Остальное — курсоры, размеры, зоны, правое выравнивание — переносится на плату без единой правки, потому что это один и тот же API Adafruit GFX.
- Базовый шрифт — клетка 6×8: 21 символ и 8 строк на 128×64; size(2) — 10×4, size(3) — 7×2
- setTextSize размножает пиксели без сглаживания — крупный шрифт ступенчатый, это нормально
- setCursor принимает пиксели от левого верхнего угла; ставьте курсор явно перед каждой печатью
- Перенос по умолчанию включён: длинная строка уползает на следующую строку сетки; setTextWrap(false) обрезает
- Правое выравнивание: x = 128 − 6×длина для size(1), 12×длина для size(2)
- Штатный шрифт — только ASCII: подписи латиницей, кириллица потребует своего шрифта
- 1. Какая клетка у базового шрифта setTextSize(1)?
- 2. Сколько символов влезает в строку при setTextSize(2)?
- 3. Чем println отличается от print?
- 4. Строка длиннее 21 символа при включённом по умолчанию переносе. Что станет с хвостом?
- 5. В каких единицах setCursor принимает координаты?
Экран собран из зон, значения стоят по считалке, статус прижат к правому краю — текстовый слой раздела пройден. Если вы приходили сюда после LCD 16x2 по I2C, почувствуйте разницу: у HD44780 курсор ходит по знакоместам 5×8, у SSD1306 — по отдельным пикселям, и раскладку можно собирать с точностью до точки. Дальше по разделу — графика Adafruit GFX: линии, рамки и круги поверх тех же зон, и там же пригодится приём с чёрным ластиком, когда надо перерисовать один элемент, не трогая остальной кадр.
Домашнее задание: соберите в симуляторе экран своего прибора из трёх зон — шапка с правым статусом, крупное значение, подвал с единицами. Потом посчитайте на бумаге курсор для статуса из трёх символов (114 при size 1) и перенесите скетч на реальную плату: раскладка должна совпасть пиксель в пиксель. Если где-то не совпало — значит, считалку вы сделали не до кода, и это самый дешёвый урок из всех возможных.
Частые вопросы по теме урока
- Какая клетка у базового шрифта setTextSize(1)?
- Глиф 5×7 плюс пиксель зазора дают клетку 6×8. Из неё считаются и 21 символ в строке, и 8 строк экрана. 6×8 пикселей вместе с зазором
- Сколько символов влезает в строку при setTextSize(2)?
- Клетка удваивается до 12×16: 128 / 12 = 10 символов в строке и 4 строки по высоте. 10
- Чем println отличается от print?
- println уводит курсор на строку ниже той же высоты шрифта, print оставляет его сразу за последним символом. После печати переводит курсор в начало следующей строки
- Строка длиннее 21 символа при включённом по умолчанию переносе. Что станет с хвостом?
- setTextWrap(true) включён по умолчанию: хвост продолжится на следующей строке и может задеть чужую зону. Отключите перенос или укоротите строку. Уйдёт на следующую строку сетки
- В каких единицах setCursor принимает координаты?
- Курсор ставится в пикселях: (0, 0) — левый верхний угол, ось y растёт вниз. Сетка символов — производная от клетки шрифта. В пикселях от левого верхнего угла