К третьей неделе раздела у меня на столе лежали пять рабочих кусков: фоторезистор с делителем отличал сумерки от лампы, ШИМ плавил яркость, кнопки после тренировки с дребезгом нажимались предсказуемо, map() переводила диапазоны, а EEPROM бережно хранила байты без питания. Каждый кусок работал в своём уроке — и ни один из них не работал вместе. Ночник из урока про фоторезистор щёлкал между нулем и 160, кнопки ссорились со светом, а после каждого выключения питания всё возвращалось к заводским числам. Устройство из проверенных деталей отказывалось работать целиком, потому что целого скетча у него ещё не было.
Финальный проект раздела — ночник, который стоит на столе как настоящий прибор. Три режима на двух кнопках: авто — светит тем ярче, чем темнее в комнате; ручной — яркость ступенями второй кнопкой; выключен. Все переходы плавные, через ШИМ из урока про analogWrite, без щелчков и вспышек. И главное — выбранный режим переживает выключение питания: он живёт в EEPROM, и ночник просыпается именно тем, каким вы его оставили.
Проект намеренно не добавляет ни одной новой детали и ни одной новой функции: пять пинов и один скетч. Это сборка — самая частая работа в реальных проектах и самая недооценённая: соединить проверенные куски так, чтобы они не перегрызались между собой, отдельного искусства стоит. Вся механика сборки ниже разбирается по слоям, и каждый слой — прямая ссылка на урок, где он был написан. Проверять удобнее тоже послойно: сначала свет в авто, потом кнопки, потом память — тогда при поломке вы точно знаете, какой слой врёт, потому что остальные уже проверены ранее.
Меню на двух кнопках: режимы вместо развилок
Три режима — это switch по номеру, и оформлены они ровно так, как в уроке про switch-case: case 0 — авто, case 1 — ручной, case 2 — выключено. Кнопка на D2 перелистывает режимы по кругу, и каждый листок — маленький сценарий, а не один громоздкий if с вложенными флагами. Номер режима — единственное глобальное состояние: никаких дублирующих флагов «свет горит», иначе однажды они разойдутся с номером, и прибор начнёт жить врозь со своим меню. Когда режимов станет пять, switch вырастет на три строки, а каркас не изменится: в этом и есть сила меню именно режимами — оно масштабируется арифметикой, а не переписыванием.
В авто-режиме цель по яркости считается из света: tselevaya = map(constrain(svet, 100, 900), 100, 900, 255, 0). Обратите внимание на обратный порядок крайних значений: чем меньше света, тем больше цель — шкала инвертирована прямо в аргументах map(), без единой вычитающей строки. Зажим 100–900 — это страховка краёв из урока про потенциометр и map: полная тьма и прямой луч лампы не должны выпрыгивать за шкалу, они просто прижимаются к краям, и все переходы остаются в пределах 0–255. Сами границы 100 и 900 не с потолка: их снимают Serial-ом — десять секунд со свечой и в темноте, два числа, как учил урок про фоторезистор, только теперь они стали аргументами.
Кнопки — те же фронты, что в уроке про дребезг: сравнили с прошлым состоянием, сработали на переход HIGH в LOW, пауза 30 мс. Вторая кнопка на D3 действует только в ручном режиме: каждое нажатие поднимает яркость ступенью 85 — ноль, 85, 170, 255 — и на четвёртом нажатии снова ноль. В авто и выключенном режимах она мертва. Мёртвая кнопка в чужом режиме — не недоработка, а признак продуманного меню: прибор честно говорит, что в этом режиме настраивать нечего.
Плавность — второй слой. Наивная версия ставит analogWrite(tselevaya) разом: в тёмной комнате свет выстреливает полной яркостью, и я всякий раз жмурился, проходя мимо лестницы. Поэтому ночник не прыгает к цели, а шагает: за один проход loop яркость меняется на единицу, к цели идёт с любой стороны. При паузе 3 мс в хвосте цикла полный ход от нуля до максимума — 255 шагов, примерно восемь десятых секунды; хотите медленнее — увеличьте паузу, быстрее — уменьшите, шкала всегда линейная и предсказуемая. Бонус достаётся бесплатно: tekuschaya стартует с нуля, поэтому после каждой перезагрузки платы ночник всегда разгорается из темноты — мягкое включение не нужно программировать отдельно, оно само следствие шага.
Три миллисекунды — компромисс: за это время успевают опроситься обе кнопки, и фейд не съедает отзывчивость. Но держать это в голове надо честно: как только к ночнику добавится вторая неторопливая задача — датчик, журнал, запись в память, — delay(3) в каждом проходе станет вредной привычкой, и дорога одна: переделывать цикл на millis без delay. Для учебного ночника блокирующий шаг — разумная цена за читаемость, и я плачу её без сожалений.
Память режима — третий слой. Ячейка 0 хранит номер режима, и обновляет её EEPROM.update: он сравнивает новое число со старым и физически пишет ячейку только при изменении. За вечер вы переключите режимы два десятка раз — write при таком темпе наматывал бы циклы стирания к каждому ужину, а update расходует ресурс ровно на реальные смены. Механика износа с числами и графиками — в уроке про update и ресурс ячеек; здесь берём готовое правило: частые записи — только update. Для масштаба: под EEPROM на Uno отведён килобайт, ресурс каждой ячейки — около 100 000 циклов записи; при двадцати сменах режима за вечер это десятки лет на одну-единственную ячейку.
Полный скетч: пины, фейд и память
#include <EEPROM.h> const int LDR = A0; // делитель: LDR к 5V, 10 кОм к GNDconst int BTN_REZHIM = 2;const int BTN_YAR = 3;const int SVETODIOD = 9; // ШИМ-пин: на Uno это 3, 5, 6, 9, 10, 11const int ADRES = 0; int rezhim = 0; // 0 авто, 1 ручной, 2 выключенint ruchnaya = 85;int tekuschaya = 0;int tselevaya = 0;bool btnByla = HIGH;unsigned long posledniiPechat = 0; void setup() { pinMode(BTN_REZHIM, INPUT_PULLUP); pinMode(BTN_YAR, INPUT_PULLUP); Serial.begin(9600); rezhim = EEPROM.read(ADRES); if (rezhim > 2) rezhim = 0; // свежая плата читает 255 Serial.print("Rezhim iz pamyati: "); Serial.println(rezhim);} void loop() { bool sejchas = digitalRead(BTN_REZHIM); if (btnByla == HIGH && sejchas == LOW) { rezhim = rezhim + 1; if (rezhim > 2) rezhim = 0; EEPROM.update(ADRES, rezhim); // пишем только при смене if (rezhim == 0) Serial.println("Rezhim: avto"); if (rezhim == 1) Serial.println("Rezhim: ruchnoi, yarkost na D3"); if (rezhim == 2) Serial.println("Rezhim: vyklyuchen"); delay(30); } btnByla = sejchas; if (rezhim == 1 && digitalRead(BTN_YAR) == LOW) { ruchnaya = ruchnaya + 85; if (ruchnaya > 255) ruchnaya = 0; Serial.print("Yarkost: "); Serial.println(ruchnaya); delay(250); // шаг не чаще четырёх раз в секунду } if (rezhim == 0) { int svet = analogRead(LDR); tselevaya = map(constrain(svet, 100, 900), 100, 900, 255, 0); } else if (rezhim == 1) { tselevaya = ruchnaya; } else { tselevaya = 0; } if (tekuschaya < tselevaya) tekuschaya = tekuschaya + 1; if (tekuschaya > tselevaya) tekuschaya = tekuschaya - 1; analogWrite(SVETODIOD, tekuschaya); if (millis() - posledniiPechat > 400) { posledniiPechat = millis(); Serial.print("svet="); Serial.print(analogRead(LDR)); Serial.print(" yarkost="); Serial.println(tekuschaya); } delay(3);}| Режим | Откуда яркость | Кнопка D3 | Что вижу на столе |
|---|---|---|---|
| 0 — авто | map от света: темнее — ярче | мёртва | захожу вечером — свет уже горит и ярче, чем глубже сумерки |
| 1 — ручной | ступени 0 / 85 / 170 / 255 | шаг по нажатию | волной догоняю комфортную яркость |
| 2 — выключен | цель 0 | мёртва | ночник гаснет плавно, за секунду, без вспышки |
| старт платы | чтение ячейки 0 EEPROM | — | печатает Rezhim iz pamyati и встаёт в сохранённый режим |
Что этот скетч склеил
Пять источников, один скетч: свет пришёл из аналогового входа, шкала — из map(), мягкость — из ШИМ, фронты — из тренировки с дребезгом, память — из EEPROM. Ни один блок не писался с нуля: каждый приехал из урока, где был разобран до винтика, и потому сборка заняла вечер, а не неделю. Это и есть главный навык, который закрывает раздел: узнавать новую задачу как комбинацию уже решённых. Устройство из проверенных деталей, собранное по этой логике, отлаживается по кускам — вы всегда знаете, какой слой врёт, потому что остальные проверены ранее.
Куда расширять — по нарастающей. Держать в EEPROM не только режим, но и ручную яркость: вторая ячейка обновляется тем же update одной строкой. В авто-режиме добавить минимум 40: полная тьма — ночник не выключается, а приглушается, лестница никогда не встречает вас чернотой. Завести четвёртый режим «рассвет» с медленным подъёмом на таймере. Каждый пункт — строка или две в готовой структуре, и это лучший тест на понимание: расширяемость читается по тому, сколько строк стоит новое поведение.
Раздел «Основы» этим уроком закрыт: от мигающего светодиода на первом шаге до прибора с меню, датчиком и памятью. Дальше — раздел про Serial, где плата заговорит полноценным протоколом с командами и числами, а за ним — продвинутые приёмы со временем и памятью. Ночник оставьте собранным на столе: к такой макетке удобно возвращаться всякий раз, когда новая техника требует полигона.
- Сохраняйте ручную яркость в ячейке 1 тем же EEPROM.update и восстанавливайте её на старте.
- В авто-режиме добавьте минимум 40: полная тьма — ночник не выключается, а приглушается.
- Заведите режим «рассвет»: яркость медленно нарастает минуту — шаг и пауза в скетче уже есть.
- Печатайте пары svet= и yarkost= чаще, раз в 100 мс, и смотрите в Serial Plotter, как кривая догоняет свет.
- 1. Почему цель по свету считается через map(constrain(svet, 100, 900), 100, 900, 255, 0)?
- 2. Что делает EEPROM.update, когда новое значение совпало со старым?
- 3. Сколько занимает полный ход фейда при шаге 1 и паузе 3 мс?
- 4. Что напечатает свежая плата на первом старте без проверки диапазона?
- 5. Кнопка D3 в авто-режиме?
- Меню — switch по номеру режима: авто, ручной, выключен; кнопка D2 листает, D3 живёт только в ручном.
- Цель по свету — map с обратным порядком краёв и зажимом 100–900: инверсия шкалы без вычитаний.
- Фейд — шаг 1 за проход loop при паузе 3 мс: 255 шагов, полный ход около 0,8 с, глазу свет не бьёт.
- Режим хранит EEPROM.update в ячейке 0 — запись только при смене; чтение на старте всегда с проверкой диапазона.
- Сборка из проверенных кусков — главный навык финала: каждый блок приехал из своего урока готовым.
Частые вопросы по теме урока
- Почему цель по свету считается через map(constrain(svet, 100, 900), 100, 900, 255, 0)?
- Обратный порядок выходных значений инвертирует шкалу без единого вычитания, а зажим 100–900 не даёт тьме и прямому лучу выпрыгивать за пределы 0–255. Края шкалы прижимаются, а инверсия «темнее — ярче» задана обратным порядком крайних значений
- Что делает EEPROM.update, когда новое значение совпало со старым?
- Update сначала читает и сравнивает: совпало — физической записи нет, ячейка не изнашивается. Именно поэтому частые переключения режима ему не страшны. Не пишет ничего: ресурс ячейки не тратится
- Сколько занимает полный ход фейда при шаге 1 и паузе 3 мс?
- Один шаг за проход loop: 255 шагов по 3 мс плюс мелкий расход на опрос кнопок — порядка 0,8 секунды от нуля до полной яркости. 255 шагов — около восьми десятых секунды
- Что напечатает свежая плата на первом старте без проверки диапазона?
- Заводская ячейка EEPROM — байт из всех единиц, то есть 255. Без строки if (rezhim > 2) rezhim = 0 ночник встанет вне меню с погашенным светом. Rezhim iz pamyati: 255
- Кнопка D3 в авто-режиме?
- Обработка D3 спрятана за условием rezhim == 1. В авто и выключенном режимах кнопка мертва — так меню честно сообщает, что настраивать здесь нечего. Ничего не делает: она действует только в ручном режиме