- Что в итоге?
- Что нам мешает жить и как с этим бороться?
- Зачем всё это нужно?
- Как расшифровывается мка? значения аббревиатур и сокращений на сайте
- Куда вставлять мой код, который зависит от нажатий кнопки?
- Немного теории
- Откуда берутся события?
- План-а: if {} elsif {} else {}
- План-б: таблица указателей на функции
- План-в: золотая середина или switch { switch {}}
- Практика
Что в итоге?
В итоге, в теории, после прочтения этой статьи должно появиться желание заменить в любимом ардуиновом скетче свой нечитаемый глючный говнокод на красивый структурированный и с использованием МКА.
Обратите внимание, что в функции loop(), которая в нашем случае лишь генерит события, нет ни одной задержки. Таким образом, после приведённого кода можно писать что-то ещё: генерить события для других МКА, выполнять ещё какой-то свой код, не содержащий delay();
Я очень надеюсь, что эта статья помогла вам понять, что такое МКА и как их реализовать в вашем скетче.
Что нам мешает жить и как с этим бороться?
Нам мешает жить функция delay(), я её поборол при помощи МКА и написал свой класс SmartDelay. Я уже сделал новый класс (SmartDelayMs) порождённый от SmartDelay, там всё то же самое, но в миллисекундах. В данной статье я не использую эту библиотеку, просто хвастаюсь.
Чтобы не утомлять читателя повторно рассказом о том, как плохо пользоваться функцией delay() и как жить без неё, я рекомендую сначала прочитать мою старую статейку Замена delay() для неблокирующих задержек в Arduino IDE. Я немножко повторю основные тезисы здесь ещё раз ниже по мере написания кода.
Зачем всё это нужно?
Когда чайник, уперевшись в необходимость отойти от простой последовательности действий, задаёт на хабре вопрос типа «как сделать вот это?», ему с вероятностью 70% отвечают «погугли конечные автоматы» и 30% «используй finite state machine» в зависимости от страны работодателя профессионала.
На следующий вопрос «а как?» отправляют в гугл. Идёт такой чайник, что только закончил мигать светодиодом и вытер пот со лба, что учил в школе немецкий и всю жизнь работал бульдозеристом в этот гугл и видит там статьи типа Википедия про конечные автоматы с формулами и в которых понятны только предлоги.
Так как я тоже чайник, но до бульдозера работал программистом 30 лет назад, наступив на множество граблей по мере освоения программирования микроконтроллеров, решил написать эту статью простым языком для начинающих.
Итак, задача стоит, например, научить ардуину понимать нажатие кнопки типа клик, нажатие и удержание, то есть, короткий тык в кнопку, длинный и очень длинный. Любой начинающий может сделать это в функции loop() и гордиться этим, но как быть, если кнопок несколько?
Как расшифровывается мка? значения аббревиатур и сокращений на сайте
Если представленная расшифровка аббревиатуры мка недостаточна, Вы можете обратиться к ресурсам:
Куда вставлять мой код, который зависит от нажатий кнопки?
В табличке мы не учитывали наличие других МКА сосредоточившись на нашем обработчике событий кнопки. На практике же требуется не только радоваться тому, что кнопка работает, но и выполнять другие действия.
У нас есть два важных места для вставки хорошего кода:
case Release:
switch(btState) {
case Click:
// Вот здесь был клик и отпустили кнопку. это точно был клик.
break;
case WaitDebounce:
switch (st) {
case PreClick:
btState=Click;
// Вот здесь случился клик. Кнопка может быть потом ещё нажата и будет ещё и "нажатие" итп.
break;
Лучше всего, чтобы не портить красоту, написать свои функции типа offClick() и onClick(), в которых будет обрабатываться уже эти события, возможно, что они передадут его другому МКА 😀
Я обернул всю логику работы МКА кнопки в класс C . Это удобно, так как не отвлекает от написания основного кода, все кнопки работают самостоятельно, мне не надо изобретать имена кучи переменных. Только это совсем другая история и я могу написать, как оформлять ваши МКА в классы и делать из них библиотеки для Arduino. Пишите в комментариях пожелания.
Немного теории
Так или иначе, но у вас есть объекты. Вы можете называть их кнопками, дисплеями, светодиодными лентами, роботами и измерителем уровня воды в бачке. Если ваш код выполняется не в одну ниточку последовательно, а по каким-то событиям, вы храните состояние объектов.
Вы можете называть это как угодно, но это факт. Для кнопки есть, например, состояния «нажата» и «отпущена». Для робота, например: стоит, едет прямо, поворачивает. Количество этих состояний конечно. Ну, в нашем случае, да. Добавим состояния «клик», «нажатие» и «удержание».
Уже пять состояний, которые нам надо различать. Причём, интересны нам лишь последние три. Этими пятью состояниями живёт кнопка. В страшном внешнем мире происходят события, которые от кнопки в общем-то никак не зависят: тыкания в неё пальцем, отпускания, удержания на разное время итп. Назовём их событие.
Итак, мы имеем объект «кнопка» у которого конечное количество состояний и на него действуют события. Это и есть конечный автомат (КА). В теории КА список возможных событий называют словарём, а события словами. Я дико извиняюсь, я это проходил 30 лет назад и плохо помню терминологию. Вам же не терминология нужна, правда?
В данной статье мы напишем замечательный код, который будет переводить наш КА из состояния в состояние в зависимости от событий. Он и является собственно машиной конечных автоматов (МКА).
Не обязательно всё ваше устройство запихивать в один огромный КА, можно разбить на отдельные объекты, каждый из которых обслуживается своей МКА и даже находится в отдельном файле кода. Многие свои объекты вы сможете поместить на GitHub в виде готовых библиотек ардуины и использовать потом не вникая в их реализацию.
Если ваш код не требует переносимости и повторного использования, можно всё лепить в один большой файл, что и делают как раз начинающие.
Откуда берутся события?
Настало время ненадолго покинуть нашу уютную кнопку и окунуться в ужасный внешний мир функции loop().
void loop() {
// запоминаем текущий счётчик времени
unsigned long mls = millis();
// генерим события
// нажатие кнопки
if (digitalRead(btPin)) doEvent(Press)
else doEvent(Release)
// ожидание дребезга прошло
if (mls - pressTimeStamp > SmartButton_debounce) doEvent(WaitDebounce);
// ожидание нажатия прошло
if (mls - pressTimeStamp > SmartButton_hold) doEvent(WaitHold);
// ожидание удержания прошло
if (mls - pressTimeStamp > SmartButton_long) doEvent(WaitLongHold);
// совсем перебор по времени
if (mls - pressTimeStamp > SmartButton_idle) doEvent(WaitIdle);
}
Итак, имея под рукой генератор событий, можно приступить к реализации МКА.
На самом деле, события могут генерироваться не только по подъёму уровня сигнала на ноге контроллера и по времени. События могут передаваться одним объектом другому. Например, вы хотите, чтобы нажатие одной кнопки автоматически «отпускало» другую. Для этого достаточно вызвать её doEvent()
План-а: if {} elsif {} else {}
Это первое, что приходит в голову начинающему. На самом деле, это неплохой вариант, если в таблице совсем мало заполненных клеток и действия сводятся только к смене состояний.
void doAction(enum event e) {
if (e == Press && btState == Idle) { // нажата кнопка в состоянии кнопка отпущена
btState=PreClick; // переходим в состояние ожидания дребезга
pressTimeStamp=millis(); // запоминаем время нажатия кнопки
}
if (e == Release) { // отпущена кнопка
btState=Idle; // Переходим в состояние кнопка отпущена
}
if (e == WaitDebounce && btState == PreClick) { // Прошло время дребезга
btState=Click; // считаем, что это клик
}
// и так далее для всех сочетаний событий и состояний
}
Плюсы:
Минусы:
План-б: таблица указателей на функции
Другая крайность — это таблица переходов или функций. Я про такой подход писал в статье Обработка нажатий кнопок для Arduino. Скрестить ООП и МКА.. В двух словах, вы создаёте для каждой разной клетки таблицы отдельную функцию и делаете таблицу из указателей на них.
// определяем тип, так проще
typedef void (*MKA)(enum event e);
// делаем таблицу
MKA action[6][6]={
{&toDebounce,$toIdle,NULL,NULL,NULL,NULL},
{NULL,&toIdle,&toClick,NULL,NULL,NULL},
{NULL,&toIdle,NULL,&toHold,NULL,NULL},
{NULL,&toIdle,NULL,NULL,&toLongHold,NULL},
{NULL,&toIdle,NULL,NULL,NULL,&toVeryLongHold},
{NULL,&toIdle,NULL,NULL,NULL,NULL}
};
// функция doEvent получается совсем простой
void doEvent(enum event e) {
if (action[btState][e] == NULL) return;
(*(action[btState][e]))(e);
}
// Примеры функций из таблицы
// В состояние "не нажата"
void toIdle(enum event e) {
btState=Idle;
}
// В состояние ожидания дребезга
void toDebounce(enum event e) {
btState=PreClick;
pressTimeStamp=millis();
}
// и так далее
Плюсы:
Минусы:
Минус оказался таким жирным, что я после написания Обработка нажатий кнопок для Arduino. Скрестить ООП и МКА. и первого же применения кода в деле выпилил это всё нафиг. В итоге я пришёл к золотой середине «switch { switch {}}».
План-в: золотая середина или switch { switch {}}
Самый распространённый, средний вариант — это использование вложенных операторов switch. На каждое событие как case оператора switch надо писать switch с текущим состоянием. Получается длинно, но более-менее понятно.
void doEvent(enum event e) {
switch (e) {
case Press:
switch (btState) {
case Idle:
btState=PreClick;
pressTimeStamp=millis();
break;
}
break;
case Release:
btState=Idle;
break;
// ... так далее ...
}
}
Умный компилятор всё-равно построит таблицу переходов, как описано в плане Б выше, но эта таблица будет в флеше, в коде и не будет занимать столь дорогую нам память переменных.
Плюсы:
Минусы:
На самом деле, можно использовать план-В и план-А, то есть, switch по событиям, но if внутри по состояниям. На мой вкус, switch и на то и на то понятнее и удобнее, если потребуется что-то потом поменять.
Практика
Основой проектирования МКА является таблица.
Для кнопки это будет выглядеть вот так:
Зачем так сложно? Надо же описать все возможные состояния и учесть все возможные события.
События:
Время вы можете поставить своё, как вам удобно. Чтобы не менять потом в разных местах цифры, лучше сразу заменить их буквами 🙂
#define SmartButton_debounce 20
#define SmartButton_hold 250
#define SmartButton_long 1000
#define SmartButton_idle 3000
Состояния:
Итак, переводим русский язык на язык C
// Нога контроллера для кнопки
byte btPin;
// События
enum event {Press, Release, WaitDebounce, WaitHold, WaitLongHold, WaitIdle};
// Состояния
enum state {Idle, PreClick, Click, Hold, LongHold, ForcedIdle};
// Текущее состояния
enum state btState = Idle;
// Время от нажатия кнопки
unsigned long pressTimeStamp;
Обращу внимание, что и события и состояния указаны не как целая переменная типа byte или int, а как enum. Такая запись не столь любима профессионалами так как не даёт экономить биты столь дорогой в микроконтроллерах памяти, но, с другой стороны очень наглядна. При использовании enum нас не заботит каким числом закодирована каждая константа, мы пользуемся словами, а не числами.
Давайте уже заполним таблицу. Значком -> я обозначу переход в новое состояние.
Для реализации таблицы мы сделаем функцию void doEvent(enum event e). Эта функция получает событие и выполняет действие как описано в таблице.