Поливалка в теплице была моим самым дисциплинированным проектом: скетч каждые десять секунд записывал в ячейку 0 последнее показание влажности — чтобы после любого сбоя знать, что происходило до него. Через неполные двенадцать суток показания замерли на числе 41 и больше не двигались ни при каком поливе. Ячейка отработала свой ресурс. Арифметика была честная: раз в десять секунд — это 8640 записей в сутки, и 100000 гарантированных циклов кончаются ровно на 11,6 дня.
Я писал «подряд», а надо было писать «по делу». Инструмент для этого встроен в библиотеку и называется EEPROM.update(addr, значение): перед записью он читает ячейку и кладёт новое значение только тогда, когда оно отличается от лежащего. Совпало — ячейка не трогается вовсе: ни записи, ни стирания, ни трёх с половиной миллисекунд ожидания.
Но update — не волшебная таблетка. Если показания реально меняются каждые десять секунд, ресурс кончится с тем же успехом: 8640 изменённых значений в сутки не переписать никакой библиотекой. Смысл update в другом: write сжигает цикл записи даже на повторе того же числа, update — нет. А в самодельных скетчах, как показала та же поливалка, подавляющая часть записей — как раз холостые повторы: влажность-то меняется не каждые десять секунд, а по погоде.
План урока: механика update изнутри, демонстрация с двумя счётчиками — сколько циклов сжёг бы write и сколько реально потратил update, — правила событийной записи и приём чередования ячеек, который умножает ресурс на число адресов.
Внутри update: прочитай, сравни, запиши
Механика умещается в одну строку: EEPROM.update(addr, val) — это EEPROM.read(addr), сравнение и EEPROM.write(addr, val) при отличии. Совпадение — и всё, функция вышла: ячейка пережила вызов без единого цикла записи. Время выполнения при совпадении — время чтения, микросекунды; при отличии — те же 3,3 миллисекунды, что у write.
Почему же write изнашивает ячейку даже при записи того же значения? Потому что запись — это всегда физический цикл: стереть карман заряда, запрограммировать заново. Электронике всё равно, изменился байт или нет: оксидный слой отработал проход и понемногу состарился. Гарантированные 100000 циклов — нижняя граница по даташиту; реальные ячейки в любительских тестах живут дольше паспорта, но считать проект надо по минимуму: запас, который «наверняка есть», — не запас.
В библиотеке EEPROM есть и пара put/get: EEPROM.put(addr, переменная) копирует в память переменную любого размера, раскладывая её по байтам, и внутри вызывает тот же update — бережная запись зашита и туда. EEPROM.get(addr, переменная) собирает обратно. В симуляторе put и get не воспроизводятся, поэтому здесь мы работаем тремя базовыми командами; на реальной плате put — правильный выбор для настройки целиком.
Из этого следует неожиданный вывод про скорость: в горячем цикле, где значение почти всегда прежнее, update не только бережёт ячейку, но и быстрее write — чтение микросекунды против записи в 3,3 мс. Экономия идёт по двум статьям сразу: ресурс и время loop.
| Способ | Поведение при том же значении | Когда выбирать |
|---|---|---|
| EEPROM.write | Полный цикл записи, ресурс минус один | Значение точно новое — сразу после собственного сравнения |
| EEPROM.update | Только чтение, ячейка не трогается | По умолчанию; правило: сомневаешься — update |
| EEPROM.put | То же, что update, но берёт переменную любого размера | Настройка целиком: структура или несколько полей (реальная плата) |
const int addrCnt = 0; // счётчик событий int lastUroven = -999;int zapisano = 0;unsigned long virtualWrite = 0;unsigned long otchet = 0; void setup() { Serial.begin(9600); int n = EEPROM.read(addrCnt); if (n == 255) { n = 0; } Serial.print("sobytiy_v_pamyati="); Serial.println(n);} void loop() { int uroven = analogRead(A0) / 4; virtualWrite = virtualWrite + 1; // циклов, которые сжёг бы write в loop if (abs(uroven - lastUroven) > 4) { lastUroven = uroven; int n = EEPROM.read(addrCnt); if (n == 255) { n = 0; } n = n + 1; EEPROM.update(addrCnt, n); // ячейка тратится только здесь zapisano = zapisano + 1; Serial.print("sobytie="); Serial.println(n); } if (millis() - otchet > 3000) { otchet = millis(); Serial.print("write_by_zapisal="); Serial.print(virtualWrite); Serial.print(" update_zapisal="); Serial.println(zapisano); } delay(200);}Смотрите на два счётчика. virtualWrite растёт на каждом проходе — ровно так вёл бы себя write, посаженный в loop без всяких условий: пять проходов в секунду, за минуту около трёхсот, за сутки 432000 циклов. Ресурс ячейки в сто тысяч закончился бы к вечеру следующего дня — моя поливалка с её раз в десять секунд прожила дольше только потому, что писала реже.
zapisano растёт только при событии — когда ручку A0 реально крутанули сильнее порога. За минуту неподвижной ручки это ноль записей против трёхсот виртуальных. Разница между двумя счётчиками на экране — буквально сэкономленные циклы, и при типичной жизни прибора экономия измеряется в сотни раз. Порог в четыре единицы — это меньше полутора процентов полного диапазона ручки: дрожь внутри порога на запись не влияет, попадает только честное движение.
delay(200) здесь — учебная рамка, чтобы счётчики ползли наглядно. В настоящем скетче поливалки так же плохо, только наоборот: delay глушит всю остальную логику. Цикл без блокировок собирается на millis(), как разобрано в уроке про millis вместо delay, — и тогда счётчик износа из этого урока встраивается один в один.
Правила бережной записи
Правило первое — событие вместо расписания. Сохраняйте при нажатии кнопки, при изменении значения, при переходе порога — но не «каждые N секунд» по таймеру. Гистерезис из урока про ночник и температурный гистерезис термостата — тот же принцип, повернутый на запись: значение дрожит у порога, а ячейка должна видеть только честные переходы, а не дрожь.
Правило второе — чередование ячеек, мини-версия wear leveling. Счётчик пишется не в одну ячейку, а по кругу из десяти адресов: ячейка 0, потом 1, потом 2 и так до 9 и снова в 0. Ресурс умножается на десять: при записи раз в минуту одна ячейка живёт 69 дней, круг из десяти — почти два года. Чтение тогда тоже идёт по кругу: последней считается ячейка с наибольшим записанным числом, и никакого массива из десяти байтов не понадобится — только та же арифметика остатка.
Правило третье — честная классификация данных. Настройка меняется руками раз в день — ей в EEPROM. Счётчик тикает раз в минуту — в EEPROM с чередованием. Показания каждую секунду — уже не EEPROM: поток уезжает на SD-карту в уроке про логирование, а в EEPROM остаётся только «последнее значение на случай сбоя», обновляемое редко. Многобайтовые значения раскладываются по двум ячейкам, как в уроке про счётчик включений.
И мера пользы, которой я пользуюсь перед каждым EEPROM.write: сколько раз в сутки эта строка исполнится? Делим 100000 на это число — получаем срок жизни ячейки в сутках. Меньше пары тысяч дней — переписываем на update и ищем событие. Правило простое до примитива, но оно заставило меня переписать три собственных проекта.
- Оставьте ручку A0 неподвижной на две минуты: write_by_zapisal растёт, update_zapisal стоит. Умножьте разницу на 720 и получите суточный счёт циклов для write-варианта.
- Замените update на write и повторите наблюдение: счётчики сравняются — вы только что воспроизвели сценарий смерти ячейки поливалки.
- Сузьте порог события с 4 до 1: событий станет заметно больше. Прикиньте, при каком пороге ресурс ячейки кончится быстрее всего.
- Добавьте чередование: пишите счётчик по кругу адресов 0–9 и напечатайте, во сколько раз вырос общий ресурс записи.
- 1. Чем EEPROM.update(addr, val) отличается от EEPROM.write(addr, val)?
- 2. Ячейку пятьдесят раз записывают одним и тем же значением через write(). Сколько циклов ресурса потратится?
- 3. Счётчик пишут раз в 10 секунд. Когда кончится ресурс 100000 циклов?
- 4. Чем EEPROM.put() отличается от update()?
- 5. Поливалка пишет показание влажности раз в 10 секунд. Первая помощь ячейке?
- EEPROM.update = read + сравнение + write при отличии; при совпадении ячейка не тратит ни одного цикла
- write сжигает цикл записи даже на том же самом значении — физика не смотрит на содержание
- Раз в 10 секунд = 8640 записей в сутки = смерть ячейки за 11,6 дня; пишите по событию и с гистерезисом
- put() и get() — обёртки над update для переменных любого размера (реальная плата)
- Чередование ячеек по кругу умножает ресурс на число адресов: десять ячеек при записи раз в минуту — почти два года
- Срок жизни ячейки в сутках = 100000 / число записей в сутки; считайте перед каждой новой строкой EEPROM.write
Теперь у вас полный комплект бережной записи: update вместо write, события вместо расписания, гистерезис вместо дрожи и чередование ячеек на вырост. Осталась одна честная граница: EEPROM — про настройки и счётчики, а не про потоки. Как только данных становится больше пары тысяч записей в сутки, дорога ведёт на SD-карту — там ни ресурса ячеек, ни потолка в 1024 байта.
И напоследок привычка, которая окупается всегда: перед каждой новой строкой с EEPROM.write в чужом или своём коде задавайте один вопрос — сколько раз в сутки это исполнится? Ответ меньше десятка — пишите спокойно. Ответ в сотнях — значит, вы смотрите на будущую мёртвую ячейку, и рядом обязательно лежит событие, которого здесь не хватает.
Частые вопросы по теме урока
- Чем EEPROM.update(addr, val) отличается от EEPROM.write(addr, val)?
- update = чтение + сравнение + запись при отличии. Совпало — ячейка не трогается: ни цикла записи, ни 3,3 мс ожидания. Перед записью читает ячейку и пишет только при отличии
- Ячейку пятьдесят раз записывают одним и тем же значением через write(). Сколько циклов ресурса потратится?
- Физика записи не смотрит на содержание: стереть и запрограммировать заново — полный цикл, даже если байт тот же. Так write и сжигает ресурс впустую. Пятьдесят — каждая запись это полный цикл
- Счётчик пишут раз в 10 секунд. Когда кончится ресурс 100000 циклов?
- 8640 записей в сутки: 100000 / 8640 = 11,6 дня. Поэтому пишут по событию и update-ом, а не по секундному расписанию. Примерно через 11,6 суток
- Чем EEPROM.put() отличается от update()?
- put раскладывает переменную любого типа по байтам и для каждой ячейки выполняет бережную запись update. В симуляторе put не воспроизводится — там три базовые команды. Принимает переменную любого размера и внутри вызывает тот же update
- Поливалка пишет показание влажности раз в 10 секунд. Первая помощь ячейке?
- Ресурс определяется частотой реальных записей. Гистерезис + update + событие вместо расписания сокращают записи в сотни раз. Писать по изменению значения через update, а лучше по редкому событию