Простая схема управления сервоприводом на базе фоторезистора | 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. Цели урока
- 2. Контекст: как работает “свет → угол”
- 3. Переменные проекта: что означает raw, flt и другие
- 4. analogRead(): как Arduino читает LDR (raw)
- 5. map() и constrain(): как получить угол (angle)
- 6. Servo + стабильность: attach(), write(), фильтр, abs(), delay()
- 7. Практика: задачи (с решениями)
- 8. Чек‑лист самопроверки
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.
Дальше программа делает простую цепочку:
Напряжение на A0 → analogRead() → raw (0..1023) # Напряжение с делителя поступает на вход A0 и получает цифровой уровень (от 0 до 1023)
raw → (сглаживание) → flt (0..1023) # Полученный уровень сглаживается (фильтруется)
flt → map() → angle (0..180) # сглаженное значение получает значение Угла поворота сервопривода
angle → Servo.write(angle) → поворот сервы # вычисленное Значение угла преобразуется в реальный Угол поворота Сервы
дальше будет раъяснено подробно.
Частая путаница
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
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
raw может уменьшаться. Это не ошибка — это просто направление зависимости.Мини‑пример чтения A0
void setup() {
Serial.begin(9600); // запускаем Serial один раз при старте
}
void loop() {
int raw = analogRead(A0); // читаем АЦП
Serial.print("raw=");
Serial.println(raw); // печатаем и переводим строку
delay(200);
}
Типичные ошибки
Ошибка: 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
y = (x - inMin) * (outMax - outMin) / (inMax - inMin) + outMinВ Arduino
map() работает с целыми числами, поэтому результат округляется вниз (из‑за целочисленного деления).int raw = 512;
int angle = map(raw, 0, 1023, 0, 180); // angle будет около 90
Зачем нужен 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
Как сделать инверсию зависимости
Если вам нужно “чем светлее, тем меньше угол” (или наоборот), проще всего инвертировать значение АЦП:
int raw = analogRead(A0);
raw = 1023 - raw; // инверсия: 0↔1023
int angle = map(raw, 0, 1023, 0, 180);
angle = constrain(angle, 0, 180);
Типичные ошибки
Ошибка: перепутать диапазоны в map()
Нужно: вход 0..1023, выход 0..180.
// Плохо (пример): перепутаны диапазоны
int angle = map(raw, 0, 180, 0, 1023);
Ошибка: не использовать 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() {}
Что такое flt (фильтр) и alpha
Даже если освещённость меняется идеально, raw может немного “шуметь”. Чтобы серва не получала множество мелких команд, мы используем сглаживание (экспоненциальный фильтр).
// filtered — “плавная версия” raw
filtered = filtered + alpha * (raw - filtered);
alpha— “скорость реакции”. Пример:0.12означает, что за один шаг фильтр делает примерно 12% “приближения к требуемому значению”.filtered— float, чтобы фильтр был плавным.flt— целое число (округление), с ним удобно делатьmap()и печатать в Serial.
int flt = (int)(filtered + 0.5); // округление float → int
Зачем нужны abs() и delay()
abs(x) возвращает модуль числа (абсолютное значение). Мы используем это для “мертвой зоны”: не обновлять серву, если угол изменился слишком мало.
if (abs(angle - lastAngle) >= 2) {
sv.write(angle);
lastAngle = angle;
}
delay(ms) останавливает программу на указанное число миллисекунд. В этом проекте мы используем delay(100), чтобы печатать в Serial не слишком часто (так лог проще читать).
delay() “замораживает” выполнение. Для учебного проекта это нормально. В более сложных проектах часто переходят на таймер через millis(), чтобы не блокировать цикл.Типичные ошибки
Ошибка: отправлять на серву raw вместо angle
raw = 0..1023, а серва ждёт угол 0..180.
// Плохо:
int raw = analogRead(A0);
sv.write(raw);
Ошибка: порог обновления есть, но 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);
}
Блок 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);
}
Задача 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);
Блок 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);
}
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 и объяснить каждую строку |
