Курс Arduino с нуля · Движение
Урок 24. Проект: шлагбаум
Собираем законченное устройство: кнопка поднимает стрелу, светодиоды показывают состояние, зуммер предупреждает. Заодно учимся строить программу из состояний, а не из пауз.
Что понадобится
- Плата Uno из набора
- Кабель USB
- Макетная плата
- Сервопривод SG90
- Тактовая кнопка
- Светодиод красный
- Светодиод зелёный
- 2 резистора 470 Ом
- Зуммер
- 10 проводов «папа-папа»
- Полоска плотного картона для стрелы
Все детали есть в обучающем наборе, по которому идёт курс.
Пора собрать из всего выученного законченное устройство. Сегодня это шлагбаум — тот самый, что стоит на въезде во двор: нажали кнопку — стрела поднялась, машина проехала — стрела опустилась сама.
Проект соединит четыре детали, знакомые по прошлым урокам: сервопривод, кнопку, два светодиода и зуммер. А главное — мы научимся строить программу так, как её строят в настоящей технике.
Что должно получиться
Список того, что шлагбаум должен уметь:
- по нажатию кнопки поднимать стрелу — плавно, а не рывком;
- держать её поднятой несколько секунд и закрываться самому;
- показывать состояние: красный — закрыто, зелёный — открыто;
- пищать, пока стрела едет, — как настоящий;
- слушать кнопку всё время, даже во время движения.
Собираем схему
Ничего нового: сервопривод — как в двадцать втором уроке, светодиоды с резисторами — как в шестом, зуммер — как в девятнадцатом, кнопка — как в десятом.
Отключите USB и собирайте по частям. Сначала светодиоды — проверьте их отдельной программой. Потом кнопку и зуммер. Сервопривод — последним. Так вы будете точно знать, где искать ошибку.
Шаг 1. Проверяем узлы
Первая программа ничего не делает — она просто ставит шлагбаум в исходное положение. Если стрела опустилась, а красный светодиод загорелся, значит собрано верно.
#include <Servo.h>
Servo arm;
const int RED = 4;
const int GREEN = 7;
const int CLOSED = 10; // угол «стрела опущена»
void setup() {
arm.attach(9);
pinMode(RED, OUTPUT);
pinMode(GREEN, OUTPUT);
arm.write(CLOSED); // опускаем стрелу
digitalWrite(RED, HIGH); // и зажигаем красный
digitalWrite(GREEN, LOW);
}
void loop() {
// Пока ничего: проверяем, что стрела внизу и горит красный.
}
Проверьте заодно, куда поворачивается качалка. Если стрела уходит не в ту сторону, поменяйте местами числа CLOSED и OPENED в следующих программах — или просто снимите качалку с вала и наденьте её в другом положении.
Шаг 2. Кнопка открывает шлагбаум
Самый простой рабочий вариант — «в лоб»: нажали, открыли, подождали, закрыли.
#include <Servo.h>
Servo arm;
const int BUTTON = 2;
const int RED = 4;
const int GREEN = 7;
const int CLOSED = 10; // стрела опущена
const int OPENED = 100; // стрела поднята
void setup() {
arm.attach(9);
pinMode(BUTTON, INPUT_PULLUP);
pinMode(RED, OUTPUT);
pinMode(GREEN, OUTPUT);
arm.write(CLOSED);
digitalWrite(RED, HIGH);
}
void loop() {
if (digitalRead(BUTTON) == LOW) {
// Открываем
digitalWrite(RED, LOW);
digitalWrite(GREEN, HIGH);
arm.write(OPENED);
delay(3000); // машина проезжает
// Закрываем
digitalWrite(GREEN, LOW);
digitalWrite(RED, HIGH);
arm.write(CLOSED);
delay(500);
}
}
Работает. Но у этой программы есть неприятная особенность, и её стоит увидеть своими глазами: пока идёт delay(3000), устройство не замечает вообще ничего. Нажмите кнопку ещё раз, пока стрела поднята, — плата не отреагирует. А стрела дёргается вверх рывком, потому что мы просто отдаём команду «встань на 100 градусов».
Как думают настоящие устройства
Приём, который решает обе проблемы сразу, называется состояния. Идея простая: устройство всегда находится в одном из нескольких понятных положений, а программа только решает, когда пора перейти в следующее.
Заметьте, что здесь нет ни одного «подожди». Есть только вопросы: не нажали ли кнопку? не доехала ли стрела? не вышло ли время? Программа проверяет их тысячи раз в секунду и потому всегда готова откликнуться.
Шаг 3. Четыре состояния
#include <Servo.h>
Servo arm;
const int BUTTON = 2;
const int RED = 4;
const int GREEN = 7;
const int CLOSED = 10;
const int OPENED = 100;
const int STEP_MS = 12; // пауза между градусами
const unsigned long HOLD = 4000; // сколько стоять открытым
// Четыре состояния шлагбаума.
const int ST_CLOSED = 0;
const int ST_OPENING = 1;
const int ST_OPEN = 2;
const int ST_CLOSING = 3;
int state = ST_CLOSED;
int angle = CLOSED;
unsigned long lastStep = 0;
unsigned long openedAt = 0;
bool wasPressed = false;
void setup() {
arm.attach(9);
pinMode(BUTTON, INPUT_PULLUP);
pinMode(RED, OUTPUT);
pinMode(GREEN, OUTPUT);
arm.write(angle);
}
void loop() {
bool pressed = digitalRead(BUTTON) == LOW;
bool justPressed = pressed && !wasPressed;
wasPressed = pressed;
// Светофор: зелёный горит, только когда стрела поднята.
digitalWrite(GREEN, state == ST_OPEN);
digitalWrite(RED, state != ST_OPEN);
if (state == ST_CLOSED) {
if (justPressed) {
state = ST_OPENING;
}
} else if (state == ST_OPENING) {
if (moveTo(OPENED)) {
state = ST_OPEN;
openedAt = millis();
}
} else if (state == ST_OPEN) {
if (millis() - openedAt >= HOLD) {
state = ST_CLOSING;
}
} else if (state == ST_CLOSING) {
if (moveTo(CLOSED)) {
state = ST_CLOSED;
}
}
}
// Двигает стрелу на один градус к цели.
// Возвращает true, когда доехали.
bool moveTo(int target) {
if (angle == target) {
return true;
}
if (millis() - lastStep >= STEP_MS) {
lastStep = millis();
if (angle < target) {
angle++;
} else {
angle--;
}
arm.write(angle);
}
return false;
}
Разберём главное. Переменная state хранит, где мы сейчас. В loop идёт цепочка if — по одной ветке на состояние, и в каждой только проверка «пора ли дальше».
Движение вынесено в отдельную функцию moveTo. Она сдвигает стрелу на один градус, когда пришло время, и отвечает true, если доехала. Такая функция — как маленький помощник: главная программа не вникает, как именно он двигает стрелу, ей важен только ответ «доехал или нет».
И ещё одна мелочь, которая экономит четыре строки: digitalWrite(GREEN, state == ST_OPEN). Сравнение даёт «да» или «нет», а для платы это то же самое, что HIGH и LOW.
Шаг 4. Готовый шлагбаум
Добавим то, что превращает поделку в устройство: предупреждающий сигнал во время движения, продление времени по второму нажатию и возможность передумать, пока стрела опускается.
#include <Servo.h>
Servo arm;
const int BUTTON = 2;
const int RED = 4;
const int GREEN = 7;
const int BUZZER = 12;
const int CLOSED = 10;
const int OPENED = 100;
const int STEP_MS = 12;
const unsigned long HOLD = 4000;
const int ST_CLOSED = 0;
const int ST_OPENING = 1;
const int ST_OPEN = 2;
const int ST_CLOSING = 3;
int state = ST_CLOSED;
int angle = CLOSED;
unsigned long lastStep = 0;
unsigned long openedAt = 0;
unsigned long lastBeep = 0;
bool wasPressed = false;
void setup() {
arm.attach(9);
pinMode(BUTTON, INPUT_PULLUP);
pinMode(RED, OUTPUT);
pinMode(GREEN, OUTPUT);
pinMode(BUZZER, OUTPUT);
arm.write(angle);
Serial.begin(9600);
}
void loop() {
bool pressed = digitalRead(BUTTON) == LOW;
bool justPressed = pressed && !wasPressed;
wasPressed = pressed;
digitalWrite(GREEN, state == ST_OPEN);
digitalWrite(RED, state != ST_OPEN);
// Пока стрела едет — короткие предупреждающие сигналы.
if (state == ST_OPENING || state == ST_CLOSING) {
if (millis() - lastBeep >= 500) {
lastBeep = millis();
tone(BUZZER, 1500, 80);
}
}
if (state == ST_CLOSED) {
if (justPressed) {
state = ST_OPENING;
Serial.println("открываю");
}
} else if (state == ST_OPENING) {
if (moveTo(OPENED)) {
state = ST_OPEN;
openedAt = millis();
}
} else if (state == ST_OPEN) {
// Новое нажатие продлевает открытое время — как будто едет ещё машина.
if (justPressed) {
openedAt = millis();
Serial.println("продлеваю");
}
if (millis() - openedAt >= HOLD) {
state = ST_CLOSING;
Serial.println("закрываю");
}
} else if (state == ST_CLOSING) {
// Передумали: нажали во время закрытия — едем обратно вверх.
if (justPressed) {
state = ST_OPENING;
Serial.println("передумали, открываю");
} else if (moveTo(CLOSED)) {
state = ST_CLOSED;
}
}
}
bool moveTo(int target) {
if (angle == target) {
return true;
}
if (millis() - lastStep >= STEP_MS) {
lastStep = millis();
if (angle < target) {
angle++;
} else {
angle--;
}
arm.write(angle);
}
return false;
}
Обратите внимание, как дёшево обошлись новые возможности. «Продлить время» — это одна строка openedAt = millis(). «Передумать при закрытии» — одна проверка внутри ветки ST_CLOSING. Так и должно быть: если устройство разложено по состояниям, новое поведение добавляется в одно место.
Делаем стрелу
Вырежьте из плотного картона полоску длиной 15–20 сантиметров и шириной около сантиметра, раскрасьте её красно-белыми полосами и приклейте к качалке или примотайте ниткой. Картон лёгкий — сервопривод поднимет его без труда.
Не делайте стрелу тяжёлой и длинной. Сервопривод рассчитан на маленькое усилие: длинная линейка или деревянная палочка перегрузят его, он загудит и может сломаться. Сам сервопривод при этом стоит закрепить — иначе он будет подпрыгивать вместо того, чтобы поднимать стрелу.
Не работает? Разбираемся
- Стрела дёргается и гудит, плата перезагружается. Не хватает питания: стрела слишком тяжёлая или сервопривод упирается. Уменьшите стрелу и проверьте, что качалка свободно ходит между двумя углами.
- Шлагбаум открывается сам, без кнопки. Провод кнопки попал не в тот столбик, или земля кнопки не дошла до шины. Проверьте перемычку между шинами «−» верхней и нижней половин.
- Светодиоды горят наоборот. Перепутаны выводы 4 и 7 — либо провода на макетке, либо номера в программе.
- Зуммер молчит. Проверьте полярность: «плюс» зуммера должен смотреть в столбик 14, к выводу 12.
- Стрела едет рывком, а не плавно. Вы залили программу второго шага. Плавный ход появляется начиная с третьего.
- Кнопка срабатывает через раз. Так бывает, если нажатие ловится по состоянию, а не по моменту. В наших программах за это отвечает пара
pressedиwasPressed— проверьте, что обе строки на месте.
Задания для тренировки
- РазминкаПусть зелёный светодиод мигает последнюю секунду перед закрытием — как предупреждение «поторопитесь».
- ПосложнееЗамените кнопку датчиком: пусть шлагбаум открывается сам, когда машина подъезжает. Подойдёт фоторезистор — тень от машины закрывает свет.
- Со звёздочкой ⭐Добавьте пятое состояние «АВАРИЯ»: если кнопку держат больше трёх секунд, шлагбаум остаётся открытым, а красный и зелёный мигают по очереди, пока не нажмут снова. Схема состояний из этого урока подскажет, куда добавить новую ветку.
Что мы узнали
- Готовое устройство — это не одна большая программа, а несколько понятных состояний и правила перехода между ними.
- Команда
delayудобна для первых опытов, но в законченном устройстве от неё отказываются: она делает программу глухой. - Свои функции прячут подробности: главная программа спрашивает «доехал?» и не думает, как именно это происходит.
- Новое поведение добавляется в одно место — это главная награда за аккуратную структуру.
- Схему состояний полезно нарисовать на бумаге до того, как писать код.
На этом блок «Движение» закончен: вы умеете поворачивать на угол, крутить мотор и строить устройства, которые не замирают. Дальше — экраны: научим плату показывать цифры и буквы.
Частые вопросы
Как сделать шлагбаум на Arduino?
Нужен сервопривод, который поворачивает стрелу примерно на 90 градусов, кнопка для открытия и два светодиода для показа состояния. Стрела вырезается из плотного картона и надевается на качалку сервопривода.
Почему в готовом устройстве нельзя пользоваться delay?
Во время delay плата не замечает ничего: ни нажатий кнопки, ни датчиков. Шлагбаум с паузой не отреагирует на вторую машину и не даст передумать. Вместо пауз используют состояния и проверку времени через millis.
Что такое машина состояний простыми словами?
Это способ описать устройство через несколько понятных положений — например закрыто, поднимается, открыто, опускается — и правила перехода между ними. Программа в каждом проходе только проверяет, не пора ли перейти в следующее состояние.
Из чего сделать стрелу шлагбаума?
Из плотного картона: полоска 15–20 сантиметров длиной и около сантиметра шириной. Тяжёлую и длинную стрелу — линейку или деревянную палочку — ставить нельзя, маленький сервопривод её не поднимет и может сломаться.
Почему шлагбаум дёргается и плата перезагружается?
Сервоприводу не хватает питания от USB. Причина обычно в нагрузке: стрела слишком тяжёлая или качалка во что-то упирается. Уменьшите стрелу, закрепите сервопривод или дайте ему отдельный источник 5 вольт с общей землёй.
