Шкалу уровня на OLED я рисовал уверенно, со второго раза, и был уверен в каждом аргументе — пока не увидел на экране половину круга. Индикатор стоял у правого края: fillCircle(120, 32, 10) должен был дать диск радиусом десять, а дал полумесяц. Я перепроверил питание, прогнал сканер шины, перезалил скетч — картинка упрямо обрезалась по прямой. Виноват оказался не экран и не шина: 120 плюс 10 — это 130, а правый край экрана — 127.
У примитивов Adafruit GFX есть свойство, о котором редко пишут: они молча обрезают всё, что вылезает за край. Ни предупреждения компилятора, ни слова в Serial — просто нет пикселей там, где им быть не может. Знание этого свойства стоит дороже выученной таблицы аргументов. Это не брешь библиотеки, а осознанный выбор: проверять каждый пиксель — лишние такты на контроллере, который и так занят. Плата за скорость — ваша арифметика границ.
Здесь соберу полный набор примитивов с сигнатурами и арифметикой занимаемого места, разберу ластик SSD1306_BLACK и слоёный кадр. Рамку, разделитель и круг-индикатор мы уже рисовали в уроке про графику — теперь доведём набор до конца, до треугольников и скруглений.
Полный набор: от точки до треугольника
Сигнатуры у GFX однообразные, и это удобно. drawPixel(x, y, цвет) зажигает точку. drawLine(x0, y0, x1, y1, цвет) тянет линию из точки в точку. drawRect(x, y, w, h, цвет) — контур прямоугольника, fillRect — тот же прямоугольник с заливкой. drawCircle(x, y, r, цвет) — окружность, fillCircle — диск. drawRoundRect и fillRoundRect добавляют скругление углов радиусом r, drawTriangle(x0, y0, x1, y1, x2, y2, цвет) — треугольник по трём вершинам, fillScreen заливает весь экран; вариант fillScreen(SSD1306_BLACK) — быстрый способ очистить кадр целиком, по скорости сравнимый с clearDisplay. Порядок аргументов везде один: точка привязки, потом размеры, цвет последним.
Арифметика занимаемого места своя у каждого. drawRect с числами (x, y, w, h) рисует контур ровно w на h пикселей, толщиной в один пиксель; fillRect с теми же числами зальёт ту же площадь целиком. Круг ходит по формуле два радиуса плюс один: fillCircle(64, 32, 10) накрывает квадрат 21 на 21 пиксель. У roundRect радиус скругления не больше половины меньшей стороны, иначе контур заворачивается сам на себя. Все пять-шесть чисел удобно прикинуть на клетчатой бумаге: лист 128 на 64 клеточки — и каждый примитив превращается в закрашенный прямоугольник, который либо влезает, либо нет.
Цвета три, и это честные числа: SSD1306_WHITE — единица, пиксель горит; SSD1306_BLACK — ноль, пиксель гаснет; SSD1306_INVERSE переворачивает биты зоны. Писать можно и 1 с 0, но имена читаются лучше и переживают смену библиотеки — про смену имён цветов между версиями мы говорили в уроке про установку GFX.
Кадр — слоёный пирог: порядок вызовов и есть порядок наложения. Белую плашку под шапку заливают fillRect до печати текста, чёрный текст поверх кладут после; рамку-контур обычно рисуют последней, чтобы её не срезали заливки. Этот порядок я проговорю ещё раз на разборе скетча ниже. Частный случай слоёв — плашка под выбранный пункт меню: заливка белым по строке, чёрный текст поверх, и пункт читается как нажатая кнопка. На LCD то же делали инверсией, здесь — двумя вызовами.
| Примитив | Что рисует | Занимает на экране | Мой случай |
|---|---|---|---|
| drawLine(x0,y0,x1,y1,c) | линия в 1 px из точки в точку | от (x0,y0) до (x1,y1) | разделители зон |
| drawRect(x,y,w,h,c) | контур толщиной 1 px | ровно w×h | рамка экрана |
| fillRect(x,y,w,h,c) | заливка целиком | ровно w×h | плашки, ластик зоны |
| fillCircle(x,y,r,c) | залитый диск | 2r+1 по обеим осям | индикатор, шкалы |
Тихое обрезание и ластик
Обрезание работает у всех примитивов одинаково: пикселей за краем нет. Прямоугольник с x = 120 и w = 20 даст полосу шириной восемь — от 120 до 127, без предупреждений. Круг с центром у края останется половинкой. Линия с обеими точками за экраном даст чистое место. Мои проверки перед вызовом заняли один абзац в шпаргалке: x + w ≤ 128 и y + h ≤ 64 для прямоугольников, x − r ≥ 0 и x + r ≤ 127 для кругов — и половинок на экране больше не появлялось. Отдельно помню про ось y: 64 строки, нумерация с нуля — фраза «до 64» в комментариях к чужим скетчам стоила мне одного вечера с обрезанной рамкой. Правило я вывел после третьей обрезанной фигуры: сначала бумага, потом вызов — два лишних минутных наброска экономят вечер отладки.
Ластик — то же рисование, но чёрным. SSD1306_BLACK в аргументе гасит пиксели буфера: стереть фигуру значит нарисовать её же чёрным, а очистить зону — залить fillRect чёрным по границам. На этом держится движение: стёрли старое место, нарисовали новое, один display() — и точка поехала. Принцип тот же, что у ластика из урока про буфер, а полный разбор движения ждёт в уроке про анимацию.
Цены копеечные: каждый примитив — цикл из drawPixel по буферу, микросекунды процессора и ноль байтов ОЗУ сверх уже выделенного кадра. Дорогая операция одна — display() со своими 92 миллисекундами передачи, которые мы считали в уроке про графику. Отсюда стиль: собрал кадр целиком — показал один раз. В том же стиле строится и анимация из следующего урока: фигур на кадр там меньше, а дисциплина одна — показ только после сборки.
#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(); // слой 1: контур по периметру, ровно 128x64 display.drawRect(0, 0, 128, 64, SSD1306_WHITE); // слой 2: белая плашка шапки со сдвигом на 1 px внутрь рамки display.fillRect(1, 1, 126, 11, SSD1306_WHITE); // слой 3: чёрный текст поверх белого поля display.setTextColor(SSD1306_BLACK); display.setTextSize(1); display.setCursor(4, 3); display.print("PRIBOR v1.2"); // слой 4: тело на чёрном фоне — белым display.setTextColor(SSD1306_WHITE); display.drawLine(0, 12, 127, 12, SSD1306_WHITE); // граница шапки display.fillCircle(14, 32, 6, SSD1306_WHITE); // залит = всё хорошо display.setCursor(28, 29); display.print("SET DONE"); display.drawTriangle(100, 28, 112, 32, 100, 36, SSD1306_WHITE); // стрелка display.setCursor(0, 56); display.print("drawRect fillRect fillCircle"); display.display();} void loop() {}Мини-проект: шкала уровня в рамке
Разберём скетч выше по слоям. Первым идёт контур: drawRect(0, 0, 128, 64) обводит экран по периметру, и всё внутреннее сдвигается на пиксель внутрь. Шапка — fillRect(1, 1, 126, 11): белое поле, поверх которого печатается чёрный текст — приём-плашка из урока про текст и setCursor. Разделитель drawLine(0, 12, 127, 12) отрезает шапку от тела кадра.
Индикатор состояния — fillCircle(14, 32, 6): залитый круг значит всё в порядке, контурный drawCircle на том же месте — внимание, стоит проверить. Стрелка drawTriangle собрана из трёх вершин: (100, 28), (112, 32), (100, 36). Пять примитивов — целый прибор, и всё это микросекунды процессора поверх буфера, который в любом случае уйдёт на экран целиком. Числа для шкалы прикидываются заранее: в симуляторе рамка занимает клетки с первой по девятнадцатую — девятнадцать ячеек под полосу; map от уровня 0–100 на 1–19 даёт минимум один знак при нуле, чтобы контур не пустовал, и максимум девятнадцать при сотне, чтобы полоса не наезжала на правую черту. На плате та же считалка переводится в пиксели: fillRect от x = 8 шириной map(level, 0, 100, 1, 120).
Шкала уровня в симуляторе показана символами: рамка из дефисов, полоса из знаков равно, длина зависит от потенциометра на A0. На реальном OLED тот же каркас рисует fillRect пикселями: clearDisplay, полоса шириной от уровня, display() раз в 100 мс — числа map в обоих случаях одинаковы.
- Порядок аргументов единый: точка привязки, размеры или радиус, цвет последним
- drawRect — контур ровно w×h толщиной 1 px; fillRect — заливка той же площади; круг занимает 2r+1 пикселя
- SSD1306_BLACK — ластик: рисование чёрным гасит пиксели буфера, стирание фигуры = та же фигура чёрным
- За краем 0..127 и 0..63 пиксели молча не рисуются: проверяйте x + w ≤ 128 и x + r ≤ 127 до вызова
- Кадр — слои в порядке вызовов: заливка до текста, контуры после; display() один на кадр
- 1. Что означают первые два аргумента drawRect(x, y, w, h, цвет)?
- 2. Чем fillCircle(64, 32, 10) отличается от drawCircle(64, 32, 10)?
- 3. fillCircle(120, 32, 10) на экране 128×64 — что появится?
- 4. Как убрать фигуру с буфера, не трогая остальной кадр?
- 5. Сколько пикселей высотой контур drawRect при любых аргументах?
Рамка, плашка, индикатор и шкала собираются из пяти примитивов — этого хватает для девяти приборов из десяти, а треугольники и скругления допишутся по той же схеме. Прогресс из символов на символьном LCD мы уже делали в уроке про свой символ прогресса — сравните с пиксельной шкалой: там потолок — знакоместа 5×8, здесь точность до пикселя. Пять примитивов и считалка границ умещаются в шпаргалку на одной стороне листа — я такую держу над столом.
Домашнее задание: соберите рамку с плашкой и шкалой от потенциометра на реальной плате, потом подведите центр круга к правому краю — увидите ровную половинку. Верните x на 14 и убедитесь, что арифметика границ работает. Проверьте и крайний случай: уровень 100 при полосе в 19 клеток — последний знак упирается в правую черту, и зазор в клетку лучше заложить в map заранее. Дальше оживим картинку: урок про анимацию двигает объект по экрану без мерцания, на том же ластике и том же бюджете кадра.
Частые вопросы по теме урока
- Что означают первые два аргумента drawRect(x, y, w, h, цвет)?
- Точка привязки GFX — левый верхний угол: x растёт вправо, y вниз. Ширина и высота отсчитываются от неё. Левый верхний угол
- Чем fillCircle(64, 32, 10) отличается от drawCircle(64, 32, 10)?
- draw — контур, fill — заливка. Диск при радиусе 10 накрывает 21×21 пикселей: два радиуса плюс центр. drawCircle рисует окружность в 1 px, fillCircle — залитый диск
- fillCircle(120, 32, 10) на экране 128×64 — что появится?
- 120 + 10 = 130 против края 127: выступающие пиксели отбрасываются без предупреждений. Центр держите минимум в радиусе от границ. Левая половина диска, правая молча обрезана
- Как убрать фигуру с буфера, не трогая остальной кадр?
- Чёрный цвет гасит пиксели — рисование чёрным работает ластиком. clearDisplay не подходит: он обнуляет весь буфер. Нарисовать её же или fillRect зоны цветом SSD1306_BLACK
- Сколько пикселей высотой контур drawRect при любых аргументах?
- drawRect — контур толщиной в один пиксель по периметру w×h. Толстые линии собирают вложенными контурами или fillRect. Ровно один пиксель