Курс 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 вольт с общей землёй.

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