Сеть из шести датчиков в цеху опрашивала сервер, и каждый узел обязан был назваться при подключении: Котёл, Насос, Стол-2 — иначе на панели путались одинаковые платы. Имя я хранил в EEPROM как переменную String и написал EEPROM.put(0, name) — недолго думая. В ячейках оказался набор байтов, из которых буквы собирались только через раз: после перезапуска узел назывался то «Котёл», то кракозябрами. В уроке про структуры настроек я просил не класть String в put и обещал разобрать, как текст хранить правильно — обещание держу.
Причина короткая: внутри String спрятан указатель на буфер в оперативной памяти, а не сами буквы. put честно записывает байты переменной — то есть адрес и служебные поля, — и в EEPROM уезжает ссылка, которая после перезапуска указывает куда угодно. Текст — это данные известной длины, и хранить его надо так же, как int и float из урока про побайтовую запись: договориться о раскладке и писать байты самому.
Схема урока простая и работает десятилетиями: ячейка 0 — длина строки, ячейки 1..30 — символы по одному байту. Запись — с обрезкой до границы, чтение — с проверкой длины: мусор вроде 255 превращается в пустую строку и заводское имя по умолчанию. В симуляторе EEPROM по-прежнему не эмулируется — модель памяти массив mem; печатайте имя в монитор порта, и модель покажет и длину, и байты, а кнопка устроит мусор в ячейке длины, чтобы проверить валидацию руками.
Байт длины и тридцать ячеек символов
Раскладка: адрес 0 держит длину — сколько символов в имени, адреса 1..30 — сами символы, по байту на букву. Латиница, цифры, дефис и точка — это символы до кода 128, они занимают ровно один байт: у типа char, о котором шла речь в уроке про переменные и типы, диапазон как раз до 127 для букв. Границу в 30 символов я выбрал не с потолка: серверу хватает 12-16 знаков, а 30 оставляют запас под серийники вроде STOL-2-2024-07. Главное — граница живёт в константе MAXLEN, а не в моей голове: в прошлый раз, когда граница жила в голове, имя в 34 символа тихо переписало соседние ячейки.
Запись идёт посимвольно: EEPROM.update(1 + i, name[i]) — String отдаёт символы по индексу, как мы уже брали строки из монитора в уроке про команды из порта. Перед записью длина обрезается до MAXLEN — пользователь вводит что угодно, а память получает ровно 30 и ни байтом больше. А вот длину пишем последней, после всех символов: если питание пропадёт посреди записи, в ячейке длины останется старое значение, и читатель не примет недописанные байты за имя. Для калибровок и денег этого мало — там нужна контрольная сумма из урока про протоколы и CRC, — но для имени устройства порядок «символы, потом длина» закрывает самый частый сценарий сбоя.
Чтение начинается с ячейки длины, и вот здесь прячется главная проверка. Свежая плата вернёт 255 — это не «имя длиной 255 символов», а «здесь ещё не писали». Мусор после сбоя может дать и другие числа больше тридцати. Правило одно: если len больше MAXLEN — длина битая, имя считаем пустым и ставим дефолт, как мы поступали с magic-байтом ещё в первом уроке про настройки. Легально пустой может быть только len равный 0 — его отличаем от мусора и относимся как к заводскому состоянию.
#include <EEPROM.h> const int ADDR_LEN = 0; // ячейка длиныconst int MAXLEN = 30; // максимум символов имени String defName = "UZEL-1"; // заводское имя по умолчанию void saveName(String name) { int len = name.length(); if (len > MAXLEN) { len = MAXLEN; // обрезаем, а не верим пользователю } for (int i = 0; i < len; i++) { EEPROM.update(ADDR_LEN + 1 + i, name[i]); // символ за символом } EEPROM.update(ADDR_LEN, len); // длина пишется самой последней} String loadName() { int len = EEPROM.read(ADDR_LEN); if (len > MAXLEN) { // 255 на свежей плате или мусор после сбоя Serial.println("dlina bitaya - stavlyu defolt"); saveName(defName); // дефолт сразу в память return defName; } String name = ""; for (int i = 0; i < len; i++) { name = name + char(EEPROM.read(ADDR_LEN + 1 + i)); } return name;} void setup() { Serial.begin(9600); pinMode(2, INPUT_PULLUP); pinMode(3, INPUT_PULLUP); Serial.print("imya="); Serial.println(loadName());} void loop() { // кнопки смены имени или приём строки с монитора - по вашей схеме delay(100);}В loadName строка собирается посимвольно: name = name + char(b) — байт из ячейки превращается в символ и доклеивается к результату. Байт и есть ячейка EEPROM, ничего конвертировать не нужно — раскладку мы обсуждали при int и float. Обратите внимание на saveName в ветке битой длины: дефолт не только возвращается, но и сразу записывается в память. После такого самолечения при следующем чтении не приходится гадать — длина либо честная, либо её действительно нет. Я лечу память сразу, потому что однажды три дня ловил узел, который «иногда» называться не хотел: он читал мусорную длину, ставил дефолт в воздух и на следующем опросе снова читал тот же мусор.
Сколько это стоит памяти: 1 + 30 = 31 ячейка из 1024 на Uno — три процента EEPROM за имя. Записей имя требует мало: меняется при монтаже и после, значит ресурс ячеек с учётом update-семантики хватит на десятилетия. Но если переписывать имя при каждом включении ради проверки записи — считайте частоту, как мы делали в уроке про ресурс памяти: ежесекундная запись съест ячейку длины за 27 часов, и никакая аккуратная раскладка этого не отменит.
И строка против массива char. Внутри struct для put мы клали char name[16] — фиксированный массив честно едет внутри ящика настроек. Схема «длина плюс символы» — та же идея без struct: годится и как самостоятельный блок по соседству с журналом, и внутри структуры, если поле length тоже включить в раскладку. Разница только в границе: массив жёстко задаёт размер на этапе компиляции, а MAXLEN можно поднять до 64 без переписывания форматов — ячейки, на которые вырастет блок, должны быть свободны по карте памяти.
| Адрес | Содержимое | Пример для имени PUMP-1 |
|---|---|---|
| 0 | длина строки в символах | 6 |
| 1..6 | коды символов P, U, M, P, дефис, 1 | 80 85 77 80 45 49 |
| 7..30 | пустые ячейки | 255 |
| итого | 31 ячейка на имя до 30 символов | 3 процента EEPROM Uno |
Валидация: мусор в ячейке длины превращается в пустое имя
Соберу проверку чтения в одну таблицу решений. len равный 255 — свежая плата, имени нет, ставим дефолт и пишем его в память. len от 31 до 254 — мусор после сбоя или чужой раскладки: та же реакция, дефолт, но в Serial стоит добавить строку про битую длину, чтобы в журнале остался след. len равный 0 — легально пустое имя: это заводское состояние, отличайте его от мусора, если серверу важна разница. И только len от 1 до 30 читается посимвольно в имя. Четыре ветки — и ни одна не оставляет плату с выдуманной строкой из чужих байтов.
И разделите в голове проверку и лечение. Валидация — про то, как читать: сверить длину с границей и не поверить чужим байтам. Лечение — про то, что писать обратно: у меня в скетче это немедленный saveName с заводским именем, но в другом проекте лечение может быть просто предупреждением в Serial и продолжением работы с пустым именем — если сервер спокойно принимает анонимов, лечить память на месте незачем. Выбор за вами, но сделать его надо один раз и записать в комментарии: без этого через полгода в коде появятся оба поведения одновременно.
С этим уроком трёхчастная конструкция складывается целиком: кольцевой журнал из первого урока пишет последние события, кнопка из второго выдаёт историю, а имя из этого урока различает узлы между собой. Вместе они образуют паспорт устройства: кто это, что с ним случилось и когда. Дальше по курсу эти блоки переезжают в проекты как есть — раскладка «длина плюс символы» и кольцо с head не требуют переделок под другой скетч, только свободные адреса по карте памяти.
- Текст в EEPROM: ячейка 0 — длина, дальше символы по байту; латиница — 1 байт на букву, кириллица UTF-8 — 2
- Граница длины живёт в константе MAXLEN: 30 символов плюс байт длины — 31 ячейка из 1024 на Uno
- String напрямую не пишут — внутри ссылка на SRAM; только посимвольно через update, символ за символом
- Длину пишем последней: обрыв питания посреди записи не превратит недописанные байты в имя
- Чтение с проверкой: len больше MAXLEN — имя пустое, дефолт ставится и сразу записывается в память
- Журнал с кольцом плюс имя узла — паспорт устройства: что это, что с ним случилось и когда
- 1. Что лежит в ячейке 0 при хранении текста по схеме урока?
- 2. Имя из 30 символов — сколько ячеек EEPROM оно займёт?
- 3. Читаем длину и получаем 255. Что это значит?
- 4. String из пяти русских букв — что вернёт length()?
- 5. Почему длину строки записывают после всех символов?
Этими тремя уроками раздел собрал полный набор для энергонезависимой логики: настройки и счётчики из первых уроков, кольцевой журнал, отчёт по кнопке и текст с проверкой. Дальше эти блоки работают как конструктор: берёте карту памяти, расставляете адреса — и плата помнит и то, что ей настроили, и то, что с ней случилось, и как её зовут. Проверьте свой последний проект: если он хранит хоть что-то из этого списка в переменных — самое время переселить байты в ячейки, пока не случился первый сбой питания, который всё расставит по местам без вашего участия.
Частые вопросы по теме урока
- Что лежит в ячейке 0 при хранении текста по схеме урока?
- Байт длины идёт первым: он говорит читателю, сколько ячеек дальше занимает имя. Без него текст пришлось бы искать по завершающему нулю или маркеру. Длина строки в символах
- Имя из 30 символов — сколько ячеек EEPROM оно займёт?
- Ячейка длины плюс 30 ячеек символов — 31. Завершающий ноль в этой схеме не хранится: длина уже известна из первого байта. 31 — плюс байт длины
- Читаем длину и получаем 255. Что это значит?
- Свежая EEPROM читается единицами — 255. Имя длиннее 30 символов схема не пишет вообще, значит это либо первый запуск, либо мусор: в обоих случаях ставим дефолт. Ячейка свежая, имени тут нет — ставим дефолт
- String из пяти русских букв — что вернёт length()?
- Строки в Arduino хранятся в UTF-8, русские буквы двухбайтовые. Пять букв — десять байт, и в ячейки с границей 30 поместится только 15 русских букв. 10 — кириллица в UTF-8 занимает два байта на букву
- Почему длину строки записывают после всех символов?
- Длина — ключ чтения: пока она не записана, читатель работает со старым значением и не примет частично обновлённые байты за новое имя. Обрыв питания посреди записи оставит старую длину — недописанные байты не сойдут за имя