Курс Arduino с нуля · Датчики

Урок 18. Проект: умный ночник

Собираем из датчиков блока цельное устройство: включается в темноте, слушается кнопки, настраивается крутилкой и светит плавно.

Урок 18 из 31⏱ примерно 60 минутОбновлено: 26 июля 2026

Что понадобится

  • Плата Uno из набора
  • Кабель USB
  • Макетная плата
  • Фоторезистор
  • Резистор 10 кОм
  • Потенциометр 10 кОм
  • Тактовая кнопка
  • Светодиод (любого цвета)
  • Резистор 470 Ом
  • 8 проводов «папа-папа»

Все детали есть в обучающем наборе, по которому идёт курс.

Пять уроков подряд мы разбирали датчики по одному. Пора собрать из них настоящее устройство — такое, которое не стыдно поставить на полку и которым будут пользоваться дома.

Наш ночник умеет вот что: сам включается, когда стемнело, светит ровно с той яркостью, которую вы задали крутилкой, и слушается кнопки — авто, всегда включён, выключен. Свет разгорается и гаснет плавно, а не рывком.

Этот урок — не про новые команды. Всё, что понадобится, вы уже знаете. Он про то, как из отдельных приёмов собирается цельная вещь: как разложить программу по полочкам, как проверять сборку по частям и как не запутаться, когда деталей становится много.

Как устроено устройство

Прежде чем брать провода, полезно нарисовать на бумаге, что откуда берётся. У нашего прибора три источника данных и один исполнитель:

Фоторезистортемно или светлоКрутилкакакая яркостьКнопкакакой режимРешениекакой должна бытьяркость прямо сейчасtargetПлавностьcurrent тянется к targetпо одному шагуanalogWrite(LED, current)Программа разделена на части: сначала собрать данные, потом принять решение, потом показать результат.Так устроено почти любое устройство — и так его легко дорабатывать.
Собрать данные → принять решение → показать результат. Так устроено почти любое устройство.

Это разделение — главный приём урока. Пока программа маленькая, кажется, что можно писать как придётся. Но стоит появиться третьему датчику, и без порядка код превращается в кашу, в которой невозможно ничего поменять.

Три режима

Автоавтосветит, когда темноВсегда включёнсветит в любом случаеВыключенне светит совсемПри переключении ночник моргает столько раз, какой режим включён, — отдельный индикатор не нужен.
Кнопка листает режимы по кругу, а ночник подмигивает столько раз, какой режим включён.

Обратите внимание на приём с индикацией: отдельный светодиод-индикатор не нужен, устройство сообщает о себе само собой. Так делают в настоящих приборах, где каждая лишняя деталь стоит денег и места.

Собираем схему

Отключите USB. Схема большая, поэтому собирайте по частям и проверяйте каждую: сначала делитель со светом, потом крутилку, потом кнопку, потом светодиод. Искать ошибку в готовой куче проводов вчетверо дольше.

++abcdefghij1102030491123262817191234561Фоторезистор из шины «+» в столбик 4, резистор 10 кОм из него же в шину «−».2Потенциометр: три ножки в столбики 9, 10 и 11; крайние — на шины «+» и «−».3Кнопка верхом на канавке (столбики 17 и 19); снизу от канавки — перемычка в нижнюю шину «−».4Светодиод: резистор 470 Ом и длинная ножка; короткая — в шину «−».5Сигналы: фоторезистор → A0, крутилка → A1, кнопка → вывод 2, светодиод → вывод 10.6Питание: 5V → шина «+», GND → шина «−», перемычка соединяет верхнюю и нижнюю шины «−».
Всё вместе: фоторезистор на A0, крутилка на A1, кнопка на выводе 2, светодиод на 10.

Проверьте перед включением: фоторезистор и резистор 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, режимы — переносятся из проекта в проект без изменений.

Блок «Датчики» закончен. Ваша плата научилась видеть свет, чувствовать наклон и удары, замечать магниты и слушаться крутилки — и всё это вы уже умеете соединять в цельное устройство.

Дальше устройство обретёт голос: в следующем блоке появится зуммер, а с ним — сигналы, мелодии и игра на скорость реакции.

Частые вопросы

С чего начинать сборку большого устройства?

С проверки по частям. Сначала соберите и проверьте один узел, потом добавьте следующий. Первая программа должна не работать красиво, а доказать, что плата видит все входы: печатайте показания в монитор порта.

Как сделать, чтобы свет включался плавно?

Завести две переменные: целевую яркость и текущую. Решение о целевой принимается сразу, а текущая подтягивается к ней по одному шагу за проход цикла. Задержки в секунды при этом не нужны, и устройство продолжает следить за кнопкой.

Почему ночник включается и выключается сам по себе?

Скорее всего, светодиод светит на фоторезистор: лампа включилась, датчик увидел свет и погасил её. Разверните детали в разные стороны или разнесите по макетной плате. Помогает и увеличение разрыва между порогами.

Как показать режим работы без второго светодиода?

Пусть устройство моргает основным светодиодом столько раз, какой режим включён. Это дешевле лишней детали и заодно подтверждает, что нажатие кнопки принято.

← Все уроки курса