View Categories

Переключение «режимов»

На практике часто встречается задача переключения режимов одной или несколькими кнопками. Во всех случаях это конечный автомат, каждое состояние которого - режим. Состояние может переходить только к следующему или предыдущему соседнему. Для такого удобно использовать обычную целочисленную переменную: делать ей ++ или -- при переключении режима и обрабатывать значение в 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 режимами с асинхронным автоматом.

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

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