Вольтметр на OLED я собрал за вечер, а пользоваться им не смог и недели: число из analogRead в углу экрана требовало фокусировки, на улице растворялось, и вообще — цифра это всегда чтение, а показания хочется видеть. Выход подсказал старый стрелочный прибор: полоса, которая едет по шкале, считывается с двух шагов и боковым зрением. Тот же трюк спасает индикатор заряда, уровень звука, глубину заполнения бака — всё это рисуется четырьмя командами и не требует ни битмапов, ни библиотек поверх Adafruit_GFX.
Команд всего четыре, но у них есть асимметрия, на которой спотыкаются почти все. Система координат проста: ноль — левый верхний угол, X растёт вправо до 127, Y вниз до 63. drawLine(x0, y0, x1, y1, цвет) берёт две точки и ведёт между ними линию. drawRect(x, y, w, h, цвет) берёт ЛЕВЫЙ ВЕРХНИЙ УГОЛ плюс ширину и высоту. А вот drawCircle(x, y, r, цвет) — и это главное отличие — берёт ЦЕНТР и радиус: икса и игрека правого нижнего края у круга просто нет. Я один раз минут десять двигал круг по экрану, передавая ему угол, и не мог понять, почему он упрямо живёт в четверти, где не был зван.
Заливка — парная к каждому контуру: fillRect красит прямоугольник целиком тем же набором аргументов, fillCircle — круг. Базовые механики этих же команд я уже раскладывал в уроке про линии и фигуры, сегодня же собираем из них три конкретные вещи: рамку по периметру, шкалу гистограммы и столб, который дышит вместе с ручкой на A0. К конце урока у вас будет скелет, который я потом переносил в индикатор заряда и в указатель уровня без переделки логики — менялись только координаты.
Шкала, которая не съедена рамкой
Гистограмма — это арифметика ширины, и её стоит проговорить до кода. Столб рисуется fillRect от левого края, значит ширина — единственная переменная часть. Сырое значение analogRead живёт в диапазоне 0-1023, а шкале у меня выделено 122 пикселя: 128 минус два пикселя полей слева и справа плюс один пиксель под саму рамку. Переводит диапазоны map(raw, 0, 1023, 0, 122) — и столб никогда не вылезет за рамку, что бы вы ни крутанули движок. Если шкала узкая, например 60 пикселей, тот же map просто получает другой потолок — формула не меняется.
Два края диапазона проверяйте руками, это дёшево. При raw = 0 ширина столба равна нулю — fillRect с шириной меньше единицы молча ничего не рисует, ни падения, ни артефактов, пусто и всё. При потолке столб упирается в правую границу шкалы ровно вплотную, если поля посчитаны честно. А вот что происходит с заливкой, уехавшей за край экрана: библиотека обрезает её по границе буфера, в чужую память она не пишет —(fillRect(120, 0, 40, 20) даст столбик из оставшихся восьми пикселей, а не мусор. Толщина линий отдельно: drawLine всегда в один пиксель, и рамка из неё выглядит нервно на расстоянии — для утолщения я провожу fillRect высотой два-три пикселя, это дешевле, чем четыре параллельных линии.
Верстать кадр я привык от края к центру: сначала рамка и толстая полоса-шапка, потом контур шкалы, и только затем столб и подписи. Такой порядок гарантирует, что значения ложатся поверх разметки, а не наоборот: заливка, нарисованная раньше контура, оказалась бы перекрыта рамкой на пиксель, и правый край столба выглядел срезанным. Числа для конкретного экрана я записываю комментариями в шапке скетча — 124 на контур, 122 на заливку, по пикселю полей с боков, — и следующий прибор собираю уже без подбора координат. Пять минут на бумажную разметку экономят тот час, который я однажды потратил на подбор позиций тыком.
| Команда | Аргументы | Что берёт на вход | Типовая роль |
|---|---|---|---|
| drawLine | x0, y0, x1, y1 | две крайние точки | линии, деления шкалы, сетки |
| drawRect | x, y, w, h | угол + размер | контуры рамок, граница шкалы |
| fillRect | x, y, w, h | угол + размер | столбы, заливка, полосы |
| drawCircle | x, y, r | ЦЕНТР + радиус | индикаторы-лампочки, точки |
Обратите внимание на третий столбец таблицы: прямоугольники живут углом, круг — центром. Отсюда практическое следствие для раскладки кадра: прямоугольную шкалу удобно позиционировать от углов зоны, а круглый индикатор — от центра свободного места, и координаты у них считаются по-разному. Круглую лампочку радиуса 4 в правом нижнем углу я ставлю в точку (114, 58): 114 плюс 4 равно 118, до правого края остаётся девять пикселей, снизу 62 — пиксель зазора до линии на 63. Каждую такую координату я когда-то подгонял на глаз, а потом записал в комментарий скетча — экономит время при следующем переносе.
Ещё одна граница применимости, о которой редко пишут: примитивы этой четвёрки — строго монохромные. Есть цвет — SSD1306_WHITE — и нет цвета, и никакого сглаживания между ними: ступенчатый край столба на большом экране выдал бы генерацию, здесь же лесенка из пикселей — родная фактура панели. Сглаженные фигуры умеют только векторные шрифты, а примитивы дают резкость, которая на 0.96 дюйма смотрится честнее. Точки по одной, если понадобится, ставятся drawPixel — у меня про неё отдельный урок, здесь же работаем пакетами.
Отдельно про порядок аргументов: на этой четвёрке я видел больше всего промахов. У прямоугольников идут x, y, w, h — угол и два размера; глаз, привыкший к четырём координатам drawLine, тянет тот же ритм в drawRect, и вместо рамки 124 на 42 выходит узкая полоса или вовсе ничего. Моя страховка грубая, но рабочая: ставя размеры, вслух проговариваю «угол — ширина — высота». Второй промах тоньше — залить столб раньше контура и получить рамку, перекрытую заливкой у правого края; поэтому в скетче заливка всегда на пиксель уже контура, и рамка остаётся целой.
#include <Wire.h>#include <Adafruit_GFX.h>#include <Adafruit_SSD1306.h> Adafruit_SSD1306 display(128, 64, &Wire, -1); void setup() { Serial.begin(9600); display.begin(SSD1306_SWITCHCAPVCC, 0x3C); display.setTextColor(SSD1306_WHITE); display.setTextSize(1);} void loop() { int raw = analogRead(A0); // движок: 0..1023 int barW = map(raw, 0, 1023, 0, 122); // ширина столба, поля уже учтены display.clearDisplay(); // рамка: толстая полоса сверху из fillRect, тонкая линия снизу из drawLine display.fillRect(0, 0, 128, 2, SSD1306_WHITE); display.drawLine(0, 63, 127, 63, SSD1306_WHITE); // контур шкалы: угол (2,10), размер 124x42 display.drawRect(2, 10, 124, 42, SSD1306_WHITE); // столб гистограммы: заливка на пиксель уже контура display.fillRect(3, 11, barW, 40, SSD1306_WHITE); // лампочка: кругу дают ЦЕНТР; при raw выше середины - заливаем display.drawCircle(114, 58, 4, SSD1306_WHITE); if (raw > 512) display.fillCircle(114, 58, 3, SSD1306_WHITE); // сырые значения в верхнюю строку, между полосой и шкалой display.setCursor(2, 3); display.print("A0 = "); display.print(raw); display.display(); delay(100);}В скетче два момента, которые легко пропустить. Первый: кадр пересобирается с нуля на каждом проходе loop — clearDisplay стирает всё, включая рамку, и рамка рисуется заново. Это не расточительство, а нормальный цикл работы с буфером: стирание и рисование — операции над байтами в памяти контроллера, шина при этом молчит, и десяток примитивов стоят копейки. Второй: столб заливается на пиксель уже контура шкалы — иначе fillRect перекрывает линию drawRect, и рамка выглядит обгрызенной. Двухпиксельный зазор (контур на 2, заливка на 3) — моя рабочая привычка после пары кадров с визуально сломанной шкалой.
Про симулятор — честная оговорка. Тренажёр внизу показывает текстовую сетку экрана и примитивы НЕ отрисовывает: drawLine и братья для него тихие пустышки, поэтому бар там собран из символов решётки. На настоящей плате тот же скетч рисует всё пикселями — запустите код-блок выше и сравните. Для плавной перерисовки без delay в живом приборе переведите цикл на millis-такт — в третьем уроке этой серии так и сделано.
Куда это прикрутить
Из этих четырёх команд собирается большая часть моих экранов. Индикатор заряда — та же шкала с делениями из коротких drawLine каждые двадцать пикселей. Уровень звука с микрофона — столб, у которого ширина дрожит с амплитудой, плюс пиковая чёрточка, плавно сползающая вправо. Индикатор сети или нагрева — три круглые лампочки, заливаемые по порогам. Ни одна из этих задач не потребовала битмапов: примитивы быстрее в разработке и в разы легче по памяти — битмап 32×32 съедает 128 байт флеша, а вызов drawCircle не стоит ничего.
Вертикальную версию столба и управление полосой слайдером я разбирал в уроке про бар и слайдер — там же про плавные ступени. Общая механика кадра и текст на том же экране живут в базовом уроке про графику, а сырое значение для столба снимается с движка ровно так, как описано в уроке про analogRead. Дальше по серии — заглянем внутрь буфера и посчитаем, сколько стоит один кадр.
Последний довод в пользу примитивов — предсказуемый вес. Шкала со столбом и рамкой не требует ни таблиц, ни конвертеров: битмап иконки 32×32 стоит 128 байт флеша и похода через image2cpp, а вызов drawCircle не стоит ничего. Для прибора, где экран — это три-четыре индикатора и одна шкала, примитивов хватает с избытком; спрайты подключаются точечно, когда без них не обойтись, — про их изготовление у меня есть урок про битмапы.
- 1. Что передаётся первым аргументом в drawCircle?
- 2. Сколько пикселей красит fillRect(0, 30, 60, 20)?
- 3. Ширина столба получилась нулевой: map вернул 0. Что сделает fillRect?
- 4. Заливка уехала за правый край экрана, например fillRect(120, 0, 40, 20). Что будет?
- 5. Как утолщить линию с одного пикселя до трёх?
- Система координат: (0,0) — левый верхний угол, X до 127, Y до 63.
- Прямоугольники берут угол плюс w/h, круг — центр плюс радиус; заливка парна к каждому контуру.
- fillRect с размером меньше единицы молча ничего не рисует; вывод за края аккуратно клипуется.
- Гистограмма: map(raw, 0, 1023, 0, 122) — поля и рамка учтены заранее, столб не вылезет.
- Примитивы — операции над буфером в SRAM: шина I2C молчит до вызова display().
Частые вопросы по теме урока
- Что передаётся первым аргументом в drawCircle?
- Круг задаётся центром и радиусом, в отличие от прямоугольников, которые живут углом плюс шириной и высотой. Это главная асимметрия четвёрки. Центр окружности
- Сколько пикселей красит fillRect(0, 30, 60, 20)?
- Ширина 60 на высоту 20 — 1200 точек. Аргументы идут именно в таком порядке: x, y, w, h — сначала угол, потом размеры. 1200
- Ширина столба получилась нулевой: map вернул 0. Что сделает fillRect?
- Библиотека проверяет размеры и пропускает рисование при w или h меньше единицы. Ни падения, ни артефактов — просто пустая зона. Молча ничего не нарисует
- Заливка уехала за правый край экрана, например fillRect(120, 0, 40, 20). Что будет?
- GFX аккуратно клипует вывод по границам экрана: в чужую память библиотека не пишет. За краем просто обрезается. Заливка обрежется по границе буфера
- Как утолщить линию с одного пикселя до трёх?
- У drawLine нет параметра толщины: либо параллельные вызовы, либо узкий fillRect. setTextSize растит только текст. Провести fillRect высотой 3 пикселя