На практике часто встречается задача переключения режимов одной или несколькими кнопками. Во всех случаях это конечный автомат, каждое состояние которого - режим. Состояние может переходить только к следующему или предыдущему соседнему. Для такого удобно использовать обычную целочисленную переменную: делать ей ++
или --
при переключении режима и обрабатывать значение в switch
или по таблице. Давайте рассмотрим варианты.
Далее по тексту:
mode
- текущий режимlen
- количество режимов
Только вперёд #
Допустим, режимы могут переключаться только одним событием, например кликом по кнопке или по таймеру. В этом случае переключать будем в одном направлении и переходить в первый из последнего - переполнение:
Такой переход можно описать одной строкой:
// вперёд с переполнением
if (++mode >= len) mode = 0;
Два направления #
Пусть режимы можно переключать вперёд и назад - двумя кнопками или одной (например клик или удержание). Тут есть два варианта.
С ограничением #
Режим ограничивается в крайних точках:
// вперёд с ограничением
if (mode < len - 1) ++mode;
// назад с ограничением
if (mode > 0) --mode;
С переполнением #
При переполнении режим переключается на противоположный:
// вперёд с переполнением
if (++mode >= len) mode = 0;
// назад с переполнением (тип int)
if (--mode < 0) mode = len - 1;
// назад с переполнением (тип int или uint)
mode = mode ? (--mode) : (len - 1);
Пример 1 #
Автомат Мура #
Напишем переключение задач по таймеру - типичный "скелет" гирлянды:
#define MODES_LEN 3 // кол-во режимов
uint8_t mode = 0;
uint32_t tmr;
// функции режимов
void mode0() {
Serial.println("mode 0");
}
void mode1() {
Serial.println("mode 1");
}
void mode2() {
Serial.println("mode 2");
}
void setup() {
Serial.begin(115200);
Serial.println("start");
}
void loop() {
// машина состояний - таймер на 2 сек
if (millis() - tmr >= 2000) {
tmr = millis();
// следующий режим, с переполнением
if (++mode >= MODES_LEN) mode = 0;
}
// вызов режимов
switch (mode) {
case 0: mode0(); break;
case 1: mode1(); break;
case 2: mode2(); break;
}
}
В этом примере функции вызываются постоянно в loop
в зависимости от состояния - конечный автомат Мура.
Автомат Мили #
Если внести вызов в таймер - они станут вызываться однократно, автомат станет автоматом Мили:
#define MODES_LEN 3 // кол-во режимов
uint8_t mode = 0;
uint32_t tmr;
// функции режимов
void mode0() {
Serial.println("mode 0");
}
void mode1() {
Serial.println("mode 1");
}
void mode2() {
Serial.println("mode 2");
}
void setup() {
Serial.begin(115200);
Serial.println("start");
}
void loop() {
// машина состояний - таймер на 2 сек
if (millis() - tmr >= 2000) {
tmr = millis();
// следующий режим, с переполнением
if (++mode >= MODES_LEN) mode = 0;
// вызов режимов
switch (mode) {
case 0: mode0(); break;
case 1: mode1(); break;
case 2: mode2(); break;
}
}
}
Массив функций #
Для такой организации программы можно использовать и другую конструкцию - заменить switch
на массив функций. Код станет чуть тяжелее в оперативке, но более удобным для расширения:
uint8_t mode = 0;
uint32_t tmr;
// функции режимов
void mode0() {
Serial.println("mode 0");
}
void mode1() {
Serial.println("mode 1");
}
void mode2() {
Serial.println("mode 2");
}
// массив задач-функций
void (*modes[])() = {mode0, mode1, mode2};
void setup() {
Serial.begin(115200);
Serial.println("start");
}
void loop() {
// машина состояний - таймер на 2 сек
if (millis() - tmr >= 2000) {
tmr = millis();
// следующий режим, с переполнением
if (++mode >= sizeof(modes) / sizeof(void*)) mode = 0;
// вызов режимов при переключении
modes[mode]();
}
// вызов режимов постоянно
// modes[mode]();
}
Также здесь я заменил количество режимов на длину массива функций - не будем делать за компилятор его работу.
Переключение кнопкой #
Полученный автомат очень легко переделать под переключение кнопкой - просто заменить таймер на проверку клика:
void loop() {
// условная функция проверки клика по кнопке
if (buttonClick()) {
// следующий режим, с переполнением
if (++mode >= sizeof(modes) / sizeof(void*)) mode = 0;
// вызов режимов при переключении
modes[mode]();
}
// вызов режимов постоянно
// modes[mode]();
}
Или две кнопки, чтобы переключать вперёд и назад:
void loop() {
// кнопка "вперёд"
if (buttonClickUp()) {
// следующий режим, с ограничением
if (mode < sizeof(modes) / sizeof(void*) - 1) ++mode;
// вызов режимов при переключении
modes[mode]();
}
// кнопка "назад"
if (buttonClickDown()) {
// предыдущий режим, с ограничением
if (mode > 0) --mode;
// вызов режимов при переключении
modes[mode]();
}
// вызов режимов постоянно
// modes[mode]();
}
Пример 2 #
Рекомендуется изучить следующие уроки:
Рассмотрим ещё один пример с более осмысленными режимами, пусть это будут:
- Светодиод горит
- Светодиод мигает с частотой 1 Гц
- Светодиод мигает с частотой 2 Гц
- Светодиод выключен
Первый и последний режимы не требуют постоянного исполнения кода, они могут выполнить его однократно при переходе в состояние. Второй и третий режимы должны вызывать код постоянно для работы таймера. Получаем автомат из четырех состояний, вход - клик по кнопке, по нему автомат переходит в следующий режим, при достижении последнего - в первый.
Для читаемости кода программы будем использовать константы enum class
, чтобы не задумываться о названиях и нумерации:
#define BTN_PIN 3
enum class States {
On,
Blink_1,
Blink_2,
Off,
_len,
};
States state = States::On;
uint32_t tmr;
bool flag;
// функция возвращает true при нажатии кнопки
bool buttonClick() {
static bool pState;
bool state = !digitalRead(BTN_PIN);
if (pState != state) {
pState = state;
delay(20);
return state;
}
return false;
}
// применить текущее состояние
void apply() {
switch (state) {
case States::On:
digitalWrite(LED_BUILTIN, HIGH);
break;
case States::Off:
digitalWrite(LED_BUILTIN, LOW);
break;
default: break;
}
}
void setup() {
pinMode(BTN_PIN, INPUT_PULLUP);
pinMode(LED_BUILTIN, OUTPUT);
apply();
}
void loop() {
if (buttonClick()) {
// переключить с переполнением
state = States((int)state + 1);
if (state == States::_len) state = States(0);
// применить
apply();
}
// постоянный вызов согласно режиму
switch (state) {
case States::Blink_1:
if (millis() - tmr > 500) {
tmr = millis();
digitalWrite(LED_BUILTIN, flag = !flag);
}
break;
case States::Blink_2:
if (millis() - tmr > 250) {
tmr = millis();
digitalWrite(LED_BUILTIN, flag = !flag);
}
break;
default: break;
}
}
Данную программу можно ещё много оптимизировать, но цель урока она отражает как нужно - переключение между enum
режимами с асинхронным автоматом.