Клапан полива в теплице стоял у меня на 137 градусах — положение, подобранное неделю проб и ошибок. Ночью мигнул свет, плата перезапустилась, и клапан послушно встал в 90 из setup(): не сломался, просто забыл. Я неделю подбирал угол — скетч об этом не знал ровным счётом ничего, потому что переменная tek жила в RAM, а RAM стирается с питанием за микросекунды.
Между включениями у платы есть своя память — EEPROM: килобайт ячеек, которые держат байты без питания годами и переживают любой морг света. Приём этого урока: угол живёт в EEPROM, скетч при старте его читает и возвращает клапан туда, где тот стоял, — причём первый ход после включения делается мягко, без рывка через полразмаха. С меню, где режим и угол сохраняются байтом по кнопке, мы работали в уроке про меню джойстика — здесь же полный приём с защитой от первой прошивки и от износа ячейки.
Разберём арифметику ресурса, роль update() и троттлинга, а в конце — мягкий первый ход. Базовые приёмы чтения и записи я считаю известными из урока про первый байт в EEPROM, но каждая строка ниже будет снабжена объяснением на всякий случай.
И клапан не одинок. В той же теплице забывала калибровку хода заслонка, обнулялся счётчик поливов, сбрасывалось расписание, набитое через Serial за вечер. Всё это — кандидаты в тот же килобайт ячеек, который на Uno лежит нетронутым в девяти проектах из десяти. Приём урока одинаков для любой величины: выбрать адрес, придумать контрольное число, писать через update() и читать при старте; угол — просто самый наглядный первый кандидат, потому что его потерю видно глазами.
Что писать, когда писать и чем защищаться
Первое решение — писать цель, а не текущий градус. Цель меняется, когда вы меняете её руками: раз в час, раз в день. Текущий градус в движении меняется до сотни раз за один ход — и каждая смена превращается в запись, если писать в лоб. Второе — update() вместо write(): функция сравнивает новое значение с лежащим в ячейке и пишет только при отличии, экономя ресурс. Ресурс этот конечен: ячейка EEPROM на ATmega328P переживает около 100 000 циклов записи по даташиту, а запись байта занимает около 3,3 мс — потому и троттлинг: не чаще раза в пять секунд.
Третья защита — magic-байт. Свежая прошитая плата читает EEPROM как 255 во всех ячейках: памяти ещё никто не касался, а вы уже готовы поверить «углу 255». В ячейку 0 при первой записи уголка кладётся контрольное число 170; при старте скетч смотрит: 170 — угол настоящий, 255 — это первая прошивка, берём дефолт. Такой же приём стоит за меню настроек на LCD и вообще за любыми настройками, которые обязаны пережить первый запуск.
Теперь честная арифметика троттлинга. Рука на потенциометре дрожит: без гистерезиса цель прыгает каждые пару чтений, и троттлинг «раз в 5 с» пропустит до 17 280 записей в сутки — ячейка выгорит за шесть дней дрожи. Гистерезис «цель сменилась больше чем на 2 градуса» режет эту цифру до единиц записей в час в покое: 100 000 циклов превращаются из проблемы в абстракцию. Сами приёмы успокоения ручки я собирал в уроке про гистерезис.
Пара практических подробностей про запись. Один вызов update() занимает около 3,3 мс — раз в пять секунд это незаметно, но писать из обработчика прерывания нельзя: на время записи прерывания запрещены, и millis начнёт заметно врать, если EEPROM вдруг окажется в ISR. Второе — карта адресов: килобайт общий для всего скетча, и однажды второй проект на той же плате перепишет ваши углы, если адреса выбраны наугад. Моя привычка — комментировать раскладку в шапке: ноль — magic, один — угол, десяток и дальше — под настройки будущих величин.
#include <EEPROM.h> const int ADRES_MAGIC = 0; // 170 = здесь лежит настоящий уголconst int ADRES_UGOL = 1; // угол 0-180 влезает в один байтconst unsigned long T_ZAPIS = 5000; // троттлинг записи int sokhraneno = 90;unsigned long tZapis = 0;int zapisi = 0; void vosstanovit() { if (EEPROM.read(ADRES_MAGIC) == 170) { sokhraneno = EEPROM.read(ADRES_UGOL); Serial.print("vosstanovlen ugol: "); Serial.println(sokhraneno); } else { EEPROM.write(ADRES_MAGIC, 170); // первая прошивка: свежая ячейка читается как 255 Serial.println("pervaya proshivka: berem defolt 90"); }} void sokhranit(int cel, unsigned long now) { if (cel == sokhraneno) return; // update() сравнивает и так, но счётчик честнее if (now - tZapis < T_ZAPIS) return; // не чаще раза в 5 секунд tZapis = now; EEPROM.update(ADRES_UGOL, cel); zapisi++; sokhraneno = cel; Serial.print("zapis #"); Serial.println(zapisi);}| Приём | Что даёт | Без него | Цена |
|---|---|---|---|
| Угол одним байтом | 0-180 влезает в ячейку целиком | лишние байты и конвертации | нет: углы больше 255 не бывают |
| Magic-байт 170 | отличает первую прошивку от реального угла | клапан прыгает на «угол 255» | одна ячейка и одна строка кода |
| update() вместо write() | пишет только отличия | одинаковые значения жгут ресурс зря | вызов чуть длиннее на сравнение |
| Троттлинг 5 с | сотни записей в сутки вместо 17 280 | ячейка выгорает за дни дрожи | угол между включениями может отстать на 5 с |
Функция vosstanovit() вызывается в setup() до attach: скетч сначала решает, чему верить, и только потом заводит серво. Если magic-байта нет, он появится ровно один раз за жизнь платы — при первой прошивке, и с тех пор каждый перезапуск начнётся с правильного угла. Сколько байт вы ни сэкономите где-то ещё, эти два адреса я рекомендую не трогать от проекта к проекту: привычная раскладка «0 — magic, 1 — угол» уже спасала меня при переносе скетча на новую плату.
Расширение на случай, если свет мигает во время самой записи. Два байта — magic и угол — пишутся не атомарно: морг питания между ними оставит новую контрольную метку со старым углом или наоборот. Лекарство — байт-двойник: угол пишется в две ячейки подряд, при старте скетч сверяет их и доверяет только совпадению; разошлись — берём дефолт и пишем заново. Три байта и одно сравнение закрывают сцену «морг света ровно в момент записи», которая за год случается с каждым. Для одного угла двойник честнее CRC по цене: он стоит сравнение, а не таблицу полиномов.
Первый ход после включения: мягкий возврат
Момент, который портит половину готовых решений: write(sokhraneno) прямо в setup(). Серво получает приказ доехать с текущего положения до сохранённого мгновенно — на клапане это рывок всей тяги, на шторке — хлопок. Порядок, к которому я пришёл: прочитали угол, поставили рог в безопасный стартовый угол вашей механики, а дальше довозим до сохранённого по градусу, тем же каркасом, что в уроке про шаг по градусу на millis. Первый ход занимает секунду-другую, зато ни одна деталь не получает удар на холодную.
В симуляторе ниже есть то, чего живой плате не видно на глаз: счётчик фактических записей. Подвигайте ползунок — увидите, как гистерезис и троттлинг вместе держат zapisi на единицах, хотя цель меняется на глазах. Углы переживают перезапуск и в симуляторе: значения EEPROM движок хранит между запусками, так что эксперимент «сменил угол — перезапустил — всё на месте» проделывается прямо на этой странице. Для проектов с десятком сохраняемых величин дальше по курсу есть урок про жизнь без питания — с картой всей памяти платы.
И про честность формулировки: EEPROM хранит желание, а не факт. Если механику сдвинули рукой при выключенном питании, скетч вернёт её к сохранённому углу — и для клапана это правильное поведение, серво догонит позицию. Но если створку физически заблокировали, первый же мягкий ход упрётся, и здесь пригодится концевик из прошлого урока: факт вместо приказов.
Безопасный стартовый угол у каждой механики свой: клапан я начинаю с закрытого состояния, шторку — с нижнего края, качалку кормушки — с «заслонка закрыта». Логика одна: положение, в котором механизм не под нагрузкой и ни о что не бьётся, если серво проснётся не там. И про перешивки: перепрошивка скетча через загрузчик EEPROM не трогает — угол переживёт десяток обновлений кода; стирает её полная очистка чипа при записи загрузчика с нуля, и тогда magic-байт честно скажет «первая прошивка», а не подсунет мусор.
Куда это растёт дальше. На ESP32 вместо ручных ячеек есть Preferences с пространствами имён: угол записывается одной командой и переживает то же самое, только сырой адресной арифметики меньше. На классических платах следующим этажом становится упаковка настроек в структуру — один вызов сохраняет угол, расписание и флаги разом, а magic-байт превращается в версию формата; как уложить структуру в байты без сюрпризов выравнивания, разобрано в уроке про структуру и дамп байтов. Но начните с одного угла: приём, который вы отладите здесь, переезжает в любой проект без изменений, и я до сих пор копирую в новые скетчи те же две функции 2021 года.
Там, где сохраняемое значение дороже одного байта — угол из writeMicroseconds с шагом в 10 мкс, счётчик циклов за год, — байт сменяется двумя, а старший читается первым: мелочь, которая однажды сэкономит вечер поиска «угол доезжает только до 128». И адресная привычка из скетча выше — magic на нулевом адресе — означает, что новая версия скетча может добавлять настройки дальше по карте, не ломая старую: версия формата в magic-байте подскажет, с чем скетч имеет дело после обновления.
- RAM стирается с питанием: угол, который должен пережить морг света, живёт в EEPROM
- Magic-байт 170 отличает первую прошивку от настоящего угла
- update() пишет только отличия — ресурс ячейки 100 000 циклов не тратится зря
- Троттлинг 5 с плюс гистерезис 2-3° превращают тысячи записей в единицы
- Первый ход после включения — мягкий, по градусу, без рывка через полразмаха
- 1. Почему в EEPROM пишем цель, а не текущий градус серво?
- 2. Что вернёт EEPROM.read(0) на свежепрошитой плате?
- 3. Рука дрожит, цель прыгает каждые 200 мс, троттлинг 5 с. Сколько записей за сутки максимум?
- 4. Чем update() отличается от write()?
- 5. Почему первый ход после включения я веду по градусу, а не write(sokhraneno) в setup?
Частые вопросы по теме урока
- Почему в EEPROM пишем цель, а не текущий градус серво?
- Плавный ход меняет градус десятки раз за секунды: писать каждый — сотни записей за один проход. Цель меняется только руками. Цель меняется редко, градус в движении — до сотни раз за ход
- Что вернёт EEPROM.read(0) на свежепрошитой плате?
- Свежая EEPROM читается как 255 во всех ячейках. Магический байт 170 записывается один раз и с тех пор отличает реальный угол от первой прошивки. 255 — EEPROM ещё никто не касался
- Рука дрожит, цель прыгает каждые 200 мс, троттлинг 5 с. Сколько записей за сутки максимум?
- 86 400 секунд в сутках, запись не чаще раза в 5 с — до 17 280. От самой дрожи спасает гистерезис: цель меняется только при сдвиге больше 2°. До 17 280 — ячейка выгорит за дни
- Чем update() отличается от write()?
- write() жжёт цикл перезаписи каждый вызов, update() — только когда значение поменялось. Ресурс ячейки экономится без всяких условий. Сравнивает с ячейкой и пишет только при отличии
- Почему первый ход после включения я веду по градусу, а не write(sokhraneno) в setup?
- write() в setup заставляет серво разгоняться через весь ход разом. Безопасный стартовый угол плюс довоз по градусу занимают секунду и берегут редуктор. Мгновенный приказ — рывок тяги и удар по механике