Машина состояний (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; // сбросить флаг
// ...
}
}
В следующих уроках рассмотрим более сложные примеры конечных автоматов.