Логгер включений насоса вёл себя безупречно ровно до того дня, когда я решил его почистить. Счётчик за год дожил до четырёхзначного, и я добавил в скетч «зачистку»: EEPROM.write(адрес, 0) при долгом удержании кнопки. С тех пор сброс давал ноль — и счётчик после каждого удержания честно начинал с нуля, что и требовалось. А вот вернуть ячейку в состояние «как с завода» записью 255 уже не получилось бы без осознания, что запись 255 — это и есть стирание, и что «чистая» и «грязная» запись отличаются не устройством, а тем, какие биты вы трогаете.
Путаница стоила мне ещё одного вечера: я искал в скетче лишнюю запись, которая «портит» нулевой счётчик, хотя ноль был записан моими же руками. EEPROM не знает слова «очистить» — у неё есть только стирание, возвращающее все восемь битов в единицы, и программирование, пробивающее нули. Всё, что мы делаем через write и update, — комбинация этих двух проходов, и цена у них разная.
В этом уроке спустимся на уровень битов: почему пустая ячейка читается 255, почему ноль в единицу без стирания не возвращается, какие три режима записи описывает даташит ATmega328P и где в этом всём проходит бюджет в сто тысяч циклов. Физику ячейки и ресурс мы уже считали в уроке про износ, дисциплину update — в уроке про выборочную запись; здесь — то, что происходит под ними, на границе битов.
Пустая ячейка — все единицы
Каждый бит ячейки — плавающий затвор: карман заряда под изолятором. Стирание выкачивает заряд из всех восьми карманов разом — биты становятся единицами, ячейка читается 255. Программирование наоборот: закачивает заряд в выбранные биты — они становятся нулями. Отсюда два следствия, которые объясняют половину сюрпризов EEPROM. Первое: свежая плата вся читается как 255, и именно по этому признаку ловят первый запуск, как в уроке про 0xFF. Второе: превратить ноль обратно в единицу нельзя никак, кроме полного стирания ячейки.
Значит, запись байта — это всегда стирание плюс программирование: библиотека Arduino делает оба прохода за один вызов, атомарно, и именно поэтому она всегда записывает байт целиком. Полутона не существует: поменять в байте один бит из восьми через write нельзя — перепишутся все восемь. Вот почему запись нуля не «очистка», а обычная запись, и вот почему моя «зачистка» счётчика была ничем не лучше записи любого другого числа.
Отсюда же — цена вопроса. Даташит отводит на атомарный проход стирание+запись 3,4 мс, а на каждый из проходов по отдельности — 1,8 мс: стирание отдельно и запись отдельно идут вдвое быстрее суммарного атомарного. Десять записей подряд — 34 мс тишины в скетче: таймеры продолжают тикать, но плотный цикл спотыкается заметно. Эти цифры — паспортные, типовые; у конкретного экземпляра разброс невелик, но проектировать надо по паспорту.
#include <EEPROM.h> // Ячейка-бюджет: 255 - восемь единичных битов. Дописка значения,// у которого единицы - подмножество уже стоящих, ложится программированием// без стирания: стереть и записать заново не нужно, если нули не превращаются// в единицы.const int YACH = 100;const int SCHET = 101; // сколько раз ячейка проходила стирание int svobodnyh(byte b) { int n = 0; for (int i = 0; i < 8; i++) { if ((b & 1) == 1) { n++; } b = b / 2; } return n;} void pechat(byte b) { Serial.print("yachejka="); Serial.print(b); Serial.print(" edinichnyh bitov="); Serial.println(svobodnyh(b));} void setup() { Serial.begin(9600); pechat(EEPROM.read(YACH));} byte b = 255; void loop() { delay(2000); if (b > 0) { b = b & (b - 1); // пробиваем младший единичный бит в ноль EEPROM.write(YACH, b); Serial.print("dopiska: "); pechat(b); } else { EEPROM.write(YACH, 255); // стирание: все восемь битов в единицы b = 255; byte s = EEPROM.read(SCHET); EEPROM.update(SCHET, s + 1); Serial.print("stiranie vypolneno, ciklov poterjano: 1, vsego: "); Serial.println(s + 1); pechat(b); }}Три режима и окно четырёх тактов
Даташит ATmega328P описывает три режима через биты EEPM1 и EEPM0 в регистре EECR. Ноль-ноль — атомарное стирание и запись одним проходом, 3,4 мс: так работает библиотека Arduino, и трогать эти биты она не умеет. Ноль-один — только стирание, 1,8 мс. Один-ноль — только запись, 1,8 мс: законна, когда новое значение получается из старого пробивкой нулей, как в дописке из скетча выше. Наивная надежда «пробивкой нулей сэкономлю ресурс» не подтверждается: паспортный износ приносит каждый проход программирования, и экономия тут времени, а не циклов — честная граница приёма. Сотне тысяч циклов EEPROM фокус и не нужен; он живёт во FLASH-логах с их десятью тысячами.
Сырая запись через регистры выглядит буднично: дождаться, пока EEPE упадёт в ноль, поднять EEMPE, следующим движением поставить EEPE — и ехать дальше, не ожидая завершения прохода. Библиотека делает всё то же внутри write, но добавляет к нему стирание — потому у неё и нет отдельной «дописки». Лезть в EECR стоит только тогда, когда экономия двух миллисекунд на проходе реальна, а таких мест в любительском проекте обычно ни одного.
Второй даташитный сюрприз — процедура запуска записи. Чтобы её начать, надо поднять бит EEMPE и в течение четырёх тактов поставить EEPE — на 16 МГц это окно в четверть микросекунды. Если в окно вклинилось прерывание, EEMPE успевает сброситься аппаратно, и вызов write завершается без единого изменённого байта — тихо, без признаков. Совет из даташита и с форумов одинаковый: вокруг критичной записи — cli() и sei(), а в хитрых узлах — EEPROM Ready-прерывание, которое сообщает о готовности ячейки к следующему проходу.
Практический вывод для обычного скетча скромнее, чем звучит: четыре такта — узкое окно, попадание в него прерывания редкое, но на плате с пачкой ISR и записью раз в минуту вероятность за год — не ноль. Если настройка пишется по кнопке и человек видит результат — потери нет. Если пишется то, что не проверишь глазами, — сумма из урока про контрольную сумму рядом со значением стоит дешевле, чем разбор с молча пропавшей записью.
Бюджет в цифрах
Сто тысяч циклов на ячейку — паспортная нижняя граница, и её стоит пересчитать в срок жизни хотя бы раз, до прошивки, а не после первого обугленного места. Запись раз в сутки — 274 года: забудьте о ресурсе. Раз в час — 11 лет: тоже можно не думать. Раз в 10 секунд — около 12 дней: вот тут уже осторожно. Раз в секунду — чуть больше суток: ячейка умрёт раньше, чем вы допишете документацию. Одна строчка арифметики перед выбором частоты записи экономит плату.
Инструменты экономии уже собраны в разделе, здесь только карта. update вместо write при неизменном значении — ноль циклов вместо одного, дисциплина, которую мы вынесли ещё с первых уроков раздела. Частая запись одного значения — кольцо ячеек, размазывающее износ, из урока про wear leveling. Контроль остатка — бюджетный счётчик из урока про циклы записи: знать, сколько уже израсходовано, полезнее, чем надеяться.
Моё правило для новых проектов: частота записи, помноженная на срок службы, должна быть меньше ста тысяч с запасом в десять раз. Конфиг, который пишется при каждой правке оператором, живёт в этом бюджете легко; логгер, пишущий каждую секунду, не живёт ни в каком — его спасает только кольцо и ужимание частоты. Между этими полюсами лежит всё любительское хозяйство, и теперь вы знаете цену каждой записи: 3,4 мс и один цикл из ста тысяч.
И про цену самого окна. Запись занимает 3,4 мс, и питание, пропавшее в этом промежутке, оставляет полуфабрикат: половина байта новая, половина — старая. Конфиг, переписываемый раз в месяц, этим почти не рискуют; счётчик, который пишется ровно в момент щелчка рубильника, рискуют всегда. Отсюда та же лестница, что и с блоками: контрольная сумма рядом со значением — как в уроке про CRC блока, а для особо ценных — две копии и выбор по сумме. Моргание сети в сарае — не гипотеза, а график отключений.
| Операция | Время | Цикл ресурса | Когда брать |
|---|---|---|---|
| write(addr, x) — атомарно | 3,4 мс | 1 полный цикл | Редкие события: конфиг, режим, порог |
| update(addr, x) при том же x | Около нуля: только чтение | 0 | Всегда как вариант по умолчанию |
| Стирание отдельно (EEPM=01) | 1,8 мс | Проход старения есть | Сырые регистры, подготовка блока |
| Запись отдельно (EEPM=10) | 1,8 мс | 1 программный проход | Дописка нулей: счётчики в сыром режиме |
- 1. Что читается в свежей, ни разу не записанной ячейке EEPROM?
- 2. В ячейке записан 170. Как получить из него 255?
- 3. Что происходит, если прерывание вклинилось в окно между EEMPE и EEPE?
- 4. Сколько циклов ресурса экономит дописка нулей в режиме «только запись» против атомарной записи?
- 5. Счётчик пишется раз в 10 секунд. Сколько проживёт одна ячейка?
- Пустая ячейка — все единицы: стирание возвращает 255, программирование пробивает нули
- Ноль в единицу без стирания не вернуть: запись байта — всегда стирание плюс программирование
- Даташит: атомарно 3,4 мс, стирание отдельно и запись отдельно — по 1,8 мс; библиотека всегда атомарна
- Окно EEMPE — четыре такта: прерывание в нём оставляет запись несостоявшейся, cli и sei дешевле разбора
- Бюджет: раз в сутки — 274 года, раз в 10 секунд — 12 дней; update при равенстве — ноль циклов
Частые вопросы по теме урока
- Что читается в свежей, ни разу не записанной ячейке EEPROM?
- Стирание возвращает все восемь битов в единицы. Пустая память — сплошные 255, и по этому признаку ловят первый запуск, подтверждая его маркером. 255 — все биты в единицах после стирания
- В ячейке записан 170. Как получить из него 255?
- Единственный путь — запись 255: библиотека выполнит атомарное стирание и программирование за 3,4 мс и потратит один полный цикл ресурса. Дописка умеет только пробивать нули. Записать 255 — это и есть стирание плюс программирование
- Что происходит, если прерывание вклинилось в окно между EEMPE и EEPE?
- Окно — четыре такта, четверть микросекунды на 16 МГц. Вышло дольше — бит разрешения сбросился, write завершается без изменения байтов. Критичную запись держат внутри cli и sei. EEMPE сбрасывается, и запись молча не начинается
- Сколько циклов ресурса экономит дописка нулей в режиме «только запись» против атомарной записи?
- Экономится время — 1,8 мс против 3,4 мс, но каждый проход программирования изнашивает оксид так же. Фокус полезен во FLASH с его меньшим ресурсом, а не в EEPROM. Ни одного: проход программирования старит так же
- Счётчик пишется раз в 10 секунд. Сколько проживёт одна ячейка?
- 8640 записей в день при стотысячном ресурсе — около 12 дней. Спасают кольцо ячеек и update: неизменное значение не тратит циклы вовсе. Около 12 дней