Заказчик насосной автоматики попросил дублировать моточасы на дисплей каждые пять минут: «чтобы отчёт был свежий». Расчёт занял минуту: три записи в час — это 72 в сутки и 26280 в год, гарантия ячейки 100000 циклов кончается меньше чем через четыре года. Я отказал: счёт уехал в оперативную память с готовностью потерять хвост при сбое, а в EEPROM по-прежнему пишется итог раз в час. Заказчик спорил, пока не увидел таблицу бюджета записи — после неё подписал как есть.
Формула бюджета одна: лет жизни ячейки = 100000 / (записей в сутки × 365). Часовой флаш моточасов — 24 записи в сутки — живёт 11,4 года; журнал событий по сорок в сутки — 6,8 года; конфиг с правкой раз в сезон переживает саму плату. За числами стоят обещания даташита ATmega328P: 100000 циклов записи на ячейку и 20 лет хранения при 85 °C — при 25 °C байт держится обещанные 100 лет. У свежих AVR в даташитах стоит уже 1000000 циклов, но закладываться надо в цифру своего листка, а не соседнего чипа.
В этом уроке — бюджет всей памяти прибора и кольцо для счётчика: четыре ячейки по два байта, где маркер не нужен вовсе, потому что счётчик растёт сам, и каждая ячейка получает четверть износа. К концу у вас будет скетч часового счётчика и таблица, по которой видна судьба каждого жильца памяти до первой строчки кода.
Кольцо для счётчика: максимум вместо маркера
Конфиг в кольце из урока про wear leveling требует маркер-поколение: значения могут повторяться, и без номера свежую запись не отличить от старой. Счётчику повезло: он растёт монотонно, каждая запись больше предыдущей, — значит, свежий слот это просто слот с наибольшим значением. Никакого отдельного байта-маркера и никакой девятой ячейки, умирающей первой: износ размазан по четырём слотам ровно вчетверо, и все они равноправны.
Второй подарок монотонности — порядок байтов при обрыве питания. Пишу младший байт раньше старшего: посреди записи слот временно держит заниженное значение, и максимум по кольцу остаётся на предыдущем слоте. Обрыв в момент записи стоит один шаг — час наработки, — а не счётчик целиком. Проверка на бумаге: переход 511 в 512, в слоте было 0x01FF, записали младший 0x00 — слот стал 0x0100, меньше старого, максимум держится; записали старший 0x02 — слот стал 0x0200, новый валидный. Промежуточное значение при таком порядке никогда не выше старого.
Часы в двух байтах — это 65535 часов наработки: семь с половиной лет непрерывного моторного времени, счётчику реального прибора с паузами хватит на десятилетия. Мотор, который работает нон-стоп, просит третий байт — арифметика не меняется. И про привязку: запись идёт по millis-разности, не по сумме — правило из урока про millis переживает переполнение таймера на 49,7 сутки, сумма — нет.
#include <EEPROM.h> // Карта: 19 - маркер записи, 20-27 - кольцо из 4 слотов по 2 байта часов.// Свежий слот - с наибольшим значением: счётчик растёт монотонно.const int A_MAGIC = 19;const int A_RING = 20;const byte SLOTS = 4;const byte PIN_BTN = 2;const byte PIN_LED = 12;const unsigned long CHAS = 3600000UL; int tek = 0;unsigned long motoCh = 0;unsigned long tLast = 0; void zapisChas() { int base = A_RING + tek * 2; EEPROM.update(base, motoCh % 256); // младший байт первым EEPROM.update(base + 1, motoCh / 256); // старший - вторым Serial.print("zapis: slot "); Serial.print(tek); Serial.print(" ch="); Serial.println(motoCh); tek = tek + 1; if (tek >= SLOTS) tek = 0; digitalWrite(PIN_LED, HIGH); delay(80); digitalWrite(PIN_LED, LOW);} void skanRing() { if (EEPROM.read(A_MAGIC) != 170) { // свежая плата motoCh = 0; tek = 0; EEPROM.update(A_RING, 0); EEPROM.update(A_RING + 1, 0); EEPROM.update(A_MAGIC, 170); // маркер - последним Serial.println("pervyi zapusk: kolco obnuleno"); return; } for (byte i = 0; i < SLOTS; i++) { int base = A_RING + i * 2; unsigned long v = EEPROM.read(base) + (unsigned long)EEPROM.read(base + 1) * 256; Serial.print("slot "); Serial.print(i); Serial.print(" = "); Serial.println(v); if (v > motoCh) { motoCh = v; tek = i + 1; if (tek >= SLOTS) tek = 0; } } Serial.print("vostanovleno chasov: "); Serial.println(motoCh);} void setup() { Serial.begin(9600); pinMode(PIN_BTN, INPUT_PULLUP); pinMode(PIN_LED, OUTPUT); skanRing(); tLast = millis();} void loop() { if (millis() - tLast >= CHAS) { // час работы tLast = tLast + CHAS; motoCh = motoCh + 1; zapisChas(); } if (digitalRead(PIN_BTN) == LOW) { // тестовый час для проверки кольца motoCh = motoCh + 1; zapisChas(); delay(400); }}На свежей плате все четыре слота читаются как 0xFFFF — шестьдесят пять тысяч часов, которых не было; счётчик стартовал бы с потолка. Ловится это маркером на соседнем адресе: его нет — кольцо обнуляется, пишется первый слот, маркер ставится последним. Роль та же, что у слова из прошлого урока, только на байт короче: проверять надо факт записи, а не происхождение байтов.
Кнопка на D2 — тестовый час: она добавляет единицу и делает запись вручную, чтобы увидеть кольцо раньше, чем через год эксплуатации. В рабочем приборе она не нужна: флаш привязан к millis-разности в 3600000 миллисекунд, и разность переживает любые перезагрузки таймера. Вспышка на D12 — отметка записи: если прибор за сутки мигнул чаще, чем прошёл наработка, где-то в скетче лишний вызов zapisChas, и бюджет уже не тот, что в таблице.
Скан кольца при старте печатает все четыре слота: значения идут по кругу и отличаются на единицы-десятки часов. Ровная лесенка — здоровое кольцо; пропущенный номер или ноль посреди лесенки — след наезда чужой зоны: счётчик писал поверх чужого, или его писал кто-то чужой. Раскладку адресов держите в карте памяти — наезд ловится на бумаге за минуту.
В модели ручка A0 задаёт период «раз в несколько часов», каждая запись прибавляет счётчик износа целевой ячейке, а строка монитора печатает прогноз в годах. Оставьте одну ячейку при периоде в час и подождите: на сороковой записи светодиод загорается — ячейка сдала. Переключите кольцо кнопкой: та же нагрузка делится на четыре, строка прогноза вырастает вчетверо, и первый тревожный вердикт уезжает за горизонт видимости.
Прогноз считается формулой из урока: 100000, умноженные на период в часах, делённые на 8760, и множитель кольца, если оно включено. Износ в симе — модель: реальный чип не показывает деградацию, её можно только посчитать заранее; этим и занимается таблица ниже. На живой плате счёт перезаписей ведёт скетч из урока про ресурс — число в мониторе вместо гаданий.
Бюджет записи: считаем до прошивки
| Жилец памяти | Записей в сутки | Годы одной ячейки | Что делаю |
|---|---|---|---|
| Конфиг: пороги, уставки | 1-2 за сезон | десятки тысяч лет | update — и больше ничего |
| Моточасы: флаш раз в час | 24 | 11,4 | кольцо из 4 ячеек — 45,7 года |
| Журнал: события реле | 30-40 | 7-9 | кольцевой журнал, слоты по кругу |
| Телеметрия раз в 5 минут | 288 | 0,95 | в EEPROM не пишу: RAM или SD-карта |
Таблица строится до прошивки и отвечает на вопрос, что класть в EEPROM, а что не класть. Правило такое: жилец, чья строка короче двух лет, в EEPROM не попадает — его берёт оперативная память с готовностью потерять хвост при сбое или внешняя микросхема. 24C256 даёт тридцать два килобайта и ту же арифметику со страницей записи — кольца там меряются сотнями слотов.
И про честность формулы: 100000 — гарантия, под которой подписался производитель, а не дата смерти; ячейка за лимитом часто живёт ещё долго, но расплата за веру в «примерно» приходит в сезон. Три приёма снижают частоту до комфорта: RAM-буфер с флашем по шагу, кольцо из этого урока и update, пропускающий неизменившиеся байты. Частота реальных изменений, в отличие от частоты записей, не лечится ничем — только переносом жильца из EEPROM.
- 1. Сколько циклов записи держит ячейка EEPROM ATmega328P по даташиту?
- 2. Часовой флаш счётчика в одну ячейку — сколько живёт ячейка?
- 3. Почему кольцу счётчика не нужен маркер-поколение?
- 4. Какой байт пишется первым и почему?
- 5. Телеметрия должна писаться в EEPROM раз в пять минут. Что по бюджету?
- Формула бюджета: лет = 100000 / (записей в сутки × 365); часовой флаш живёт 11,4 года, пятиминутная телеметрия — меньше года
- Кольцу счётчика маркер не нужен: значения монотонно растут, свежий слот — максимум по кольцу; четыре ячейки — вчетверо дольше жизнь
- Младший байт пишется первым: обрыв посреди записи даёт заниженный слот, максимум остаётся валидным, потери — один шаг
- Свежая плата читает слоты как 0xFFFF — маркер записи обязателен, иначе счётчик стартует с 65 тысяч часов
- Двух байт хватает на 65535 часов наработки; третий байт — если мотор работает без пауз
Частые вопросы по теме урока
- Сколько циклов записи держит ячейка EEPROM ATmega328P по даташиту?
- Даташит обещает 100000 циклов EEPROM (флеш — 10000) и 20 лет хранения при 85 °C. У свежих AVR нормируют до 1000000 — смотрите свой листок. 100000
- Часовой флаш счётчика в одну ячейку — сколько живёт ячейка?
- 24 записи в сутки — 8760 в год; 100000 делим на 8760, получаем 11,4 года. Кольцо из четырёх ячеек растягивает срок до 45,7 года. Около 11,4 года
- Почему кольцу счётчика не нужен маркер-поколение?
- Каждая запись больше предыдущей, поэтому максимум по кольцу — всегда самая свежая запись. Конфигу маркер нужен потому, что его значения могут повторяться. Счётчик растёт монотонно: свежий слот — с наибольшим значением
- Какой байт пишется первым и почему?
- При записи младшего первым промежуточное значение никогда не выше старого: переход 0x01FF в 0x0200 через 0x0100 не выдаёт ложный максимум. Младший: обрыв посреди записи даёт заниженный слот, максимум остаётся валидным
- Телеметрия должна писаться в EEPROM раз в пять минут. Что по бюджету?
- 288 записей в сутки — 105120 в год, гарантия кончается меньше чем за год. Такой жилец не проходит бюджет: оперативка или внешний носитель. Ячейка живёт около года — телеметрии место в RAM или на SD