Первая структура в EEPROM пишется легко — EEPROM.put по адресу ноль, и готово. Вторая начинается, когда записей становится несколько: три будильника полива, пять профилей термостата, десять точек калибровки. Мой первый подход был честным варварством: отдельный адрес под каждое поле каждой записи, вручную, арифметика в уме — и, конечно, однажды я поправил будильник два по адресу будильника три. Выход из этого тупика один: записи обязаны быть однотипными, а адрес вычисляться формулой. Один вызов put для одной структуры разобран в уроке про put и get — здесь надстраивается этаж: массив структур, где адрес каждого элемента считается, а не помнится.
Схема такая: struct Slot из четырёх полей byte — busy, weekday, hour, minute; восемь слотов с базы 10. Адрес слота i — база плюс i умножить на sizeof(Slot), то есть 10, 14, 18, 22 и дальше. Поле busy — маркер занятости: 1 — слот живёт, 0 — свободен, 0xFF — свежая память, тоже свободна. Запись, чтение и удаление работают по одному и тому же адресу, и ни один слот физически не может задеть соседний — арифметика исключает это по построению, что проверялось в уроке про адресную арифметику массива.
Слот как единица: put, get и выравнивание
Структуру я специально собрал из четырёх byte подряд: такое поле не имеет дырок выравнивания, sizeof(Slot) ровно 4, и адресная арифметика прозрачна как стекло. Как только в структуре появляется int или float, компилятор вставляет прокладки, sizeof растёт, а мусор в дырках ломает контрольные суммы — эта грабля целиком разобрана в уроке про выравнивание и sizeof, и мой совет тот же: пока вы не уверены, держите слоты байтовыми. Четыре байта на будильник хватает, а тысяча ячеек Uno вмещает двести пятьдесят таких слотов — больше, чем вы осмысленно используете.
Запись слота выглядит как EEPROM.put(база + i * sizeof(Slot), slot) — одна строка. Важная деталь под капотом: put на AVR работает через update, то есть перезаписывает только изменившиеся байты — правка часа в слоте три трогает ровно один байт, и ресурс соседних ячеек не тратится. Чтение — EEPROM.get по тому же адресу в такую же структуру. Удаление — не стирание, а установка busy в ноль: следующая запись в этот слот перезапишет всё, а стирание байт по байтам ресурса не бережёт.
Поиск свободного слота — цикл из восьми get: busy равен 1 — занято, всё остальное — свободно. Мой порядок поиска — с нуля, «первый свободный», и для будильников этого достаточно; если вам важно равномерно изнашивать слоты, ищите свободный начиная с последнего использованного индекса — идея та же, что в кольцевом журнале из урока про износ и ресурс, только масштаб — слоты, а не байты.
#include <EEPROM.h> struct Slot { byte busy; // 1 - занят, 0 - свободен, 0xFF - свежая память byte weekday; // 0..6 byte hour; // 0..23 byte minute; // 0..59}; const int BASE = 10; // слоты живут с ячейки 10const int N = 8; // восемь слотовconst int SLOT_SIZE = sizeof(Slot); // ровно 4 байта: четыре byte подряд int slotAddr(int i) { return BASE + i * SLOT_SIZE; // вся арифметика в одном месте} int findFree() { for (int i = 0; i < N; i = i + 1) { Slot s; EEPROM.get(slotAddr(i), s); if (s.busy != 1) return i; // 0 или 0xFF - свободен } return -1;} bool addSlot(byte wd, byte hh, byte mm) { int i = findFree(); if (i < 0) { Serial.println("[слоты] свободных нет"); return false; } Slot s; s.busy = 1; s.weekday = wd; s.hour = hh; s.minute = mm; EEPROM.put(slotAddr(i), s); // put внутри работает как update Serial.print("[слоты] записан слот "); Serial.print(i); Serial.print(" по адресу "); Serial.println(slotAddr(i)); return true;} void dumpSlots() { for (int i = 0; i < N; i = i + 1) { Slot s; EEPROM.get(slotAddr(i), s); Serial.print("слот "); Serial.print(i); Serial.print(" @"); Serial.print(slotAddr(i)); Serial.print(": busy="); Serial.print(s.busy); Serial.print(" дн="); Serial.print(s.weekday); Serial.print(" "); Serial.print(s.hour); Serial.print(":"); if (s.minute < 10) Serial.print("0"); Serial.println(s.minute); }} void setup() { Serial.begin(9600); Serial.print("sizeof(Slot) = "); Serial.println(SLOT_SIZE); // проверка: ровно 4, без дырок dumpSlots(); addSlot(1, 7, 30); // понедельник, 07:30 addSlot(3, 22, 5); // среда, 22:05 dumpSlots();} void loop() {}В модели видно всё, что на плате спрятано внутри put: каждая запись — четыре байта по соседним адресам, дамп показывает раскладку 10, 14, 18, 22 и busy-маркеры, а клавиша занятости работает по формуле «первый свободный». Удаление в модели не показано, чтобы не раздувать скетч, — на плате это writeSlot(i, 0, 0, 0, 0): busy в ноль, слот свободен, следующая запись перезатрёт остатки.
Перенос на плату — прямая замена writeSlot на EEPROM.put и чтений на EEPROM.get, остальное без изменений. Две проверки на старте я держу всегда: печать sizeof(Slot) — если не четыре, в структуре завелись дырки, и печать дампа — по нему сразу видно, что запись слота не задела соседей. И помните про потолок: восемь слотов по четыре байта — тридцать две ячейки, но тот же механизм с базой и sizeof работает и для таблицы на двести слотов, лишь бы адресная арифметика оставалась в одном месте, а не размазывалась по скетчу.
| Задача | Приём | Байт на запись | Грабля |
|---|---|---|---|
| одна структура настроек | put по адресу 0 + magic-байт | sizeof структуры | дырки выравнивания при int/float |
| несколько однотипных записей | слоты: база + i × sizeof | sizeof слота | ручные адреса в уме — забудьте |
| удаление записи | busy = 0, перезапись при новом использовании | 1 | настоящее стирание жжёт ресурс зря |
| равномерный износ слотов | поиск свободного с последнего индекса | — | первый свободный пинает слот 0 всегда |
Куда эта схема растёт дальше. Слоты — идеальный каркас для профилей: четыре профиля термостата по шесть байт, десять точек калибровки по четыре — всё это живёт на одной базе с разными N. Когда записей становится много и они меняются часто, ресурс конкретных ячеек начинает считаться, и тогда слоты переносятся на внешнюю 24C256 с сотней тысяч ячеек — подключение и постраничная запись разобраны в уроке про 24C256. Если записи пишутся сами, без человека, — сначала посчитайте частоту по методике урока про расчёт износа, а потом выбирайте носитель.
И граница между слотами и журналом: слоты — это текущее состояние, «что действует сейчас», и их ровно столько, сколько реально бывает активных записей. Журнал — история с дозаписью в конец, механика в уроке про журнал событий. Смешивать их в одной области памяти я перестал после первого же конфликта: журнал, который ростом задел слоты настроек, чинился вечером и навсегда отбил желание экономить на разметке памяти. Карту раскладки рисуйте до первого put — и пусть она лежит в комментарии шапки скетча.
- 1. Где живёт слот номер 3 при базе 10 и sizeof(Slot) = 4?
- 2. Как опознаётся свободный слот?
- 3. Что происходит внутри EEPROM.put на AVR?
- Адрес слота = база + i × sizeof(Slot), и вся формула живёт в одной функции slotAddr().
- Слоты из одних byte не имеют дырок выравнивания: sizeof честен, арифметика прозрачна.
- Занятость — поле busy: 1 занято, 0 удалено, 0xFF свежая память; удаление — это busy в ноль, не стирание.
- Put на AVR пишет только изменённые байты — правка слота не тратит ресурс соседей.
- Смена состава структуры — это смена раскладки: держите версию в памяти и мигрируйте осознанно.
Частые вопросы по теме урока
- Где живёт слот номер 3 при базе 10 и sizeof(Slot) = 4?
- Адрес слота i = база + i × sizeof: 10 + 3 × 4 = 22. Слоты лежат с шагом в sizeof, поэтому соседей они не задевают по построению. По адресу 22
- Как опознаётся свободный слот?
- Занятость — ровно busy == 1. Ноль ставит удаление, 0xFF лежит в свежей памяти, и обе эти ситуации означают: слот можно занимать. busy равен 0 или 0xFF — свежая память тоже свободна
- Что происходит внутри EEPROM.put на AVR?
- Put на AVR переписывает только отличающиеся байты: правка одного поля трогает одну ячейку, и ресурс соседних не тратится. Модель в уроке повторяет это update'ами по байтам. Только изменившиеся байты — put работает через update