View Categories

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

Для урока понадобится
В наборе GyverKIT START IOT EXTRA
Arduino Nano
Макетная плата
Кнопка 12 мм

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

Полезные страницы #

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

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