Термостат теплицы жил на даче без присмотра и раз в час подстраивал порог проветривания: скетч пересчитывал его по времени суток и сохранял через EEPROM.write. Проработал год с небольшим, потом настройки начали плыть — сначала редко, потом стабильно раз в неделю плата просыпалась с порогом по умолчанию. Диагноз скучный: ячейка с порогом выработала ресурс. Даташит на ATmega328P обещает каждой ячейке EEPROM 100000 циклов стирания-записи, и никакой мистики тут нет — чистая арифметика: 24 записи в сутки дают 8760 в год, гарантия кончается к одиннадцатому году. Мой термостат сдался раньше срока не потому, что ячейка была плохой, а потому, что вместе с часовым графиком порог переписывался ещё и при каждом нажатии кнопки — счётчика перезаписей я не вёл и узнал о ресурсе постфактум.
Цифры по износу у нас уже были: в уроке про ресурс ячеек разбирались, на что именно тратится цикл, а в уроке про update и write — почему update аккуратнее к памяти. Здесь считаю в обратную сторону: от частоты изменения конфига к годам жизни, и завожу счётчик перезаписей, чтобы срок службы держался не на ощущениях, а на числе в мониторе порта.
План такой: сначала кто тратит цикл, а кто его не трогает вовсе, затем счётчик перезаписей рядом с конфигом, прикидка на салфетке для четырёх типовых частот и лаборатория, где износ виден в процентах и годах. Все выкладки я проверял на живом скетче из code-блока: он считает перезаписи конфига за вас и печатает возраст настроек прямо в мониторе порта. По итогам станет ясно, когда ресурс можно игнорировать до конца проекта, а когда пора переходить к записи по кругу — она в следующем уроке.
Что тратит цикл и что его не трогает
Цикл стирания-записи — физика ячейки: при каждом пробое через оксидный слой плавающего затвора тот понемногу деградирует, и после гарантированных ста тысяч циклов производитель перестаёт отвечать за содержимое. Чтение ячейку не трогает: хоть каждую миллисекунду — ресурсу без разницы, читается копия заряда без пробоя. Тратит только запись, и каждая запись начинается со стирания в 0xFF, поэтому перезапись того же значения стоит полный цикл. Отсюда же популярная гибель EEPROM: строка EEPROM.write в loop() молотит по ячейке десять раз в секунду, даже если байт не менялся неделями.
Правило экономии номер один: не писать то же значение. EEPROM.update читает байт и запускает цикл только при отличии — для настроек, которые пользователь трогает раз в сезон, это экономит практически весь ресурс. Но update не волшебник: когда значение меняется по-настоящему, цикл тратится как тратился. Часовой график порога менял значение честно — никакая обёртка бы не спасла, нужна стратегия из следующего урока. Границу я формулирую так: update снимает дублирующие записи, но не снимает саму частоту реальных изменений — она лечится только изменением раскладки памяти.
Второе правило — не путать износ с долговечностью хранения. Даташит обещает записанному байту 100 лет хранения при 25 °C и 20 лет при 85 °C — это про ячейку, которую записали и забыли. Износ считает циклы, удержание считает годы, и слабое место всегда первое: плате, переписывающей конфиг раз в час, долго ждать и того, и другого. Отсюда вывод для батарейных узлов: если конфиг записан один раз при настройке и дальше лежит, он переживёт саму плату, а вот счётчики и журналы с постоянной записью считайте в циклах.
Счётчик перезаписей — два байта EEPROM по соседству с конфигом: при каждом сохранении конфига счётчик растёт на единицу. Печатая его в Serial, вы видите возраст конфига в циклах: 8760 — год часовых записей, 87600 — десять лет. Это диагностика, не защита: счётчик живёт теми же циклами, что и конфиг, и умирает в тот же момент. Зато он честно показывает, сколько гарантии съедено и сколько осталось, — решать с такой цифрой гораздо проще, чем на глаз.
Хранить счётчик стоит с поправкой на свежую память: стёртая ячейка читается как 0xFF, поэтому пара FF FF — это «счётчика ещё не было», а не 65535. Ту же оговорку мы проговаривали в уроке про расчёт износа по частоте — там таблица частот, здесь скетч. Пара байтов uint16_t дотягивает до 65535 и замирает: для диагностики хватает, за пределами гарантии отвечать всё равно некому.
Грабля счётчика — соблазн инкрементировать его в loop() на всякий случай. Счётчик должен расти строго синхронно с записью конфига, иначе он измеряет не износ, а время. Достаточно печатать его раз в сутки в Serial и раз в год сверять с планом: цифра 52560 в конце первого года означает, что запись раз в десять минут — это всерьёз. И держите в скетче команду печати счётчика из монитора: пять строк кода экономят приезд к плате с ноутбуком.
#include <EEPROM.h> // Конфиг - порог вентиляции 0-100, адрес 0.// Счётчик перезаписей конфига - адреса 10 и 11 (старший и младший байт).// Кнопка D2 меняет порог и сохраняет. LED D12 горит после 87600 записей -// десяти лет работы при сохранении раз в час. const int ADRES_POROGA = 0;const int ADRES_SCHETCHIKA = 10;uint16_t schetchik = 0;uint8_t porog = 40; void chitajVse() { porog = EEPROM.read(ADRES_POROGA); if (porog > 100) porog = 40; // свежая память или мусор schetchik = EEPROM.read(ADRES_SCHETCHIKA); schetchik = (schetchik << 8) | EEPROM.read(ADRES_SCHETCHIKA + 1); if (schetchik == 0xFFFF) schetchik = 0; // счётчика ещё не было} void sohranit() { EEPROM.update(ADRES_POROGA, porog); // цикл только при отличии if (schetchik < 0xFFFF) { schetchik = schetchik + 1; EEPROM.update(ADRES_SCHETCHIKA, schetchik >> 8); EEPROM.update(ADRES_SCHETCHIKA + 1, schetchik & 0xFF); } Serial.print("porog="); Serial.print(porog); Serial.print(" perezapisej="); Serial.println(schetchik);} void setup() { Serial.begin(9600); pinMode(2, INPUT_PULLUP); pinMode(12, OUTPUT); chitajVse(); Serial.print("posle starta: porog="); Serial.print(porog); Serial.print(" perezapisej="); Serial.println(schetchik); if (schetchik >= 87600) digitalWrite(12, HIGH);} void loop() { if (digitalRead(2) == LOW) { porog = porog + 5; if (porog > 100) porog = 20; sohranit(); delay(350); // антидребезг кнопки }}Прикидка на салфетке
Арифметика выводится в одну строку: годы равны 100000, делённым на записей в год. Раз в сутки — 365 записей, 274 года гарантии: ресурс перестаёт быть вопросом. Раз в час — 8760 и 11.4 года. Раз в десять минут — 52560 и 1.9 года. Раз в минуту — 525600 и 70 дней. Раз в секунду — 31536000 и около 28 часов, меньше суток с четвертью. Четыре строки таблицы ниже закрывают почти любой проект: найдите свою частоту и честно посмотрите в правую колонку. Я держу эту таблицу под рукой: минута со сравнением частоты и лет экономит вечер с калькулятором.
Мой любимый пример — счётчик моточасов генератора, сохранявший наработку каждую секунду: ячейка обязана была умереть за 28 часов работы, и она умирала. Как такую наработку спасли перераскладкой по памяти — в уроке про моточасы, приём тот же, что в следующем уроке. Если же запись у вас реже раза в сутки — забудьте про ресурс до конца проекта: 274 года гарантии переживут и плату, и теплицу.
Границы прикидки честные. Ста тысяч — гарантия даташита, а не ожидаемый день смерти: при комнатной температуре ячейки обычно живут заметно дольше, но закладываться на это — плохая привычка, особенно для платы в гараже или на улице, где жара и мороз меняются местами. Вторая оговорка тоньше: прикидка предполагает, что все записи попадают в одну ячейку. Если в скетче два адреса на один параметр или массив значений, у каждой ячейки свои циклы — считайте каждую. Третья оговорка — температура: 85 °C в закрытом щитке на солнце не экзотика, а обычный июль, и удержание там уже вдвое короче обещанного.
| Частота записи конфига | Записей в год | Гарантия ячейки (100000 циклов) |
|---|---|---|
| Раз в сутки | 365 | 274 года — ресурс не вопрос |
| Раз в час | 8760 | 11.4 года |
| Раз в 10 минут | 52560 | 1.9 года |
| Раз в секунду | 31536000 | около 28 часов |
- Цикл тратится на запись одной ячейки: стирание в 0xFF плюс запись; чтение ресурс не тратит вовсе
- Даташит ATmega328P: 100000 циклов на ячейку, 100 лет хранения при 25 °C и 20 лет при 85 °C
- EEPROM.update запускает цикл только при отличии значения: на редко меняющихся настройках экономит почти весь ресурс
- Счётчик перезаписей — два байта рядом с конфигом: растёт синхронно с записями и показывает остаток гарантии
- Прикидка: раз в час — 11.4 года, раз в 10 минут — 1.9 года, раз в секунду — 28 часов; дальше — wear leveling
- 1. Плата вызывает EEPROM.write(0, одно и то же значение) десять раз в секунду. Сколько циклов износа тратится за сутки?
- 2. Конфиг сохраняется раз в час. Сколько лет гарантии ячейки по даташиту?
- 3. Что вообще не тратит ресурс EEPROM?
- 4. Чем EEPROM.update отличается от EEPROM.write?
- 5. Свежая EEPROM после заводского стирания. Что вернёт EEPROM.read по любому адресу?
Счётчик — инструмент измерения, а не лечения: когда цифра растёт быстрее плана, ячейку спасает запись по кругу — wear leveling из следующего урока, тот же приём, что вытащил счётчик моточасов. А чтобы часовой график не превращался в delay-цикл, держите под рукой планировщик на millis(): сохранять раз в час — задача на millis, и только на них. Мой чек-лист простой: посчитать частоту, завести счётчик, сверить с таблицей — и только потом решать, нужен ли круг. После перехода на круг вернитесь к счётчику — он покажет, как равномерно легла нагрузка на слоты, и подскажет, когда слотов стало мало.
Частые вопросы по теме урока
- Плата вызывает EEPROM.write(0, одно и то же значение) десять раз в секунду. Сколько циклов износа тратится за сутки?
- write гоняет полный цикл стирание-запись даже при том же значении. 86400 записей в сутки вырабатывают год гарантии за час — вот зачем нужен update и запись по событию. 864000 — ячейка мертва к утру второго дня
- Конфиг сохраняется раз в час. Сколько лет гарантии ячейки по даташиту?
- 8760 записей в год при 100000 циклов дают 11.4 года. Ста лет — это про хранение записанного байта при 25 °C, а не про ресурс циклов. Около 11 лет
- Что вообще не тратит ресурс EEPROM?
- Чтение копирует заряд без пробоя через оксид — ресурсу без разницы. Тратят только записи, включая перезапись того же значения. Чтение байта
- Чем EEPROM.update отличается от EEPROM.write?
- update сначала читает ячейку и запускает цикл только при отличии. На редко меняющихся настройках это экономит почти весь ресурс. Читает байт и пропускает запись при равенстве
- Свежая EEPROM после заводского стирания. Что вернёт EEPROM.read по любому адресу?
- Стирание ставит все биты в единицы — ячейка читается как 0xFF, то есть 255. Пара FF FF — не 65535, а признак нетронутой памяти. 255