View Categories

Машина состояний и флаги

Машина состояний (State Machine) или конечный автомат - математическая абстракция, состоящая из набора состояний и правил перехода между ними при входящих данных или событиях, в каждый момент времени система может находиться только в одном состоянии. По конечным автоматам есть огромное количество философской теории, в которую мы не будем углубляться и сосредоточимся на практике.

Данный инструмент позволяет сделать например навигацию по меню, переключать режимы работы прибора, описать логику игры или автоматического устройства (кофемашина, робот-пылесос), написать режимы для гирлянды или просто выделить из текста фрагмент нужного формата...

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

Конечный автомат замечательно работает в суперцикле - система всегда находится в известном состоянии и ждёт события. Задачу с асинхронным автоматом можно вызывать в цикле совместно с другими задачами - они не будут тормозить друг друга.

Автоматы Мили и Мура #

Конечные автоматы можно разделить на два типа - Мили и Мура - по именам учёных, которые их придумали. В автоматах Мура выходное значение зависит от состояния автомата, условно такой автомат будет выполнять код, пока длится соответствующее состояние. В автомате Мили выход меняется при смене состояния - то есть условный код будет выполняться при переходе между состояниями. Запоминать это необязательно, нужно просто иметь в виду, что есть два типа автоматов.

Например, автомат Мура - переключатель эффектов для гирлянды. В зависимости от выбранного режима постоянно выполняется функция, внутри которой воспроизводится нужный эффект. Автомат Мили - кнопочное меню: при нажатии на кнопку однократно происходит действие, например переключение пункта меню или смена экрана.

Флаг #

Простейшим конечным автоматом является флаг - логическая переменная, которая имеет всего два состояния - true и false и может переходить между ними вручную или при помощи инверсии:

bool flag = true;
flag = false;   // опускание флага
flag = !flag;   // инверсия флага

"Поднятием" флага считается присваивание значения true, а опусканием - false

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

Пример со светодиодом #

Самый простой пример - с мигающим светодиодом. Модифицируем пример из предыдущего урока так, чтобы подавать сигнал один раз за вызов задачи, а не два, как в оригинале:

Статическая переменная имеет область видимости внутри функции, в которой объявлена, но сохраняет своё значение на всём протяжении работы программы

void blinkLED() {
    static bool ledState;       // флаг запоминает состояние светодиода
    ledState = !ledState;       // инвертировать
    digitalWrite(LED_BUILTIN, ledState);    // переключить в состояние
    delay(500);
}

void setup() {
    pinMode(LED_BUILTIN, OUTPUT);
}

void loop() {
    blinkLED();
}

У нас получилась простейшая машина из двух состояний - светодиод включен и светодиод выключен. Каждый вызов функции blinkLED переключает состояние светодиода, как программное (флаг), так и физическое - через digitalWrite. Вход автомата - просто вызов функции, выход - состояние, в которое нужно переключить светодиод. Мы избавились от половины цикла мигания: вместо того, чтобы включить-ждать-выключить-ждать мы делаем просто переключить-ждать. Но от задержки мы пока не избавились - это тема следующего урока.

Пример с кнопкой #

Второй пример - с кнопкой.

Вместо ожидания нового состояния кнопки, как в предыдущем уроке, мы просто запомним старое:

#define BTN_PIN 3

void checkButton() {
    static bool pState;     // флаг запоминает прошлое состояние кнопки
    bool state = !digitalRead(BTN_PIN);     // state == 1 - кнопка нажата

    if (pState) {           // кнопка была нажата
        if (!state) {       // и не нажата сейчас
            Serial.println("Кнопка отпущена");
            pState = false; // переход в состояние "отпущена"
            delay(20);      // гашение дребезга
        }
    } else {                // кнопка не была нажата
        if (state) {        // и нажата сейчас
            Serial.println("Кнопка нажата");
            pState = true;  // переход в состояние "нажата"
            delay(20);      // гашение дребезга
        }
    }
}

void setup() {
    Serial.begin(115200);
    pinMode(BTN_PIN, INPUT_PULLUP);
}

void loop() {
    checkButton();
}

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

В данном примере используется "гашение дребезга" при помощи задержки delay. На практике нужно делать по другому, как и зачем - читайте в уроке про обработку кнопки. Здесь я пишу delay для простоты, т.к. этот урок - про другое

Кстати, эту конструкцию можно "свернуть", исходя из того что у нас есть всего два бинарных состояния:

void checkButton() {
    static bool pState; // флаг запоминает прошлое состояние кнопки
    bool state = !digitalRead(BTN_PIN); // state == 1 - кнопка нажата

    if (pState != state) {  // состояние изменилось
        pState = state;     // запомнить новое состояние
        state ? Serial.println("Кнопка нажата") : Serial.println("Кнопка отпущена");
        delay(20);
    }
}

Можно пойти дальше и сделать независимую функцию, которая будет обрабатывать кнопку. Действия по нажатию кнопки будем делать в другом месте программы, чтобы эту функцию как готовый инструмент можно было использовать и в других примерах. Например так:

#define BTN_PIN 3

// функция возвращает true при нажатии кнопки
bool checkButton() {
    static bool pState;
    bool state = !digitalRead(BTN_PIN);

    if (pState != state) {
        pState = state;
        delay(20);
        return state;
    }
    return false;
}

void setup() {
    Serial.begin(115200);
    pinMode(BTN_PIN, INPUT_PULLUP);
}

void loop() {
    if (checkButton()) Serial.println("Кнопка нажата");
}

Кнопка и светодиод #

Можно объединить два предыдущих примера - пусть кнопка переключает состояние светодиода. Задачу можно решить по-другому, цель примера - показать, как две машины состояний взаимодействуют между собой:

#define BTN_PIN 3

// функция возвращает true при нажатии кнопки
bool checkButton() {
    static bool pState;
    bool state = !digitalRead(BTN_PIN);

    if (pState != state) {
        pState = state;
        delay(20);
        return state;
    }
    return false;
}

// переключить состояние светодиода
void toggleLED() {
    static bool ledState;
    ledState = !ledState;
    digitalWrite(LED_BUILTIN, ledState);
}

void setup() {
    Serial.begin(115200);
    pinMode(BTN_PIN, INPUT_PULLUP);
    pinMode(LED_BUILTIN, OUTPUT);
}

void loop() {
    // если кнопка нажата - переключить светодиод
    if (checkButton()) toggleLED();
}

Передача событий #

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

bool flag;

void task1() {
    if (...) flag = true;   // поднять флаг
}

void task2() {
    if (flag) {
        flag = false;   // сбросить флаг
        // ...
    }
}

В следующих уроках рассмотрим более сложные примеры конечных автоматов.

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

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