Термостат в гараже пережил первую же грозу: рубильник на столбе щёлкнул ровно в тот вечер, когда плата переписывала уставку с 19 на 24 градуса. Свет вернулся через минуту, плата поднялась и заработала — на 41 градусе. В ячейке уставки оказалась рваная смесь старого и нового значения, гистерезис заодно тоже поплыл, и отопление честно грелось, пока я не увидел счёт за электричество. С тех пор любой блок настроек в EEPROM у меня уходит на старт со сверкой контрольной суммы, и эта история — лучший её рекламный ролик.
Идея проще, чем звучит: по полям блока считается короткая контрольная сумма — один байт — и записывается рядом с ними же, последним байтом блока. При старте скетч считает сумму заново по тому, что реально лежит в ячейках, и сравнивает с записанной. Совпало — настройки пережили отключение целыми. Разошлось — блок забракован, ему нет веры: заливаем дефолты и переписываем всё заново. Никакой магии, одна арифметика над байтами, зато она ловит то, что не ловит ни magic-байт, ни проверка диапазона по отдельности.
Про сами ячейки и дисциплину записи мы договаривались в уроке-знакомстве с EEPROM, многобайтные значения по соседним адресам раскладывали в уроке про int и float. Здесь добавляется последний этаж — блок как единое целое. Соберём функцию CRC8, разберём, почему она замечает перевёрнутый бит в любом месте блока, и устроим в симуляторе тренажёр обрыва питания: с кнопкой, которая портит конфиг у вас на глазах.
Что обрыв делает с блоком
Запись одной ячейки занимает около 3,3 мс — цифру мы мерили ещё в первом уроке раздела. Блок из шести байтов пишется почти двадцать миллисекунд, и всё это окно — мишень: питание пропадает в середине, часть ячеек получает новые значения, часть остаётся со старыми. На свежей плате картина ещё нагляднее: пустая EEPROM читается как 255, поэтому блок, оборванный на третьем байте, выглядит как три нормальных поля и хвост из 255. Читать это будет без всяких нареканий — плата не отличит рваную запись от законченной, пока вы сами не попросите её сверить арифметику.
Второй источник порчи — не обрыв, а возраст. Ячейка, выработавшая ресурс около 100000 циклов записи, начинает врать непредсказуемо, и бит в середине блока переворачивается сам, без всякой грозы. Сверка на старте ловит это до первого решения: термостат не греет гараж до 41 градуса, он сначала проверяет байты против чека. Третий случай — мусор от чужой прошивки, писавшей в те же адреса другую раскладку; об этом честнее скажет байт версии, и ему посвящён следующий урок.
Претендентов на роль суммы три: арифметическая сумма по модулю 256, XOR всех байтов и CRC8. Сумма и XOR стоят в одну строчку кода, но чётные перевороты у XOR гасятся парами, а у суммы компенсируются встречными плюсами и минусами. CRC8 — это деление потока байтов на полином 0x07, остаток умещается в один байт: одиночный перевёрнутый бит меняет остаток гарантированно, а случайный мусор проходит проверку с шансом примерно один из 256. Протоколы связи десятилетиями живут на этом приёме — мы разбирали его в уроке про контрольную сумму в протоколе; здесь поток свой, внутренний, но арифметика та же.
#include <EEPROM.h> // Раскладка блока: 10 уставка, 11 гистерезис, 12 режим, 13 CRC8.// Сумма считается по адресам 10-12 и лежит последним байтом блока. const int A_UST = 10;const int A_GIS = 11;const int A_REZH = 12;const int A_CRC = 13; int crc8(int first, int last) { int c = 0; for (int a = first; a <= last; a++) { c = c ^ EEPROM.read(a); for (int b = 0; b < 8; b++) { if ((c & 128) > 0) { c = (c << 1) ^ 7; // полином 0x07 } else { c = c << 1; } c = c & 255; } } return c & 255;} void zapisiDefolty() { EEPROM.update(A_UST, 210); EEPROM.update(A_GIS, 2); EEPROM.update(A_REZH, 1); EEPROM.update(A_CRC, crc8(A_UST, A_REZH)); // сумма после полей Serial.println("blok zabrakovana: zapisany defolty + svezhaya summa");} void setup() { Serial.begin(9600); int schitali = crc8(A_UST, A_REZH); int lezhit = EEPROM.read(A_CRC); Serial.print("crc schitali="); Serial.print(schitali); Serial.print(" lezhit="); Serial.println(lezhit); if (schitali != lezhit) { zapisiDefolty(); }} void loop() {}Почему CRC в конце блока и что она ловит
Функция crc8() проходит по адресам от первого поля до последнего и на каждый байт делает восемь шагов: сдвиг влево и, если старший бит был единицей, XOR с полиномом 7. На выходе — остаток от деления всего потока на полином, те самые 8 бит. Сам байт суммы в расчёт не входит: он лежит за последним полем, и если бы мы включили его в проход, сумма сверяла бы сама с собой — классическая ловушка, о которую спотыкается половина самодельных хранителей настроек.
Куда класть сумму — в начало блока или в конец — на форумах спорят десятилетиями, и явного победителя я не вижу: главное — держать её вне диапазона расчёта и не путать адреса. Я кладу в конец, потому что тогда запись блока выглядит одной последовательностью: поля, последним — сумма. Проверка при старте двухэтажная: сначала magic-байт отсеивает чистую память, потом сумма решает судьбу блока. Она не замена маркеру, а второй барьер: на свежей памяти из 255 контрольная сумма от трёх полей-255 совпадёт с записанной 255-кой в одном случае из 256, и остаток шанса закрывает именно маркер. По отдельным полям сумму мы уже считали в уроке про целостность настроек — там же сравнение XOR против CRC8; здесь блок взят целиком, без разбиения, потому что адреса полям даёт раскладка из урока про структуру-конфиг.
Что происходит после отбраковки, зависит от характера устройства, а моё правило скучное: настройки чинят себя сами, поведение — нет. Залить дефолты, переписать блок со свежей суммой, напечатать причину в мониторе — и жить дальше. Термостат вернётся к 21 градусу, полив — к расписанию по умолчанию. Худший вариант — пропустить сверку и поверить байтам на слово: рваная запись читается без единого симптома ровно до первого последствия.
| Способ | Ловит | Что пропускает |
|---|---|---|
| Сумма по модулю 256 | Одиночный перевёрнутый бит | Встречные перевороты: плюс восемь и минус восемь компенсируются |
| XOR всех байтов | Одиночный перевёрнутый бит | Любые парные перевороты: чётность гасится |
| CRC8 с полиномом 0x07 | Одиночный бит всегда; случайный мусор — 255 из 256 | Подобранный блок: защита от сбоя, не от злоумышленника |
| Вторая копия блока рядом | Любую порчу одной из копий | Обрыв ровно в момент записи обеих копий — редкий, но недешёвый случай |
Тренажёр порчи
Честно о границах: движок книги в этом уроке EEPROM не трогает — блок настроек живёт в переменных, и на арифметике это не сказывается: crc8() считает по массиву ровно те же шаги, что по ячейкам. Кнопка на D2 играет роль обрыва питания: одно нажатие — и в поле уставки переворачивается четвёртый бит, будто запись оборвалась на полбайта. LED на D12 — вердикт проверки: горит, пока блок сходится с чеком, гаснет в момент расхождения.
Нажмите кнопку и смотрите в монитор: строка с двумя суммами — посчитанной и лежащей — это вся диагностика, которая нужна на реальной плате. Нажмите второй раз: бит вернётся на место и блок снова сойдётся; в жизни так не бывает — порченый байт сам не восстанавливается, но для тренажёра удобно. Дальше перенесите скетч из code-блока выше на настоящую плату: функции совпадают буква в букву, меняется только источник байтов — EEPROM.read вместо массива.
- Запись блока из N байтов держит окно уязвимости около N на 3,3 мс: обрыв внутри оставляет смесь старых, новых и чистых 255
- CRC8 с полиномом 0x07: восемь шагов сдвиг-XOR на байт, остаток в один байт; одиночный перевёрнутый бит меняет его всегда
- Байт суммы кладётся последним и в собственный расчёт не входит — иначе проверка пропустит что угодно
- Проверка двухэтажная: magic-байт отсеивает чистую память, сумма решает судьбу блока; случайный мусор проходит один из 256
- Отбраковка — это заливка дефолтов, перезапись блока со свежей суммой и строка причины в мониторе
- 1. Обрыв питания случился в середине записи блока из шести байтов. Что вернёт чтение этих ячеек после включения?
- 2. Чем CRC8 с полиномом 0x07 лучше суммы по модулю 256 для блока настроек?
- 3. Байт CRC8 лежит в ячейке сразу после последнего поля. Почему он не входит в собственный расчёт?
- 4. На совершенно новой плате весь блок — 255, и записанной суммы нет: тоже 255. Что спасает от ложного «всё сходится»?
- 5. Сверка на старте разошлась: блок забракован. Какой порядок действий рабочий?
Домашнее задание короткое: добавьте crc8() в свой текущий проект и посчитайте сумму по своим полям — один байт и двадцать строк кода закрывают целый класс тихих сбоев. Если расхождение случилось один раз, это сбой питания и проверка отработала; если сходится с боем каждое включение — первым делом сверяйте раскладку: чаще всего два блока пересекаются адресами. Если расхождение случилось один раз, это сбой питания и проверка отработала; если сходится с боем каждое включение — первым делом сверяйте раскладку: чаще всего два блока пересекаются адресами. Для внешней EEPROM на 24C256 приём тот же, диапазон только шире; как хранить там копии блока, разобрано в уроке про резервные копии на внешней памяти. Сумма отвечает на вопрос «блок цел?», но не на вопрос «тот ли это блок по раскладке» — за вторым ответом отправляйтесь в урок про маркер в нулевом адресе и версию структуры.
Частые вопросы по теме урока
- Обрыв питания случился в середине записи блока из шести байтов. Что вернёт чтение этих ячеек после включения?
- Запись идёт по ячейкам по 3,3 мс и не атомарна: часть байтов успела, часть нет, недописанные на чистой плате читаются как 255. Опасно именно тем, что читается без единого симптома. Смесь старых, новых и чистых 255 — без нареканий
- Чем CRC8 с полиномом 0x07 лучше суммы по модулю 256 для блока настроек?
- Сумма наивна: плюс восемь в одном байте и минус восемь в другом дают тот же результат. CRC8 проходит блок как единый поток, и встречные одиночные перевороты меняют остаток. Ловит то, что гасится в сумме: встречные перевороты битов компенсируются
- Байт CRC8 лежит в ячейке сразу после последнего поля. Почему он не входит в собственный расчёт?
- Чек, включённый в проход, всегда сходится с самим собой: проверка вырождается. Поэтому сумма кладётся вне диапазона расчёта — последним байтом блока. Иначе сумма сверяет сама с собой и пропускает любой мусор
- На совершенно новой плате весь блок — 255, и записанной суммы нет: тоже 255. Что спасает от ложного «всё сходится»?
- Сумма от полей-255 совпадает с записанной 255 в одном случае из 256 — остаток шанса закрывает маркер: у чистой памяти его нет. Сначала маркер, потом сумма. Magic-байт как первый барьер проверки
- Сверка на старте разошлась: блок забракован. Какой порядок действий рабочий?
- Настройки чинятся сами и молча, строка в мониторе оставляет след для вас. Вера в байты без чека возвращает проблему на следующий старт, полная зачистка — вдвое дороже и не нужна. Залить дефолты, переписать блок со свежей суммой, напечатать причину