Метеостанция на столе у знакомого после подачи питания стояла тёмной секунд пять, пока поднимались шина, датчики и сеть. За эти пять секунд он успевал нажать reset ещё раз — «наверное завис». Устройство было исправно, но молчало, а молчащее устройство для человека выглядит сломанным: экран обязан что-то сказать в первые секунды жизни, даже если данных у него ещё нет.
Чтобы заставка не выглядела произволом, старт полезно разложить на слагаемые: питание и подъём шины — десятки миллисекунд, begin() экрана — ещё пара десятков, первое честное чтение DHT — до двух секунд, SD-карта инициализируется за 100-300 мс, а сеть на ESP32 ловит роутер секунды, в плохом случае все десять. Всё это время прибор уже жив, но молчит, и человек судит о нём по тёмному стеклу. Сплэш с этапами — не украшение, а интерфейс к ожиданию: он переводит «наверное, завис» в «ждёт сеть» и снимает лишние нажатия reset — ровно ту сцену, с которой начинается урок.
Отвечают за это две вещи: сплэш-скрин с логотипом и прогресс-бар инициализации. Первая говорит «я включился и узнал себя», второй — «работа идёт, вот сколько осталось». Приёмы не новые: спиннер и прогресс от реальных шагов я разбирал в уроке про анимации загрузки, здесь соберу старт целиком и добавлю рамку с полосой через drawRect и fillRect — и порог, ниже которого заставка вообще не нужна.
Порог — это мнение, и оно с условием: заставка уместна, когда старт редкий и долгий, и лишняя, когда устройство просыпается часто. Батарейный узел, который просыпается по таймеру десять раз в сутки и каждый раз рисует заставку полторы секунды, тратит пятнадцать секунд экранного времени и ток впустую — про циклы сна и пробуждения я писал в уроке про watchdog и сон. Настольный прибор с питанием от сети — случай ровно про заставку.
Заставка: полторы секунды и честный процент
Заставка — минимальная: название прибора крупным шрифтом, версия прошивки, мелкая строка автора. Логотип можно рисовать картинкой: байт-массив через drawBitmap, конвертацию делает image2cpp — конвертер разобран в отдельном уроке, а работа с битмапами рядом. На мой вкус, текстовый логотип честнее: полноэкранная картинка съедает килобайт флеша, а говорит то же самое, что две строки текста.
Длительность — полторы секунды, и это не магия, а два порога. Менее полусекунды глаз не успевает опознать картинку — заставка превращается во вспышку. Дольше трёх секунд — и при частых перезагрузках, а макет с отладкой перезагружается каждые полминуты, она начинает раздражать раньше, чем успевает радовать. Полторы секунды проходит и тот, кто смотрит в первый раз, и тот, кто перешивает скетч десятый раз за вечер.
С верхней границей у меня личный счёт: в одной версии заставка с анимацией жила пять секунд — из IDE казалось красиво. На тридцатой перепрошивке за вечер я уже ждал её с ненавистью, а домашние на второй день попросили «убрать эту заставку вообще». С тех пор моя полоса — 1,5-2 секунды: меньше секунды логотип не опознаётся и превращается во вспышку, дольше трёх — приветствие становится препятствием, человек перестаёт его читать и начинает искать, чем перезапустить. Единственное исключение — прибор, который включают раз в день и ждут долго: там и три секунды не заметит никто, но и пользы от них ноль. Красивая длинная заставка стоит дороже, чем выглядит из редактора.
Главное правило прогресса: процент считается от шагов, а не от времени. Шагов инициализации три-четыре — экран, датчики, карта или сеть, настройки из EEPROM, — и каждый шаг двигает бар на равную долю: три шага по 33 процента, четыре по 25. Таймерный бар, который ползёт сам по delay(), врёт дважды: при быстрой шине притворяется работой, при медленной обгоняет реальность и замирает на 90 % до конца загрузки. Геометрию бара — рамка drawRect, заполнение fillRect шириной в процент — я разбирал в уроке про прогресс-бар и слайдер.
#include <Wire.h>#include <DHT.h>#include <Adafruit_GFX.h>#include <Adafruit_SSD1306.h> Adafruit_SSD1306 display(128, 64, &Wire, -1);DHT dht(2, DHT11); void splash() { display.clearDisplay(); display.drawRect(0, 0, 128, 64, SSD1306_WHITE); // рамка на весь экран display.setTextSize(2); display.setCursor(10, 14); display.print("MOISTURE"); display.setTextSize(1); display.setCursor(10, 40); display.print("ver 1.4 masterskaya"); display.display(); // без display() экран пуст delay(1500);} void shag(const char* tekst, int procent) { display.clearDisplay(); display.setCursor(0, 10); display.print(tekst); display.drawRect(0, 30, 128, 12, SSD1306_WHITE); // рамка бара display.fillRect(2, 32, procent * 124 / 100, 8, SSD1306_WHITE); // заполнение display.setCursor(0, 50); display.print(procent); display.print(" %"); display.display();} void setup() { Serial.begin(9600); display.begin(SSD1306_SWITCHCAPVCC, 0x3C); // шаг 0: экран и шина display.setTextColor(SSD1306_WHITE); splash(); // 1,5 с: прибор представился shag("Datchiki...", 33); dht.begin(); shag("Karta...", 66); // SD.begin(10); // карта или сеть — что есть в приборе shag("Nastrojki...", 90); // читаем конфиг из EEPROM shag("Gotovo", 100);} void loop() { }Заставка и шаги живут в setup: экран поднимается первым, дальше каждый шаг честно двигает бар. Внутри splash() стоит delay(1500) — и здесь delay оправдан: loop ещё не начат, прерывать заставку нечему. Но если у прибора есть сервисная кнопка, заставку стоит учить пропускать: короткая проверка digitalRead внутри splash — и перешиваемый макет перестаёт платить полторы секунды за каждый прогон.
Сколько этапов брать — отдельное решение: меньше трёх бар не успевает показать ход, больше пяти — доли мельчают, глаз перестаёт различать движение между соседними шагами, и полоса дёргается как стробоскоп. В скетче урока этапа четыре — экран, датчики, карта, настройки, — по 25-33 процента на каждый. Если шагов потенциально больше, близкие объединяются: три датчика идут одним этапом «датчики», чтение конфига и проверка контрольной суммы — одним «настройки». За пять лет ни один мой прибор не потребовал больше пяти этапов: бар нужен, чтобы показывать ход, а не инвентаризацию кода.
Проценты в скетче подобраны под четыре шага и не обязаны быть равными: экран и шина поднимаются быстро, датчики — десятки миллисекунд, карта — сотни, сеть может тянуть секунды. Если хочется, чтобы бар двигался плавно и внутри шага, делите шаг на подшаги — но не заводите таймерный бар: он врёт в обе стороны, и врёт именно в тот момент, когда вам нужно знать правду.
| Устройство | Заставка | Почему |
|---|---|---|
| сетевой узел: шина, датчики, сеть | 1,5-2 с, уместна | старт редкий и долгий, экран закрывает пустоту |
| батарейный узел с глубоким сном | не нужна | пробуждения 10+ раз в сутки: 15 с экрана и ток впустую |
| настольный прибор от сети | 1-1,5 с | брендинг и проверка, что стекло живо |
| макет под частой перепрошивкой | не нужна или пропускается кнопкой | полторы секунды за каждый прогон — десятки за вечер |
Ловушка буфера: display() после каждого шага
Классика жанра: скетч нарисовал красивую заставку, а экран тёмный. Код собирается без замечаний, Serial живёт, логика течёт — картинки нет. Причина в модели дисплея: print, drawRect и fillRect пишут в буфер в ОЗУ, и до вызова display() стекло ничего не знает о том, что произошло. Буфер и вывод — две разные операции, и правило одно: кадр = clearDisplay, рисование, display(). Если экран тёмный, начинайте с поиска display() в кадре, а не с проводов — полный протокол диагностики в уроке про чёрный экран.
В прогрессе ловушка перевёрнута: display() нужно звать много раз. Каждый shag() обязан заканчиваться display(), иначе бар останется на нуле, а потом весь сразу допрыгнет до сотни — классика скетчей, где шаги рисовали, а выводить забыли. Проверка простая: если Serial печатает шаги быстрее, чем шевелится бар, какой-то из шагов рисует в никуда.
И последняя деталь, о которую спотыкаются при переносе: заставку рисуют до begin(). Команды рисования до display.begin() уходят в буфер, который begin() перезаписывает при инициализации, — стекло честно остаётся тёмным. Порядок в setup всегда один: Serial, begin() экрана, потом splash, потом шаги. После переноса скетча на другую плату начинайте проверку с этого порядка.
Кнопка в макетке единственная и перезапускает загрузку, чтобы заставку и бар можно было смотреть сколько угодно раз без перепрошивки. Полоса в симуляторе из решёток и дефисов, в текстовой сетке, потому что fillRect движок не рендерит; на плате полоса — белый прямоугольник в рамке, и глаз видит заполнение, а не символы. Логика процента и порядок шагов при этом идентичны плате один в один.
Потрогайте темп: в скетче шаги идут по 400-500 мс для наглядности, на реальной плате датчик поднимается за десятки миллисекунд, карта — за сотни, сеть может тянуть секунды. Именно поэтому бар должен двигаться от шагов: когда сеть повиснет, бар честно замрёт на своей отметке, и вместо «прибор завис» вы увидите «зависла сеть». Разница между этими двумя диагнозами — один вечер жизни.
- 1. Заставка нарисована, экран тёмный, Serial работает. Что искать первым?
- 2. Сколько живёт заставка и почему именно столько?
- 3. Как строить честный прогресс инициализации?
- 4. Рисование до display.begin() — что увидит экран?
- 5. Батарейный узел с глубоким сном просыпается 12 раз в сутки. Заставка?
- Кадр = clearDisplay + рисование + display(); в прогрессе display() после каждого шага, иначе бар стоит на нуле
- Заставка: 1-2 секунды, название и версия; рисовать только после display.begin(), который перезаписывает буфер
- Процент от шагов, не от времени: честный бар замирает там, где повисла инициализация
- drawRect кладёт рамку, fillRect шириной procent * 124 / 100 — заполнение; обе — для платы, в симе полоса из символов
- Мнение с порогом: частым пробуждениям и частым перепрошивкам заставка не нужна — она платит секундами и миллиамперами
Частые вопросы по теме урока
- Заставка нарисована, экран тёмный, Serial работает. Что искать первым?
- Рисование пишёт в буфер; без display() стекло ничего не покажет, а Serial и логика будут жить как ни в чём не бывало. забытый display() после рисования кадра
- Сколько живёт заставка и почему именно столько?
- Два порога: полсекунды — глаз не опознаёт картинку, три секунды — начинается раздражение при частых перезагрузках. около 1,5 с: ниже 0,5 не опознаётся, выше 3 раздражает при перезагрузках
- Как строить честный прогресс инициализации?
- Таймерный бар врёт в обе стороны; бар от шагов замирает там, где повисла инициализация, и это правда. процент от числа шагов: экран, датчики, сеть
- Рисование до display.begin() — что увидит экран?
- Буфер перезаписывается при begin(), а display() с заставкой не вызывался: стекло остаётся тёмным при живом Serial. ничего: begin() перезапишет буфер при инициализации
- Батарейный узел с глубоким сном просыпается 12 раз в сутки. Заставка?
- 12 пробуждений по 1,5 с — 18 секунд экрана в сутки и лишние миллиамперы; частым пробуждениям заставка противопоказана. не нужна: 18 секунд экранного времени и ток в никуда