Рассмотрим один из классических примеров использования конечных автоматов - светофор, состоящий из трёх светодиодов: красного, жёлтого и зелёного. Подключим их к плате вот таким образом:
По-хорошему, каждому светодиоду нужен отдельный резистор, чтобы они могли работать на полную яркость независимо друг от друга. Я поставил общий резистор для упрощения схемы, к тому же одновременно у нас могут гореть только красный с жёлтым, и то один раз
Чтобы не придумывать работу светофора самому, я нагуглил картинку - четыре светофора с разными режимами регулируют условный перекрёсток. Отлично, так будет даже интереснее:
Наша задача - реализовать это "расписание" в программе. Начнём с самого простого примера для одного светофора (верхнего на схеме), затем улучшим его до асинхронного автомата, который потом выделим в отдельный универсальный "модуль управления светофором", и уже на его основе напишем программу для нескольких светофоров. Предлагаю ограничиться тремя светофорами.
На финальной схеме добавятся ещё два светофора, а также кнопка. Что с ней делать - придумаем позже. Данную схему собирать необязательно - она вместе с финальной программой доступна в онлайн симуляторе по ссылке.
Третья схема - из онлайн симулятора, я оставил один резистор для упрощения. Можно вообще его убрать - в симуляторе ничего не сгорит =)
Подход 1 #
Первая итерация - напишем код в стиле ардуино-скетча: с задержками и прямиком в loop
:
#include <Arduino.h>
#define LED_R 2
#define LED_Y 3
#define LED_G 4
void setup() {
pinMode(LED_R, OUTPUT);
pinMode(LED_Y, OUTPUT);
pinMode(LED_G, OUTPUT);
}
void loop() {
// зелёный горит 9 с
digitalWrite(LED_G, 1);
delay(9 * 1000);
// зелёный мигает 3 раза
digitalWrite(LED_G, 1);
delay(700);
digitalWrite(LED_G, 0);
delay(300);
digitalWrite(LED_G, 1);
delay(700);
digitalWrite(LED_G, 0);
delay(300);
digitalWrite(LED_G, 1);
delay(700);
digitalWrite(LED_G, 0);
delay(300);
// жёлтый горит 4 с
digitalWrite(LED_Y, 1);
delay(4 * 1000);
digitalWrite(LED_Y, 0);
// красный горит 12 с
digitalWrite(LED_R, 1);
delay(12 * 1000);
// красный+жёлтый горят 4 с
digitalWrite(LED_Y, 1);
delay(4 * 1000);
digitalWrite(LED_Y, 0);
digitalWrite(LED_R, 0);
}
Очень лёгкая, простая и читаемая программа, выполняется понятно и полностью линейно, написана прямо по картинке с режимом работы светофора. А минусы будут? Будут:
- Программа может делать только одну эту задачу (если не использовать прерывания или костыли)
- Размер программы сильно зависит от расписания
- Для изменения расписания нужно переписать всю программу
- Добавление второго светофора заставит напрячься, чтобы расположить все переключения между задержками
- Добавление третьего светофора будет сниться в кошмарах
Подход 2 #
Давайте сократим код: сделаем одну функцию для управления всеми тремя цветами:
void setRYG(bool r, bool y, bool g) {
digitalWrite(LED_R, r);
digitalWrite(LED_Y, y);
digitalWrite(LED_G, g);
}
Для упрощения я не буду проверять или хранить состояния светодиодов - функция будет переключать все три независимо от их состояния. С одной стороны - это не очень оптимально (переключение занимает время), с другой - хранение состояний светодиодов занимает память. Время переключения настолько незначительно по сравнению с тем, что делает наша программа, что выбираю первый вариант.
#include <Arduino.h>
#define LED_R 2
#define LED_Y 3
#define LED_G 4
void setRYG(bool r, bool y, bool g) {
digitalWrite(LED_R, r);
digitalWrite(LED_Y, y);
digitalWrite(LED_G, g);
}
void setup() {
pinMode(LED_R, OUTPUT);
pinMode(LED_Y, OUTPUT);
pinMode(LED_G, OUTPUT);
}
void loop() {
// зелёный горит 9 с
setRYG(0, 0, 1);
delay(9 * 1000);
// зелёный мигает 3 раза
setRYG(0, 0, 1);
delay(700);
setRYG(0, 0, 0);
delay(300);
setRYG(0, 0, 1);
delay(700);
setRYG(0, 0, 0);
delay(300);
setRYG(0, 0, 1);
delay(700);
setRYG(0, 0, 0);
delay(300);
// жёлтый горит 4 с
setRYG(0, 1, 0);
delay(4 * 1000);
// красный горит 12 с
setRYG(1, 0, 0);
delay(12 * 1000);
// красный+жёлтый горят 4 с
setRYG(1, 1, 0);
delay(4 * 1000);
}
Заметили? Программа свернулась до однотипных действий: подать три сигнала и подождать.
Подход 3 #
Теперь можно сформировать расписание работы в более удобном виде - в виде массива. Можно сделать 4 массива (3 под цвета и 1 под время), либо сделать массив структур. Второй вариант более красивый:
struct TSchedule {
bool r, y, g; // сигналы цветов
uint16_t time; // время в мс
};
Теперь можно создать и инициализировать массив структур TSchedule
следующим образом:
TSchedule schedule[] = {
{0, 0, 1, 10000}, // зелёный на 10 сек
{1, 1, 0, 5000}, // красный+жёлтый на 5 сек
// ...
};
Останется заполнить массив прямо по предыдущему коду с задержками, добавить цикл и программа свернётся буквально в несколько строк:
#include <Arduino.h>
#define LED_R 2
#define LED_Y 3
#define LED_G 4
struct TSchedule {
bool r, y, g;
uint16_t time;
};
TSchedule schedule[] = {
{0, 0, 1, 9000},
{0, 0, 1, 700},
{0, 0, 0, 300},
{0, 0, 1, 700},
{0, 0, 0, 300},
{0, 0, 1, 700},
{0, 0, 0, 300},
{0, 1, 0, 4000},
{1, 0, 0, 12000},
{1, 1, 0, 4000},
};
void setRYG(bool r, bool y, bool g) {
digitalWrite(LED_R, r);
digitalWrite(LED_Y, y);
digitalWrite(LED_G, g);
}
void setup() {
pinMode(LED_R, OUTPUT);
pinMode(LED_Y, OUTPUT);
pinMode(LED_G, OUTPUT);
}
void loop() {
for (size_t i = 0; i < sizeof(schedule) / sizeof(TSchedule); i++) {
setRYG(schedule[i].r, schedule[i].y, schedule[i].g);
delay(schedule[i].time);
}
}
Здесь sizeof(schedule) / sizeof(TSchedule)
- размер расписания в количестве ступеней (размер массива в количестве элементов), полностью посчитается компилятором
Теперь визуальный размер исполняемого кода, логика и вызовы функций вообще не зависят от расписания работы светофора - оно задаётся в другом месте, а программа просто его выполняет. Да, теперь у нас в оперативной памяти занято много места под хранение чисел - но это можно с лёгкостью обойти при помощи PROGMEM, а в некоторых архитектурах такие константы автоматически хранятся во flash памяти, так что программа всё равно стала более оптимальной.
Есть ещё один неприятный момент по оптимизации - хотелось бы хранить периоды в секундах в типе uint8_t
и умножать их на 1000
в задержке, чтобы сэкономить место, но в режиме работы у нас есть периоды длиной меньше секунды. Можно было бы перейти на деци-секунды, но я решил оставить как есть для читаемости, это всё таки пример.
Подход 4 #
Обратите внимание, у нас в суперцикле теперь остался только цикл с задержкой - именно такую ситуацию мы разбирали в уроке про таймер и превращали её в асинхронный конечный автомат. Сделаем то же самое - достаточно добавить "таймер на миллис" и вынести счётчик в глобальную область:
#include <Arduino.h>
#define LED_R 2
#define LED_Y 3
#define LED_G 4
struct TSchedule {
bool r, y, g;
uint16_t time;
};
TSchedule schedule[] = {
{0, 0, 1, 9000},
{0, 0, 1, 700},
{0, 0, 0, 300},
{0, 0, 1, 700},
{0, 0, 0, 300},
{0, 0, 1, 700},
{0, 0, 0, 300},
{0, 1, 0, 4000},
{1, 0, 0, 12000},
{1, 1, 0, 4000},
};
uint32_t tmr;
size_t i;
void setRYG(bool r, bool y, bool g) {
digitalWrite(LED_R, r);
digitalWrite(LED_Y, y);
digitalWrite(LED_G, g);
}
void setup() {
pinMode(LED_R, OUTPUT);
pinMode(LED_Y, OUTPUT);
pinMode(LED_G, OUTPUT);
// первая ступень режима
setRYG(schedule[0].r, schedule[0].y, schedule[0].g);
}
void loop() {
if (millis() - tmr >= schedule[i].time) {
tmr = millis();
if (++i >= sizeof(schedule) / sizeof(TSchedule)) i = 0;
setRYG(schedule[i].r, schedule[i].y, schedule[i].g);
}
}
Тут есть два интересных момента:
- В начале программы приходится вручную подать сигналы первой ступени расписания, потому что таймер сработает через 9 секунд и переключит уже на вторую ступень
- Таймер ожидает время текущей ступени. Когда он срабатывает - переключаем ступень на следующую и выводим уже её сигналы на светодиоды. И следующая проверка таймера в
loop
будет уже по времени новой ступени
Добавляем светофор #
Чтобы добавить в программу ещё один светофор, нужно создать новое расписание и новую машину состояний, а также не забыть про пины и светодиоды. Я пока что добавлю третье по счёту расписание с картинки из начала урока, а со вторым разберёмся позже - есть интересный момент. С третьим всё очень просто - 12 секунд зелёного и 20 красного:
#include <Arduino.h>
// светофор 1
#define LED_R1 2
#define LED_Y1 3
#define LED_G1 4
// светофор 2
#define LED_R2 5
#define LED_Y2 6
#define LED_G2 7
struct TSchedule {
bool r, y, g;
uint16_t time;
};
// светофор 1
TSchedule schedule1[] = {
{0, 0, 1, 9000},
{0, 0, 1, 700},
{0, 0, 0, 300},
{0, 0, 1, 700},
{0, 0, 0, 300},
{0, 0, 1, 700},
{0, 0, 0, 300},
{0, 1, 0, 4000},
{1, 0, 0, 12000},
{1, 1, 0, 4000},
};
// светофор 2
TSchedule schedule2[] = {
{0, 0, 1, 12000},
{1, 0, 0, 20000},
};
uint32_t tmr1, tmr2;
size_t i1, i2;
// светофор 1
void setRYG1(bool r, bool y, bool g) {
digitalWrite(LED_R1, r);
digitalWrite(LED_Y1, y);
digitalWrite(LED_G1, g);
}
// светофор 2
void setRYG2(bool r, bool y, bool g) {
digitalWrite(LED_R2, r);
digitalWrite(LED_Y2, y);
digitalWrite(LED_G2, g);
}
void setup() {
// светофор 1
pinMode(LED_R1, OUTPUT);
pinMode(LED_Y1, OUTPUT);
pinMode(LED_G1, OUTPUT);
// светофор 2
pinMode(LED_R2, OUTPUT);
pinMode(LED_Y2, OUTPUT);
pinMode(LED_G2, OUTPUT);
// первая ступень режима
setRYG1(schedule1[0].r, schedule1[0].y, schedule1[0].g);
setRYG2(schedule2[0].r, schedule2[0].y, schedule2[0].g);
}
void loop() {
// светофор 1
if (millis() - tmr1 >= schedule1[i1].time) {
tmr1 = millis();
if (++i1 >= sizeof(schedule1) / sizeof(TSchedule)) i1 = 0;
setRYG1(schedule1[i1].r, schedule1[i1].y, schedule1[i1].g);
}
// светофор 2
if (millis() - tmr2 >= schedule2[i2].time) {
tmr2 = millis();
if (++i2 >= sizeof(schedule2) / sizeof(TSchedule)) i2 = 0;
setRYG2(schedule2[i2].r, schedule2[i2].y, schedule2[i2].g);
}
}
Появилось не просто дублирование кода - мы написали ещё одну аналогичную программу и разместили её между строк первой! Это явный знак того, что пора выделять "светофор" в независимый модуль. Можно сделать это в Си-стиле, набором функций, но на дворе не 1970 год - сделаем сразу класс.
Подход 5 #
Рекомендуется изучить следующие уроки:
Задача класса - принять пины светодиодов, расписание и его размер. Всю логику и таймеры поместим в тикер - его нужно будет вызывать в суперцикле.
#include <Arduino.h>
#include "TLight.h"
TSchedule schedule[] = {
{0, 0, 1, 9000},
{0, 0, 1, 700},
{0, 0, 0, 300},
{0, 0, 1, 700},
{0, 0, 0, 300},
{0, 0, 1, 700},
{0, 0, 0, 300},
{0, 1, 0, 4000},
{1, 0, 0, 12000},
{1, 1, 0, 4000},
};
TLight TL1(2, 3, 4, schedule, sizeof(schedule));
void setup() {
}
void loop() {
TL1.tick();
}
#pragma once
#include <Arduino.h>
struct TSchedule {
bool r, y, g;
uint16_t time;
};
class TLight {
public:
TLight(uint8_t r, uint8_t y, uint8_t g, TSchedule* shedule, size_t size)
: _r(r), _y(y), _g(g), _shedule(shedule), _len(size / sizeof(TSchedule)) {
pinMode(_r, OUTPUT);
pinMode(_y, OUTPUT);
pinMode(_g, OUTPUT);
apply();
}
void apply() {
digitalWrite(_r, _shedule[_step].r);
digitalWrite(_y, _shedule[_step].y);
digitalWrite(_g, _shedule[_step].g);
}
void tick() {
if (millis() - _tmr >= _shedule[_step].time) {
//_tmr = millis();
_tmr += _shedule[_step].time;
if (++_step >= _len) _step = 0;
apply();
}
}
private:
uint8_t _r, _y, _g;
TSchedule* _shedule;
uint8_t _len;
uint8_t _step;
uint32_t _tmr;
};
Отлично, теперь добавление второго светофора выполняется буквально в пару строк кода:
#include <Arduino.h>
#include "TLight.h"
// светофор 1
TSchedule schedule1[] = {
{0, 0, 1, 9000},
{0, 0, 1, 700},
{0, 0, 0, 300},
{0, 0, 1, 700},
{0, 0, 0, 300},
{0, 0, 1, 700},
{0, 0, 0, 300},
{0, 1, 0, 4000},
{1, 0, 0, 12000},
{1, 1, 0, 4000},
};
// светофор 2
TSchedule schedule2[] = {
{0, 0, 1, 12000},
{1, 0, 0, 20000},
};
TLight TL1(2, 3, 4, schedule1, sizeof(schedule1));
TLight TL2(5, 6, 7, schedule2, sizeof(schedule2));
void setup() {
}
void loop() {
TL1.tick();
TL2.tick();
}
Подход 6 #
Посмотрите на первый и второй режим светофора - они идентичны, только второй сдвинут относительно первого: начинается с 8 шага. Значит достаточно добавить в класс возможность выбора шага вручную:
// === TLight.h
#pragma once
#include <Arduino.h>
struct TSchedule {
bool r, y, g;
uint16_t time;
};
class TLight {
public:
TLight(uint8_t r, uint8_t y, uint8_t g, TSchedule* shedule, size_t size)
: _r(r), _y(y), _g(g), _shedule(shedule), _len(size / sizeof(TSchedule)) {
pinMode(_r, OUTPUT);
pinMode(_y, OUTPUT);
pinMode(_g, OUTPUT);
apply();
}
void apply() {
digitalWrite(_r, _shedule[_step].r);
digitalWrite(_y, _shedule[_step].y);
digitalWrite(_g, _shedule[_step].g);
}
void setStep(uint8_t step) {
_step = step;
apply();
}
void tick() {
if (millis() - _tmr >= _shedule[_step].time) {
//_tmr = millis();
_tmr += _shedule[_step].time;
if (++_step >= _len) _step = 0;
apply();
}
}
private:
uint8_t _r, _y, _g;
TSchedule* _shedule;
uint8_t _len;
uint8_t _step;
uint32_t _tmr;
};
Добавим третий светофор (второе расписание) - подключим ему расписание первого светофора и при запуске укажем нужный шаг:
#include <Arduino.h>
#include "TLight.h"
// светофор 1
TSchedule schedule1[] = {
{0, 0, 1, 9000},
{0, 0, 1, 700},
{0, 0, 0, 300},
{0, 0, 1, 700},
{0, 0, 0, 300},
{0, 0, 1, 700},
{0, 0, 0, 300},
{0, 1, 0, 4000},
{1, 0, 0, 12000},
{1, 1, 0, 4000},
};
// светофор 2
TSchedule schedule2[] = {
{0, 0, 1, 12000},
{1, 0, 0, 20000},
};
TLight TL1(2, 3, 4, schedule1, sizeof(schedule1));
TLight TL2(5, 6, 7, schedule1, sizeof(schedule1));
TLight TL3(8, 9, 10, schedule2, sizeof(schedule2));
void setup() {
TL2.setStep(8);
}
void loop() {
TL1.tick();
TL2.tick();
TL3.tick();
}
Теперь код расширяется очень просто - можно добавлять любое количество светофоров, они будут работать асинхронно и программа может делать и другие вещи - код светофоров практически не нагружает процессор. Например - можно опрашивать кнопку и мигать светодиодом на плате:
#include <Arduino.h>
#include "TLight.h"
#define BTN_PIN 11
// светофор 1
TSchedule schedule1[] = {
{0, 0, 1, 9000},
{0, 0, 1, 700},
{0, 0, 0, 300},
{0, 0, 1, 700},
{0, 0, 0, 300},
{0, 0, 1, 700},
{0, 0, 0, 300},
{0, 1, 0, 4000},
{1, 0, 0, 12000},
{1, 1, 0, 4000},
};
// светофор 2
TSchedule schedule2[] = {
{0, 0, 1, 12000},
{1, 0, 0, 20000},
};
TLight TL1(2, 3, 4, schedule1, sizeof(schedule1));
TLight TL2(5, 6, 7, schedule1, sizeof(schedule1));
TLight TL3(8, 9, 10, schedule2, sizeof(schedule2));
// переключить светодиод по клику
void buttonLED() {
static bool bState;
static bool ledState;
bool state = !digitalRead(BTN_PIN);
if (bState != state) {
bState = state;
if (bState) digitalWrite(LED_BUILTIN, ledState = !ledState);
delay(20);
}
}
void setup() {
TL2.setStep(8);
pinMode(LED_BUILTIN, OUTPUT);
pinMode(BTN_PIN, INPUT_PULLUP);
}
void loop() {
TL1.tick();
TL2.tick();
TL3.tick();
buttonLED();
}
В опросе кнопки есть delay
- как обойтись без него рассмотрим в другом уроке.