Перейти к содержимому

Простая система управления сервоприводом уровнем освещенности | Arduino

Простая система управления сервоприводом уровнем освещенности | Arduino

В этом уроке вы разберёте проект для Arduino UNO: фоторезистор (LDR) управляет углом сервопривода. Это статья для начинающего: мы подробно объясним не только переменные (raw, flt, alpha), но и методы/функции, которые часто выглядят “магией”: analogRead(), map(), constrain(), Servo.attach(), Servo.write(), Serial.print(), abs(), delay(). В конце — практика с решениями и чек‑лист самопроверки.

Главная мысль: Arduino измеряет не “свет”, а напряжение на A0, превращая его в число АЦП 0…1023. Мы переводим это число в угол 0…180 через map() и отправляем на сервопривод через Servo.write(angle). Для устойчивости добавляем фильтр и порог обновления угла.

Содержание

1. Цели урока

  • Понять, что такое raw и почему он в диапазоне 0…1023.
  • Разобраться, как работает analogRead() (АЦП) и от чего зависит результат.
  • Понять, как работает map() и почему после него часто нужен constrain().
  • Научиться управлять сервоприводом через Servo.attach() и Servo.write().
  • Сделать управление спокойным: фильтр flt, коэффициент alpha, порог по углу.
  • Уметь читать отладочный вывод в Serial.
Результат: вы можете объяснить код по цепочке raw → flt → angle → servo и понимаете назначение каждой функции.
↑ К оглавлению

2. Контекст: как работает “свет → угол”

LDR — это резистор, который меняет сопротивление от освещённости. Arduino не умеет измерять сопротивление напрямую, поэтому LDR подключают через делитель напряжения: два резистора последовательно между +5V и GND, а середина — на вход A0.

bandicam_2026-02-17_21-37-23-481_e1e11.jpg

Дальше программа делает простую цепочку:

Напряжение на A0 → analogRead() → raw (0..1023)
raw → (сглаживание) → flt (0..1023)
flt → map() → angle (0..180)
angle → Servo.write(angle) → поворот сервы
Plain text

Частая путаница

Запомните: raw — это не “люксы” и не “проценты света”. Это просто число АЦП, которое зависит от вашей схемы делителя, питания и конкретного LDR.
↑ К оглавлению

3. Переменные проекта: что означает raw, flt и другие

Словарь переменных

ИмяТипЧто этоДиапазон
PIN_LDR const byte Пин, куда подключена середина делителя с LDR A0
PIN_SERVO const byte Пин управляющего сигнала сервопривода например 9
raw int Raw (сырое) значение с АЦП прямо сейчас 0…1023
filtered float Состояние фильтра (плавно “догоняет” raw) примерно 0…1023
flt int Filtered (сглаженное) значение, округлённое до целого 0…1023
alpha const float Коэффициент фильтра: скорость реакции обычно 0.05…0.2
angle int Целевой угол сервопривода 0…180
lastAngle int Последний угол, который реально отправили на серву 0…180 (старт: “маркер”)
Самое важное: raw — “как датчик измерился сейчас”, flt — “то же, но сглаженное”, angle — “команда серве”.

Как читать Serial‑лог

Мы печатаем три ключевых числа в одной строке:

raw=975 | flt=960 | angle=169
Plain text
  • raw может прыгать быстрее (шум, мгновенное изменение освещения).
  • flt обычно меняется плавнее (это цель фильтра).
  • angle — уже готовая команда сервоприводу.

Типичные ошибки

Ошибка: ожидать “проценты” вместо 0…1023

raw — это число АЦП, а не процент.

Если хотите проценты, сделайте отдельную переменную: percent = map(raw, 0, 1023, 0, 100).

Ошибка: считать, что flt всегда должен отличаться от raw

Если освещённость стабильная, фильтр “догонит” raw, и числа станут почти одинаковыми.

↑ К оглавлению

4. analogRead(): как Arduino читает LDR (raw)

Что возвращает analogRead()

analogRead(A0) измеряет напряжение на входе A0 и возвращает целое число: 0 примерно соответствует 0V, а 1023 — примерно опорному напряжению (обычно около 5V для UNO).

int raw = analogRead(A0);  // raw: 0..1023
C++
Важно: если делитель собран “наоборот”, то при увеличении освещённости raw может уменьшаться. Это не ошибка — это просто направление зависимости.

Мини‑пример чтения A0

void setup() {
  Serial.begin(9600); // запускаем Serial один раз при старте
}

void loop() {
  int raw = analogRead(A0); // читаем АЦП
  Serial.print("raw=");
  Serial.println(raw);      // печатаем и переводим строку
  delay(200);
}
C++

Типичные ошибки

Ошибка: raw всегда 0 или всегда 1023

Обычно это означает, что A0 фактически сидит на GND (0) или на +5V (1023), а не в середине делителя.

Ошибка: печатают в Serial, но забыли Serial.begin()

Serial нужно включить в setup(): Serial.begin(9600).

↑ К оглавлению

5. map() и constrain(): как получить угол (angle)

Как работает map() (с формулой)

Функция map(x, inMin, inMax, outMin, outMax) делает линейное преобразование диапазона. То есть она “растягивает/сжимает” число x из одного диапазона в другой.

Идея такая: если x находится посередине входного диапазона, результат будет посередине выходного диапазона.

map(x, 0, 1023, 0, 180)
0    → 0
1023 → 180
примерно 512 → примерно 90
Plain text
Формула (упрощённо):
y = (x - inMin) * (outMax - outMin) / (inMax - inMin) + outMin
В Arduino map() работает с целыми числами, поэтому результат округляется вниз (из‑за целочисленного деления).
int raw = 512;
int angle = map(raw, 0, 1023, 0, 180); // angle будет около 90
C++

Зачем нужен constrain()

Важный момент: map() не “зажимает” (не ограничивает) значения. Если x случайно станет меньше inMin или больше inMax, результат может выйти за пределы outMin…outMax.

Поэтому после map() часто добавляют constrain(value, min, max) — это “зажим” значения в диапазон.

int angle = map(flt, 0, 1023, 0, 180);
angle = constrain(angle, 0, 180); // гарантируем 0..180
C++

Как сделать инверсию зависимости

Если вам нужно “чем светлее, тем меньше угол” (или наоборот), проще всего инвертировать значение АЦП:

int raw = analogRead(A0);
raw = 1023 - raw; // инверсия: 0↔1023

int angle = map(raw, 0, 1023, 0, 180);
angle = constrain(angle, 0, 180);
C++

Типичные ошибки

Ошибка: перепутать диапазоны в map()

Нужно: вход 0..1023, выход 0..180.

// Плохо (пример): перепутаны диапазоны
int angle = map(raw, 0, 180, 0, 1023);
C++

Ошибка: не использовать constrain()

Иногда из‑за шумов/неожиданных значений угол может стать меньше 0 или больше 180 — constrain это предотвращает.

↑ К оглавлению

6. Servo + стабильность: attach(), write(), фильтр, abs(), delay()

Servo.attach() и Servo.write()

Библиотека Servo.h управляет сервой через объект Servo:

  • sv.attach(pin) — подключить серву к конкретному пину (настроить управление).
  • sv.write(angle) — отправить команду угла (обычно 0…180).
#include <Servo.h>

Servo sv;

void setup() {
  sv.attach(9);   // сигнал сервы на D9
  sv.write(90);   // поставить примерно в центр
}

void loop() {}
C++

Что такое flt (фильтр) и alpha

Даже если освещённость меняется идеально, raw может немного “шуметь”. Чтобы серва не получала множество мелких команд, мы используем сглаживание (экспоненциальный фильтр).

// filtered — “плавная версия” raw
filtered = filtered + alpha * (raw - filtered);
C++
  • alpha — “скорость реакции”. Пример: 0.12 означает, что за один шаг фильтр делает примерно 12% “приближения к требуемому значению”.
  • filtered — float, чтобы фильтр был плавным.
  • flt — целое число (округление), с ним удобно делать map() и печатать в Serial.
int flt = (int)(filtered + 0.5); // округление float → int
C++

Зачем нужны abs() и delay()

abs(x) возвращает модуль числа (абсолютное значение). Мы используем это для “мертвой зоны”: не обновлять серву, если угол изменился слишком мало.

if (abs(angle - lastAngle) >= 2) {
  sv.write(angle);
  lastAngle = angle;
}
C++

delay(ms) останавливает программу на указанное число миллисекунд. В этом проекте мы используем delay(100), чтобы печатать в Serial не слишком часто (так лог проще читать).

Важно: delay() “замораживает” выполнение. Для учебного проекта это нормально. В более сложных проектах часто переходят на таймер через millis(), чтобы не блокировать цикл.

Типичные ошибки

Ошибка: отправлять на серву raw вместо angle

raw = 0..1023, а серва ждёт угол 0..180.

// Плохо:
int raw = analogRead(A0);
sv.write(raw);
C++

Ошибка: порог обновления есть, но lastAngle не обновляется

Нужно обязательно: lastAngle = angle; после sv.write(angle).

↑ К оглавлению

7. Практика: задачи (с решениями)

Блок 1: raw и Serial

Задача 1: вывести raw в Serial

Считать A0 и печатать raw раз в 200 мс.

void setup() {
  Serial.begin(9600);
}

void loop() {
  int raw = analogRead(A0);
  Serial.print("raw=");
  Serial.println(raw);
  delay(200);
}
C++

Блок 2: map → angle

Задача 2: вывести angle, рассчитанный через map()

Посчитать угол из raw и печатать обе величины.

void setup() {
  Serial.begin(9600);
}

void loop() {
  int raw = analogRead(A0);
  int angle = map(raw, 0, 1023, 0, 180);
  angle = constrain(angle, 0, 180);

  Serial.print("raw="); Serial.print(raw);
  Serial.print(" | angle="); Serial.println(angle);

  delay(200);
}
C++

Задача 3: сделать инверсию зависимости

Добавить raw = 1023 - raw и посмотреть, как меняется angle.

int raw = analogRead(A0);
raw = 1023 - raw;

int angle = map(raw, 0, 1023, 0, 180);
angle = constrain(angle, 0, 180);
C++

Блок 3: фильтр и “мертвая зона”

Задача 4: собрать полный скетч (raw → flt → angle → servo)

Реализуйте сглаживание, порог обновления угла и Serial‑лог. Обратите внимание на комментарии — это и есть “документация коду”.

#include <Servo.h>

// --- Настройка пинов ---
const byte PIN_LDR   = A0;  // куда подключена середина делителя с LDR
const byte PIN_SERVO = 9;   // сигнал сервопривода

Servo sv;

// --- Фильтр (сглаживание) ---
float filtered = 0.0;       // внутреннее “плавное” значение (float)
const float alpha = 0.12;   // скорость фильтра: больше → быстрее, меньше → плавнее

// --- Порог обновления угла ---
int lastAngle = -999;       // последний отправленный угол (стартовый “маркер”)

void setup() {
  Serial.begin(9600);       // включаем Serial для отладки
  sv.attach(PIN_SERVO);     // подключаем сервопривод к пину

  // Инициализируем фильтр текущим чтением, чтобы старт был “с реального значения”
  filtered = analogRead(PIN_LDR);

  Serial.println("Start: LDR -> Servo");
}

void loop() {
  // 1) Сырое чтение АЦП (0..1023)
  int raw = analogRead(PIN_LDR);

  // 2) Экспоненциальный фильтр:
  // filtered плавно “догоняет” raw
  filtered = filtered + alpha * (raw - filtered);

  // 3) Округляем float → int (это и есть наше flt)
  int flt = (int)(filtered + 0.5);

  // (Опционально) инверсия зависимости:
  // flt = 1023 - flt;

  // 4) Перевод 0..1023 в 0..180
  int angle = map(flt, 0, 1023, 0, 180);

  // 5) Ограничение угла на всякий случай
  angle = constrain(angle, 0, 180);

  // 6) “Мёртвая зона”: обновляем серву только если угол изменился заметно
  if (abs(angle - lastAngle) >= 2) {
    sv.write(angle);
    lastAngle = angle;
  }

  // 7) Отладочный вывод: видно raw / flt / angle
  Serial.print("raw=");   Serial.print(raw);
  Serial.print(" | flt=");   Serial.print(flt);
  Serial.print(" | angle="); Serial.println(angle);

  // 8) Пауза, чтобы лог был читаемым
  delay(100);
}
C++
↑ К оглавлению

8. Чек‑лист самопроверки

Отметьте пункты, которые вы действительно понимаете и можете объяснить без подсказок.

НавыкПроверка
Понимаю raw Могу объяснить, что raw — это результат analogRead() (0…1023)
Понимаю analogRead() Могу объяснить, что функция измеряет напряжение на A0 и возвращает число
Понимаю map() Могу объяснить идею и формулу линейного преобразования диапазонов
Понимаю constrain() Могу объяснить, что функция ограничивает значение заданным диапазоном
Понимаю Servo.attach()/write() Могу подключить серву к пину и отправить угол 0…180
Понимаю filtered/flt и alpha Могу объяснить, что такое сглаживание и как alpha влияет на реакцию
Понимаю abs() Могу объяснить, как модуль помогает сделать “мертвую зону” по углу
Понимаю delay() Могу объяснить, что delay “останавливает” выполнение на N миллисекунд
Понимаю Serial.begin/print/println Могу включить Serial и вывести лог в удобном формате raw | flt | angle
Могу собрать полный скетч Могу написать код: raw → flt → angle → servo и объяснить каждую строку
↑ К оглавлению
Конспект:
Вторник, 17 февраля 2026
Простая система управления сервоприводом уровнем освещенности | Arduino