Почему прошивке нужны автоматы состояний

Прошивка фундаментально реактивна: внешние события (нажатие кнопки, тик таймера, завершение DMA) управляют переходами между рабочими режимами. Без явной структуры автомата состояний эта логика оседает в виде глубоко вложенных цепочек if-else, разбросанных по ISR и циклам задач — поначалу корректно, при масштабировании необслуживаемо.

Эта статья строит три реализации КА для одного устройства: сенсор с батарейным питанием, которому нужно управлять своими состояниями питания.

Состояния: IdleMeasuringTransmittingSleepingIdle События: buttonPress, measureDone, txDone, wakeTimer


Подход 1: enum + switch

Отправная точка — явный, читаемый, хрупкий при масштабировании:

 1enum class State { Idle, Measuring, Transmitting, Sleeping };
 2
 3State currentState = State::Idle;
 4
 5void handleEvent(Event event) {
 6    switch (currentState) {
 7
 8    case State::Idle:
 9        if (event == Event::ButtonPress) {
10            startMeasurement();
11            currentState = State::Measuring;
12        }
13        break;
14
15    case State::Measuring:
16        if (event == Event::MeasureDone) {
17            startTransmission(latestReading);
18            currentState = State::Transmitting;
19        }
20        break;
21
22    case State::Transmitting:
23        if (event == Event::TxDone) {
24            enterSleep(30000);   // сон 30 с
25            currentState = State::Sleeping;
26        }
27        break;
28
29    case State::Sleeping:
30        if (event == Event::WakeTimer) {
31            currentState = State::Idle;
32        }
33        break;
34    }
35}

Хорошо: нет накладных расходов, очевидная структура, работает везде. Проблема: добавление нового состояния или события требует перебора всех case switch. Действия входа/выхода (например, отключение периферии при входе в сон) не структурированы — легко забыть в одном case.


Добавление действий входа и выхода

 1void enterState(State next) {
 2    // Действия выхода для текущего состояния
 3    switch (currentState) {
 4    case State::Measuring:    ADC_Stop();      break;
 5    case State::Transmitting: RADIO_Disable(); break;
 6    case State::Sleeping:     RTC_StopAlarm(); break;
 7    default: break;
 8    }
 9
10    currentState = next;
11
12    // Действия входа для следующего состояния
13    switch (currentState) {
14    case State::Measuring:    ADC_Start();    break;
15    case State::Transmitting: RADIO_Enable(); break;
16    case State::Sleeping:
17        RTC_SetAlarm(30000);
18        LowPower_Enter(SLEEP_MODE);
19        break;
20    default: break;
21    }
22}

Теперь каждый переход проходит через enterState — действия входа/выхода невозможно пропустить. Но код удвоился в длину.


Подход 2: таблица переходов

Замените switch таблицей данных. Каждая строка — один допустимый переход:

 1struct Transition {
 2    State   from;
 3    Event   on;
 4    State   to;
 5    void  (*action)();   // вызывается при переходе (nullptr = нет действия)
 6};
 7
 8static const Transition transitions[] = {
 9    { State::Idle,         Event::ButtonPress, State::Measuring,    startMeasurement  },
10    { State::Measuring,    Event::MeasureDone, State::Transmitting, startTransmission },
11    { State::Transmitting, Event::TxDone,      State::Sleeping,     enterSleep        },
12    { State::Sleeping,     Event::WakeTimer,   State::Idle,         nullptr           },
13};
14
15State currentState = State::Idle;
16
17void handleEvent(Event event) {
18    for (auto& t : transitions) {
19        if (t.from == currentState && t.on == event) {
20            if (t.action) t.action();
21            currentState = t.to;
22            return;
23        }
24    }
25    // Нет подходящего перехода — игнорируем или логируем
26}

Добавление нового перехода — одна строка в таблице. Никакого switch трогать не нужно. Вся структура КА видна с первого взгляда.

Действия входа/выхода с таблицей:

 1struct StateConfig {
 2    State  state;
 3    void (*onEnter)();
 4    void (*onExit)();
 5};
 6
 7static const StateConfig stateConfigs[] = {
 8    { State::Idle,         nullptr,       nullptr        },
 9    { State::Measuring,    ADC_Start,     ADC_Stop       },
10    { State::Transmitting, RADIO_Enable,  RADIO_Disable  },
11    { State::Sleeping,     enterSleepHW,  nullptr        },
12};
13
14void transitionTo(State next) {
15    // Вызвать выход для текущего
16    for (auto& cfg : stateConfigs)
17        if (cfg.state == currentState && cfg.onExit) cfg.onExit();
18
19    currentState = next;
20
21    // Вызвать вход для следующего
22    for (auto& cfg : stateConfigs)
23        if (cfg.state == currentState && cfg.onEnter) cfg.onEnter();
24}

Подход 3: паттерн объектов-состояний

Каждое состояние становится классом. Специфичное для состояния поведение инкапсулировано внутри него. Контекст хранит указатель на текущий объект-состояние.

 1// Предварительные объявления
 2class SensorFsm;
 3
 4// Базовое состояние — по умолчанию: игнорировать все события
 5class SensorState {
 6public:
 7    virtual void onButtonPress(SensorFsm&) {}
 8    virtual void onMeasureDone(SensorFsm&, float value) {}
 9    virtual void onTxDone(SensorFsm&) {}
10    virtual void onWakeTimer(SensorFsm&) {}
11
12    virtual void enter(SensorFsm&) {}
13    virtual void exit(SensorFsm&) {}
14    virtual ~SensorState() = default;
15};
16
17// Контекст — хранит указатель на активное состояние, перенаправляет события
18class SensorFsm {
19    SensorState* current_;
20public:
21    explicit SensorFsm(SensorState* initial) : current_(initial) {
22        current_->enter(*this);
23    }
24
25    void transitionTo(SensorState* next) {
26        current_->exit(*this);
27        current_ = next;
28        current_->enter(*this);
29    }
30
31    void onButtonPress()              { current_->onButtonPress(*this); }
32    void onMeasureDone(float value)   { current_->onMeasureDone(*this, value); }
33    void onTxDone()                   { current_->onTxDone(*this); }
34    void onWakeTimer()                { current_->onWakeTimer(*this); }
35};

Конкретные классы состояний:

 1class IdleState : public SensorState {
 2public:
 3    void onButtonPress(SensorFsm& fsm) override {
 4        startMeasurement();
 5        fsm.transitionTo(&MeasuringState::instance());
 6    }
 7};
 8
 9class MeasuringState : public SensorState {
10public:
11    void enter(SensorFsm&) override { ADC_Start(); }
12    void exit(SensorFsm&)  override { ADC_Stop();  }
13
14    void onMeasureDone(SensorFsm& fsm, float value) override {
15        startTransmission(value);
16        fsm.transitionTo(&TransmittingState::instance());
17    }
18
19    static MeasuringState& instance() {
20        static MeasuringState s;
21        return s;
22    }
23};
24
25class TransmittingState : public SensorState {
26public:
27    void enter(SensorFsm&) override { RADIO_Enable(); }
28    void exit(SensorFsm&)  override { RADIO_Disable(); }
29
30    void onTxDone(SensorFsm& fsm) override {
31        fsm.transitionTo(&SleepingState::instance());
32    }
33
34    static TransmittingState& instance() { static TransmittingState s; return s; }
35};
36
37class SleepingState : public SensorState {
38public:
39    void enter(SensorFsm&) override { RTC_SetAlarm(30000); }
40    void exit(SensorFsm&)  override { RTC_CancelAlarm(); }
41
42    void onWakeTimer(SensorFsm& fsm) override {
43        fsm.transitionTo(&IdleState::instance());
44    }
45
46    static SleepingState& instance() { static SleepingState s; return s; }
47};

Использование:

1SensorFsm fsm(&IdleState::instance());
2
3// В ISR кнопки:
4fsm.onButtonPress();
5
6// В callback завершения DMA АЦП:
7fsm.onMeasureDone(latestVoltage);

Преимущества:

  • Добавление нового состояния = новый файл класса, без изменений в существующих состояниях
  • Специфичное для состояния поведение сосредоточено — нет разбросанных case switch
  • Компилятор предотвращает доступ к данным не того состояния

Стоимость: виртуальный вызов на событие. На Cortex-M4 на 168 МГц: ~3 нс — несущественно, если только события не приходят с мегагерцовой частотой.


Выбор правильного подхода

Критерий enum+switch Табличный Объекты-состояния
Состояния < 5 5–15 > 10
Событий на состояние < 3 любое любое
Данные конкретного состояния нет нет да
Действия входа/выхода неудобно структурировано естественно
Добавление нового состояния править switch добавить строку добавить класс
Накладные расходы нет небольшие (линейный поиск) vtable
Тестирование один большой юнит валидация таблицы юнит-тесты на состояние

Для простого КА с 3 состояниями и 2 событиями в ISR — используйте enum+switch. Для контроллера HVAC с 10 состояниями и сложной логикой входа/выхода — используйте объекты-состояния.


Итоги

  • enum+switch: 1–5 состояний, нет входа/выхода, быстро писать — встраиваемый стандарт
  • Табличный: 5–15 состояний, структурированные переходы, одна строка на ребро
  • Объекты-состояния: 10+ состояний, данные по состоянию, действия входа/выхода, независимо тестируемые

Что дальше