Курс Arduino с нуля · Датчики
Урок 18. Проект: умный ночник
Собираем из датчиков блока цельное устройство: включается в темноте, слушается кнопки, настраивается крутилкой и светит плавно.
Что понадобится
- Плата Uno из набора
- Кабель USB
- Макетная плата
- Фоторезистор
- Резистор 10 кОм
- Потенциометр 10 кОм
- Тактовая кнопка
- Светодиод (любого цвета)
- Резистор 470 Ом
- 8 проводов «папа-папа»
Все детали есть в обучающем наборе, по которому идёт курс.
Пять уроков подряд мы разбирали датчики по одному. Пора собрать из них настоящее устройство — такое, которое не стыдно поставить на полку и которым будут пользоваться дома.
Наш ночник умеет вот что: сам включается, когда стемнело, светит ровно с той яркостью, которую вы задали крутилкой, и слушается кнопки — авто, всегда включён, выключен. Свет разгорается и гаснет плавно, а не рывком.
Этот урок — не про новые команды. Всё, что понадобится, вы уже знаете. Он про то, как из отдельных приёмов собирается цельная вещь: как разложить программу по полочкам, как проверять сборку по частям и как не запутаться, когда деталей становится много.
Как устроено устройство
Прежде чем брать провода, полезно нарисовать на бумаге, что откуда берётся. У нашего прибора три источника данных и один исполнитель:
Это разделение — главный приём урока. Пока программа маленькая, кажется, что можно писать как придётся. Но стоит появиться третьему датчику, и без порядка код превращается в кашу, в которой невозможно ничего поменять.
Три режима
Обратите внимание на приём с индикацией: отдельный светодиод-индикатор не нужен, устройство сообщает о себе само собой. Так делают в настоящих приборах, где каждая лишняя деталь стоит денег и места.
Собираем схему
Отключите USB. Схема большая, поэтому собирайте по частям и проверяйте каждую: сначала делитель со светом, потом крутилку, потом кнопку, потом светодиод. Искать ошибку в готовой куче проводов вчетверо дольше.
Проверьте перед включением: фоторезистор и резистор 10 кОм сходятся в одном столбике; у крутилки крайние ножки на разных шинах, а средняя идёт на A1; кнопка сидит верхом на канавке; обе шины «−» соединены перемычкой; светодиод стоит длинной ножкой к резистору.
Шаг 1. Проверяем сборку, а не программу
Самая частая ошибка новичка — сразу залить большую программу и потом гадать, что не работает: схема или код. Правильный путь другой: сначала убедиться, что плата видит все три входа.
const int LIGHT = A0; // фоторезистор
const int POT = A1; // крутилка яркости
const int BUTTON = 2; // кнопка режимов
const int LED = 10; // сам ночник
void setup() {
Serial.begin(9600);
pinMode(BUTTON, INPUT_PULLUP);
pinMode(LED, OUTPUT);
Serial.println("свет крутилка кнопка");
}
void loop() {
Serial.print(analogRead(LIGHT));
Serial.print(" ");
Serial.print(analogRead(POT));
Serial.print(" ");
Serial.println(digitalRead(BUTTON));
delay(200);
}
Откройте монитор порта и проверьте по очереди:
- накройте фоторезистор ладонью — первое число должно упасть;
- покрутите ручку — второе число должно проехать от 0 до 1023;
- нажмите кнопку — третье число должно смениться с 1 на 0.
Если хоть один пункт не сработал — дело в схеме, и дальше идти бессмысленно. Заодно запишите свои значения освещённости: они понадобятся на следующем шаге.
Шаг 2. Автоматика
Первая полезная функция: светить, когда темно, с яркостью, заданной крутилкой. Здесь встречаются два приёма — гистерезис из пятнадцатого урока и map из четырнадцатого.
const int LIGHT = A0;
const int POT = A1;
const int LED = 10;
// Свои пороги возьмите из монитора порта (урок 15).
const int TURN_ON = 380;
const int TURN_OFF = 460;
bool lampOn = false;
void setup() {
pinMode(LED, OUTPUT);
}
void loop() {
int light = analogRead(LIGHT);
// Гистерезис: включаем в темноте, выключаем, когда явно посветлело.
if (!lampOn && light < TURN_ON) {
lampOn = true;
}
if (lampOn && light > TURN_OFF) {
lampOn = false;
}
int bright = map(analogRead(POT), 0, 1023, 0, 255);
if (lampOn) {
analogWrite(LED, bright);
} else {
analogWrite(LED, 0);
}
}
Пороги TURN_ON и TURN_OFF поставьте свои — те, что записали на прошлом шаге. Накройте датчик — светодиод загорится; покрутите ручку — яркость изменится. Половина устройства готова.
Шаг 3. Режимы и кнопка
Автоматика — это прекрасно, но иногда свет нужен днём, а иногда не нужен вовсе. Добавим кнопку и три режима, а вместе с ними — уже знакомый антидребезг.
const int LIGHT = A0;
const int POT = A1;
const int BUTTON = 2;
const int LED = 10;
const int TURN_ON = 380;
const int TURN_OFF = 460;
const unsigned long BOUNCE_MS = 30;
// 0 — авто, 1 — всегда включён, 2 — выключен.
int mode = 0;
bool lampOn = false;
int wasState = HIGH;
unsigned long lastChange = 0;
bool buttonPressed() {
int nowState = digitalRead(BUTTON);
bool pressed = false;
if (nowState != wasState) {
if (millis() - lastChange > BOUNCE_MS && nowState == LOW) {
pressed = true;
}
lastChange = millis();
wasState = nowState;
}
return pressed;
}
// Ночник моргает столько раз, какой режим включили.
void blinkMode(int times) {
for (int i = 0; i < times; i++) {
analogWrite(LED, 255);
delay(120);
analogWrite(LED, 0);
delay(160);
}
}
void setup() {
pinMode(BUTTON, INPUT_PULLUP);
pinMode(LED, OUTPUT);
}
void loop() {
if (buttonPressed()) {
mode = mode + 1;
if (mode > 2) {
mode = 0;
}
blinkMode(mode + 1);
}
int light = analogRead(LIGHT);
if (!lampOn && light < TURN_ON) {
lampOn = true;
}
if (lampOn && light > TURN_OFF) {
lampOn = false;
}
int bright = map(analogRead(POT), 0, 1023, 0, 255);
if (mode == 0) {
if (lampOn) {
analogWrite(LED, bright);
} else {
analogWrite(LED, 0);
}
}
if (mode == 1) {
analogWrite(LED, bright);
}
if (mode == 2) {
analogWrite(LED, 0);
}
}
Посмотрите на структуру loop(): сначала кнопка, потом датчик света, потом крутилка, и только в самом конце — что зажечь. Каждый блок занимается своим делом и не лезет в чужое. Именно это позволяет спокойно добавить четвёртый режим или второй датчик.
Функция blinkMode — маленькая, но важная: она превращает молчаливый прибор в понятный. Нажали кнопку — ночник моргнул два раза, значит, включён режим номер два.
Шаг 4. Готовое устройство
Последний штрих отличает поделку от вещи: свет должен нарастать плавно. Приём тот же, что в разборе «лампы настроения» из девятого урока: программа помнит текущую яркость и подтягивает её к нужной по одному шагу за проход.
// УМНЫЙ НОЧНИК
// Режимы: 0 — авто (светит в темноте), 1 — всегда включён, 2 — выключен.
// Яркость задаёт крутилка, режим переключает кнопка.
const int LIGHT = A0;
const int POT = A1;
const int BUTTON = 2;
const int LED = 10;
const int TURN_ON = 380;
const int TURN_OFF = 460;
const unsigned long BOUNCE_MS = 30;
int mode = 0;
bool lampOn = false;
int current = 0; // яркость сейчас
int target = 0; // яркость, к которой стремимся
int wasState = HIGH;
unsigned long lastChange = 0;
bool buttonPressed() {
int nowState = digitalRead(BUTTON);
bool pressed = false;
if (nowState != wasState) {
if (millis() - lastChange > BOUNCE_MS && nowState == LOW) {
pressed = true;
}
lastChange = millis();
wasState = nowState;
}
return pressed;
}
void blinkMode(int times) {
for (int i = 0; i < times; i++) {
analogWrite(LED, 255);
delay(120);
analogWrite(LED, 0);
delay(160);
}
current = 0;
}
void reportMode() {
Serial.print("режим ");
Serial.print(mode);
Serial.print(" — ");
if (mode == 0) {
Serial.println("авто");
}
if (mode == 1) {
Serial.println("всегда включён");
}
if (mode == 2) {
Serial.println("выключен");
}
}
void setup() {
Serial.begin(9600);
pinMode(BUTTON, INPUT_PULLUP);
pinMode(LED, OUTPUT);
Serial.println("Умный ночник готов.");
reportMode();
}
void loop() {
// 1. Слушаем кнопку.
if (buttonPressed()) {
mode = mode + 1;
if (mode > 2) {
mode = 0;
}
blinkMode(mode + 1);
reportMode();
}
// 2. Смотрим на свет.
int light = analogRead(LIGHT);
if (!lampOn && light < TURN_ON) {
lampOn = true;
}
if (lampOn && light > TURN_OFF) {
lampOn = false;
}
// 3. Решаем, какой должна быть яркость.
int bright = map(analogRead(POT), 0, 1023, 0, 255);
if (mode == 0) {
if (lampOn) {
target = bright;
} else {
target = 0;
}
}
if (mode == 1) {
target = bright;
}
if (mode == 2) {
target = 0;
}
// 4. Плавно подтягиваем текущую яркость к нужной.
if (current < target) {
current = current + 1;
}
if (current > target) {
current = current - 1;
}
analogWrite(LED, current);
delay(6);
}
Две переменные, current и target, — это и есть весь секрет плавности. Решение о том, какой должна быть яркость, принимается мгновенно; а приходит к ней светодиод постепенно. Никаких delay на секунды — всё по одному маленькому шагу.
Почему current = 0 внутри blinkMode? После морганий светодиод погашен, а программа об этом не знает и думает, что яркость прежняя. Одна строчка возвращает её в согласие с реальностью. Такие мелочи и отличают программу, которая просто работает, от программы, которая работает всегда.
Всё, устройство готово. Отключите его от компьютера и подключите любую зарядку от телефона к разъёму USB — ночник будет работать сам по себе. Поставьте его в коридоре и убедитесь вечером, что он включается.
Не работает? Разбираемся
- Ночник горит всегда или не горит никогда. Пороги взяты не свои. Залейте программу из первого шага, посмотрите свои числа и поставьте
TURN_ONиTURN_OFFпо ним. - Свет мигает на границе. Пороги слишком близко друг к другу. Разведите их: между ними должно быть хотя бы 60–80 единиц.
- Ночник «видит» сам себя. Светодиод светит на фоторезистор, тот решает, что светло, и гасит лампу. Разверните их в разные стороны или разнесите по макетке.
- Кнопка перескакивает через режим. Антидребезг не работает: проверьте, что
buttonPressedвызывается один раз за проход и чтоBOUNCE_MSне меньше 20. - Яркость не меняется крутилкой. Проверьте, что средняя ножка идёт на A1, а крайние — на разные шины. И что в программе
POT— этоA1, а неA0. - Свет разгорается слишком медленно. Уменьшите
delay(6)до 2 или прибавляйте яркость не по единице, а по два.
Задания для тренировки
- РазминкаСделайте, чтобы крутилка задавала не яркость, а порог темноты, при котором ночник включается. Яркость при этом пусть будет постоянной.
- ПосложнееДобавьте четвёртый режим — «ночной»: ночник светит совсем слабо, на четверти яркости, чтобы не будить. Не забудьте про количество вспышек-индикаций.
- Со звёздочкой ⭐Прицепите датчик Холла из прошлого урока: пусть ночник загорается на пятнадцать секунд, когда открыли дверь, — даже если в комнате светло. Разбор ниже.
Показать разбор «ночник с датчиком двери»
Датчик Холла подключается к свободному входу A2: питание и земля — на те же шины, сигнал — на плату. Магнит крепится на дверь, датчик остаётся на косяке.
const int LIGHT = A0;
const int POT = A1;
const int HALL = A2; // датчик Холла на косяке двери
const int BUTTON = 2;
const int LED = 10;
const int TURN_ON = 380;
const int TURN_OFF = 460;
const int DOOR_LEVEL = 40; // отклонение, ниже которого дверь считается открытой
const unsigned long BOUNCE_MS = 30;
const unsigned long DOOR_LIGHT_MS = 15000; // сколько светить после открытия
int mode = 0;
bool lampOn = false;
int zero = 512;
int current = 0;
int target = 0;
bool doorOpen = false;
unsigned long doorSince = 0;
int wasState = HIGH;
unsigned long lastChange = 0;
bool buttonPressed() {
int nowState = digitalRead(BUTTON);
bool pressed = false;
if (nowState != wasState) {
if (millis() - lastChange > BOUNCE_MS && nowState == LOW) {
pressed = true;
}
lastChange = millis();
wasState = nowState;
}
return pressed;
}
void setup() {
Serial.begin(9600);
pinMode(BUTTON, INPUT_PULLUP);
pinMode(LED, OUTPUT);
// Ноль датчика Холла — при закрытой двери.
long sum = 0;
for (int i = 0; i < 50; i++) {
sum = sum + analogRead(HALL);
delay(10);
}
zero = sum / 50;
Serial.println("Ночник с датчиком двери готов.");
}
void loop() {
if (buttonPressed()) {
mode = mode + 1;
if (mode > 2) {
mode = 0;
}
}
int light = analogRead(LIGHT);
if (!lampOn && light < TURN_ON) {
lampOn = true;
}
if (lampOn && light > TURN_OFF) {
lampOn = false;
}
// Дверь: магнит уехал — значит, открыли.
int power = abs(analogRead(HALL) - zero);
if (!doorOpen && power < DOOR_LEVEL) {
doorOpen = true;
doorSince = millis();
Serial.println("дверь открыли — включаю свет");
}
if (doorOpen && power > DOOR_LEVEL * 2) {
doorOpen = false;
}
bool doorLight = doorOpen || (millis() - doorSince < DOOR_LIGHT_MS);
int bright = map(analogRead(POT), 0, 1023, 0, 255);
if (mode == 0) {
// В авторежиме светим, если темно ИЛИ только что открыли дверь.
if (lampOn || doorLight) {
target = bright;
} else {
target = 0;
}
}
if (mode == 1) {
target = bright;
}
if (mode == 2) {
target = 0;
}
if (current < target) {
current = current + 1;
}
if (current > target) {
current = current - 1;
}
analogWrite(LED, current);
delay(6);
}
Главная строка здесь — if (lampOn || doorLight). Два знака || означают «или»: достаточно, чтобы выполнилось хотя бы одно условие. Свет включается либо потому, что темно, либо потому, что открыли дверь.
И обратите внимание, как аккуратно устройство разрослось: мы добавили целый датчик и новое поведение, но старый код почти не тронули. Именно за это мы и раскладывали программу по полочкам.
Что мы узнали
- Устройство удобно проектировать по схеме «собрать данные → принять решение → показать результат». Каждый блок
loop()занимается своим делом. - Большую схему собирают и проверяют по частям, а первая программа должна не работать красиво, а доказать, что плата видит все входы.
- Разделение «какой должна быть яркость» и «какая она сейчас» даёт плавность без единого
delay. - Прибор должен уметь рассказывать о себе — хотя бы миганием. Это дешевле лишнего индикатора и здорово выручает при отладке.
- Все приёмы блока — гистерезис, антидребезг,
map, режимы — переносятся из проекта в проект без изменений.
Блок «Датчики» закончен. Ваша плата научилась видеть свет, чувствовать наклон и удары, замечать магниты и слушаться крутилки — и всё это вы уже умеете соединять в цельное устройство.
Дальше устройство обретёт голос: в следующем блоке появится зуммер, а с ним — сигналы, мелодии и игра на скорость реакции.
Частые вопросы
С чего начинать сборку большого устройства?
С проверки по частям. Сначала соберите и проверьте один узел, потом добавьте следующий. Первая программа должна не работать красиво, а доказать, что плата видит все входы: печатайте показания в монитор порта.
Как сделать, чтобы свет включался плавно?
Завести две переменные: целевую яркость и текущую. Решение о целевой принимается сразу, а текущая подтягивается к ней по одному шагу за проход цикла. Задержки в секунды при этом не нужны, и устройство продолжает следить за кнопкой.
Почему ночник включается и выключается сам по себе?
Скорее всего, светодиод светит на фоторезистор: лампа включилась, датчик увидел свет и погасил её. Разверните детали в разные стороны или разнесите по макетной плате. Помогает и увеличение разрыва между порогами.
Как показать режим работы без второго светодиода?
Пусть устройство моргает основным светодиодом столько раз, какой режим включён. Это дешевле лишней детали и заодно подтверждает, что нажатие кнопки принято.
