Почему прошивке нужны автоматы состояний
Прошивка фундаментально реактивна: внешние события (нажатие кнопки, тик таймера,
завершение DMA) управляют переходами между рабочими режимами. Без явной структуры
автомата состояний эта логика оседает в виде глубоко вложенных цепочек if-else,
разбросанных по ISR и циклам задач — поначалу корректно, при масштабировании необслуживаемо.
Эта статья строит три реализации КА для одного устройства: сенсор с батарейным питанием, которому нужно управлять своими состояниями питания.
Состояния: Idle → Measuring → Transmitting → Sleeping → Idle
События: 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+ состояний, данные по состоянию, действия входа/выхода, независимо тестируемые
Что дальше
- Конечные автоматы — три подхода — проектирование КА на архитектурном уровне
- Событийная архитектура — подача событий в КА из шины
- Задачи и очереди FreeRTOS — одна задача на автомат состояний