Зарядная станция в моём гараже живёт своей жизнью: свет мигнёт, сосед включит сварочник — и питание прыгает. Мне хотелось знать, сколько раз плате приходилось стартовать заново: это честный индикатор качества электричества на объекте. Идея «счётчик включений в EEPROM» заняла пять минут. Работало отлично... ровно до 256-го включения.
На 256-м старте счётчик обнулился. Я сидел над кодом как обманутый: логика верная, число растёт, а тут раз — и ноль. Причина была в том, чего я тогда не понимал: EEPROM хранит байты, а байт — это числа от 0 до 255. Мой счётчик жил в одной ячейке и на 256-м шаге переполнился обратно в ноль. Тупик лечится знанием: числа больше 255 нужно резать на два байта и складывать по соседним адресам.
В этом уроке сделаем счётчик включений правильно: с защитой от пустой EEPROM, которая читается как 255, и с хранением int в двух байтах через деление и остаток. А в конце заставим плату помнить яркость светильника — применений у приёма «запомнить при старте» десятки, от громкости будильника до калибровки весов.
В симуляторе EEPROM.read сохраняет значения между запусками, поэтому счётчик можно погонять честно: запуск — плюс один, запуск — ещё плюс один. Поддержаны все команды урока: read, write, update и length.
Счётчик включений: три строки в setup()
Логика счётчика укладывается в три действия: при старте прочитать ячейку, прибавить единицу, записать обратно. Делать это надо в setup(): функция выполняется один раз за включение, а нам и нужно ровно одно увеличение на одно включение. Если бы счётчик жил в loop(), он тикал бы тысячи раз в секунду, и ячейка выработала бы свои 100000 записей за часы.
Первый подводный камень — пустая EEPROM. Свежая плата читает любую ячейку как 255. Бездумный алгоритм «прочитал, прибавил, записал» на первом же включении запишет 0 вместо 1: 255 плюс 1 равно 256, а байт не может быть больше 255 — переполнение в ноль. Поэтому обязательная проверка: если прочитали 255, считаем, что счётчик пуст, и начинаем с нуля.
Число 255 как признак «пусто» работает, пока счётчик сам не доберётся до 255 — а он доберётся: 255 включений при ежедневных перезапусках — это меньше года. Так что однобайтовый счётчик годится для отладки и коротких экспериментов, а для постоянной работы нужны два байта. До них дойдём через минуту — сначала почувствуйте механику на одном.
Где это пригождается, кроме гаража: диагностика питания (счётчик растёт сам по себе — ловите просадки), учёт циклов для обслуживания (фильтр, помпа, реле), отлов случайных Reset при плохом контакте USB. Дешёвая телеметрия одним байтом.
#include <EEPROM.h> const int addrCount = 0; // счётчик включений, ячейка 0 void setup() { Serial.begin(9600); int count = EEPROM.read(addrCount); if (count == 255) { // пустая EEPROM - стартуем с нуля count = 0; } count = count + 1; EEPROM.update(addrCount, count); Serial.print("Включение номер: "); Serial.println(count);} void loop() { // пусто: вся работа сделана один раз, в setup()}Один байт — это 0…255: храним число в двух байтах
Разложим проблему по битам. Ячейка EEPROM — 8 бит: числа 0–255. Тип int на Arduino (платы AVR) — 16 бит: от −32768 до 32767, а в беззнаковой версии unsigned int — от 0 до 65535. EEPROM адресуется побайтно, поэтому любое 16-битное число — это две соседние ячейки: старший байт и младший.
Как разрезать число на два байта без битовой магии — школьной арифметикой. Старший байт: value / 256 — целочисленное деление показывает, сколько раз 256 помещается в числе. Младший байт: value % 256 — остаток от деления, всё, что не поместилось. Проверим на 1000: 1000 / 256 = 3, 1000 % 256 = 232. И правда, 3 × 256 + 232 = 1000. Обратная сборка: value = high × 256 + low.
В библиотеке Arduino есть готовые помощники — highByte() и lowByte(): они делают ровно это разрезание. А «взрослая» запись через сдвиги — старший байт это value, сдвинутый на 8 бит вправо, младший — value, наложенный на маску 255, — работает по той же арифметике: сдвиг вправо на 8 бит и есть деление на 256, а маска — взятие остатка. Концептуально это одно и то же, просто деление и остаток нагляднее для начала.
Два байта дают счётчик до 65535. При включениях раз в день это 179 лет, раз в час — 7,4 года. Договоритесь с собой о порядке ячеек и не меняйте его: у меня старший байт всегда лежит по меньшему адресу. Пара адресов 0 и 1 занята счётчиком, 2 — яркостью. Заведите в шапке скетча комментарий-карту памяти, какие адреса чем заняты, — через полгода без неё вы не разберётесь в собственном коде.
#include <EEPROM.h> // Карта памяти EEPROM: 0-1 - счётчик включений, 2 - яркостьconst int addrHigh = 0; // старший байт счётчикаconst int addrLow = 1; // младший байт счётчикаconst int addrBright = 2; // яркость светильника void setup() { Serial.begin(9600); int high = EEPROM.read(addrHigh); int low = EEPROM.read(addrLow); long count = 0; if (high == 255 && low == 255) { count = 0; // память пустая } else { count = high; // переносим в long ДО умножения, count = count * 256 + low; // иначе 255*256 не влезет в int } count = count + 1; int highNew = count / 256; // старший байт int lowNew = count % 256; // младший байт EEPROM.update(addrHigh, highNew); EEPROM.update(addrLow, lowNew); Serial.print("Включение номер: "); Serial.println(count); pinMode(9, OUTPUT); int bright = EEPROM.read(addrBright); if (bright == 255) { bright = 100; } analogWrite(9, bright); Serial.print("Яркость из EEPROM: "); Serial.println(bright);} void loop() { int pot = analogRead(A0) / 4; if (abs(pot - EEPROM.read(addrBright)) > 2) { EEPROM.update(addrBright, pot); analogWrite(9, pot); } delay(100);}Читаем обе ячейки: high из нулевой, low из первой. Признак пустой памяти теперь пара 255 и 255: свежая EEPROM читается всеми единицами в обеих ячейках. Если обе — 255, счётчик стартует с нуля; иначе собираем число через long. Приём с промежуточным присваиванием защищает от переполнения: high × 256 при high = 255 даёт 65280, что уже больше потолка знакового int (32767), а long спокойно тянет миллиарды.
Запись обратно зеркальна: EEPROM.update(0, count / 256) и EEPROM.update(1, count % 256). При count до 65535 оба результата влезают в байт. Единственный крайний случай — счётчик дотянулся до 65535 и прибавил ещё: 65536 / 256 = 256, а это уже не байт. Лекарство — третий байт, но 65535 включений даже по одному в час хватит на 7,4 года.
Вторая половина скетча — яркость: читаем ячейку 2, фильтруем 255 дефолтом, analogWrite(9, bright). Плюс сохранение при вращении ручки с гистерезисом из прошлого урока. Смотрите, как аккуратно разложилась память: 0–1 — счётчик, 2 — яркость. Три ячейки — и плата знает и свою историю, и предпочтения хозяина.
Яркость, которая переживает выключение
Приём «прочитал при старте — применил» масштабируется на всё, что пользователь настраивает руками: громкость будильника, целевую температуру термостата, номер последнего режима гирлянды, ноль калибровки весов. Схема всегда одна: одна настройка — одна ячейка (или договорённое количество), чтение в setup(), запись по событию с update().
В симуляторе всё это можно проверить прямо сейчас: EEPROM.read, EEPROM.write, EEPROM.update и EEPROM.length() работают по-настоящему, а значения живут между запусками, как на реальной плате между выключениями. Запустите скетч — счётчик покажет 1; нажмите Стоп, запустите снова — 2; покрутите A0 — яркость сохранится и вернётся после перезапуска.
- Запустите симулятор три раза подряд: счётчик должен показать 1, 2, 3. Затем добавьте условие «мастер-сброса»: если analogRead(A1) больше 500, счётчик обнуляется — обе ячейки получают 0.
- Попробуйте записать 700 в одну ячейку: EEPROM.update(3, 700) сохранит остаток от деления на 256, то есть 188. Проверьте чтением, а потом уложите 700 правильно: байты 2 и 188 по соседним адресам.
- Сделайте счётчик трёхбайтовым: добавьте третий байт — count / 65536 — и собирайте число с множителем 65536. Такой счётчик переживёт 16,7 миллиона включений.
- Храните в ячейке 5 выбранный режим (1–3) и печатайте его при старте; переключайте режим, когда A1 переходит границу 500.
- Счётчик включений: в setup() читаем ячейку, прибавляем 1, пишем update-ом; в loop() это делать нельзя — сожжём ресурс
- Пустая EEPROM читается как 255 (в двух байтах — 255 и 255): проверка обязательна, иначе первый запуск запишет ноль
- Байт — 0–255; int на AVR — 2 байта (−32768…32767); полный диапазон 0–65535 — это unsigned int
- Разрез на байты: старший = value / 256, младший = value % 256; сборка: value = high × 256 + low; highByte() и lowByte() делают то же
- Собирайте многобайтовое число в long: 255 × 256 уже не влезает в знаковый int
- Ведите карту памяти: комментарий в шапке скетча — какой адрес что хранит
- 1. Почему счётчик включений увеличивают в setup(), а не в loop()?
- 2. Свежая плата. Что вернут EEPROM.read(0) и EEPROM.read(1)?
- 3. Число 1000 разложили на два байта: value / 256 и value % 256. Что получится?
- 4. Как собрать число обратно из старшего и младшего байтов?
- 5. Сколько вмещает int на Arduino Uno?
- 6. Что сохранится в ячейке после EEPROM.update(3, 700)?
У вас в руках полный приём «пережить розетку»: настройки — по байту, счётчики — по два байта, сборка и разбор через деление и остаток. Следующий логичный шаг — скрестить два мира: EEPROM хранит конфигурацию и счётчики, SD-карта — поток измерений. Логгер, который при каждом старте печатает свой номер включения и подхватывает пороги, работает месяцами без присмотра — и вы уже умеете собирать такой.
И последнее. Теперь, когда вы знаете про ресурс ячеек и пустые 255, чужой код с EEPROM.write в loop() будет резать глаз, как молоток по стеклу. Это хороший глаз. Сохраняйте его — и свои настройки.
Частые вопросы по теме урока
- Почему счётчик включений увеличивают в setup(), а не в loop()?
- Одно включение — одна запись. В loop() счётчик тикал бы тысячи раз в секунду, и ячейка исчерпала бы 100000 циклов за часы. setup() выполняется один раз за включение — ресурс ячейки тратится по назначению
- Свежая плата. Что вернут EEPROM.read(0) и EEPROM.read(1)?
- Пустая EEPROM — все единичные биты: 255 в каждой ячейке. По паре 255/255 распознают первый запуск. 255 и 255
- Число 1000 разложили на два байта: value / 256 и value % 256. Что получится?
- 1000 / 256 = 3 (целочисленно), 1000 % 256 = 232. Проверка: 3 × 256 + 232 = 1000. 3 и 232
- Как собрать число обратно из старшего и младшего байтов?
- Старший байт — количество полных 256-к, младший — остаток: value = high × 256 + low. Макросы highByte/lowByte — про то же. high × 256 + low
- Сколько вмещает int на Arduino Uno?
- int на AVR — 16 бит со знаком: −32768…32767. Диапазон 0–65535 — это unsigned int, до двух миллиардов — long (4 байта). −32768…32767
- Что сохранится в ячейке после EEPROM.update(3, 700)?
- Ячейка — один байт. 700 по модулю 256 = 188. Числа больше 255 режьте на байты вручную. 188 — ячейка хранит байт, лишнее отрезалось по модулю 256