Сигнализацию в гараже я научил сохранять ящик настроек при каждой смене режима с брелока. Режим меняется часто: охрана, продувка, работа, снова охрана. Через месяц я поймал себя на мысли, которую боялся озвучить вслух: я ведь перезаписываю все шесть байт ради одного бита режима — не жгу ли я ячейки впустую? Документация библиотеки успокоила наполовину: большая часть бокса действительно не трогается. Но один байт горел при каждом сохранении, и это был не бит режима. Итог замера тогда вышел такой: из шести байт реально менялись один-два, остальные четыре стояли как вкопанные.
Механика в документации сформулирована коротко: put раскладывает переменную на байты и пишет каждый через update — сравнил с лежащим, записал только если отличается. Update мы разбирали по косточкам в уроке про побайтовую экономию; здесь смотрим, как та же семантика работает внутри контейнерного вызова и что из этого следует скетчам, которые пишут бокс часто. Побайтовая экономия звучит как микрооптимизация, пока не посчитаешь сутки непрерывной работы: на частых записях разница между write и put — это разница между годом и десятилетием.
План: механика и время записи, эксперимент с millis и дампом, понятие горячих байтов — и арифметика на реальном сроке жизни, где выяснится, что умная запись спасает бокс настроек, но не спасает его внутренние счётчики. Отдельно оговорю границы: всё сказанное — про встроенный EEPROM AVR; внешние 24-серии и эмуляция на ESP32 живут по своим правилам, о них были отдельные уроки.
Внутри put стоит update
Внутри put скрыт цикл по sizeof байтов: прочитать ячейку, сравнить, записать при отличии. Совпало — ячейка не трогается вовсе: ни цикла стирания, ни трёх с половиной миллисекунд ожидания. Изменился — полный цикл записи, на ATmega328P это порядка 3,3 мс на байт. Отсюда приятное следствие: время сохранения пропорционально числу изменившихся байтов, и его можно мерить прямо в скетче, без лаборатории. Чтение ячейки — микросекунды, поэтому нетронутый байт стоит почти ничего: вся цена сосредоточена в изменённых. Библиотека делает это прозрачно: размер переменной она берёт из типа, число итераций — из sizeof.
В скетче ниже кнопка на D2 листает режим: каждое нажатие меняет один байт из шести и зовёт put. Монитор печатает длительность сохранения и строку байтов после записи. У меня этот замер стабильно даёт три-четыре миллисекунды на нажатие — один цикл вместо шести. Если бы я писал write-ом весь бокс, цифра держалась бы под двадцать, а каждая ячейка получала бы цикл без пользы. Замер в millis — грубый, но честный: библиотека не возвращает число записанных байтов, зато время о нём рассказывает.
Заодно скетч показывает правило события: сохранение висит на фронте кнопки, а не на уровне. Уровень держится всё нажатие — каждые полсекунды нажатия это пятнадцать проходов loop и пятнадцать вызовов. Проверку фронта мы делали в уроке про счётчик включений; здесь она та же, только вместо инкремента — сохранение бокса. Пятнадцать вызовов вместо одного — пятнадцать лишних циклов на каждую ячейку бокса, если писать по уровню.
#include <EEPROM.h> const int addrBox = 0;const int pinSave = 2; // кнопка на GND, INPUT_PULLUP struct Settings { byte magic; // 170 byte version; // 1 int porog; // два байта byte bright; byte mode;}; // sizeof = 6 на Uno Settings box;int knopkaByla = HIGH; void saveBox() { unsigned long t0 = millis(); EEPROM.put(addrBox, box); // внутри - побайтовый update unsigned long dt = millis() - t0; Serial.print("save: "); Serial.print(dt); Serial.println(" ms"); Serial.print("bajty: "); for (int a = 0; a < 6; a++) { Serial.print(EEPROM.read(a)); Serial.print(" "); } Serial.println("");} void pechat() { Serial.print("porog="); Serial.print(box.porog); Serial.print(" bright="); Serial.print(box.bright); Serial.print(" mode="); Serial.println(box.mode);} void setup() { Serial.begin(9600); pinMode(pinSave, INPUT_PULLUP); EEPROM.get(addrBox, box); if (box.magic != 170) { box.magic = 170; box.version = 1; box.porog = 60; box.bright = 80; box.mode = 0; saveBox(); // первый запуск: дефолты в ячейки } pechat();} void loop() { int knopka = digitalRead(pinSave); if (knopka == LOW && knopkaByla == HIGH) { // фронт нажатия box.mode = box.mode + 1; if (box.mode > 2) box.mode = 0; saveBox(); // изменился один байт - put потратит один цикл } knopkaByla = knopka; delay(20);}Дамп — второй глаз этого эксперимента. Снимите его по образцу из урока про структуру под микроскопом и сравните до и после: после нажатия кнопки изменился ровно один адрес — тот, где живёт режим. Остальные пять байт стоят как стояли, и циклов записи им компилятор не потратил. Один изменившийся адрес из шести — и есть вся экономия, увиденная своими глазами. Строка bajty в скетче показывает то же самое без отдельного дампа.
Теперь про то, где умная запись перестаёт спасать. Контрольная сумма бокса из урока про CRC пересчитывается на каждом сохранении — и почти наверняка отличается от прежней, даже если настройки никто не трогал. Счётчик сохранений, штамп времени, любое поле, которое скетч меняет сам по факту записи, — та же история. Я зову их горячими байтами: put проходит мимо нетронутых полей, но горячие горят по полной программе. В моём гаражном боксе горячим оказался счётчик смен режима, который я добавил «для статистики» — классическая ловушка из примечания ниже.
Лечение у горячих байтов одно — их не должно быть в холодном боксе. Штампы и счётчики выносятся на отдельные адреса или в отдельную структуру, где их циклы видны и считаются; для часто пишущихся журналов есть чередование ячеек из урока про расчёт износа и кольцевая организация из урока про журнал истории. Настройки меняются редко — и бокс под них обязан оставаться холодным. Правило простое: боксы с разной частотой изменений не живут в одной структуре.
| Событие | Что реально перезаписал put | Цена по ресурсу |
|---|---|---|
| Поменялся один байт из шести | только этот байт | один цикл, около 3,3 мс |
| Повторное сохранение без изменений | ни одного байта | ноль циклов |
| Каждое сохранение с CRC в боксе | байты контрольной суммы — всегда | 2-4 цикла на каждое сохранение |
| Тот же бокс write-ом целиком | все шесть байтов | шесть циклов каждый раз |
Горячие байты и срок жизни
Арифметика на сроке жизни. Стенд в магазине: продавец крутит настройки триста раз в сутки. Горячий байт получает 109500 циклов в год — гарантированные даташитом 100000 кончаются на одиннадцатом месяце, и это честный потолок: реальная ячейка живёт дольше паспорта, но проект считают по минимуму. Нетронутые байты бокса при put получают ноль циклов — их ресурс не тратится вообще. Цифра 100000 в даташите идёт в паре с двадцатью годами хранения без питания; в комнатных условиях запас выше, но паспорт есть паспорт.
Тот же стенд с write-ом всего бокса: каждая ячейка получает свои 109500 циклов и к концу года умирает — весь бокс, а не один байт. Разница между put и write здесь не в удобстве, а в выживании пяти полей из шести. Пять нетронутых байт — пять ячеек, которые переживут само устройство: их ресурс не тратился ни разу за весь срок службы. Write не разбирает, какое поле нужно: шесть байт бокса — шесть ячеек с одинаковым приговором. Почему даташитная цифра — нижняя граница, а не приговор, мы считали в уроке про износ; ресурс ячейки при частых записях — отдельная дисциплина.
Проверка на живой плате — три замера, все из этой серии уроков: дамп до и после показывает, какие адреса тронуты; millis вокруг put показывает, во что обошлось сохранение; счётчик изменённых байтов переводит это в циклы. Три числа за вечер дают полную картину износа бокса — без осциллографа и без веры на слово. Включите печать в saveBox — и каждый запуск будет вести хронику: когда, что и во сколько обошлось.
В симуляторе структур и EEPROM нет, поэтому побайтовый пересчёт выписан руками: поля лежат в переменных, сохранённая копия — в массиве, функция сохранения сравнивает байты и печатает, сколько из них пришлось записать. На реальной плате ровно этот пересчёт прячет внутри себя put. Двухбайтовый порог в модели — два вызова сравнения: младший и старший байты считаются отдельно, ровно как внутри put.
Качните ручку порога на A0 — светодиод на D12 зажжётся: есть несохранённое. Кнопка на D2 запускает пересчёт: при изменённом пороге запишутся два байта int, при повторном нажатии без изменений — ни одного. Счётчик циклов в мониторе копится только на реальных записях: та же бухгалтерия, которую ведёт put на плате. LED гаснет на каждом сохранении — даже на холостом: состояние бокса совпало с копией, и скетч честно говорит «сохранил».
- внутри put стоит побайтовый update: совпало — ячейка не трогается, изменилось — полный цикл записи около 3,3 мс
- время сохранения пропорционально числу изменившихся байтов: millis вокруг put показывает экономию в миллисекундах
- горячие байты — CRC, счётчики, штампы — горят при каждом сохранении: update их не спасает
- горячие байты выносят на отдельные адреса: 300 сохранений в сутки — 109500 циклов в год, гарантия 100000 кончается за год
- проверка на плате: два дампа EEPROM.read до и после put плюс millis вокруг сохранения
- 1. Бокс из шести байт сохранили put-ом второй раз подряд без изменений. Сколько циклов записи потрачено?
- 2. Сколько примерно длится сохранение, если изменился один байт?
- 3. Какие байты бокса горят при каждом сохранении, даже когда настройки не менялись?
- 4. Спасёт ли put горячий байт, если его значение чередуется между двумя числами?
- 5. Как на живой плате своими глазами увидеть, что put переписал не весь бокс?
Частые вопросы по теме урока
- Бокс из шести байт сохранили put-ом второй раз подряд без изменений. Сколько циклов записи потрачено?
- put идёт по байтам через update: совпало — ячейка не трогается. Повторное сохранение того же бокса дешевле одного прохода loop. ноль — не совпало ни одного байта
- Сколько примерно длится сохранение, если изменился один байт?
- Изменённый байт — полный цикл записи, на ATmega328P это порядка 3,3 мс. Нетронутые байты стоят только микросекунды чтения. около 3,3 мс — полный цикл записи одного байта
- Какие байты бокса горят при каждом сохранении, даже когда настройки не менялись?
- Горячие байты — те, что скетч сам меняет при каждом сохранении: CRC бокса, счётчик, штамп. Update их не спасает: значение каждый раз новое. контрольная сумма и счётчики — всё, что пересчитывается при каждом save
- Спасёт ли put горячий байт, если его значение чередуется между двумя числами?
- Update сравнивает только с текущим значением. Чередование 0-1-0-1 — запись каждый раз; спасает лишь чередование адресов, а не update. нет: каждый возврат к прежнему числу — новая запись
- Как на живой плате своими глазами увидеть, что put переписал не весь бокс?
- Два дампа рядом показывают, какие адреса изменились. Дополнение — millis вокруг put: время сохранения пропорционально числу новых байтов. снять дамп EEPROM.read до и после put — изменившиеся адреса видны