View Categories

Пример «Светофор»

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

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

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

Наша задача - реализовать это "расписание" в программе. Начнём с самого простого примера для одного светофора (верхнего на схеме), затем улучшим его до асинхронного автомата, который потом выделим в отдельный универсальный "модуль управления светофором", и уже на его основе напишем программу для нескольких светофоров. Предлагаю ограничиться тремя светофорами.

На финальной схеме добавятся ещё два светофора, а также кнопка. Что с ней делать - придумаем позже. Данную схему собирать необязательно - она вместе с финальной программой доступна в онлайн симуляторе по ссылке.

Третья схема - из онлайн симулятора, я оставил один резистор для упрощения. Можно вообще его убрать - в симуляторе ничего не сгорит =)

Подход 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 - как обойтись без него рассмотрим в другом уроке.

0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
Прокрутить вверх