Мой узел с тремя датчиками на 8266 к осени оброс расписанием: мигалка статуса каждые 400 мс, опрос DHT22 каждые 2500, строка в Serial раз в 1000, переподключение WiFi раз в 30 с. По уроку про планировщик задач это собралось за вечер, но через месяц я путался в четырёх переменных lastX и четырёх if, разбросанных по loop(). Каждая новая задача добавляла две строки в начало loop и одну в конец, и однажды я не обновил lastSensor — датчики молчали полдня, а скетч был формально жив. Ticker выносит расписание из кода: объект помнит интервал сам и зовёт функцию без единого if.
Здесь разбираю встроенную библиотеку Ticker.h: attach для секунд, attach_ms для миллисекунд, once для одноразовых звонков и detach для отписки. Главная ложка дёгтя — контекст: колбэк тикера зовётся не из loop, а из системного таймера, поэтому delay и Serial внутри способны уронить плату в рестарт. Покажу правило коротких колбэков и соберу мигалку с датчиком в двух тикерах — сперва скетч для живой платы, потом та же логика в симуляторе.
Ticker: три будильника в четырёх строках
Ticker.h входит в ядро 8266 — ставить ничего не нужно. Механика: глобальный объект Ticker blinker; в setup() — blinker.attach(0.4, miganie). Первый аргумент — интервал в секундах с плавающей точкой, второй — функция без аргументов и без возвращаемого значения; с этого момента ядро зовёт miganie само, каждые 0.4 секунды, чем бы ни был занят loop. Для коротких интервалов есть attach_ms(200, otchet) — те же будильники в целых миллисекундах. Одноразовый звонок — blinker.once(10.0, upomni): через десять секунд функция выстрелит один раз и тикер погаснет.
Отписка и диагностика: blinker.detach() снимает задачу, blinker.active() отвечает, запущен ли тикер. Имена совпадают в ESP32-ядре — скетч с Ticker переезжает между платами без правок, различается только то, где именно живёт колбэк: на 8266 это контекст системного таймера, на ESP32 — отдельная задача. Тикеров можно завести сколько нужно: в моём узле их четыре, по одному на задачу, — вместо четырёх if и четырёх lastX.
Сразу оговорю границу применимости, чтобы потом не удивляться. Тикер — не секундомер и уж точно не часы: интервалы выдерживаются с точностью до очереди колбэков, и когда WiFi-стек пережёвывает пакеты, звонок мигалки может припоздниться на миллисекунды. Календарь и время суток на тикере не строят вовсе — для этого есть время из сети; тикер про другое: стабильный ритм мелких задач, которому не страшна занятость loop. Мигалка, сердцебиение, мигание статусом — его территория; всё, что требует метки реального времени, — территория часов.
#include <ESP8266WiFi.h>#include <Ticker.h> const int LED = 2; // D4, бортовой светодиод, горит при LOWconst int BTN = 14; // D5, кнопка на землю Ticker blinker; // мигалкаTicker reporter; // отчёт раз в две секунды bool reportOn = true;volatile bool doReport = false; // флаг для loop: колбэк только ставит его void miganie() { // колбэк: только digitalWrite, никакого delay digitalWrite(LED, !digitalRead(LED));} void otchet() { doReport = true; } // Serial внутри не зовём void setup() { Serial.begin(115200); pinMode(LED, OUTPUT); pinMode(BTN, INPUT_PULLUP); blinker.attach(0.4, miganie); // секунды: 0.4, не 400! reporter.attach_ms(2000, otchet); // миллисекунды Serial.println("start: blink 400ms, report 2000ms");} void loop() { if (doReport) { doReport = false; Serial.print("report t="); Serial.println(millis()); } // кнопка на D5 включает и снимает задачу-отчёт: attach_ms / detach bool btn = digitalRead(BTN) == LOW; // кнопка на землю: нажата = LOW static bool was = false; if (btn && !was) { if (reportOn) { reporter.detach(); Serial.println("report detach"); } else { reporter.attach_ms(2000, otchet); Serial.println("report attach"); } reportOn = !reportOn; } was = btn;}Почему в колбэке нельзя delay
Колбэк тикера — не вторая нить, а вызов из контекста системного таймера: ядро прерывает текущее дело, зовёт вашу функцию и возвращается. delay() внутри такого вызова ждать нечего — тики приходят от того же механизма, который сейчас занят вами: система начинает ждать саму себя. Кончается это предсказуемо: софт-сторож молчит три-четыре секунды и рестартует плату. Тот же запрет для Serial и сетевых вызовов: они пользуются прерываниями, и звать их из контекста таймера — значит сбивать собственные будильники. Симптом характерный: скетч с delay в колбэке живёт ровно до первого звонка тикера, перезагружается и повторяет круг.
Правило короткого колбэка: внутри — digitalWrite, смена флага, пара арифметических операций. Всё, что печатает, ходит в сеть или ждёт, — в loop по флагу, как в скетче выше: otchet() только ставит doReport, а печать ведёт loop. Колбэк, вылезший за десяток миллисекунд, — это не стилистика, а заявка на рестарт: сторож на 8266 двухэшелонный, софт-сторож держит около трёх секунд, аппаратный добивает к восьмой; механику сторожей я разбирал в уроке про watchdog и сон.
Отдельная история — Serial из колбэка: плату он валит не всегда, но ведёт себя как ненадёжный свидетель. На моём первом тикерном скетче строки отчёта то терялись, то шли кусками не в том порядке, и два вечера я лечил несуществующий глюк монитора, пока не догадался перенести печать в loop. С тех пор колбэк у меня меняет только память платы: флаг, счётчик, состояние светодиода. Всё, что должно вылезти наружу — в монитор, в сеть, в файл, — делает это loop, когда до него дойдёт очередь. Разница в поведении между «почти работает» и «работает всегда» лежит ровно в этом переносе.
| Способ | Блокирует loop | Такт | Когда беру |
|---|---|---|---|
| delay(1000) | да, целиком | ровный, но loop мёртв | мигалка из первого скетча, дальше — никогда |
| millis-планировщик | нет | копейки дрейфа от сравнения | две-три задачи, когда нужен полный контроль |
| Ticker.attach / attach_ms | нет | тик от системного таймера | мигалка и сердцебиение, четыре и больше задач |
| Ticker.once | нет | тот же тик | отложенное действие: напомнить о себе через 10 с |
Два тикера — не два потока: колбэки встают в очередь и исполняются по одному, каждый должен кончить, прежде чем начнёт следующий. Длинный колбэк сдвигает короткого: если мигалка зовётся каждые 0.4 с, а на второй тикер повесили функцию на 100 мс, вспышки поплывут. Поэтому мой размер задачи: всё, что дольше пары миллисекунд, переезжает из колбэка в loop. Зато с короткими колбэками тикер даёт то, чего millis-планировщик не умеет: светодиод дёргается ровно, даже когда loop на секунду утонул в переподключении WiFi — с одним ядром 8266 это единственный независимый от сети такт; у ESP32 для тяжёлых задач есть второе ядро, я показывал его в уроке про задачи на двух ядрах.
Слово «кооперативность» тут точное: задачи не мешают друг другу, пока каждая короткая, и рушат расписание, как только одна залезла в чужое время. Взрослые версии того же подхода — очереди и семафоры — я раскладывал в уроке про два API и кооперативность; для мигалки и датчика хватает двух объектов Ticker и одного флага. Сам опрос DHT22 в колбэк не лезет — чтение датчика занимает миллисекунды и десятки, его место в loop или по флагу, как в уроке про подключение DHT11.
Мой расклад интервалов сложился практикой и почти не меняется от узла к узлу. Статусный светодиод — 200-500 мс: глазу приятна, дёргается незаметно. Сердцебиение в журнал — раз в секунду или реже. Датчики — в loop по millis-расписанию, потому что их чтение слишком длинное для колбэка. Сеть — тем более в loop, с таймаутами. Интервалы короче пары миллисекунд на тикере теряют смысл: сам вызов функции через системный таймер стоит сопоставимого времени, и расписание утонет в накладных расходах. Если задачу хочется звать так часто — это уже не расписание, а прерывание, и решается другими инструментами.
Стенд: та же логика в симуляторе
Перенос из стенда на плату механический: каждый блок if с меткой времени сворачивается в объект Ticker и одну строку attach, а lastX-переменные исчезают вовсе — их теперь помнит библиотека, как и в базовом раскладе из урока про millis вместо delay. Обратный перенос полезен при отладке: когда поведение тикера подозрительно, я разворачиваю задачу назад в millis-расклад и смотрю, где теряются такты. Логика одна в обоих мирах — интервал, метка последнего звонка, тело задачи; Ticker лишь прячет первые два пункта. И последнее: тикер не заменяет планировщик, а дополняет его — точный такт у тикера, работа с сетью и датчиками у loop. Так у меня и живут все узлы: сердце мигает по тикеру, остальное — по millis.
Проверка расписания на живой плате у меня примитивная и оттого надёжная: строка с интервалом печатается в setup() — blinker 400ms, reporter 2000ms, — и первые же строки монитора сверяются с ней глазами. Секундомер на телефоне закрывает вопрос: тридцать секунд наблюдений дают семьдесят пять миганий и пятнадцать отчётов, расхождение больше одного — повод копать. Экономия в десять секунд настройки возвращается сторицей, когда задач становится пять, а не две, и глаз уже не отличает 400 от 450.
- Ticker.h встроен в ядро 8266: attach(0.4, fn) держит секунды с плавающей точкой, attach_ms — целые миллисекунды, once — одноразовый звонок, detach и active — отписка и статус
- Колбэк зовётся из контекста системного таймера: delay, Serial и сетевые вызовы внутри уводят плату в рестарт по сторожу
- Правило короткого колбэка: digitalWrite и флаги внутри, печать и сеть — в loop по флагу
- Два тикера — очередь, а не потоки: длинный колбэк сдвигает короткого, всё дольше пары миллисекунд — из колбэка в loop
- attach ест секунды: attach(400) — это шесть с лишним минут; миллисекунды живут в attach_ms
- 1. Какой вызов заставит функцию звонить каждые полсекунды?
- 2. Что произойдёт, если поставить delay(2000) внутрь колбэка тикера на 8266?
- 3. Где печать в Serial после звонка тикера по правилу короткого колбэка?
- 4. Кнопка должна включать и снимать задачу-отчёт. Какой парой команд?
- 5. Мигалка висит на тикере каждые 0.4 с, на второй тикер повесили функцию на 100 мс. Что с ритмом вспышек?
Частые вопросы по теме урока
- Какой вызов заставит функцию звонить каждые полсекунды?
- attach ест секунды с плавающей точкой: 0.5 — это полсекунды. attach(500) назначил бы будильник на 500 секунд, а once сработал бы один раз и погас. blinker.attach(0.5, fn)
- Что произойдёт, если поставить delay(2000) внутрь колбэка тикера на 8266?
- Колбэк живёт в контексте системного таймера и ждёт сам себя; сторож молчит около трёх секунд и рестартует плату. В колбэке — только короткие операции и флаги. софт-сторож не увидит тиков и рестартует плату
- Где печать в Serial после звонка тикера по правилу короткого колбэка?
- Колбэк только ставит флаг, loop его замечает и печатает. Serial пользуется прерываниями, и звать его из контекста таймера — значит сбивать собственные будильники. в loop по флагу, который выставил колбэк
- Кнопка должна включать и снимать задачу-отчёт. Какой парой команд?
- detach снимает задачу, повторный attach_ms ставит её с тем же интервалом. active() лишь отвечает, запущен ли тикер, а once — одноразовый звонок. reporter.attach_ms(2000, otchet) и reporter.detach()
- Мигалка висит на тикере каждые 0.4 с, на второй тикер повесили функцию на 100 мс. Что с ритмом вспышек?
- Колбэки встают в очередь: длинная задача сдвигает звонок мигалки. Всё, что дольше пары миллисекунд, переезжает из колбэка в loop. вспышки поплывут: колбэки исполняются по очереди