Десять самодельных плат термостата уехали к друзьям и в мастерскую: прошивка у всех одна, корпуса одинаковые, этикетки никто не клеил. Через полгода вернулась одна «с глюками» — и я не мог даже сказать, какая именно из десяти: журнал вести было нечем, платы без паспорта. Заводской номер — не бюрократия, а способ через год отличить плату от платы, когда они близнецы и всё, что их различает, должно жить внутри самой платы, а не на приклеенной бумажке, которая отклеится первой.
К серийнику требования жёстче, чем к настройкам. Настройку можно перезаписать, перекалибровать, сбросить — это её обычная жизнь. Номер должен быть написан один раз и пережить всё: обновления прошивки, переделки макетки, чужие любопытные руки. Поэтому сердце скетча — не генерация номера, а замок, который делает повторную запись недостижимой даже самому невнимательному владельцу.
Разберём: magic в роли замка — и чем эта роль отличается от привычной проверки чистой памяти, откуда на плате без генератора случайных чисел берётся случайность, как печатать номер в Serial при каждом старте и как выглядит вечер производства из десяти плат с чайником.
Magic как замок, а не флажок
Magic-байт в настройках играл роль флажка: не нашёл — считай память чистой, пиши дефолты; нашёл — читай. Здесь та же ячейка работает замком: если в нулевой ячейке лежит 165, ветка с записью не выполняется вовсе — не «записывает то же самое», а именно не трогает память ни одним байтом. Изменить номер можно только намеренной перезаписью или процедурой вроде заводского сброса, и то сброс лечит настройки, а паспорт платы у этого сброса спрашивать некому.
Откуда случайность: у ATmega328P нет генератора случайных чисел, зато плавающий аналоговый вход ловит наводки от сети, блоков питания и пальцев — код на неподключённом A5 гуляет от запуска к запуску. randomSeed(analogRead(A5)) превращает этот шум в зерно, random(100000, 1000000) — в шестизначный номер, который влезает в unsigned long с огромным запасом. Аналоговый вход как источник энтропии — приём известный, с честной оговоркой: энтропии в одном чтении немного, и к концу урока я покажу, где её перестаёт хватать.
Номер уезжает в ячейки 1-4 младшим байтом вперёд: при чтении — умножение на 256UL, 65536UL и 16777216UL, как у счётчика ватт-часов из этого же раздела; сдвиги на 24 бита я не использую по той же причине — int на AVR шестнадцатибитный и старшие разряды срезает. Пять занятых ячеек из 1024 — капля в карте памяти: настройки живут с десятого адреса и с паспортом не пересекаются. Вывод в Serial при каждом старте — вторая половина ценности номера: строка SN=482913 печатается в монитор разработчика и в журнал приёмки, а пара имя-значение — та же дисциплина читаемости, что в первом уроке про монитор порта.
#include <EEPROM.h> const int PIN_BTN = 2; // нажатие = выключили и включили питаниеconst int PIN_LED = 12; // вспышка — прошит новый номер const byte ADDR_MAGIC = 0; // 165 — серийный номер уже прошитconst byte ADDR_SN0 = 1; // номер: четыре байта 1-4 unsigned long sn = 0;int cellsWritten = 0; // сколько ячеек тронули за весь запуск void proshivkaNomer() { int m = EEPROM.read(ADDR_MAGIC); if (m == 165) { sn = (unsigned long)EEPROM.read(ADDR_SN0) + (unsigned long)EEPROM.read(ADDR_SN0 + 1) * 256UL + (unsigned long)EEPROM.read(ADDR_SN0 + 2) * 65536UL + (unsigned long)EEPROM.read(ADDR_SN0 + 3) * 16777216UL; Serial.print("SN="); Serial.print(sn); Serial.print(" (uzhe v pamyati, zapisi: 0)"); return; // замок закрыт: память не трогаем } // чистая память: номер из шума плавающего входа A5 randomSeed(analogRead(A5)); sn = random(100000, 1000000); EEPROM.update(ADDR_SN0, sn & 255); EEPROM.update(ADDR_SN0 + 1, (sn >> 8) & 255); EEPROM.update(ADDR_SN0 + 2, (sn >> 16) & 255); EEPROM.update(ADDR_SN0 + 3, (sn >> 24) & 255); EEPROM.update(ADDR_MAGIC, 165); cellsWritten += 5; Serial.print("SN="); Serial.print(sn); Serial.print(" (NOVYJ, zapisano yacheek: "); Serial.print(cellsWritten); Serial.print(")"); digitalWrite(PIN_LED, HIGH); delay(150); digitalWrite(PIN_LED, LOW);} void setup() { Serial.begin(9600); pinMode(PIN_LED, OUTPUT); pinMode(PIN_BTN, INPUT_PULLUP); Serial.println("--- plata vkluchena ---"); proshivkaNomer();} void loop() { int btn = digitalRead(PIN_BTN); if (btn == LOW) { delay(200); Serial.println(); Serial.println("--- vyklyuchili i vkluchili pitanie ---"); proshivkaNomer(); delay(200); } delay(30);}Прошивка трогает пять ячеек — четыре байта номера и magic — и делает это один раз за жизнь платы: пять циклов из 100 000 паспортных, вопрос износа здесь даже не обсуждается. Каждый следующий старт читает номер и печатает его с пометкой «записей: 0» — счётчик ячеек в скетче для наглядности стенда, на серийной плате он лишний: замок и так закрыт, а строка о новом номере просто не появится.
Кнопка D2 в стенде изображает выдёргивание питания: нажатие перезапускает инициализацию, и в мониторе снова та же строка SN — номер никуда не делся, ни одной записи не случилось. Светодиод вспыхивает ровно в тот момент, когда ячейки реально писались: на производстве по нему видно, что в этот пуск плата получила паспорт, а не просто поздоровалась с монитором.
Номер обретает смысл только в паре с журналом приёмки, и журнал этот стоит скромной таблицы: дата, SN, версия прошивки, кто паял, какие датчики куда ставились. Через год по SN=317204 вы достанете всю историю платы — на каком она объекте, что с ней делали, какой у неё был класс вранья до калибровки. Без журнала номер — просто красивая строка в памяти; с журналом — ключ, по которому достаётся вся память о приборе.
Механика «пишем редко и один раз» у EEPROM не одна. Счётчик перезагрузок из урока про счётчик перезагрузок растёт по тихому событию и тоже живёт годами; прошивка номера — его вырожденный случай, счётчик, который не растёт никогда. Общее у паттернов одно: запись по редкому событию, а не по таймеру, — и потому ресурс памяти в них никогда не бывает узким местом.
| Шаг | Действие | Что в EEPROM | Записей ячеек |
|---|---|---|---|
| Первое включение платы | скетч видит пустой magic, генерирует и пишет номер | magic 165 + 4 байта номера | 5 на плату |
| Проверка | перезапуск питания, Serial печатает тот же SN | без изменений | 0 |
| Гарантийный случай | по SN из журнала находят плату и её историю | без изменений | 0 |
| Обновление прошивки | заливают новую версию скетча | номер переживает: EEPROM не тронута | 0 |
Вечер производства: десять плат
Сценарий простой до скучного: собрать плату, воткнуть USB, дождаться строки SN в мониторе, выписать номер в журнал под порядковым номером, взять следующую. Пять ячеек на плату, полминуты на штуку, десять плат — вечер с чаем. Номера у плат получаются разные, потому что шум на плавающем входе у каждой свой: наводки зависят от того, где лежит плата, чего касается рука монтажника и даже от того, что в этот момент включено в соседней розетке.
Честная граница метода: энтропии в одном analogRead — несколько бит, и у двух плат из десяти шанс совпасть номерами не ноль. Для мастерской это не трагедия — журнал различает платы порядковыми номерами, а SN служит ярлыком. Для производства, где номер — юридический факт, его шьют с сервера учёта через USB: плата получает номер из журнала, а не придумывает сама. Третий путь — вывести номер из серийника подключённой периферии, например DS18B20, у которого заводской идентификатор уже прошит изготовителем. Промежуточный вариант для малой партии — ввести номер с монитора: плата ждёт число, parseInt его читает, механика в уроке про parseInt, а оператор сразу записывает в журнал пару плата-номер.
Номер кладут в EEPROM, а не константой в скетч, ещё и по экономной причине: прошивки обновляются, константа в них одинакова у всех плат, а вписывать её перед каждой заливкой — работа, которая однажды забудется, и десять плат получат один номер. EEPROM перепрошивкам безразлична: заливайте хоть двадцать версий — паспорт остаётся при плате, как и настройки из урока про первые настройки, только трогать его умеет одна-единственная ветка скетча.
- Серийный номер пишется один раз: magic 165 в ячейке 0 закрывает ветку записи, каждый следующий пуск только читает и печатает
- Энтропия — от плавающего входа: randomSeed(analogRead(A5)) плюс random(100000, 1000000) дают шестизначный номер без генератора случайных чисел
- Номер занимает пять ячеек — четыре байта unsigned long и magic: 5 циклов из 100 000 ресурса, износ не тема
- Пустая EEPROM на плате читается 255, в симуляторе 0 — проверять чистоту можно только своим magic, не нулём и не 255
- Для серьёзного производства номер шьют с сервера учёта; генерация на плате — вариант для мастерской с журналом приёмки
- 1. Что делает скетч, если magic-байт уже равен 165?
- 2. Откуда скетч берёт случайность для номера?
- 3. Почему нельзя проверять чистоту памяти условием «прочитали 0 — значит пусто»?
- 4. Сколько ячеек EEPROM трогает прошивка серийного номера?
- 5. Почему номер кладут в EEPROM, а не константой в скетч?
Частые вопросы по теме урока
- Что делает скетч, если magic-байт уже равен 165?
- Magic здесь замок: при найденном 165 выполняется только ветка чтения и печати. Ветка записи при закрытом замке недостижима — ни одного байта не перезаписывается. читает номер из ячеек и печатает его, не трогая память
- Откуда скетч берёт случайность для номера?
- Плавающий аналоговый вход ловит наводки и даёт гуляющий от запуска к запуску код; randomSeed превращает его в зерно, random — в шестизначный номер. randomSeed(analogRead(плавающего входа)) плюс random()
- Почему нельзя проверять чистоту памяти условием «прочитали 0 — значит пусто»?
- Чистая EEPROM состоит из единиц: 255, а не 0. Проверка на ноль молча пропустит прошивку номера на любой плате с чужими данными в памяти. Надёжна только своя magic-ячейка с числом вроде 165. на живой плате пустая ячейка читается 255 — проверка примет забитую память за уже прошитую
- Сколько ячеек EEPROM трогает прошивка серийного номера?
- Шестизначный номер раскладывается в четыре байта, и рядом пишется magic, закрывающий замок: пять ячеек, около 17 мс, один раз за жизнь платы. пять — четыре байта номера и magic
- Почему номер кладут в EEPROM, а не константой в скетч?
- Номер в прошивке при обновлении либо теряется, либо одинаков у всей партии. EEPROM безразлична к заливкам новых версий — паспорт остаётся при плате. константа в прошивке одинакова у всех плат и требует правки перед каждой заливкой, а EEPROM переживает перепрошивки