Гроза в июле вырубила дачный контроллер на полчаса. Плата перезагрузилась, журнал при этом уцелел — и я открыл монитор порта, чтобы понять, что происходило до сбоя. Увидел столбик байтов: 1 12 0 37, 2 12 0 40, 1 13 0 5. Минут двадцать я переводил их на бумажке: единица — кнопка, двенадцать ноль тридцать семь — время... К вечеру до меня дошло, что расшифровку собственного журнала я делаю руками, хотя плате это удобно делать за меня — по одному нажатию кнопки.
Тупик был в привычке считать EEPROM складом, а представление данных — моей личной заботой. Байты-то лежали правильно: код события и три байта секунд из урока про кольцевой журнал. Не хватало функции, которая обходит буфер в правильном порядке, отличает пустые слоты от заполненных и печатает отчёт человеческим языком. Это чистая арифметика — и она окупается с первого же сбоя.
В этом уроке научим плату трём вещам: обходить журнал от самых старых событий к новым через всё тот же head, превращать коды в тексты, а секунды — в чч:мм:сс, и печатать всё это по кнопке, а не в каждом проходе loop. В симуляторе EEPROM по-прежнему не эмулируется — модель памяти массив mem, журнал наполняется сам и по кнопке D2 выдаёт историю; пустые слоты в отчёте видны прямо глазами.
От старых к новым: обход по модулю
Порядок обхода — единственное место, где новички спотыкаются. head указывает на следующий слот для записи, значит при полном буфере самый старый живой слот — это сам head: его вот-вот перезапишут. Отсюда формула: slot = (head + i) % SLOTS, где i бежит от 0 до SLOTS-1 — начинаем с самого старого и идём по кругу к свежему. Деление по модулю здесь делает ту же работу, что в счётчике включений: остаток всегда остаётся внутри блока, как бы i ни вырос. Если сомневаетесь в порядке — нарисуйте восемь клеток на бумажке, поставьте стрелку head и пройдите формулу вручную: два круга по бумаге стоят меньше, чем один испорченный отчёт в продакшене.
Пустые и заполненные слоты различаются по коду события: 255 — слот ещё никто не занимал. На свежем журнале половина обхода придётся на пустоту: от head до конца блока лежат 255, а события кучкуются с нулевого адреса. Пустые слоты я печатаю прочерком, а не молча пропускаю: сразу видно, сколько журнала осталось свободным и что кольцо ещё не замыкалось. Единственная оговорка — коды событий начинайте с 1 и никогда не используйте 255 как рабочий код, иначе пустота и событие смешаются.
Вот как выглядит первая печать на живом журнале: строка метки перезапуска, затем события подряд, а дальше — колонка прочерков до конца блока. Это хорошая новость, а не мусор: прочерки показывают, что кольцо ещё не замыкалось и ни одно событие не потеряно. После замыкания прочерки исчезают, и в отчёте остаётся только окно последних тридцати двух событий — ровно то, ради чего кольцо строилось. А вот если прочерки появились там, где вчера была сплошная история, проверьте питание и адрес BASE: кто-то пишет мимо блока или поверх чужих ячеек.
Формат отчёта собирается из трёх частей: номер слота, код словами и время. Коды — ваша собственная таблица: 1 — кнопка, 2 — тумблер, 3 — перезапуск после сбоя питания, которую плата пишет первой строкой в setup. Секунды обратно в часы-минуты-секунды разбираются той же арифметикой, что и многобайтовые числа в уроке про чтение и запись первого байта: часы — t / 3600, минуты — (t / 60) % 60, секунды — t % 60. Три деления — и столбик байтов превращается в столбик понятных строк.
#include <EEPROM.h> const int SLOTS = 32;const int SLOTB = 4;const int BASE = 200; int head = 0;int prevPrint = HIGH;int prevBtn = HIGH; int findHead() { for (int i = 0; i < SLOTS; i++) { if (EEPROM.read(BASE + i * SLOTB) == 255) { return i; } } return 0;} void logEvent(int code) { int base = BASE + head * SLOTB; long t = millis() / 1000; EEPROM.update(base, code); EEPROM.update(base + 1, t / 65536); EEPROM.update(base + 2, (t / 256) % 256); EEPROM.update(base + 3, t % 256); head = head + 1; if (head >= SLOTS) { head = 0; }} void printCode(int code) { if (code == 1) { Serial.print("knopka"); } else if (code == 2) { Serial.print("tumbler"); } else if (code == 3) { Serial.print("pitanie"); } else { Serial.print("kod="); // незнакомый код печатаем сырым числом Serial.print(code); }} void printJournal() { Serial.println("=== zhurnal: ot staryh k novym ==="); int busy = 0; for (int i = 0; i < SLOTS; i++) { int slot = (head + i) % SLOTS; int base = BASE + slot * SLOTB; int code = EEPROM.read(base); Serial.print("slot="); Serial.print(slot); if (code == 255) { Serial.println(" pusto"); } else { busy = busy + 1; long t = EEPROM.read(base + 1); t = t * 65536 + EEPROM.read(base + 2) * 256 + EEPROM.read(base + 3); Serial.print(" "); printCode(code); Serial.print(" t="); Serial.print(t / 3600); Serial.print(":"); int m = (t / 60) % 60; if (m < 10) { Serial.print("0"); } Serial.print(m); Serial.print(":"); int s = t % 60; if (s < 10) { Serial.print("0"); } Serial.println(s); } } Serial.print("zanyato slotov: "); Serial.println(busy);} void setup() { Serial.begin(9600); pinMode(2, INPUT_PULLUP); pinMode(3, INPUT_PULLUP); logEvent(3); // метка перезапуска: восстановление после сбоя head = findHead();} void loop() { int b = digitalRead(2); if (prevPrint == HIGH && b == LOW) { printJournal(); } prevPrint = b; int tg = digitalRead(3); if (prevBtn == HIGH && tg == LOW) { logEvent(2); } prevBtn = tg; delay(30);}В printJournal три детали, которые отличают отчёт от свалки строк. Ведущие нули: без проверки m < 10 вы получите 12:7:3, и глаза будут спотыкаться на каждой строке; лишний ноль превращает столбец времени в ровную колонку, которую сканируешь за секунду. Счётчик busy в конце отвечает на вопрос, который задаёшь себе всегда: журнал полный или ещё есть запас. Незнакомый код печатается сырым числом — журнал старше вашей фантазии, и после правки кодов в новой прошивке старые записи читать придётся как есть.
Печатать по кнопке, а не из loop, — не прихоть. Строка отчёта занимает около 40 символов, 32 строки — примерно 1300 символов; на 9600 бод каждый символ стоит около миллисекунды, как мы считали ещё в уроке про первый разговор с монитором порта. Весь отчёт — секунда с четвертью передачи. На 115200 бод тот же отчёт — около 130 миллисекунд, но монитор по умолчанию у многих стоит на 9600: считайте для своей скорости. Печатать отчёт каждые 100 мс — значит занять порт навсегда и подвесить loop на постоянные передачи. Кнопка превращает отчёт в запрос: нажали — получили — работаем дальше.
Кнопка печати живёт по тем же правилам, что любая кнопка: читаем digitalRead, ловим фронт и держим паузу на дребезг — обе темы разобраны в уроке про чтение кнопки и уроке про антидребезг. Без фронта одно нажатие напечатает журнал десяток раз подряд — проверено на себе: я целую неделю любовался двойными и тройными отчётами, пока не перенёс проверку предыдущего состояния в переменную.
| Код события | Текст в отчёте | Что логирует |
|---|---|---|
| 1 | knopka | нажатие сервисной кнопки, проход детали |
| 2 | tumbler | переключение режима работы |
| 3 | pitanie | пишется первой строкой в setup — перезапуск после сбоя |
| 255 | pusto | слот ещё не занимался, кольцо не замыкалось |
Кнопка печати истории: что ещё вывести
Минимальный отчёт — слоты, коды, время. Я добавляю к нему ещё две строки, которые экономят вечера при отладке. Первая — адрес блока BASE и размер: когда журнал соседствует с настройками на той же EEPROM, печатный адрес сразу показывает, кто на кого наехал, если карта памяти поехала. Вторая — количество занятых слотов: по нему видно, сколько жизни у кольца осталось до первого затирания. В релизной прошивке адреса можно убрать, но счётчик заполненности я оставляю всегда — это одна строка, а пользы на весь дачный сезон.
И границы метода. Кольцевой журнал в EEPROM — это чёрный ящик последних минут и часов: идеально для «что случилось перед сбоем» и «как часто срабатывает датчик». Для истории за недели и месяцы он не предназначен ни по объёму, ни по ресурсу — там начинается территория SD-карты с файлами и датами, ссылка на которую уже лежала в заметке выше. Между ними полезная промежуточная схема: EEPROM хранит последние события, а при каждом старте плата сбрасывает их в конец файла на карте — журнал переживает и сбой, и переполнение.
И ещё одна привычка, которая не раз спасала мои журналы: коды событий — это контракт между прошивками. Прошивка версии два, переопределившая код 2 с тумблера на дверь, превращает старые записи во враньё. Незнакомый код печатайте числом, а при смене смысла заводите новый номер, а не переиспользуйте старый. В ящике настроек для страховки есть поле version — журнал можно датировать тем же способом: номер прошивки в шапке отчёта стоит одну строку и решает споры о том, что означали записи прошлого месяца.
- Обход журнала: slot = (head + i) % SLOTS при i от 0 до SLOTS-1 — от самых старых событий к новым
- Пустой слот — код 255: печатайте прочерком, он показывает свободный запас кольца; рабочие коды начинайте с 1
- Секунды обратно во время: t / 3600, (t / 60) % 60, t % 60; ведущие нули делают отчёт сканируемым
- Печать — по кнопке с фронтом и антидребезгом: отчёт из 32 строк идёт около секунды на 9600 бод
- millis переполняется через 49,7 суток: порядок отчёта держит head, а не значение времени
- Незнакомый код печатайте сырым числом: старые записи важнее догадок новой прошивки
- 1. С какого слота начинать печать при заполненном буфере?
- 2. Как из общего числа секунд достать минуты?
- 3. Что печатать в слоте, где код события 255?
- 4. Сколько времени займёт печать 32 строк по 40 символов на скорости 9600 бод?
- 5. millis переполнился, секунды пошли заново. Что теперь с порядком в отчёте?
Кодам в журнале хорошо — по байту на событие и ни байтом больше. Но однажды в EEPROM просятся буквы: имя устройства, которое сервер опознаёт при подключении, название модели, серийник. Записать String напрямую не выйдет — внутри строки прячется указатель, а не текст. Следующий урок собирает правильную схему: байт длины плюс до тридцати байтов символов, с проверкой при чтении.
Частые вопросы по теме урока
- С какого слота начинать печать при заполненном буфере?
- head показывает следующий слот для записи, значит при полном кольце это самый старый из занятых. Отсюда и старт отчёта: slot = (head + i) % SLOTS. С head: он указывает на самый старый живой слот
- Как из общего числа секунд достать минуты?
- t / 60 отбрасывает секунды, а остаток от деления на 60 снимает полные часы. Так же собираются часы t / 3600 и секунды t % 60. (t / 60) % 60
- Что печатать в слоте, где код события 255?
- 255 — признак пустоты из свежей EEPROM. Прочерк честно показывает свободный запас, а лишнее стирание только тратит ресурс ячеек. Прочерк — слот пустой, ещё не занимался
- Сколько времени займёт печать 32 строк по 40 символов на скорости 9600 бод?
- На 9600 бод символ занимает около миллисекунды: 1300 символов — примерно 1,3 секунды передачи. Поэтому печать по кнопке, а не в каждом проходе loop. Около 1,3 секунды
- millis переполнился, секунды пошли заново. Что теперь с порядком в отчёте?
- Обход идёт по индексу кольца, время — просто атрибут строки. Числа после переполнения относятся к новому сеансу, но порядок слотов не трогают. Ничего не изменится: порядок ведёт head, а не значение времени