Логгер на Pro Mini писал строку замера в порт и тут же ложился спать — батарейки ради. Строки приходили в монитор обрубками: «21:04 v=512» — и дальше тишина, хвост с напряжением батареи пропадал. Я неделю подозревал питание, конденсатор у reset и сам модуль, пока не перечитал в даташите пару строк про буфер передачи. Строка не рвалась на проводе — она умирала в буфере: плата засыпала раньше, чем байты физически покинули ножку TX.
Механика такая: Serial.print не отправляет ничего сам — он копирует байты в передающий буфер размером 64 байта и немедленно возвращается. Дальше байты выгружает прерывание UART, по одному за раз, со скоростью провода. Пока скетч бодрствует, буфер пустеет незаметно; но сон выключает тактирование периферии, и всё, что не успело уйти, застывает в буфере — а в глубоком сне теряется совсем. Приёмный буфер, который мы считали в уроке про буфер UART, живёт по ту сторону, и беды у него другие: переполнение приёма теряет байты, переполнение передачи лишь тормозит отправителя.
Здесь соберу весь набор против потери хвоста: сколько времени реально уходит на вылет буфера на каждой скорости, что делает и чего не делает Serial.flush(), в каком порядке засыпать, что делать с командами, приехавшими перед сном, и чем глубокий сон ESP32 отличается от AVR. В симуляторе — модель буфера: можно усыпить скетч с flush и без и глазами увидеть, какая часть строки доехала. Настоящего сна движок сайта не умеет, поэтому модель честная: буфер-массив и выгрузка по таймеру, а не PWR_DOWN.
Время вылета: 67 мс против 5,5
Арифметика вылета стоит запоминания. Байт занимает на проводе десять бит, значит 9600 бод — это 960 байт в секунду, и полный буфер из 64 байт выезжает за 66-67 мс. На 19200 — вдвое быстрее, 33 мс; на 57600 — 11 мс; на 115200 — около 5,5 мс. Строка лога в тридцать символов на 9600 стоит 31 мс провода. Именно поэтому даже безо всякого сна перезагрузка сразу после println рвёт хвост: watchdog из урока про последний лог вызывает flush не для красоты — без него последняя строка перед сбросом доезжает наполовину.
Serial.flush() — это ожидание вылета: функция возвращает управление, когда передающий буфер опустел. Важная развилка, на которой до сих пор горят по старым статьям: до Arduino IDE 1.0 flush очищал приёмный буфер, и туториалы тех лет учат «очистить ввод через flush» — в современных ядрах это неправда, приём flush не трогает вовсе. Цена ожидания честная: flush при полном буфере на 9600 подвешивает loop на 67 мс. Для строки перед сном — раз в цикл — это копейки; внутри прерывания та же строка — уже грабля, там печать запрещена в принципе.
Тот же буфер объясняет, почему «шторм печати» растягивает loop до скорости провода: когда 64 байта полны, Serial.print перестаёт возвращаться мгновенно и крутится в ожидании свободного места. Скетч с печатью в каждом проходе цикла живёт темпом линии, а не темпом процессора, и планировщик на millis начинает врать. Сколько места осталось, показывает Serial.availableForWrite(): возвращённые ноль-пять мест — сигнал, что канал захлебнулся и печать пора резать. Сколько байт в секунду съедает строка с метками, мы считали в уроке про отладку логики — арифметика там та же, что и здесь.
| Скорость | 64 байта уезжают за | Строка в 30 символов | Вывод для сна |
|---|---|---|---|
| 9600 | 67 мс | 31 мс | flush обязателен: сон без него рвёт любую строку |
| 19200 | 33 мс | 16 мс | риск всё ещё реальный: буфер успевает наполовину |
| 57600 | 11 мс | 5,2 мс | короткие строки выживают, длинные — нет |
| 115200 | 5,5 мс | 2,6 мс | риск мал, но flush всё равно стоит ноль усилий |
// Логгер со сном: последняя строка доезжает целиком, старые// команды не срабатывают после пробуждения.#include <avr/sleep.h> const int KNOPKA = 2; // пробуждение: фронт на D2 void probudenie() { } // пустой обработчик - только разбудить void zasnut() { Serial.print("son: v=512 t=21.4 bat=3.87V"); Serial.println(); Serial.flush(); // 1) ждём вылета буфера TX while (Serial.available() > 0) { // 2) чистим приём дочиста Serial.read(); } attachInterrupt(0, probudenie, LOW); // 3) будильник на D2 set_sleep_mode(SLEEP_MODE_PWR_DOWN); sleep_mode(); // 4) спим до фронта detachInterrupt(0);} void setup() { Serial.begin(9600); pinMode(KNOPKA, INPUT_PULLUP); Serial.println("gotov: zamer - otchet - son");} void loop() { // здесь замеры и логика бодрствования zasnut(); // после пробуждения: продолжаем с этой строки}Ловушка, которую flush не решает: передающий буфер он ждёт, приёмный — нет. Команды, набранные в монитор перед сном или приехавшие от соседней платы, лежат в 64-байтном приёмном буфере; плата засыпает, а после пробуждения available() радостно вернёт старьё — и парсер выполнит протухшую команду через час после её набора. Лечение — вычитать буфер в ноль перед засыпанием: while (Serial.available()) Serial.read();. Чтение до конца и поведение available мы разбирали в уроке про available и read; здесь то же самое, только как гигиена перед сном.
Что приходит уже во время сна — отдельная история: в режиме PWR_DOWN приёмник у AVR мёртв вместе со всей периферией, и байты в это время мимо в принципе. Поэтому договор такой: всё важное плата получает до засыпания и вычитывает буфер дочиста, а во сне не ждёт ничего. Если устройству положено слушать канал круглосуточно, сон ему противопоказан — либо нужен будильник на внешнем фронте, который разбудит плату ровно к моменту, когда сосед начнёт говорить.
ESP32: сон глубже, закон тот же
У ESP32 глубокий сон выключает всё, кроме RTC-домена: процессор, UART и большая часть ОЗУ гаснут, остаются 10-150 мкА. Закон тот же, что у AVR: печать, flush, пауза для страховки — и только потом esp_deep_sleep_start. Отличий три, и все заметные. Первое: после пробуждения скетч стартует с setup, как после reset, и порт открывается заново. Второе: ROM-загрузчик при старте сам выплёвывает в порт пачку мусора на 115200 — это не поломка и не кракозябры от неверной скорости, это свист загрузчика, и монитор показывает его всегда.
Третье отличие — куда класть состояние. Порт — не хранилище ни на какой плате, а у ESP32 и промежуточного варианта нет: что не сохранено до сна, то потеряно. Счётчик циклов и последний замер держат в RTC-памяти — переменная с RTC_DATA_ATTR переживает глубокий сон и гибнет только по кнопке сброса, — а журнал, который должен дожить до утра, пишут во флеш. Бодрствующую часть я собираю на millis, как в уроке про millis вместо delay: проснулся, замерил, отчитался, уснул — без единой паузы delay.
Про батарейку стоит добавить честную цифру: заявленные 10 мкА — это голый модуль без обвязки. Devkit с лентой светодиодов питания, USB-чипом и стабилизатором ест 10-25 мА даже во сне, то есть в тысячу раз больше, и сон на нём ничего не экономит. Проверка одна: токометр в разрыв питания и взгляд на цифру перед пайкой батарейного отсека. Мой devkit пошёл на макетку, а логгер спит на голом модуле WROOM с вырезанным светодиодом — иначе весь порядок засыпания из этого урока не стоил бы ничего.
Итоговый порядок засыпания, который я держу на всех платах: журнал замера — flush — вычитать приём дочиста — поставить будильник — спать. Каждая строка чек-листа куплена чужой кровью: без flush рвётся хвост, без вычитания приёма после пробуждения выполняются протухшие команды, без будильника плата спит до reset. Тот же порядок в перевёрнутой роли работает перед watchdog-сбросом и перед перезагрузкой по кнопке: любой момент, когда порт вот-вот отрежется, требует flush.
Бюджет бодрствования считается сразу: отчёт на двести байт при 9600 держит плату включённой на 208 мс дольше, чем нужно. На цикле сна в минуту это мелочь; на цикле в десять секунд — уже заметная доля, и тогда я поднимаю скорость лога до 115200 или режу текст отчёта. Спорная привычка, за которую меня ругают: печатать последнюю строку дважды. flush гарантирует вылет, но на плате с кривым кварцем дубль — бесплатная страховка: обе дошли, линия чистая; дошла одна — вы уже знаете, что искать. Что положить в саму последнюю строку на случай сбоя, считали в уроке про чёрный ящик.
Если портов несколько — Mega с GPS на Serial1 или ESP32 с отдельным лог-каналом — чек-лист разворачивается на каждый: Serial1.flush(), Serial2.flush(), приём вычитать у каждого тоже. Спящий порт не сохраняет буфер, а теряет его, и молчащий GPS-поток, который копился в Serial1 весь ваш цикл сна, после пробуждения вывалится мусорной простынёй — на AVR приёмник во сне мёртв, а буфер приёма обнуляется вместе со всем состоянием. Поэтому перед сном у каждого порта одна судьба: вычитать, выдавить, замолчать.
- 1. Что делает Serial.flush() в современных ядрах Arduino?
- 2. Сколько времени вылетает полный буфер 64 байта на 9600 бод?
- 3. Сон сразу после Serial.println(otchet). Что увидит монитор?
- 4. Что сделать с приёмным буфером перед засыпанием?
- 5. На ядре, где flush возвращается раньше вылета последнего байта, что добавить перед сном?
- print кладёт байты в буфер 64 и возвращается: сон без flush рвёт хвост строки
- 64 байта вылетают за 67 мс на 9600 и за 5,5 мс на 115200
- flush ждёт только передачу; приём чистят вручную: while (Serial.available()) Serial.read();
- Порядок: печать — flush — чистка RX — будильник — sleep_mode
- ESP32: перед esp_deep_sleep_start тот же flush, состояние — в RTC-память, не в порт
Частые вопросы по теме урока
- Что делает Serial.flush() в современных ядрах Arduino?
- С Arduino IDE 1.0 flush — ожидание вылета буфера передачи. До 1.0 он чистил приём, и старые туториалы до сих пор учат обратному. Ждёт, пока передающий буфер опустеет и байты уйдут
- Сколько времени вылетает полный буфер 64 байта на 9600 бод?
- 9600 бод — это 960 байт/с (10 бит на байт). 64 байта делятся на 960 байт/с — примерно 66-67 мс занятого провода. Около 67 мс
- Сон сразу после Serial.println(otchet). Что увидит монитор?
- print кладёт байты в буфер и возвращается; сон глушит UART, и невыежавшие байты теряются. Между println и сном нужен flush. Обрубок строки: плата уснула раньше вылета буфера
- Что сделать с приёмным буфером перед засыпанием?
- flush приём не трогает. Команды, приехавшие перед сном, останутся в буфере и сработают после пробуждения — их вычитывают в ноль вручную. Вычитать до конца: while (Serial.available()) Serial.read();
- На ядре, где flush возвращается раньше вылета последнего байта, что добавить перед сном?
- На таких ядрах буфер пуст, а байт ещё выходит из сдвигового регистра — короткая пауза после flush закрывает вопрос. Десять миллисекунд раз в цикл сна ничего не стоят. delay(2-5) после flush