Калибровка датчика давления занимает три минуты: 240 шагов с паузами по полсекунды. Первая версия показывала прогресс честно — lcd.clear() и полный кадр с процентами на каждом шаге. Экран мигал в такт шагам так, что коллеги из соседней комнаты спросили, не с демо-стенда ли я транслирую. Диагноз давно известен по уроку про мерцание: причины и лечение: clear стирает весь DDRAM и держит экран пустым дольше миллисекунды. Здесь частный случай, который встречается чаще всего: одна строка статуса на 16 знаков, из которых живые — четыре-пять. Так устроены прошивки, калибровки, заливка массивов в EEPROM, сушка датчика — всё, где loop делает одно и то же сотни раз.
Строка делится на две части. Слева статичный заголовок вроде ZAPIS EEPROM, который пишется один раз в setup и больше не трогается. Справа — окно перерисовки: четыре-шесть ячеек с процентами и счётчиком шагов. Обновляется только окно и только когда его содержимое изменилось; общий каркас с кэшем прошлого кадра разобран в уроке про точечную перерисовку — здесь уровень ниже: не панель целиком, а строчка из пяти знаков.
Числа ради масштаба: setCursor стоит 37 микросекунд, запись байта — ещё 37-43 по даташиту HD44780, разобранному в уроке про устройство контроллера. Окно из пяти знаков — это 200-400 микросекунд раз в секунду. Даже по I2C с накладными расходами — единицы миллисекунд против десятков на полный кадр. При периоде обновления в секунду мигать нечему: экран занят на три десятых процента времени. Для сравнения: полный кадр с clear на каждом шаге занимал у меня 28-35 мс по I2C — почти треть бюджета секунды.
Окно перерисовки: пишем знаки, не кадры
Бар из клеток добавляет наглядности и стоит копейки. Десять клеток, каждая — десять процентов; классический вариант со своими символами собран в уроке про прогресс-бар. Здесь бара хватает и без CGRAM: решётки и прочерки. Дисциплина та же, что у окна процентов: пишем только ту клетку, которая изменилась, — на границе десяти процентов это одна ячейка, а не десять. Обратный ход, сброс на ноль, — единственный случай, когда бар рисуется заново целиком: десять клеток раз в операцию шины не жалко.
Проценты из шагов считаются в целых: p = 100 * i / n. При 240 шагах часть значений пропускается — окно перескакивает с 37 на 38 и с 38 на 41; это нормально, если условие перерисовки — изменение числа, а не каждый проход loop. Поле держится фиксированной ширины в четыре знака: пробелы перед числом, число, процент — тогда переход 99 на 100 не оставляет хвостов. Про begin, setCursor и печать с выравниванием — в уроке про базовые команды.
| Приём | Что пишем | Когда |
|---|---|---|
| Статичный заголовок | один раз в setup | больше никогда |
| Окно процентов, 4-5 знаков | setCursor + поле фиксированной ширины | только при смене значения |
| Бар из 10 клеток | одну изменившуюся клетку | на пересечении границы 10% |
| Смена экрана | clear + полный кадр | выход из операции, вход в меню |
Шаги, оценка времени и честный бар
Оценка оставшегося времени обходится без модных формул: скользящее среднее длительности последних десяти шагов, умноженное на число оставшихся. Печатать его в Serial раз в десять процентов достаточно; на экране это место только у операций дольше пяти минут — иначе цифры мелькают быстрее, чем читаются. Замер шага — millis() до и после, приём из урока про millis вместо delay.
Проценты честны ровно настолько, насколько одинаковы шаги. Если запись страницы занимает то 4, то 9 мс в зависимости от попадания на границу блока, прогресс будет дёргаться. Вынесите шаг в функцию — приём из урока про свои функции — и замерьте десяток шагов подряд: разброс виден сразу, и с ним либо смиряются, либо дробят крупные шаги на два.
#include <LiquidCrystal.h> LiquidCrystal lcd(8, 9, 4, 5, 6, 7); const int btn = 2; // старт операцииconst int led = 12; // горит, пока идёт запись int idet = 0;int gotovo = 0;int str = 0; // шагов сделаноconst int vsego = 512;unsigned long shagT = 0;const unsigned long SHAG_MS = 25; // темп шага операции int prevProcent = -1;int prevKletki = 0; // клеток бара уже нарисованоunsigned long zapisi = 0; // записей на экран с прошлого процента void oknoProcenta(int p) { lcd.setCursor(12, 1); // окно: колонки 12-15 if (p < 10) { lcd.print(" "); } else if (p < 100) { lcd.print(" "); } lcd.print(p); lcd.print("%"); zapisi = zapisi + 1;} void nakatBar(int kletki) { while (prevKletki < kletki && prevKletki < 10) { lcd.setCursor(prevKletki, 1); // пишем только новую клетку lcd.print("#"); prevKletki = prevKletki + 1; zapisi = zapisi + 1; }} void zapusk() { str = 0; gotovo = 0; prevProcent = -1; prevKletki = 0; zapisi = 0; idet = 1; digitalWrite(led, HIGH); lcd.setCursor(0, 0); lcd.print("ZAPIS EEPROM "); lcd.setCursor(0, 1); lcd.print("---------- "); // пустой бар, окно заполнится само shagT = millis(); Serial.println("START 512 STR");} void setup() { Serial.begin(9600); pinMode(btn, INPUT_PULLUP); pinMode(led, OUTPUT); lcd.begin(16, 2); lcd.setCursor(0, 0); lcd.print("GOTOV K RABOTE "); lcd.setCursor(0, 1); lcd.print("KNOPKA - START ");} void loop() { unsigned long now = millis(); int nowBtn = digitalRead(btn); if (prevBtn == HIGH && nowBtn == LOW) { if (idet == 0) { zapusk(); } else { idet = 0; // повторное нажатие - сброс digitalWrite(led, LOW); Serial.println("SBROS"); } } prevBtn = nowBtn; if (idet == 1 && now - shagT >= SHAG_MS) { shagT = now; str = str + 1; int p = 100 * str / vsego; // целые, без float if (p != prevProcent) { prevProcent = p; nakatBar(p / 10); oknoProcenta(p); Serial.print("P="); Serial.print(p); Serial.print(" ZAPISI="); Serial.println(zapisi); zapisi = 0; } if (str >= vsego) { idet = 0; gotovo = 1; digitalWrite(led, LOW); lcd.setCursor(0, 0); lcd.print("GOTOVO 512 STR "); Serial.println("DONE"); } }}Потенциометр в симуляторе меняет длительность шага от 5 до 60 мс — следите, как редко при медленном темпе трогается экран: на весь процент уходит пара записей ячеек. Кнопка запускает операцию, повторное нажатие сбрасывает, светодиод держится, пока запись идёт. Всё то же самое на плате — с реальной записью в EEPROM или на SD: окну всё равно, что считается шагами. Разброс темпа заметен и по Serial: при быстром шаге проценты бегут, при медленном счётчик записей падает до единицы — это лучший тест окна, чем любые слова.
Когда операция блокирующая — библиотека внутри держит loop сотни миллисекунд, — окно не спасёт: перерисовка просто станет редкой. Лечение одно: дробить операцию на шаги и разносить их по loop через millis, как любой планировщик без delay. Для записи в EEPROM и SD это работает напрямую; библиотечные функции с внутренними задержками придётся переписывать или принимать их темп.
Окно полезно не только процентам. Счётчик шагов STR 231/240 занимает девять знаков и честнее процентов, когда шаги неровные: зритель видит остаток, а не дёрганую шкалу. Я оставляю проценты для отчёта заказчику, а шаги — для себя в Serial; при отладке это две разные правды, и путать их не стоит. Тот же счётчик спасает при диагностике: если STR замер, а Serial молчит, вы застряли внутри шага, а не в печати — сразу видно, куда смотреть.
Пауза и продолжение — следующая ступень. Счётчик шагов сохраняется в EEPROM раз в десять процентов, и после перезагрузки операция стартует с последней границы: на длинных прогревах экономятся минуты. Запись ставим по границе десятка процентов, не на каждом шаге — ресурс ячеек конечен, это прямое следствие из серии про EEPROM. И не храните в EEPROM float: два байта счётчика и один байт фазы решают задачу дешевле и надёжнее.
Наконец, граница применимости приёма. Если операция состоит из фаз разной длины — нагрев десять минут, выдержка две, спуск полминуты, — один процент на всю операцию врёт по построению. Тогда окно показывает фазу и её локальный прогресс: PHAZA 2/3 45%. Ячейки те же, заголовок меняется раз за фазу, и миганий по-прежнему нет. Фазу удобно держать в одной переменной со счётчиком: старший байт — номер фазы, младший — шаг внутри неё, и EEPROM получает одну запись вместо двух.
Практика последнего года: я стал оставлять окно и после завершения операции. GOTOVO 512 STR на месте бара держится до первого нажатия — пользователь видит, чем всё закончилось, а не мигнувший на секунду экран. Три ячейки статуса, ноль лишних записей: завершение — тоже часть интерфейса, и стоит оно так же дёшево, как старт.
Про чужие библиотеки скажу отдельно: многие печатают прогресс сами, полным кадром и с clear, и никакое окно их не переделает. Варианта два: обернуть библиотеку своим шагом — вызывать её порциями и рисовать самому, или принять её темп и рисовать редкий статус поверх. Я дважды выбирал второе и оба раза не жал: честные редкие цифры лучше частых миганий.
И последнее: окно любит ровные поля. Заранее прикиньте на бумажке в клетку, какие колонки строки живые, а какие статичные, и запишите колонку окна в константу — правка раскладки потом займёт минуту, а не пересчёт setCursor-ов по всему скетчу. Мой стендовый скетч держит окно в колонках 12-15, бар в 0-9, и эта раскладка пережила три переделки интерфейса без единой правки печати.
- 1. Почему прогресс, нарисованный через clear() на каждом шаге, мигает?
- 2. Что такое окно перерисовки?
- 3. Операция занимает 240 шагов, проценты считаются как p = 100*i/240. Что с окном?
- 4. Как убрать хвост на экране при переходе 99% → 100%?
- 5. Бар из 10 клеток, процент пересёк границу 40%. Сколько клеток перерисуется?
- Строка статуса = статичный заголовок + окно 4-6 знаков справа; заголовок пишется один раз в setup
- Окно обновляется только при смене значения: 200-400 мкс против 1,52 мс clear и десятков мс на полный кадр
- Проценты в поле фиксированной ширины: переход 99 → 100 не оставляет хвостов
- Бар — по одной клетке на границе, проценты целыми из шагов: p = 100*i/n, пропуски значений нормальны
- Блокирующую операцию окно не ускорит: дробите шаги и разносите их по loop на millis
Частые вопросы по теме урока
- Почему прогресс, нарисованный через clear() на каждом шаге, мигает?
- clear занимает 1,52 мс, а перезапись всех 32 ячеек по I2C — десятки миллисекунд: каждую итерацию экран на мгновение пустеет, и глаз ловит строб. clear стирает весь DDRAM и держит экран пустым больше миллисекунды, полный кадр — десятки миллисекунд
- Что такое окно перерисовки?
- Заголовок пишется один раз в setup, живёт только окно из нескольких ячеек справа. Записей на обновление — единицы вместо тридцати двух. Правая часть строки на 4-6 знаков, которую переписывают только при смене значения
- Операция занимает 240 шагов, проценты считаются как p = 100*i/240. Что с окном?
- Целочисленное деление при 240 шагах даёт пропуски значений — окно честно перескакивает. Условие перерисовки — изменение числа, а не каждый проход loop. Часть процентов пропускается: окно меняется скачками, это нормально
- Как убрать хвост на экране при переходе 99% → 100%?
- Ячейка LCD сама не стирается: новая запись короче старой — остаётся мусор. Ширина поля с пробелами переписывает и число, и место под ним. Поле фиксированной ширины: пробелы перед числом затирают следы короче записей
- Бар из 10 клеток, процент пересёк границу 40%. Сколько клеток перерисуется?
- Бар растёт слева направо, и на границе десятка процентов заполняется ровно одна новая клетка. Остальные девять стоят на месте со старого кадра. Одну — ту, что заполнилась