Курс Arduino с нуля · Движение

Урок 24. Проект: шлагбаум

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

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

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

  • Плата Uno из набора
  • Кабель USB
  • Макетная плата
  • Сервопривод SG90
  • Тактовая кнопка
  • Светодиод красный
  • Светодиод зелёный
  • 2 резистора 470 Ом
  • Зуммер
  • 10 проводов «папа-папа»
  • Полоска плотного картона для стрелы

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

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

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

Что должно получиться

Закрытострела внизу, горит красныйОткрытострела поднята, горит зелёныйСтрела примотана к качалке и поворачивается вместе с ней: качалка вверх — и стрела вверх.
Стрела примотана к качалке и поворачивается вместе с ней. Всё движение — это поворот качалки примерно на 90 градусов.

Список того, что шлагбаум должен уметь:

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

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

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

Отключите USB и собирайте по частям. Сначала светодиоды — проверьте их отдельной программой. Потом кнопку и зуммер. Сервопривод — последним. Так вы будете точно знать, где искать ошибку.

++abcdefghij15101520+123451Красный светодиод: длинная ножка в столбик 2, короткая в 4; резистор 470 Ом из столбика 4 на шину «−».2Провод из столбика 2 идёт в вывод 4. Зелёный светодиод — так же: столбики 8 и 10, провод в вывод 7.3Зуммер: «плюс» в столбик 14 → вывод 12, вторая ножка в столбик 17 → шина «−».4Кнопка верхом на канавке: столбик 20 → вывод 2, столбик 18 → шина «−» нижней половины.5Сервопривод: коричневый — в шину «−», красный — в шину «+», оранжевый — в вывод 9.Перемычка поперёк платы соединяет шины «−» верхней и нижней половин, справа в обход — шины «+».Провод соединён только там, где на нём светлая шляпка; шины он пересекает между отверстиями.
Красный светодиод на выводе 4, зелёный на 7, зуммер на 12, кнопка на 2, сервопривод на 9.

Шаг 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 градусов».

Как думают настоящие устройства

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

ЗАКРЫТОстрела внизуПОДНИМАЕТСЯпо градусу вверхОТКРЫТОждём 4 секундыОПУСКАЕТСЯпо градусу внизнажали кнопкустрела доехалавремя вышлострела доехалаВ каждый момент шлагбаум находится ровно в одном из четырёх состояний.Программа только решает, когда пора перейти в следующее.
Четыре состояния по кругу. Такой чертёж полезно набросать на бумаге до того, как писать программу.

Заметьте, что здесь нет ни одного «подожди». Есть только вопросы: не нажали ли кнопку? не доехала ли стрела? не вышло ли время? Программа проверяет их тысячи раз в секунду и потому всегда готова откликнуться.

Шаг 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 вольт с общей землёй.

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