Дачный счётчик воды писал каждое включение крана в кольцевой журнал: тридцать два слота, старое затирается само. В воскресенье открыл историю за неделю — и не понял, что читаю: кольцо давно замкнулось, порядок выстраивался по head из урока про кольцевой буфер, но две сработки защиты по питанию за ночь посчитали head заново — и события в отчёте перемешались. Слоты целы, секунды почти совпадают, а последовательность неверная. Разобраться захотелось не из любопытства: по порядку событий видно, что шло раньше — скачок тока или открытие крана.
Спорить с секундами бесполезно — надёжнее спросить о порядке саму запись. Я добавил в слот маркер: порядковый номер события, который растёт с каждой записью и лежит прямо в EEPROM рядом с кодом и временем. Сортировка слотов по маркеру собирает хронологию независимо ни от замыкания кольца, ни от перезагрузки платы.
В этом уроке: сколько байт стоит маркер, как он ведёт себя на завороте через 254 записи, почему одна арифметика по модулю 256 заменяет и хранение head, и часы реального времени — и где проходит граница метода. К концу урока у вас будет скетч, который печатает историю в правильном порядке после любого перерыва в питании.
Маркер, который переживает перезагрузку
Маркер — один байт в начале слота. Номер стартует с единицы и растёт до 254; значения 255 и 0 не используются: 255 означает пустой слот, как на свежей плате, а ноль на бумаге неотличим от отсутствия записи. Записали 254-е событие — счётчик возвращается к единице, дальше всё повторяется. Слот вырос с четырёх байт до пяти: маркер, код события, три байта секунд. Счётчик стартует с единицы при первом включении — событие номер один существует всегда.
Заворот — слабое место, и о нём честно. Через 253 записи номера повторятся: слот с единицей может оказаться и минутной давности, и двухнедельной. Спасает арифметика по модулю 256. Возраст записи — это (lastSeq - seq + 256) % 256, где lastSeq — маркер самой свежей записи, он живёт в RAM. Чем больше возраст, тем старее событие; сортировка слотов по возрасту от большого к нулю собирает хронологию от старых к новым. Один байт различает 253 последние записи, а журнал на 32-64 слота замыкается в четыре-восемь раз чаще, чем заворачивается маркер, — запас кратный, за четыре года эксплуатации он ни разу не кончился.
Если события идут чаще двухсот в день, берите маркер в два байта: 65535 записей до заворота, при пятидесяти событиях в сутки это почти четыре года. Цена — плюс байт на слот и распил числа на старший и младший при записи; механика та же, что при хранении int в уроке про int и float. Запас проверяется заранее по методике урока про расчёт износа: частота событий известна, слоты фиксированы.
#include <EEPROM.h> const int SLOTS = 32;const int SLOTB = 5; // seq, kod, три байта секундconst int BASE = 128; // журнал со 128: карта из урока про карту int head = 0; // следующий слот для записиint lastSeq = 0; // номер последней записи - живёт в RAM void logEvent(int kod) { int base = BASE + head * SLOTB; lastSeq++; if (lastSeq > 254) lastSeq = 1; // заворот маркера long t = millis() / 1000; EEPROM.update(base, lastSeq); EEPROM.update(base + 1, kod); EEPROM.update(base + 2, t / 65536); EEPROM.update(base + 3, (t / 256) % 256); EEPROM.update(base + 4, t % 256); head = (head + 1) % SLOTS;} void printHistory() { int order[SLOTS]; for (int i = 0; i < SLOTS; i++) order[i] = i; // сортировка по возрасту маркера: старые первыми for (int i = 0; i < SLOTS - 1; i++) { for (int j = 0; j < SLOTS - 1 - i; j++) { int ageA = (lastSeq - EEPROM.read(BASE + order[j] * SLOTB) + 256) % 256; int ageB = (lastSeq - EEPROM.read(BASE + order[j + 1] * SLOTB) + 256) % 256; if (ageB > ageA) { int tmp = order[j]; order[j] = order[j + 1]; order[j + 1] = tmp; } } } for (int i = 0; i < SLOTS; i++) { int b = BASE + order[i] * SLOTB; if (EEPROM.read(b) == 255) continue; // пустой слот - пропускаем long t = (long)EEPROM.read(b + 2) * 65536 + (long)EEPROM.read(b + 3) * 256 + EEPROM.read(b + 4); Serial.print("seq="); Serial.print(EEPROM.read(b)); Serial.print(" kod="); Serial.print(EEPROM.read(b + 1)); Serial.print(" t="); Serial.println(t); }} void setup() { Serial.begin(9600); pinMode(2, INPUT_PULLUP); head = 0; while (head < SLOTS && EEPROM.read(BASE + head * SLOTB) != 255) head++; if (head == SLOTS) head = 0; // кольцо полное for (int i = 0; i < SLOTS; i++) { // самый большой маркер - последний int s = EEPROM.read(BASE + i * SLOTB); if (s != 255 && s > lastSeq) lastSeq = s; } printHistory();} void loop() { if (digitalRead(2) == LOW) { // кнопка события logEvent(1); delay(400); }}Читаем в хронологическом порядке
Секундный байт внутри слота порядок не спасает: он заворачивается каждые 256 секунд, доверять ему можно только внутри четырёх минут. Поэтому маркер главный, а время — атрибут для человека, который потом читает отчёт: полные секунды собираются из трёх байтов, как в уроке про вывод истории, где порядок держится на head. Тот способ работает, пока кольцо не замкнулось и head посчитан верно; сортировка по маркеру покрывает и этот случай. RTC-модуль стоил бы копейки, но требует шины, адреса и батарейки; журнал с маркером обходится без всего этого, и на даче, где батарейку меняют раз в три года, это решение, а не экономия.
Граница метода: после перезагрузки lastSeq в RAM пуст, и скетч восстанавливает его как самый большой маркер среди слотов. Пока маркер не завернулся, это верно всегда. Если журнал пережил заворот и плату выключили на полгода, самый большой номер может принадлежать не последней записи — тогда перепутаются максимум пара соседних по возрасту событий, и время в слотах подскажет, кто из них первым. Я считаю такую цену честной: два байта памяти против часов реального времени и батарейки. Проверка на бумаге занимает минуту: слоты с номерами 252, 253, 1, 2 при lastSeq 2 дают возрасты 2, 1, 253, 252 — хронология собирается правильно.
В модели следите за строками zapis: слоты занимаются по кругу, маркер растёт, а после восьмой записи печатается отчёт. Проверьте глазами: в хронологии адреса слотов идут не по порядку — 5, 6, 7, 0, 1 — потому что сортировка держится на возрасте маркера, а не на расположении ячеек. Возраст в каждой строке — та самая формула (lastSeq - seq + 256) % 256, от большого к нулю. Кнопкой D2 вгоните событие между автозаписями: маркеры выстроят его ровно туда, где оно стояло по времени.
На реальной плате то же делает скетч из урока: logEvent инкрементирует lastSeq и кладёт слот через EEPROM.update, printHistory сортирует индексы пузырьком и печатает отчёт. Пустые слоты в отчёте пропускаются по сравнению с 255 — признак свежей памяти из урока про первый запуск. Проверьте себя: если порядок в вашем скетче зависит от пустого слота, найдите в мониторе момент, когда кольцо полное, — с маркером такого слепого пятна нет.
| Способ упорядочить журнал | Когда работает | Когда ломается | Цена |
|---|---|---|---|
| head по первому пустому слоту | пока кольцо не замкнулось | после замыкания и после перезагрузки в полном кольце | 0 байт, уже есть в кольцевом уроке |
| Маркер seq в слоте | всегда, включая полное кольцо и перезагрузку | после заворота номеров — лечится d-арифметикой | 1 байт на слот |
| Полная метка времени | всегда, вместе с датами | нужны RTC и рабочая батарейка | 4-7 байт на слот |
| Маркер в два байта | 2 байта на слот | как seq, но без заворота на 253-й записи | минус один байт на слот против однобайтового |
Отчёт целиком нужен не всегда: после года эксплуатации печатать тридцать два слота в каждый просмотр утомительно. Сортировка по возрасту отдаёт выборку бесплатно: последние N событий — это N первых строк отсортированного списка. Я печатаю восемь последних, а полную историю прошу явно второй кнопкой — спор о вкусе, но монитор порта после года работы благодарит.
Второе мелкое улучшение — маркер старта: в setup скетч пишет событие с кодом 3, включение питания. Без него хронология честная, но не видно, где прошлая сессия кончилась и новая началась; с ним отчёт делится на отрезки по перезагрузкам, и поиск причины сбоя сжимается до одного отрезка. Сам счётчик включений живёт отдельной ячейкой — механика в уроке про счётчик включений, журнал только помечает границу.
- 1. Что кладут в слот как маркер последней записи?
- 2. Через сколько записей однобайтовый маркер возвращается к единице?
- 3. Как считается возраст записи при сортировке?
- 4. Почему порядок не восстанавливают по секундному байту слота?
- 5. Кольцо полное, плата перезагрузилась, lastSeq в RAM потерян. Что делает скетч?
- Маркер — байт номера записи в слоте: единица до 254, 255 оставлен пустоте, ноль не используется
- Возраст записи: (lastSeq - seq + 256) % 256; сортировка по возрасту собирает хронологию
- Один байт различает 253 записи; два байта — 65535, при 50 событиях в день это почти четыре года
- Слот вырос до пяти байт: маркер, код, три байта секунд; время — атрибут, а не порядок
- После перезагрузки lastSeq берётся как самый большой маркер; при завороте и долгом простое порядок уточняют по времени
Частые вопросы по теме урока
- Что кладут в слот как маркер последней записи?
- Номер растёт с каждой записью и лежит в самой записи. По разности номеров слоты сортируются в хронологию независимо от их адресов. Порядковый номер события
- Через сколько записей однобайтовый маркер возвращается к единице?
- Рабочий диапазон 1-254: значения 255 и 0 оставлены пустоте и нулю. После 254-й записи счётчик стартует с единицы. Различимых значений 253 — отсюда число в формуле оборотов. Через 253 записи
- Как считается возраст записи при сортировке?
- Модуль 256 делает разность корректной на завороте: старая запись с маленьким номером получает большой возраст, а не отрицательное число. (lastSeq - seq + 256) % 256
- Почему порядок не восстанавливают по секундному байту слота?
- Один байт вмещает 256 значений, каждые четыре с небольшим минуты счёт идёт заново. Время — атрибут для человека, порядок держит маркер. Три байта секунд решают задачу читающего, а не задачу сортировки. Он заворачивается каждые 256 секунд
- Кольцо полное, плата перезагрузилась, lastSeq в RAM потерян. Что делает скетч?
- Пока маркер не завернулся, самая свежая запись и есть самый большой номер. Граничный случай — заворот плюс долгий простой: порядок уточняется по времени в слотах. Слепое пятно метода — только заворот и долгий простой одновременно. Восстанавливает lastSeq как самый большой маркер среди слотов