Термометр для коптильного цеха я отдал заказчику с журналом показаний в EEPROM: раз в десять секунд прибор дописывал текущую температуру в таблицу — аккуратный журнал на сотни значений. На моём столе прибор жил неделями. Через двенадцать суток пришёл звонок: температуры замерли на одном числе, кнопки работают, журнал стоит. Я открыл скетч и посчитал то, что следовало посчитать до отгрузки: 100000 циклов ресурса на запись раз в десять секунд — 278 часов, одиннадцать с половиной суток. Ячейка умерла по расписанию, которое я не составлял.
Цифру 100000 я, конечно, знал — урок про ресурс прочитан давно и внимательно. Но знание без умножения на период бесполезно, как расход топлива без километража: граница между «пишет мало» и «пишет много» решается только формулой, а не ощущением. С тех пор в шапке любого проекта с EEPROM у меня живёт одна строка расчёта и одна таблица сценариев — они и составляют этот урок. Строка занимает полминуты, а экономит месяцы отладки и один неловкий разговор с заказчиком.
План такой: выведем формулу срока жизни ячейки через период записи, посчитаем её для реальных сценариев — от антипримера раз в десять секунд до моточасов раз в шесть минут, — потом удвоим и удесятерим срок кольцом из ячеек и разберём, где хранить его голову, чтобы кольцо не породило новую жертву.
EEPROM в симуляторе не эмулируется — модель памяти это массив в скетче. В сим-блоке десять ячеек пишутся по кругу, ползунок A0 задаёт период записи от одной до десяти секунд, Serial печатает счётчик записей каждой ячейки и прогноз в днях. Порог смерти ячейки сжат со 100000 до восьми записей — добралась до лимита, пискнул зуммер D8.
Формула: циклы, умноженные на период
Срок жизни ячейки — это ресурс, делённый на частоту записи, а частота — обратная величина периода. Практическая форма: срок = 100000 × период записи. Раз в десять секунд: миллион секунд — 278 часов, те самые одиннадцать суток из истории. Раз в 360 секунд: 36 миллионов секунд — 10 000 часов, около 1,1 года непрерывной работы. Раз в час — 11,4 года. Раз в день — 274 года. Одна цифра периода меняет исход на порядки, и это чистое умножение без всякой магии.
Формула работает и в обратную сторону, и это её самая полезная форма на этапе проектирования. Прибор должен писать раз в минуту и прожить год — разделите ресурс на часы года: 100000 на 8760 — получится одиннадцать с половиной записей в час, то есть раз в пять с лишним минут. Не влезает — либо режьте частоту, либо закладывайте кольцо и делите износ на его длину. Пять минут расчёта до пайки экономят ячейку и репутацию после.
К каждому числу прилагается условие «непрерывно». Моточасы из прошлого урока с шагом 360 секунд у насоса, который качает четыре часа в сутки, истощают ячейку в шесть раз медленнее — свыше шести лет вместо одного. Прибор с записью раз в десять секунд, но работающий час в день, протянет 278 дней. Формула честная, но подставлять в неё надо реальное расписание прибора, а не худший случай из головы — обычно они различаются в разы, и в вашу пользу.
Update-семантика математику не отменяет: update пропускает только неизменившиеся значения, а счётчик секунд меняется каждую секунду — все его записи настоящие, ресурс тратится по полной. Скидка update работает на настройках, которые меняются редко; на растущем счётчике экономить нечего. Единственная ручка, которая остаётся, — период, и ниже мы сожмём его из антипримера в норму.
| Сценарий | Период в одну ячейку | Ячейка умрёт через | Кольцо из 10 ячеек |
|---|---|---|---|
| Моточасы, шаг 360 с | раз в 6 минут | около 1,1 года непрерывно | около 11 лет |
| Настройки при изменении | раз в час | около 11,4 года | одной ячейки хватает |
| Счётчик включений | раз в сутки | около 274 года | запас не нужен |
| Журнал раз в 10 с | раз в 10 секунд | 278 часов (11,6 суток) | 116 суток — всё равно мало |
Кольцо: десять ячеек — в десять раз дольше
Кольцевое распределение износа — самодельный wear leveling. Заводим десять блоков и пишем по кругу: сегодня шаг уезжает в блок 3, через шесть минут — в блок 4, и так до блока 9, потом снова на 0. Каждая ячейка принимает десятую долю записей и живёт в десять раз дольше: 1,1 года превращаются в одиннадцать, а запись раз в минуту — из 69 дней в почти два года. Цена вопроса — десять блоков по четыре байта, сорок ячеек из 1024.
Хитрая часть — голова кольца. Первая мысль «отдельная ячейка с номером блока» разбивается о ту же формулу: указатель меняется при каждой записи, значит изнашивается в десять раз активнее всех блоков, — кольцо породит новую жертву ровно там, где её не ждали. Решение без указателя: во всех блоках хранится одно нарастающее значение счётчика, и голова — блок с максимумом. При старте читаем все десять, находим максимум, пишем в следующий за ним. Отдельной ячейки нет, износ ровный, а чтение — если помните урок про память без питания — ресурсу не вредит вовсе.
Сборка значений при чтении — в long, как в счётчике включений: 255 × 256 в знаковый int не влезает. И не забудьте про первую инициализацию: свежая память читается всеми 255, и максимум из десяти таких блоков сломает счёт на первом же старте — magic-байт и обнуление кольца при первом запуске обязательны. Длина кольца тоже выбирается по формуле: хотите двукратный запас — берите два блока, хотите годам двадцать — двадцать блоков съедят восемьдесят ячеек, и это всё ещё меньше десятой части килобайта.
#include <EEPROM.h> // Кольцо из 10 блоков по 4 байта: адреса 100-139.// Во всех блоках лежит одно нарастающее значение,// максимум — голова кольца, отдельный указатель не нужен.const int addrMagic = 99;const int RING_ADDR = 100;const int N = 10;const long STEP_S = 360; // пишем раз в 6 минутconst long STEP_MS = 360000; long counter = 0;unsigned long lastSave = 0; long readBlock(int i) { int a = RING_ADDR + i * 4; long v = EEPROM.read(a); v = v * 256 + EEPROM.read(a + 1); v = v * 256 + EEPROM.read(a + 2); v = v * 256 + EEPROM.read(a + 3); return v;} void writeBlock(int i, long v) { int a = RING_ADDR + i * 4; EEPROM.update(a + 3, v % 256); v = v / 256; EEPROM.update(a + 2, v % 256); v = v / 256; EEPROM.update(a + 1, v % 256); v = v / 256; EEPROM.update(a, v % 256);} int findHead() { // блок с максимумом — голова int head = 0; long best = -1; for (int i = 0; i < N; i++) { long v = readBlock(i); if (v > best) { best = v; head = i; } } return head;} void setup() { Serial.begin(9600); if (EEPROM.read(addrMagic) != 170) { // свежая память - все 255 for (int i = 0; i < N; i++) { writeBlock(i, 0); } EEPROM.update(addrMagic, 170); } int head = findHead(); counter = readBlock(head); Serial.print("counter="); Serial.println(counter);} void loop() { if (millis() - lastSave >= STEP_MS) { lastSave = millis(); int head = findHead(); counter = readBlock(head) + 1; writeBlock((head + 1) % N, counter); // пишем в следующую по кругу Serial.print("saved n="); Serial.print(counter); Serial.print(" block="); Serial.println((head + 1) % N); }}Смотрите, как ложатся числа на скетч: шаг 360 секунд по кольцу из десяти блоков — каждая ячейка получает запись раз в час и живёт 11,4 года. Тот же поток в одну ячейку — год и два месяца. Разница — сорок байтов и один цикл поиска максимума, который выполняется раз в шесть минут и съедает микросекунды. Чтение десяти блоков при старте — сорок обращений, которые ресурсу безразличны.
И граница метода, чтобы без иллюзий. Кольцо умножает срок на число блоков, но не отменяет формулу: журнал раз в десять секунд по кольцу из десяти — это 278 часов × 10 = 116 суток. Больше, чем одиннадцать, но меньше года — потоковой записи такого темпа в EEPROM не место, её дорога на SD-карту или во внешнюю память. Кольцо спасает счётчики, моточасы и периодические итоги, а не журналы.
Кольцо отлично стыкуется с моточасами из прошлого урока: если шести минут вам мало и сбой терять жалко, раскладывайте шаг по кольцу из четырёх блоков — ресурс вырастет вчетверо, до четырёх с лишним лет непрерывной работы, а потеря при сбое останется на уровне шага. Мой насос так и живёт: шаг 360 секунд по кольцу из четырёх — считать придётся так же, а обслуживать ячейку — через одно поколение плат.
- Срок жизни ячейки = 100000 × период записи: раз в 10 с — 278 часов, раз в 6 минут — 1,1 года, раз в час — 11,4 года непрерывно
- Каждому числу прилагайте расписание прибора: работа треть суток растягивает срок втрое, час в сутки — в 24 раза
- Update не спасает растущие счётчики: все их записи настоящие, ресурсом управляет только период
- Кольцо из N блоков делит износ на N: десять блоков по 4 байта — сорок ячеек — умножают срок на десять
- Голову кольца храните в самих блоках — максимум значений; отдельная ячейка-указатель изнашивалась бы в десять раз быстрее
- Свежая память читается как 255: кольцо без magic-байта и обнуления стартует со сломанным максимумом
- 1. Ячейка принимает запись раз в десять секунд. Когда её ждёт конец?
- 2. Прибор работает час в сутки и пишет раз в десять секунд. Сколько проживёт ячейка?
- 3. Почему нельзя хранить голову кольца в отдельной ячейке?
- 4. Десять блоков по 4 байта, запись раз в 6 минут. Каков срок жизни кольца?
- 5. Что случится при старте на свежей EEPROM без magic-байта?
Тройка сложилась в полный расчётный набор: массив раскладывает однотипные значения по адресам, моточасы выбирают период, формула переводит период в годы. Дальше эти блоки стоит укрепить: контрольная сумма из урока про протоколы поймает гибрид после сбоя, а перезапуски и watchdog из урока про таймеры и сон покажут, когда счётчику приходится восстанавливаться. Считайте ресурс до запуска — и мои 278 часов останутся историей, а не вашим сервисным журналом.
Частые вопросы по теме урока
- Ячейка принимает запись раз в десять секунд. Когда её ждёт конец?
- 100000 × 10 секунд = миллион секунд, 278 часов. Ровно расчёт, который я не сделал перед отгрузкой термометра, и он обошёлся в звонок заказчика. Через 278 часов — около 11,6 суток
- Прибор работает час в сутки и пишет раз в десять секунд. Сколько проживёт ячейка?
- Миллион секунд чистой работы при часе в сутки — это 278 дней. В формулу подставляют реальное расписание прибора, а не худший случай. 278 дней — срок растягивается на время простоя
- Почему нельзя хранить голову кольца в отдельной ячейке?
- Частота смены указателя равна частоте всех записей кольца: его ячейка кончится первой. Максимум значений блоков заменяет указатель бесплатно и ровно распределяет износ. Указатель меняется при каждой записи и изнашивается в десять раз быстрее блоков
- Десять блоков по 4 байта, запись раз в 6 минут. Каков срок жизни кольца?
- Кольцо делит износ на десять: 1,1 года одной ячейки превращаются в одиннадцать. Цена — сорок ячеек и поиск максимума раз в шесть минут. Около 11 лет
- Что случится при старте на свежей EEPROM без magic-байта?
- Свежие ячейки читаются как 255, и блок из четырёх байт собирается в максимум long. Обнуление кольца при первом запуске обязательно. Максимум из десяти блоков окажется 4294967295 — счёт сломается