Метеостанция на даче писала давление в EEPROM каждые десять секунд — я гордился аккуратным журналом, пока в середине августа ячейка номер пять не умерла. Значение застыло: крутишь барометр хоть вверх дном, а при чтении возвращается одно и то же число. Вечером сел с калькулятором и посчитал: сто тысяч циклов ресурса, делённые на запись раз в десять секунд — это 11,5 суток. Ячейка отработала ровно свой срок, просто я этого срока не считал.
Тупик был не в коде, а в голове: я читал цифру «100000» как «почти бесконечность». На самом деле ресурс EEPROM — это не дата и не гарантия, это счётчик записей, который крутится всегда, пока программа пишет. И если его не считать, он посчитает за вас — остановкой проекта в самый неподходящий день.
В этом уроке разберём, от чего именно изнашивается память, как по частоте записей спрогнозировать дату смерти каждой ячейки и какими тремя приёмами этот срок растягивают: update вместо write, запись по событию и ротация по нескольким ячейкам. Всё посчитаем в числах — вплоть до часов.
Симулятор поддерживает EEPROM.read, EEPROM.write, EEPROM.update и EEPROM.length(), а его память живёт между запусками. В скетче-прогнозаторе ниже счётчик фактических записей считается в оперативной памяти, а в Serial печатается остаток ресурса в часах — помучайте ползунок и посмотрите, как прогноз скачет.
Ресурс — это счётчик записей, а не дата на календаре
Физика процесса простая и неумолимая. Каждый бит EEPROM — плавающий затвор транзистора, в который заряд перекачивается через тонкий оксидный слой под высоким напряжением. Каждый цикл записи этот слой понемногу травмирует, и однажды ячейка перестаёт надёжно удерживать заряд — начинается с редких сбоев бита, заканчивается «залипанием» значения. Производитель ATmega328P гарантирует не менее 100000 циклов записи на каждую ячейку. Практика обычно щедрее: отдельные экземпляры живут в разы дольше, но проектировать на «повезёт» — плохая стратегия, гарантия на то и гарантия.
Из этого следуют два вывода, которые экономят проекты. Первый: ресурс тратит только запись, причём каждая — даже перезапись того же самого значения. Чтение не изнашивает память вообще: читайте хоть миллион раз в секунду. Второй: у EEPROM есть ещё одна цифра — срок хранения данных без перезаписи. По даташиту это порядка 20 лет при 85 градусах и заметно дольше при комнатной температуре. Поэтому записанная однажды настройка не «портится от записей» — она может забыться только от старости самой ячейки, и это отдельный, очень длинный таймер. Как EEPROM вписывается в остальные виды памяти чипа — Flash, SRAM и их разницу в назначении — я подробно разбирал в уроке про PROGMEM и память.
Скорость записи тоже стоит держать в голове: один цикл занимает около 3,3 мс, и всё это время процессор занят. Для сохранения раз в минуту — незаметно. Для контура, тикающего каждые 100 микросекунд, — катастрофа. Ещё одна причина писать редко, по делу и с обновлением только изменённых значений.
#include <EEPROM.h> const int addrVal = 0; // ячейка со значением настройки long writes = 0; // фактических записей с момента стартаlong startMs = 0;long lastPrint = 0; void setup() { Serial.begin(9600); startMs = millis(); int bright = EEPROM.read(addrVal); if (bright == 255) { bright = 100; // пустая EEPROM - заводской дефолт } Serial.print("start bright="); Serial.println(bright);} void loop() { int pot = analogRead(A0) / 4; // ручка 0..1023 сжимаем в байт int saved = EEPROM.read(addrVal); if (abs(pot - saved) > 3) { // гистерезис: ручку крутили всерьёз EEPROM.update(addrVal, pot); // update: пишет только при отличии writes = writes + 1; Serial.print("write wr="); Serial.println(writes); } long elapsed = millis() - startMs; if (elapsed >= 5000 && writes > 0 && elapsed - lastPrint >= 3000) { lastPrint = elapsed; long perMin = writes * 60000 / elapsed; // записей в минуту if (perMin > 0) { long hours = 100000 / (perMin * 60); // ресурс в часах Serial.print("forecast wr="); Serial.print(writes); Serial.print(" perMin="); Serial.print(perMin); Serial.print(" hours="); Serial.println(hours); } } delay(250);}Смотрим, где тут живёт математика. Счётчик writes — обычная переменная в оперативной памяти, и это принципиально: вести в EEPROM журнал собственных записей — значит жечь ресурс ради подсчёта того, как он тратится. С момента старта millis() даёт elapsed, из пары чисел получаем частоту: записей в минуту, а из неё — ресурс в часах по формуле 100000, делённые на записи в час. При одной записи в минуту это 100000/60 — около 1666 часов, те самые 69 дней из таблицы ниже.
Гистерезис с порогом в три единицы — не украшение: он отделяет «ручку крутят» от «ручку слегка дрожит от вибрации». Разница между единичной и десятками записей в минуту — это разница между годами и неделями жизни ячейки. Если сомневаетесь в пороге, поднимайте его: пользователь не заметит гистерезис в пять единиц, а память заметит сразу.
Такой прогнозатор удобно держать в отладочной прошивке любого проекта с настройками: сутки в кармане у реального пользователя дадут честную частоту записей, а не ту, что вы предполагали за столом. Мои предположения, как показывает история с метеостанцией, обычно оптимистичны в десять раз. Моё правило приёмки: если прогноз при реальной эксплуатации выходит хуже пяти лет, частоту записей урежьте — буферизацией, гистерезисом или ротацией, — и прогоните измерение ещё раз. Пять лет — не магия, а удобная граница: проект почти наверняка переживёт и перепрошивку, и моральное устаревание, а ячейки — тем более.
Как растянуть ресурс: событие, гистерезис, ротация
Первый и главный приём — писать по событию, а не по расписанию. Кнопка «сохранить», момент выключения, факт изменения значения — всё это события. Расписание вида «каждый проход loop» — приговор ячейке. Второй приём мы уже применяем: update() перед записью сам читает ячейку и пропускает неизменённые значения, экономя циклы без всяких усилий с вашей стороны. Третий приём — буферизация: держите текущие значения в переменных, а в EEPROM сбрасывайте раз в несколько минут или при событии. Даже запись раз в десять минут даёт ячейке почти два года жизни. С событями «питание на исходе» и перезагрузками связана отдельная история — её раскапывает урок про watchdog и таймеры.
Когда писать приходится часто, включается ротация — тот самый wear leveling из мира флешек. Счётчик ходов клапана не обязан жить в одной ячейке: заведите блок из десяти и пишите по кругу. Износ делится на десять, ресурс умножается на те же десять. В уроке про счётчик включений мы уже хранили число в паре байтов — ротация достраивает к этому второй этаж: те же байты, но адреса по кругу.
И у каждой ячейки есть конец. Симптомы смерти: значение перестаёт меняться при записи, редкие единичные биты «прилипают». Если настройки критичны, храните рядом с блоком контрольную сумму — приём из урока про протоколы и контрольную сумму ложится на EEPROM один в один: не сошлась — читаем резервный блок или ставим дефолты. Потоковые же данные — показания раз в секунду, журналы — в EEPROM не кладут вовсе, их место описано в уроке про логирование на SD-карту.
Отдельный случай — запись при пропадании питания. Идея «поймать выключение и успеть сохранить» на голой Arduino работает хуже, чем хочется: напряжение падает за миллисекунды, а одна запись занимает около 3,3 мс — успеть можно, но впритык и не всегда. Я строю такие проекты наоборот: буфер в RAM сбрасывается в EEPROM регулярно и по событиям, а «последний вдох» — лишь подстраховка. Конденсатор в сотни микрофарад на линии 5 вольт даёт процессору лишние миллисекунды — этого хватает на один-два цикла записи спокойно.
| Частота записи в ячейку | 100000 циклов закончатся через | Типичный источник таких записей |
|---|---|---|
| 10 раз в секунду | около 2,8 часа | антипример: EEPROM.write без условий в loop |
| 1 раз в секунду | около 27,8 часа | мой логгер давления с метеостанции |
| 1 раз в минуту | около 69 дней | наивные автосохранения настроек |
| 1 раз в 10 минут | почти 2 года | буфер в RAM и периодический сброс |
| 1 раз в час | около 11,4 года | запись при изменении с гистерезисом |
| 1 раз в день | около 274 года | счётчики событий, итоги смены |
- Ресурс EEPROM — минимум 100000 циклов записи на ячейку; тратит его только запись, чтение изнашивает память нисколько
- Срок жизни ячейки = 100000, делённые на записи в час: раз в секунду — 27,8 часа, раз в минуту — 69 дней, раз в час — 11,4 года
- Пишите по событию и с гистерезисом; update() сам пропускает неизменённые значения; одна запись — около 3,3 мс занятого процессора
- Частые записи — ротация по блоку ячеек: десять ячеек по кругу умножают ресурс на десять
- Загрузчик Arduino IDE EEPROM не стирает, но chip erase у программаторов и avrdude без -D — может; проверяйте свою цепочку прошивки
- Данные без перезаписи хранятся годами (порядка 20 лет даже при 85 градусах) — старение памяти и её износ записями это разные таймеры
- 1. Что изнашивает ячейку EEPROM?
- 2. Логгер пишет в одну ячейку раз в 10 секунд. Когда её ждёт конец?
- 3. Какая привычка сильнее всего продлевает жизнь настроек в EEPROM?
- 4. Счётчик пишется по кругу в 10 ячеек вместо одной. Насколько вырастет ресурс?
- 5. Сколько держится записанное в EEPROM значение, если туда больше не писать?
Теперь вы знаете цену каждой записи и умеете её считать до запуска, а не после похорон ячейки. Но вот незадача: в этой и прошлых настройках из первого урока всё отлично помещалось в один байт от 0 до 255. Попробуйте записать туда целевую температуру морозилки — минус восемнадцать — и вас ждёт сюрприз: прочитается 238. Почему минус не влез в байт и как раскладывать по ячейкам int и float — следующий урок.
Частые вопросы по теме урока
- Что изнашивает ячейку EEPROM?
- Ресурс тратится циклами записи: перекачка заряда через оксидный слой. Чтение заряд не перекачивает. Перезапись тем же значением — тоже цикл записи. Только запись, каждая, даже тем же значением
- Логгер пишет в одну ячейку раз в 10 секунд. Когда её ждёт конец?
- 100000 записей по одной за 10 секунд — это миллион секунд, около 11,5 суток. Тот самый расчёт, который я поленился сделать на даче. Примерно через 11,5 суток
- Какая привычка сильнее всего продлевает жизнь настроек в EEPROM?
- Частота записи — единственная ручка, которая реально управляет ресурсом. Update добавляет страховку, дубли ячеек умножают износ, а стирание — просто теряет настройки. Писать только по событию или при изменении значения, желательно через update()
- Счётчик пишется по кругу в 10 ячеек вместо одной. Насколько вырастет ресурс?
- Каждая ячейка блока принимает десятую долю записей, значит проживёт в десять раз дольше. Это и есть wear leveling в самодельном исполнении. В 10 раз
- Сколько держится записанное в EEPROM значение, если туда больше не писать?
- Хранение без перезаписи и ресурс записи — разные характеристики. Даташит обещает годы хранения; «пока не выключат питание» — это про SRAM. Порядка 20 лет даже при 85 градусах, при комнатной температуре дольше