Шлагбаум на въезде в наш паркинг раз в несколько дней «выключался»: створка замирала на полпути, лампочки гасли, через секунду прибор стартовал заново и работал как ни в чём не бывало. Пользователи клялись, что он сам перезагружается; я приезжал — работало, и мои уверения про «не воспроизводится» никого не грели. Serial-лог прикрутить я мог, но лог живёт в оперативке и умирает вместе с питанием в момент повисания — на месте падения записей бы не осталось.
Тупик разрешился числом. В setup() я добавил три строки: прочитать счётчик из EEPROM, прибавить единицу, записать обратно. Через неделю на объекте счётчик показал четырнадцать. Четырнадцать рестартов за семь дней при жалобах «раз в несколько дней» — значит, повисает по два раза в сутки, и каждый раз мимо чьих-то глаз. Дальше дело техники: клемма на блоке питания оказалась протянута, при скачке тока плата мигала питанием. Подтянул клемму — счётчик за месяц не сдвинулся ни на единицу.
Счётчик перезагрузок — самая дешёвая телеметрия, какая бывает: два байта EEPROM и три строки в setup(). В уроке про счётчик включений мы уже считали включения и разобрали двухбайтовую арифметику — пересказывать её не буду. Здесь практика: где стоит инкремент, чем update отличается от write на этой задаче, что делать на потолке 65535 и как обнулять счётчик по-человечески.
Три строки в setup()
Инкремент живёт строго в setup(): эта функция выполняется один раз за включение, и одна запись на включение — ровно то, что нужно. Перенесёте инкремент в loop() — счётчик тикает тысячи раз в секунду, и ячейка вырабатывает свои 100000 циклов за утро. Это первое и самое частое место, где я ломал счётчик сам, прежде чем вбить правило в голову: вся арифметика — при старте, ни строки — в цикле.
Второй вопрос, который задает каждый: update или write? При честном инкременте значение каждый раз новое, и update перед записью делает лишнее чтение, которое write пропустил бы. Но привычка ставить update ничего не стоит, а спасает в соседних сценариях: обнуление, когда кнопка сброса жмётся дважды, и повторная запись после сбоя — одинаковые значения update пропускает бесплатно. Семантику с числами мы разбирали в уроке про update.
Теперь ресурс с числами, при которых они верны. Десять рестартов в день — это 3650 записей в год, и 100000 циклов ячейки кончаются через двадцать семь лет. Сто рестартов в сутки на проблемном объекте — всё ещё почти три года. Запись раз на включение — единственный режим, где EEPROM почти не стареет, и это делает счётчик рестартов идеальной первой задачей для энергонезависимой памяти в любом приборе.
Отдельно замечу, чем рестарты отличаются от соседней задачи со счётчиком включений. Включение пользователь замечает и помнит; рестарт при повисании проходит бесшумно — прибор просто мгновенно вернулся к работе. Поэтому включения считаются ради любопытства, а рестарты — ради диагноза, и их число чувствительнее к промахам в коде: лишняя единица при каждом старте за неделю исказит картину вдвое. Считайте именно рестарты, а не включения, если проблема формулируется словом «сам перезагружается» — иначе вы измерите соседнюю величину.
#include <EEPROM.h> // Карта: 0 - старший байт счётчика, 1 - младший; соседи - после адреса 1const int addrHi = 0;const int addrLo = 1;const int knopka = 2; void setup() { Serial.begin(9600); pinMode(knopka, INPUT_PULLUP); int hi = EEPROM.read(addrHi); int lo = EEPROM.read(addrLo); long cnt = 0; if (!(hi == 255 && lo == 255)) { // пара 255-255 - пустая память cnt = hi; // сборка в long: 255*256 в int не влезает cnt = cnt * 256 + lo; } if (digitalRead(knopka) == LOW) { // кнопка зажата при старте - сброс cnt = 0; EEPROM.update(addrHi, 0); EEPROM.update(addrLo, 0); Serial.print("schetchik obnulen"); Serial.println(""); } else if (cnt < 65535) { cnt = cnt + 1; // read, плюс один, update EEPROM.update(addrHi, cnt / 256); EEPROM.update(addrLo, cnt % 256); } else { Serial.print("predel 65535"); Serial.println(""); } Serial.print("cnt="); Serial.println(cnt);} void loop() { delay(100);}Потолок uint16 и жизнь ячейки
Два байта дают 0–65535, и на повисаниях это много: при двух рестартах в сутки счётчик наполнится за девяносто лет. Но границы честнее считать по худшему объекту: при отваливающейся клемме рестарты идут по два в час, и потолок наступает через 3,7 года. Что делать на потолке — решение дизайна. Я зажимаю счётчик: инкремент только если число меньше 65535, иначе оно замирает на максимуме. Факт «рестартов было много» сохраняется, а зажатый максимум читается как красный флаг.
Третий байт растянул бы потолок до шестнадцати с лишним миллионов, но я его не беру: трёхбайтовое число выбивается из привычной пары high-low, ломает разбор в голове и экономит проблему, которой у меня не случалось ни разу — счётчик всегда снимали и обнуляли раньше потолка. Если ваши интервалы правда упираются в шестьдесят пять тысяч, сначала подумайте, не диагностика ли это отказа, а не памяти.
Альтернатива — кольцо: инкремент без проверки, после 65535 счётчик переехал бы через ноль и пошёл заново. Для диагностики это худший вариант: переехавший счётчик неотличим от нового, и диагноз «устройство чистое» встанет ровно на момент максимального износа. Есть и техническая ловушка: 65535 в двух байтах — это 255 и 255, а пара 255-255 у нас означает пустую память из урока про счётчик включений. Полный счётчик и свежая плата в такой паре байтов выглядят одинаково — ещё один довод не доводить до переезда.
Зажатый максимум — заодно маркер для техобслуживания: увидели 65535 — снимите диагноз, обнулите, зарядите счётчик заново. Печатайте число при каждом старте в Serial: строки в терминале ложатся в журнал испытаний сами собой, и на объекте достаточно поднести ноутбук на минуту. Обнуление — тоже запись, две ячейки, и по меркам раз в месяц это ноль износа на фоне ресурса. Осталось сделать сброс удобным — об этом ниже.
| Стратегия счётчика | Код инкремента | Чем оборачивается |
|---|---|---|
| Зажим на потолке | if (cnt < 65535) cnt = cnt + 1 | Останавливается на 65535: факт максимума виден всегда |
| Кольцо без проверки | cnt = cnt + 1 при каждом старте | После 65535 придёт единица: похоже на новую плату |
| Обнуление после диагноза | записать 0 в оба байта | Счётчик начал заново, износ — два цикла в месяц |
Обнуление и где это нужно
Обнулять через перепрошивку — варварство: загрузчик Arduino IDE EEPROM не трогает, и счётчик честно переживёт сотню прошивок, если вы не зовёте программатор с chip erase. Мой стандарт — кнопка при старте: зажали кнопку, подали питание, скетч в setup увидел прижатый пин и записал в обе ячейки нули. В приборах с Serial удобнее команда: парсер из урока про командный парсер съедает строку сброса и делает то же самое без вскрытия корпуса. В симуляторе ниже тумблер играет роль зажатой при старте кнопки.
Где счётчик рестартов окупается, по моему списку. Диагностика повисаний — главный сценарий из истории про шлагбаум: число есть, спорить не о чем. Контроль качества питания на объекте: рестарты сыплются вместе со скачками, и график счётчика — аргумент в разговоре с электриками. Мониторинг watchdog, который перезагружает зависший код: связка со сном и таймерами из продвинутого раздела показывает, сколько раз сторожевой таймер реально спасал, а не кажется. И совсем бытовое — учёт циклов между техобслуживанием, когда рестарт это часть регламента.
И связка с ящиком настроек из первых уроков: счётчик живёт по соседству с Config — по карте памяти это addrBox плюс sizeof, свои два байта без пересечений. Если быстрый рост счётчика совпал с пропажей настроек, питание просаживается ниже порога записи, и это уже разговор про конденсаторы из урока про фильтр питания. У меня так и было на шлагбауме: настройки слетали раз в месяц, счётчик показал двадцать рестартов — и две проблемы срослись в одну клемму. Одно число в EEPROM умеет рассказать и про софт, и про электрику — за это я и люблю его больше любого лога.
- Счётчик перезагрузок: в setup() прочитать, прибавить единицу, записать через update — одна запись на включение
- Инкремент в loop() сжигает ячейку за утро: всё, что пишется чаще раза в минуту, переносите на событие
- При десяти рестартах в день ячейка живёт 27 лет; update спасает от лишних записей при сбросах и повторах
- uint16 потолок 65535: зажимайте счётчик на пределе, иначе он переедет через ноль и будет выглядеть свежим
- Обнуление — две записи нулей: зажатая кнопка при старте или команда по Serial после снятия диагноза
- 1. Почему инкремент счётчика перезагрузок живёт в setup(), а не в loop()?
- 2. Счётчик дошёл до 65535 и прибавил ещё без проверки потолка. Что окажется в двух байтах?
- 3. Пять перезагрузок в день при одной записи за включение. Когда ячейка выработает 100000 циклов?
- 4. Зачем в счётчике update, если значение меняется при каждом старте и write дал бы тот же результат?
- 5. Счётчик смотрят раз в месяц на объекте. Как удобнее обнулять его после снятия диагноза?
Частые вопросы по теме урока
- Почему инкремент счётчика перезагрузок живёт в setup(), а не в loop()?
- Одно включение — одна запись. В loop счётчик бы делал тысячи записей в секунду и выжег 100000 циклов ячейки за утро. setup выполняется один раз за включение: одна запись на рестарт; в loop счётчик тикал бы тысячи раз в секунду
- Счётчик дошёл до 65535 и прибавил ещё без проверки потолка. Что окажется в двух байтах?
- 65536 по модулю 65536 — это ноль: в байтах окажутся нули, и счётчик переехал бы через ноль, выглядя как свежая плата. 0 и 0: старшие разряды выпали при переносе
- Пять перезагрузок в день при одной записи за включение. Когда ячейка выработает 100000 циклов?
- 3650 записей в год делят 100000 циклов примерно на 27 лет. Порядок чисел держите в голове до запуска, а не после похорон ячейки. Около 27 лет
- Зачем в счётчике update, если значение меняется при каждом старте и write дал бы тот же результат?
- Сам инкремент всегда пишет новое значение, но вокруг счётчика живут сброс и повторы — там update экономит реальные циклы записи. Update пропускает запись одинаковых значений: обнуление и повторные сохранения не жгут ресурс зря
- Счётчик смотрят раз в месяц на объекте. Как удобнее обнулять его после снятия диагноза?
- Загрузчик EEPROM не стирает, а питание ячейки не сбрасывает. Две записи нулей по кнопке или команде — дешёвый и честный способ. Зажатой кнопкой при старте или командой по Serial: записать нули в оба байта