Панель из трёх значений я впервые склеил наивно: заголовок, три println подряд — готово. Час экран жил хорошо, вторые сутки — мучительно: влажность 9.5 % сменялась 10.25 %, строка удлинялась на знак, столбец цифр гулял, и глазу не было за что зацепиться. Хуже того, я печатал поверх без очистки, и старые цифры оставляли призраки: десятибалльное давление сменялось девятибалльным, а справа висела лишняя цифра от прошлого часа. Переверстал по зонам с фиксированной шириной — прибор стал читаться с прохода, не подходя.
Правило, которое из той переделки выросло: у каждого значения — своя строка, свой столбец и своя заранее посчитанная ширина. Метки стоят слева, значения — на одном столбце; взгляд выхватывает столбец цифр, не читая надписей, — так сделаны все приборы, которые вы видели в лабораториях.
Источники данных — три разных ритма. Температура в симуляторе приходит честно — из DS3231, у модуля есть свой компенсационный термометр. Влажность и давление в симуляторе крутятся ползунками макетки: потенциометр A0 и фоторезистор A1 играют роль заглушек, а на плате их место займут DHT22 — его подключение разбирали в разделе про DHT — и датчик давления BMP280. Чтение ползунков — тот же analogRead с потенциометром, что вы гоняли на основах.
И про иерархию — правило одного героя: крупным шрифтом на панели живёт ровно одно значение, самое важное для вашей задачи. Size 2 оставляет всего четыре строки на экран: три крупных значения съедят всю сетку, и панель превратится в три будильника без воздуха. У меня героем стала температура — на даче она главная; если ваш прибор про давление, меняйте местами смело, схема зон от этого не ломается.
Зоны: разметка до кода
Мелкий шрифт даёт сетку 21 символ на восемь строк — для трёх значений её хватает с избытком, вопрос только в дисциплине. Мой расклад: шапка на строке ноль, крупная температура двойным шрифтом — герой экрана, под ней влажность и давление мелким, подвал — для служебной строки с ритмом обновления. Точную карту зон я свёл в таблицу ниже; скопируйте её себе и правьте под свой корпус, а не под мой.
Вторая половина дисциплины — ширина. Строка каждого значения собирается целиком из метки и числа и печатается одним print на зону — курсор после print и println мы уже знаем по уроку про текстовый слой. Ширина закладывается под худший случай: минус добавляет знак, 995 миллиметров растягивается в 1032 — четыре знака; место под максимум стоит одного подсчёта, зато столбец значений стоит как вбитый.
Строки у меня выходят примерно на шестнадцать-семнадцать символов из двадцати одного: метка из девяти-десяти, пробел, значение из четырёх-пяти знаков. Правый хвост из четырёх-пяти символов остаётся под статус — ON, STALE, погодную метку. Когда я попытался втиснуть длинные русские подписи целиком, влезло впритык, а отрицательная температура съела запас — короткие метки не эстетика, а техническое условие.
| Зона | Шрифт | Содержимое | Источник |
|---|---|---|---|
| Шапка, y=0 | size 1 | METEO-PANEL | константа скетча |
| Температура, y=16 | size 2 | 23.5 C — 6 знаков из 10 | DS3231; на плате — DHT22 или BMP280 |
| Влажность, y=36 | size 1 | Vlazhnost: 55 % | ползунок A0; на плате — DHT22 |
| Давление, y=46 | size 1 | Davlenie: 1013 mm | ползунок A1; на плате — BMP280 |
Два ритма: датчики и экран
Экран и датчики живут в разных темпах, и смешивать их — верный способ получить тормоза. Экран у меня перерисовывается раз в 500 мс: clearDisplay, печать зон, display() — полный кадр одной посылкой, никакого мерцания, потому что полукадров не бывает. Датчики медленнее: у DHT22 паспортный минимум — раз в две секунды, само чтение занимает миллисекунды; BMP280 по I2C можно опрашивать хоть каждый кадр; DS3231 и вовсе отдаёт температуру мгновенно.
Склеивает оба ритма кэш: значения живут в переменных, экран каждые 500 мс рисует то, что лежит в кэше, а датчики обновляют кэш по собственному таймеру — два независимых интервала в одном loop без единого delay. Это уже не трюк, а схема: периодические задачи на millis я разбирал в отдельном уроке про планировщик — здесь тот же планировщик, только с двумя задачами. Прикиньте масштаб: за сутки панель собирает кадр сто семьдесят две тысячи раз, и если каждый раз дёргать DHT22, датчик не успеет конвертировать — кэш и разнесённые ритмы придуманы не для красоты, а из физики датчика.
Инициализация на одной шине — короткая рутина: display.begin() запускает I2C, rtc.begin() садится следом, порядок не критичен, но единый шаблон setup — сначала экран, потом датчики — экономит от забытых begin. Пара экран плюс часы собирается из урока про установку GFX и SSD1306 и урока про первый RTC — здесь к ним добавляется только разметка зон.
Прикинем цену кадра в этом ритме: полный кадр — около 92 мс по шине на 100 кГц, раз в 500 мс — то есть пятая часть шины уходит на стекло. Соседи по I2C такой темп переживают спокойно — между кадрами остаётся по 400 мс тишины; а если хочется плотнее, разгон до 400 кГц сжимает кадр до 23 мс — уже пять процентов. Числа те же, что в часах и игре, — просто подставьте свой интервал и разделите.
#include <Wire.h>#include <Adafruit_GFX.h>#include <Adafruit_SSD1306.h>#include <DHT.h> DHT dht(2, DHT22);Adafruit_SSD1306 display(128, 64, &Wire, -1); double temp = 0; // кэш значений: экран рисует его,double hum = 0; // датчики обновляют по своему ритмуlong lastDht = 0;long lastRis = 0; void setup() { display.begin(SSD1306_SWITCHCAPVCC, 0x3C); display.setTextColor(SSD1306_WHITE); dht.begin();} void loop() { long now = millis(); if (now - lastDht >= 2000) { // паспортный ритм DHT22 — раз в 2 секунды lastDht = now; double t = dht.readTemperature(); double h = dht.readHumidity(); if (!isnan(t)) temp = t; // сбойное чтение не затирает кэш if (!isnan(h)) hum = h; } if (now - lastRis >= 500) { // экран рисует кэш раз в 500 мс lastRis = now; display.clearDisplay(); display.setTextSize(1); display.setCursor(0, 0); display.println("METEO-PANEL"); display.setTextSize(2); display.setCursor(0, 16); display.print(String(temp, 1) + " C"); display.setTextSize(1); display.setCursor(0, 36); display.print("Vlazhnost: " + String(hum, 0) + " %"); display.setCursor(0, 46); display.print("DHT22 na D2, ritm 2 s"); display.display(); }}Про ширину значений — один подсчёт, который стоит сделать до кода. Диапазон температуры моей дачи — от −12.5 до +30.0: худший случай занимает пять знаков против четырёх у летнего, и именно под пять знаков я оставляю место, а не под красивые 25.0 из окна IDE. Давление — четыре знака всегда: 995 и 1032 одинаково широки, влажность — целое число процентов, два знака. Когда каждое значение знает свою максимальную ширину, панель перестаёт дышать и становится таблицей.
Оговорка честности про симулятор: ползунки — не датчики, и это даже удобно. Потенциометр позволяет проиграть сутки погоды за десять секунд — резкий скачок влажности проверяет, не гуляет ли столбец, а фоторезистор на давлении проверяет четырёхзначную ширину. На плате ползунки заменяются чтениями DHT22 и BMP280 из код-блока — скелет, зоны и ритмы остаются теми же до байта.
И про выбор датчика — пара цифр, чтобы не переплатить и не разочароваться. DHT11 вчетверо грубее: точность ±2 °C против ±0.5 у DHT22, и минусов он не видит вовсе — дачная зима для него за пределами паспорта. Для панели на веранде я ставлю DHT22 без вариантов, а DHT11 оставляю на комнаты, где и два градуса погоды не меняют решения.
- Меняется — рисуем. Сравнивайте новые значения со старыми и вызывайте display() только при изменении: при неподвижных ползунках шина молчит — экономия, которую слышно в журнале.
- Свежесть данных. Заведите счётчик неудачных чтений датчика: дольше пяти секунд без успеха — в углу появляется STALE, панель честно признаёт устаревшие цифры.
- Uptime в шапку. Секунды с millis() справа в строке заголовка: ширина — пять знаков под худший случай через часы работы.
- На настоящей плате. Замените ползунки на DHT22 и BMP280 из код-блока: раскладка зон, таймер 500 мс и кэш остаются без единой правки.
- Панель из нескольких значений — это зоны: у каждого значения своя строка, свой столбец и заранее посчитанная ширина
- Строка собирается целиком и печатается одним print; место — под худший случай: минус и перенос разряда
- Два ритма без delay: экран рисует кэш раз в 500 мс, датчики обновляют кэш по своему таймеру — DHT22 не чаще раза в 2 секунды
- Полная перерисовка clearDisplay → печать → display() не мерцает: кадр уходит одной посылкой
- Хвосты старых цифр — цена печати поверх без очистки; лечится чистым кадром или ластиком по зоне
- 1. Почему столбец значений гуляет при наивной верстке из println?
- 2. Как часто паспортно можно опрашивать DHT22?
- 3. Почему полная перерисовка раз в 500 мс не даёт мерцания?
- 4. Сколько знаков оставить под температуру от −12.5 до +30.0 с одним знаком после точки?
- 5. Откуда панель в симуляторе берёт температуру?
Панель собрана по всем правилам раздела: зоны размечены до кода, ширина — под худший случай, два ритма живут в одном loop без delay, и хвосты старых цифр отсечены чистым кадром. Три урока этой партии сложились в линию: время показывать, в игру играть, за погодой следить — а каркас один и тот же: буфер, зоны, display() по расписанию. Панель из симулятора переносится на веранду одной заменой ползунков на датчики — проверено на двух моих приборах.
Задание на плату: прикрутите настоящий датчик вместо ползунка и оставьте панель на сутки — по утрам вы увидите то, чего не покажет ни один ползунок: как холодный подъём температуры приходит на экран раньше, чем термометр на стене. Заодно проверите ритм 500 мс вживую: глаз отделяет плавность кадра от суеты опроса, и это ощущение останется с вами во всех будущих дашбордах. Дальше по разделу — интерактив на этом каркасе: меню на кнопках и свои шрифты, где разметка зон из этого урока станет основой навигации.
Частые вопросы по теме урока
- Почему столбец значений гуляет при наивной верстке из println?
- println всегда начинает строку с левого края; выравнивание делает автор — фиксированной шириной строки и общим столбцом значений. Значения разной длины печатаются с левого края каждой строки
- Как часто паспортно можно опрашивать DHT22?
- 0.5 Гц — раз в две секунды: датчик конвертирует показание медленно; более частые вызовы дают повторы и мусор. Не чаще раза в 2 секунды
- Почему полная перерисовка раз в 500 мс не даёт мерцания?
- Всё рисование идёт в буфер, на стекло уходит один цельный кадр; SSD1306 показывает его мгновенно и целиком. Кадр собирается в ОЗУ и уходит одной посылкой — полукадров не бывает
- Сколько знаков оставить под температуру от −12.5 до +30.0 с одним знаком после точки?
- Минус добавляет знак: −12.5 — пять символов. Закладывайте худший случай диапазона, а не комнатное значение из окна примера. 5
- Откуда панель в симуляторе берёт температуру?
- У DS3231 есть собственный термометр для компенсации кварца: rtc.getTemperature() отдаёт его, в симуляторе это 23.5. Из компенсационного термометра DS3231