Счётчик наработки дачного насоса жил по принципу «включил — посчитал — забыл»: любое движение в щитке, моргание света или зимний демонтаж обнуляли наработку, и через сезон я честно не знал, сколько проработал агрегат. Сам счётчик при этом был безупречен — кнопка, экран, инкремент, всё как в уроке про счётчик с антидребезгом. Ломалась только память: переменная в ОЗУ умирает с последним вольтом питания, а одометр, который забывает пробег на каждой остановке, — не одометр, а украшение.
Лечение — EEPROM: килобайт энергонезависимой памяти на той же плате, который переживает и выключение, и полгода на полке. Как положить туда первый байт, я разбирал в уроке про чтение и запись первого байта; здесь практический слой — как встроить запись в живой прибор, чтобы память пережила сам прибор. Потому что у EEPROM есть жёсткий потолок: сто тысяч записей в ячейку, и наивный скетч расходует его за один день. Грубая шкала для прикидки: одна запись в секунду — сутки жизни ячейки.
План урока: счётчик циклов на LCD с ведущими нулями, число в двух ячейках EEPROM, дисциплина записи — по команде и раз в N минут по таймеру, сброс длинным удержанием три секунды и честная арифметика износа, включая ловушку «пишу каждые 500 миллисекунд». В симуляторе вся логика крутится на одной кнопке: счёт, автосохранение по таймеру, сброс удержанием.
Писать редко, писать по делу
Первое правило одометра: запись не живёт в loop. Наивный вариант — писать счётчик на каждом проходе цикла — расходует лимит мгновенно: loop на Uno крутится десятки тысяч раз в секунду, и сто тысяч записей набираются за пару секунд честной работы. Рабочая дисциплина состоит из двух пунктов: по команде — человек нажал «сохранить» или прибор завершил рабочий цикл, и по таймеру — раз в десять минут, если со счётчика что-то набежало. Таймер строится на millis без единого delay: механику планировщика я разбирал в уроке про millis вместо delay. В моём насосе счёт меняется раз в минуты, поэтому автосохранение раз в десять минут теряет не больше одного цикла даже при пропадании питания — для наработки это погрешность.
Второе правило: число шире байта — в несколько ячеек. EEPROM хранит байты от нуля до 255, а счётчик наработки легко переваливает за эту черту. Дешёвое решение — два байта: младший в ячейке ноль, старший в ячейке один, при чтении складываем младший со старшим, умноженным на 256. Такой счётчик доживает до 65535 циклов — для наработки в часах это годы, а для больших чисел берите четыре ячейки или библиотечный put, который упакует переменную целиком.
Третье правило — ведущие нули на экране. Счётчик 000742 читается прибором, а 742, перескочившее по столбцам после девяноста девяти, — черновиком: без заполнителей позиция числа плавает, и глаз перестаёт сверять показания мгновенно. Механику полей фиксированной ширины и дробей я разбирал в уроке про числа, дроби и нули; здесь достаточно целочисленного деления: шесть печатей — десятки тысяч, тысячи, сотни, десятки, единицы — каждая цифра на своём месте, нули честно стоят в разрядах.
#include <Wire.h>#include <LiquidCrystal_I2C.h>#include <EEPROM.h> LiquidCrystal_I2C lcd(0x27, 16, 2);const int btn = 2; // короткое нажатие +1, удержание 3 с — сброс long schet = 0;bool lastBtn = LOW;unsigned long nazhal = 0; // millis начала удержанияbool derzhim = false;unsigned long poslZapis = 0;bool grjazno = false; // счёт менялся — пора сохранить void zapisEeprom() { EEPROM.write(0, schet % 256); // младший байт EEPROM.write(1, (schet / 256) % 256); // старший байт grjazno = false;} void vyvod() { lcd.setCursor(0, 1); lcd.print(schet / 100000); // ведущие нули держат поле lcd.print((schet / 10000) % 10); lcd.print((schet / 1000) % 10); lcd.print((schet / 100) % 10); lcd.print((schet / 10) % 10); lcd.print(schet % 10); lcd.print(" ");} void setup() { pinMode(btn, INPUT); lcd.init(); lcd.backlight(); Serial.begin(9600); schet = (long)EEPROM.read(0) + 256L * EEPROM.read(1); lcd.setCursor(0, 0); lcd.print("NARABOTKA CIKLOV"); vyvod();} void loop() { bool b = digitalRead(btn); if (b == HIGH && lastBtn == LOW) { // фронт: старт удержания nazhal = millis(); derzhim = true; } if (b == LOW && lastBtn == HIGH) { // спад: короткое нажатие if (derzhim && millis() - nazhal < 3000) { schet = schet + 1; grjazno = true; vyvod(); } derzhim = false; } if (derzhim && b == HIGH && millis() - nazhal >= 3000) { schet = 0; // сброс только длинным удержанием zapisEeprom(); Serial.println("SBROS + zapis"); vyvod(); derzhim = false; delay(500); } if (grjazno && millis() - poslZapis > 600000UL) { zapisEeprom(); // раз в 10 минут, не в каждом loop poslZapis = millis(); Serial.println("Avtosohranenie"); } lastBtn = b; delay(20);}Для ленивых и правых способ — EEPROM.put и EEPROM.get: они работают со структурой целиком и пишут только изменившиеся байты, экономя ресурс без ваших рук. Двухбайтовое деление выше — учебное: оно показывает, куда именно уходят байты, и это знание выручает при отладке по дампу памяти. В серьёзных приборах я храню настройки структурой через put, а одометр выношу в отдельную пару ячеек, которую легко прочитать и побайтово, — анатомия структуры и выравнивание разобраны в уроке про put, get и struct.
Второй нюанс, о котором скетчи молчат: первая ячейка новой платы читается как 255, а не как ноль — EEPROM с завода заполнен единицами. Ваш одометр после первой прошивки покажет 65280 — это 255 плюс 255, умноженный на 256, — и это не поломка. Лечится проверкой метки в отдельной ячейке: если там не ваша константа, счётчик обнуляется и метка ставится один раз навсегда. Я на этом потерял полвечера, пока не заглянул в дамп памяти; тот же сюрприз ждёт всех, кто читает «пустую» EEPROM без подготовки.
Идея «сохранить при выключении питания» приходит первой и работает хуже всех. Детектор просадки даёт процессору считанные миллисекунды, а две записи по 3,3 мс плюс решения — впритык; на макетке схема с суперконденсатором и прерыванием живёт, но её надо паять, настраивать и перепроверять на каждую партию плат. Периодическая запись раз в десять минут даёт худший случай в те же десять минут потерянной наработки за всю жизнь прибора — и ни одной дополнительной детали. Я прошёл путь от детектора просадки к таймеру и обратно дважды: второй вариант экономит неделю отладки.
Сброс с удержанием и обратный отсчёт
Удержание три секунды — не перестраховка, а нормальный интерфейсный паттерн: короткое нажатие инкрементирует, длинное стирает, и два действия живут на одной кнопке без конфликтов. Сброс невозможно сделать случайно, задев кнопку рукавом на верстаке. Экран при этом обязан подсказывать: я печатаю по мере удержания обратный отсчёт — три, две, одна — и пользователь видит, что скетч понял намерение, и отпускает кнопку в нужный момент. Как разделять короткое и длинное нажатие на millis и почему порог меньше секунды опасен — отдельная история в уроке про сброс удержанием.
Есть и тонкость, которую легко пропустить: сброс выполняйте по факту удержания, а не отпускания. Если ждать отпускания, то после трёх секунд человек отпускает кнопку, скетч засчитывает ещё и короткое нажатие — и к нулевому одометру добавляется единица. В скетчах выше флаг derzhim гасится сразу после сброса, поэтому спад кнопки больше не считается нажатием. Три строки защиты, а на живом приборе именно их отсутствие рождает жалобу «сбрасываю, а счётчик показывает один».
Отдельное место занимает обратная связь записи. Счётчик, который пишет в память молча, неотличим от счётчика, который забыл про память вовсе: я печатаю в правом углу метку «EP» сразу после сохранения и стираю её при первом новом нажатии. За месяц эксплуатации на насосе я однажды увидел, что метка не появляется после автосохранения, — и поймал отказавший контакт шины до того, как он обнулил наработку. Дешевле метки только мигание подсветкой, но в тёмном цеху свет в три часа ночи раздражает всех, кроме прибора.
| Периодичность записи | Записей в сутки | Живёт ячейка | Вердикт |
|---|---|---|---|
| Каждый проход loop | Миллионы | Секунды | Никогда |
| Каждые 500 мс | 172 800 | Меньше суток | Запрещено |
| Раз в 10 минут при изменении | До 144 | Около двух лет | Приемлемо |
| По команде и при выключении | Десятки | Десятилетия | Так и делаем |
Моё спорное мнение: одометр без кнопки сброса бесполезен, а с кнопкой мгновенного сброса — опасен. Третий вариант, который я делаю сейчас, — сброс с удержанием плюс копия: раз в сутки счётчик дописывает значение в дальнюю ячейку, и даже после случайного стирания вчерашняя наработка читается мультиметром... то есть сканером памяти. Для самодельного прибора это перестраховка, для коммерческого — страховка от спора с заказчиком о том, сколько на самом деле сделано. Путь от счётчика к кольцевому журналу событий разобран в уроке про журнал в EEPROM.
Практическая деталь, которую я недооценивал: запись в EEPROM занимает до 3,3 миллисекунды на байт, и в это время процессор ждёт. Для одометра это невидимо, но если на том же таймере у вас живёт сервопривод или опрос датчика, ставьте запись в тихий момент цикла, а не посреди движения. Две записи по 3,3 мс раз в десять минут никто не заметит; те же записи в каждом проходе loop заметит уже сервопривод — дёрганьем, которое вы полдня будете искать в механике.
- 1. Почему счётчик нельзя писать в EEPROM на каждом проходе loop?
- 2. Счётчик 50000 разложен по двум ячейкам: 50000 = 195*256 + 80. Что и где лежит?
- 3. Запись каждые 500 мс — когда ячейка выйдет из ресурса?
- 4. Что сделает update() при записи того же значения?
- 5. Одометр после первой прошивки показывает 65280. Виноват…
- Запись — по команде и по таймеру раз в N минут; на каждом проходе loop сто тысяч циклов сгорают за секунды
- Число шире 255 — в две ячейки: младший байт и старший; при чтении младший плюс 256 на старший
- Каждые 500 мс — ячейка за пределами ресурса за сутки; раз в 10 минут — около двух лет жизни
- Сброс — только удержанием 3 с с отсчётом на экране; мгновенный сброс задевается рукавом
- Пустая ячейка читается как 255: метка при первом запуске спасает одометр от загадочных 65280
Частые вопросы по теме урока
- Почему счётчик нельзя писать в EEPROM на каждом проходе loop?
- Loop крутится десятки тысяч раз в секунду, а ресурс ячейки — сто тысяч циклов: наивная запись в loop выжигает ячейку мгновенно. Сто тысяч записей в ячейку исчерпаются за секунды
- Счётчик 50000 разложен по двум ячейкам: 50000 = 195*256 + 80. Что и где лежит?
- Младший байт (остаток деления на 256) — в нулевой ячейке, старший (частное) — в следующей. При чтении: 80 + 195*256 = 50000. 80 в ячейке 0, 195 в ячейке 1
- Запись каждые 500 мс — когда ячейка выйдет из ресурса?
- 172800 записей в сутки при ресурсе 100000: к концу первого дня ячейка за пределом. Рабочая периодичность — минуты, а не доли секунды. Меньше чем за сутки
- Что сделает update() при записи того же значения?
- update сравнивает значение с лежащим в ячейке и пишет только при различии. put() использует ту же механику внутри. Не тронет ячейку — ресурс не тратится
- Одометр после первой прошивки показывает 65280. Виноват…
- Пустая EEPROM читается как 255 в каждой ячейке: 255 + 255*256 = 65280. Ставьте магическую метку и обнуляйте счётчик при первом запуске. Заводской 0xFF в пустых ячейках: нужна метка и первичное обнуление