Таблица переходов

Развивая идею обратных вызовов функций, от условных операторов можно практически уйти. Адрес — не что иное, как число. Наши перечисления — это тоже, по сути, числа. Что нам мешает создать двухмерный массив из этих чисел и осуществлять переходы от одного состояния к другому, опираясь исключительно на него? Такой массив еще называется таблицей переходов (англ. transition table).

STATES / EVENTSNONEBUTTON_PRESSEDTIME_OUT
SHOW_TIMESHOW_TIMESHOW_TEMPERATURE
SHOW_TEMPERATURESHOW_TEMPERATUREADJUST_HOURSSHOW_TIME
ADJUST_HOURSADJUST_HOURSADJUST_MINUTES
ADJUST_MINUTESADJUST_MINUTESSHOW_TIME

Так как некоторые события не должны происходить в некоторых состояниях (например, таймер должен быть отключен, когда мы показываем время), то для отладки было бы неплохо ввести дополнительную функцию error() с вечным циклом. Как только устройство перестанет реагировать, вы поймете, что осуществился переход именно в эту функцию (скорее всего).

void error(void) {
    while(1);
}

Перечисления автоматически заполняются числами начиная с нуля. Соответственно, добавив в перечисление, в конец, еще одно «состояние» / «событие», мы получим размер нужного нам массива.

typedef enum {
    STATE_SHOW_TIME,        // 0
    STATE_SHOW_TEMPERATURE, // 1
    STATE_ADJUST_HOURS,     // 2
    STATE_ADJUST_MINUTES,   // 3
    STATE_MAX,              // 4
} STATE_t;
typedef enum {
    EVENT_NONE,           // 0
    EVENT_BUTTON_PRESSED, // 1
    EVENT_TIME_OUT,       // 2
    EVENT_MAX,            // 3
} EVENT_t;

Эти ненастоящие событие и состояние, но они позволят не создавать дополнительных макросов для таблицы переходов. Перепишем таблицу в соответствии с тем, как мы зарисовали ее выше, но только на языке Си.

typedef void (*TRANSITION_FUNC_PTR_t)(void);
TRANSITION_FUNC_PTR_t transition_table[STATE_MAX][EVENT_MAX] = {
     [STATE_SHOW_TIME]       [EVENT_NONE]            = show_time,
     [STATE_SHOW_TIME]       [EVENT_BUTTON_PRESSED]  = show_temperature,
     [STATE_SHOW_TIME]       [EVENT_TIME_OUT]        = error,
     [STATE_SHOW_TEMPERATURE][EVENT_NONE]            = show_temperature,
     [STATE_SHOW_TEMPERATURE][EVENT_BUTTON_PRESSED]  = show_hours,
     [STATE_SHOW_TEMPERATURE][EVENT_TIME_OUT]        = show_time,
     [STATE_ADJUST_HOURS]    [EVENT_NONE]            = show_hours,
     [STATE_ADJUST_HOURS]    [EVENT_BUTTON_PRESSED]  = show_minutes,
     [STATE_ADJUST_HOURS]    [EVENT_TIME_OUT]        = error,
     [STATE_ADJUST_MINUTES]  [EVENT_NONE]            = show_minutes,
     [STATE_ADJUST_MINUTES]  [EVENT_BUTTON_PRESSED]  = show_time,
     [STATE_ADJUST_MINUTES]  [EVENT_TIME_OUT]        = error,
};

Главный цикл, как и в прошлый раз, будет весьма лаконичным.

int main(void) {
    mcu_init();
    // …
    while(1) {
        transition_table[state][event]();
    }
}

Пожалуй, на этом мы выжали из машины состояний всё, что могли. В некоторых случаях переход может зависеть от предыдущего состояния, тогда придется добавить еще одну глобальную переменную и, вероятно, обернуть все переменные связанные с режимом работы, в одну структуруcap. Например, так:

Интересная реализация машины состояний применена в сенсорной библиотеке от ST.

typedef struct {
    ST_t current_state = STATE_SHOW_TIME;
    ST_t previous_state = STATE_SHOW_TIME;
    EV_t event = EVENT_NONE;
} STATE_MACHINE_t;

Однако оставим это вам на откуп, а сейчас самое время перейти к операционной системе и реализовать ту же функциональность средствами FreeRTOS.


Изменено: