Курс Arduino с нуля · Управляем светом
Урок 7. Настоящий светофор
Собираем светофор с настоящими фазами дорожного движения и пишем свои функции — приём, который держит длинную программу понятной.
Что понадобится
- Плата Uno из набора
- Кабель USB
- Макетная плата
- Светодиоды: красный, жёлтый, зелёный
- 3 резистора 470 Ом
- 4 провода «папа-папа»
Все детали есть в обучающем наборе, по которому идёт курс.
Бегущий огонёк был красивым эффектом. Сегодня мы сделаем настоящее устройство — светофор, который переключается по правилам дорожного движения. Разница принципиальная: у эффекта нет смысла, а у устройства есть состояния, и каждое что-то означает для человека.
Заодно наведём порядок в программе: познакомимся со своими функциями — приёмом, которым пользуются все программисты, чтобы длинный код оставался понятным.
Как на самом деле работает светофор
Прежде чем программировать, разберёмся, что именно мы повторяем. Российский транспортный светофор проходит четыре фазы по кругу:
Ключевая деталь, которую многие упускают: фаза «приготовиться» — это красный и жёлтый вместе. Именно так на наших дорогах предупреждают, что сейчас включится зелёный. А вот перед красным горит только жёлтый, без красного. Несимметрично — и это не ошибка, а правило.
Что такое состояние. В любой момент светофор находится ровно в одном из четырёх состояний, и каждое описывается тремя фактами: горит ли красный, горит ли жёлтый, горит ли зелёный. Программа устройства — это описание состояний и правил перехода между ними. Дальше в курсе так будут устроены и сигнализация, и сейф, и игра.
Собираем светофор
Схема вам уже знакома — это три канала из прошлого урока. Возьмите светодиоды нужных цветов: красный, жёлтый и зелёный. Расположите их на макетке слева направо в том же порядке, в каком они идут на настоящем светофоре сверху вниз, — так гораздо легче не запутаться.
Кабель USB — из платы. Собираем на обесточенной схеме.
Если у вас остался собранный бегущий огонёк — просто снимите два лишних канала и поменяйте светодиоды на нужные цвета. Сборка займёт пару минут.
Шаг 1. Просто три цвета по очереди
Начнём с самого очевидного: пусть цвета сменяют друг друга. Обратите внимание — номера выводов сразу записаны понятными именами:
const int RED = 10;
const int YELLOW = 9;
const int GREEN = 8;
void setup() {
pinMode(RED, OUTPUT);
pinMode(YELLOW, OUTPUT);
pinMode(GREEN, OUTPUT);
}
void loop() {
digitalWrite(RED, HIGH); // горит красный
delay(3000);
digitalWrite(RED, LOW);
digitalWrite(YELLOW, HIGH); // горит жёлтый
delay(1000);
digitalWrite(YELLOW, LOW);
digitalWrite(GREEN, HIGH); // горит зелёный
delay(3000);
digitalWrite(GREEN, LOW);
}
Прошейте. Светодиоды честно моргают по очереди… но настоящий светофор так себя не ведёт: нет фазы «красный + жёлтый», а после зелёного сразу вспыхивает красный. Водитель, глядя на такой светофор, ничего не поймёт. Сделаем правильно.
Шаг 2. Настоящие фазы
Раз в фазе может гореть несколько ламп, удобнее в каждой фазе явно указывать состояние всех трёх светодиодов — и включаемых, и выключаемых:
const int RED = 10;
const int YELLOW = 9;
const int GREEN = 8;
void setup() {
pinMode(RED, OUTPUT);
pinMode(YELLOW, OUTPUT);
pinMode(GREEN, OUTPUT);
}
void loop() {
// Фаза 1: стоять.
digitalWrite(RED, HIGH);
digitalWrite(YELLOW, LOW);
digitalWrite(GREEN, LOW);
delay(5000);
// Фаза 2: приготовиться — красный и жёлтый вместе.
digitalWrite(RED, HIGH);
digitalWrite(YELLOW, HIGH);
digitalWrite(GREEN, LOW);
delay(2000);
// Фаза 3: ехать.
digitalWrite(RED, LOW);
digitalWrite(YELLOW, LOW);
digitalWrite(GREEN, HIGH);
delay(5000);
// Фаза 4: жёлтый — внимание, сейчас будет красный.
digitalWrite(RED, LOW);
digitalWrite(YELLOW, HIGH);
digitalWrite(GREEN, LOW);
delay(2000);
}
Прошейте — теперь это настоящий светофор. Постойте рядом полминуты и проследите за циклом: стоять, приготовиться, ехать, внимание. Узнаваемо?
Программа работает правильно, но посмотрите на неё внимательно. Четыре фазы — и в каждой три почти одинаковые строки digitalWrite. Двенадцать строк, отличающихся только словами HIGH и LOW. Знакомое чувство из прошлого урока: это просит сокращения.
Шаг 3. Своя функция
В прошлый раз нас выручил цикл. Но здесь повторяются не одинаковые действия подряд, а один и тот же набор команд с разными значениями. Для такого случая существует функция — собственная команда, которую вы придумываете сами.
Вы уже пользовались чужими функциями весь курс: digitalWrite(), delay(), pinMode() — это всё функции, которые кто-то написал за вас. Пришло время написать свою:
const int RED = 10;
const int YELLOW = 9;
const int GREEN = 8;
// Наша собственная команда: включить нужные лампы светофора.
void setLights(int redState, int yellowState, int greenState) {
digitalWrite(RED, redState);
digitalWrite(YELLOW, yellowState);
digitalWrite(GREEN, greenState);
}
void setup() {
pinMode(RED, OUTPUT);
pinMode(YELLOW, OUTPUT);
pinMode(GREEN, OUTPUT);
}
void loop() {
// красный жёлтый зелёный
setLights( HIGH, LOW, LOW ); // стоять
delay(5000);
setLights( HIGH, HIGH, LOW ); // приготовиться
delay(2000);
setLights( LOW, LOW, HIGH ); // ехать
delay(5000);
setLights( LOW, HIGH, LOW ); // внимание
delay(2000);
}
Разберём новую часть построчно:
void setLights(int redState, int yellowState, int greenState)— заголовок функции.setLights— имя, которое мы придумали (по-английски «установить лампы»); в скобках — параметры: три значения, которые функция получает при вызове.voidозначает «функция ничего не возвращает обратно» — она просто делает дело.- В фигурных скобках — тело функции: то, что выполнится при каждом вызове. Внутри слова
redState,yellowStateиgreenStateозначают те значения, которые передали в этот раз. - В
loop()мы теперь вызываем свою функцию:setLights(HIGH, LOW, LOW);— «красный включить, жёлтый выключить, зелёный выключить». Одна строка вместо трёх, и читается почти как таблица фаз.
Где размещать функцию. Свои функции пишут рядом с setup() и loop() — на том же уровне, не внутри них. В нашем примере setLights стоит выше setup(), но может стоять и ниже loop() — плата найдёт её в любом случае.
Оцените выигрыш: чтобы поменять длительность фазы, достаточно поправить одно число, а чтобы понять, что делает программа, хватает беглого взгляда на четыре строки с комментариями. Именно так пишут настоящие программы: сложное прячут внутрь функций, а снаружи оставляют понятную последовательность действий.
Шаг 4. Мигающий зелёный
На многих перекрёстках зелёный перед выключением начинает мигать: «поторопитесь, время заканчивается». Добавим и это — второй своей функцией, с одним параметром:
const int RED = 10;
const int YELLOW = 9;
const int GREEN = 8;
void setLights(int redState, int yellowState, int greenState) {
digitalWrite(RED, redState);
digitalWrite(YELLOW, yellowState);
digitalWrite(GREEN, greenState);
}
// Ещё одна своя команда: зелёный мигает заданное число раз.
void blinkGreen(int times) {
for (int i = 0; i < times; i++) {
setLights(LOW, LOW, HIGH);
delay(400);
setLights(LOW, LOW, LOW);
delay(400);
}
}
void setup() {
pinMode(RED, OUTPUT);
pinMode(YELLOW, OUTPUT);
pinMode(GREEN, OUTPUT);
}
void loop() {
setLights(HIGH, LOW, LOW); // стоять
delay(5000);
setLights(HIGH, HIGH, LOW); // приготовиться
delay(2000);
setLights(LOW, LOW, HIGH); // ехать
delay(4000);
blinkGreen(3); // зелёный мигает — время заканчивается
setLights(LOW, HIGH, LOW); // внимание
delay(2000);
}
Функция blinkGreen(int times) принимает число: сколько раз мигнуть. Внутри неё работает знакомый цикл for из прошлого урока, а внутри цикла — вызов другой нашей функции, setLights. Функции спокойно вызывают друг друга, и это нормальный, повседневный приём.
В loop() появилась всего одна новая строка — blinkGreen(3);. Хотите пять миганий вместо трёх? Поменяйте число. Хотите убрать? Уберите строку. Программа осталась читаемой, хотя устройство усложнилось.
Эксперименты
- Своё время фаз. Сделайте красный длинным (10 секунд), а зелёный коротким (3 секунды) — получится светофор на второстепенной дороге.
- Скорость мигания. В функции
blinkGreenпоменяйтеdelay(400)наdelay(150)— зелёный станет нервно частить. - Ночной режим. Ночью многие перекрёстки переходят в мигающий жёлтый — «проезжайте осторожно, светофор не регулирует». Целая программа для этого — восемь строк:
const int RED = 10;
const int YELLOW = 9;
const int GREEN = 8;
void setLights(int redState, int yellowState, int greenState) {
digitalWrite(RED, redState);
digitalWrite(YELLOW, yellowState);
digitalWrite(GREEN, greenState);
}
void setup() {
pinMode(RED, OUTPUT);
pinMode(YELLOW, OUTPUT);
pinMode(GREEN, OUTPUT);
}
void loop() {
// Ночью светофор не переключает фазы, а просто мигает жёлтым:
// «перекрёсток нерегулируемый, будьте внимательны».
setLights(LOW, HIGH, LOW);
delay(700);
setLights(LOW, LOW, LOW);
delay(700);
}
Пока переключать режимы вручную — прошивкой. В уроке про кнопку мы научимся переключать их нажатием, а в уроке про датчик света светофор сам поймёт, что наступила ночь.
Не работает? Разбираемся
- Горит не тот цвет, что ожидается. Скорее всего, светодиоды воткнуты в другом порядке: проверьте, что красный сидит в канале вывода 10, жёлтый — 9, зелёный — 8. Либо поменяйте местами светодиоды, либо поменяйте числа в трёх константах — результат одинаковый.
- Фазы идут, но лампы не гаснут. В каждой фазе нужно указывать состояние всех трёх светодиодов. Если про какой-то не сказано ничего, он остаётся в прежнем состоянии — так и получается «залипший» цвет.
- Среда пишет, что функция не объявлена. Проверьте имя при вызове: язык различает большие и маленькие буквы, поэтому
setlightsиsetLights— разные имена. И убедитесь, что при вызове передано ровно столько значений, сколько указано в заголовке функции. - После добавления функции ничего не изменилось. Функция сама по себе ничего не делает — она срабатывает только при вызове. Проверьте, что в
loop()действительно есть строкиsetLights(...).
Задания для тренировки
- РазминкаНастройте светофор под оживлённый перекрёсток: зелёный — 8 секунд, красный — 8 секунд, промежуточные фазы оставьте как есть.
- ПосложнееСделайте так, чтобы перед красным жёлтый не просто горел, а мигал три раза. Подсказка: напишите функцию
blinkYellow(int times)по образцуblinkGreen. - Со звёздочкой ⭐Соберите пешеходный светофор рядом с транспортным: ещё два светодиода (красный и зелёный) на выводах 7 и 6. Пешеходам зелёный — ровно тогда, когда машинам красный, и наоборот. Подсказка: добавьте параметры в
setLightsили напишите вторую функциюsetWalk.
Показать разбор ночного режима
Ночной режим — самый простой из всех, что мы писали: одно состояние вместо четырёх. Жёлтый включается, ждёт, выключается, ждёт — и всё по кругу. Заметьте, что функция setLights пригодилась и здесь: она не привязана к конкретной фазе и работает в любой программе с этой схемой.
const int RED = 10;
const int YELLOW = 9;
const int GREEN = 8;
void setLights(int redState, int yellowState, int greenState) {
digitalWrite(RED, redState);
digitalWrite(YELLOW, yellowState);
digitalWrite(GREEN, greenState);
}
void setup() {
pinMode(RED, OUTPUT);
pinMode(YELLOW, OUTPUT);
pinMode(GREEN, OUTPUT);
}
void loop() {
// Ночью светофор не переключает фазы, а просто мигает жёлтым:
// «перекрёсток нерегулируемый, будьте внимательны».
setLights(LOW, HIGH, LOW);
delay(700);
setLights(LOW, LOW, LOW);
delay(700);
}
Это важное свойство хороших функций: написали один раз — пользуетесь везде. Именно так устроены и библиотеки, с которыми мы познакомимся в уроке про сервопривод.
Что мы узнали
- Устройство описывается состояниями: в каждой фазе важно положение всех элементов, а не только того, который меняется.
- Настоящий светофор имеет четыре фазы, и в одной из них горят два цвета сразу.
- Своя функция — это собственная команда:
void имя(параметры)и тело в фигурных скобках. - Функция выполняется только при вызове; функции могут вызывать друг друга.
- Сложное прячут внутрь функций, а в
loop()оставляют понятную последовательность — так программу можно читать, как текст.
В следующем уроке светодиод научится светить вполсилы: разберёмся, как из «включено-выключено» получается плавная яркость, и сделаем ночник, который медленно разгорается и гаснет — а заодно имитацию живого пламени свечи.
Частые вопросы
Почему в светофоре красный и жёлтый горят вместе?
Это фаза «приготовиться»: в России сочетание красного с жёлтым предупреждает, что сейчас включится зелёный. Перед красным, наоборот, горит только жёлтый. Последовательность несимметрична — так задано правилами дорожного движения.
Зачем нужны свои функции, если программа и так работает?
Функция собирает повторяющийся набор команд под одним понятным именем. Программа становится короче и читается как последовательность действий, а изменения вносятся в одном месте вместо десятка.
Чем функция отличается от цикла?
Цикл повторяет одни и те же действия подряд нужное число раз. Функция — это отдельная команда, которую можно вызывать в любых местах программы и с разными значениями. Часто их используют вместе: внутри функции работает цикл.
Что означает слово void перед именем функции?
Что функция ничего не возвращает в программу — она просто выполняет действия. Именно поэтому void стоит и перед setup(), и перед loop(). Функции, возвращающие результат, объявляются иначе, например int вместо void.
