When designing firmware for embedded systems, especially those with limited resources, managing complexity is crucial. Finite State Machines (FSMs) provide a structured approach to handle device states and transitions clearly and reliably. However, many available FSM libraries introduce unnecessary overhead or complexity, making them challenging to implement in embedded environments.
This project addresses these limitations by providing a straightforward and lightweight FSM library designed explicitly for embedded targets.
Benefits- Minimal Overhead: Designed with a low memory and flash footprint, suitable for constrained environments.
- Portability: compatible with various embedded platforms (ESP32, STM32, AVR).
- Simple API: Easy-to-use and intuitive, reducing learning curve and integration time.
- Efficient and Deterministic: Ensures predictable execution, critical for real-time applications.
Compared to other FSM libraries, this implementation stands out because it:
- Supports Mealy, Moore, and mixed state outputs
- Is specifically tailored for embedded targets, focusing on resource efficiency.
- Provides built-in support for complex event handling like composite events and state timeouts.
- Offers seamless integration with popular frameworks like ESP-IDF and STM32 HAL.
A common application is managing a button that responds differently to single click, double click, and long click events.
FSM LogicFSM states diagram
Macros
#define TICK_MS 10
#define BUTTON_DEBOUNCE_MS 40
#define BUTTON_WAIT_DOUBLE_MS 100
#define BUTTON_WAIT_LONG_MS 3000
#define BUTTON_SHORT_PRESS_MS 2000
#define BUTTON_MEDIUM_PRESS_MS 5000
#define BUTTON_LONG_PRESS_MS 10000
#define BUTTON_GPIO_NUM CONFIG_BUTTON_GPIO_NUM
Button states definition
typedef enum {
BUTTON_STATE_IDLE = 0,
BUTTON_STATE_DEBOUNCE,
BUTTON_STATE_PRESSED,
BUTTON_STATE_RELEASED,
BUTTON_STATE_SINGLE,
BUTTON_STATE_DOUBLE,
BUTTON_STATE_LONG
} button_states_t;
Functions definition
/* Device specific funtions */
static bool dev_gpio_init(int gpio) {
gpio_config_t gpio_cfg;
gpio_cfg.pin_bit_mask = 1ULL << BUTTON_GPIO_NUM;
gpio_cfg.mode = GPIO_MODE_INPUT;
gpio_cfg.pull_up_en = GPIO_PULLUP_ENABLE;
gpio_cfg.pull_down_en = GPIO_PULLUP_DISABLE;
gpio_cfg.intr_type = GPIO_INTR_DISABLE;
return gpio_config(&gpio_cfg);
}
static bool dev_get_gpio_level(int gpio) { return gpio_get_level(gpio); }
static void dev_delay_ms(uint32_t ms) { vTaskDelay(pdMS_TO_TICKS(TICK_MS)); }
static uint32_t dev_get_ms(void) { return esp_timer_get_time() / 1000; }
/* Event evaluation functions */
static bool eval_eq(int a, int b) { return (a == b); }
/* Button calback functions */
static void on_press_single(void) { printf("Single click!\r\n"); }
static void on_press_double(void) { printf("Double click!\r\n"); }
static void on_press_long(void) { printf("Long click!\r\n"); }
/* FSM callbacks functions */
static void on_check_gpio(void) { gpio_level = dev_get_gpio_level(BUTTON_GPIO_NUM); }
GPIO andFSM initialization, transitions and events added, callbacks register, and FSM run
void app_main(void) {
dev_gpio_init(BUTTON_GPIO_NUM);
/* Initialize button FSM instance */
fsm_init(&button_fsm, BUTTON_STATE_IDLE, dev_get_ms);
/* Add all the transitions and events for the button FSM instance */
fsm_trans_t *trans = NULL;
fsm_add_transition(&button_fsm, &trans, BUTTON_STATE_IDLE, BUTTON_STATE_DEBOUNCE);
fsm_add_event_cmp(&button_fsm, trans, &gpio_level, 0, eval_eq);
fsm_add_transition(&button_fsm, &trans, BUTTON_STATE_DEBOUNCE, BUTTON_STATE_IDLE);
fsm_add_event_cmp(&button_fsm, trans, &gpio_level, 1, eval_eq);
fsm_add_event_timeout(&button_fsm, trans, BUTTON_DEBOUNCE_MS);
fsm_add_transition(&button_fsm, &trans, BUTTON_STATE_DEBOUNCE, BUTTON_STATE_PRESSED);
fsm_add_event_cmp(&button_fsm, trans, &gpio_level, 0, eval_eq);
fsm_add_event_timeout(&button_fsm, trans, BUTTON_DEBOUNCE_MS);
fsm_add_transition(&button_fsm, &trans, BUTTON_STATE_PRESSED, BUTTON_STATE_RELEASED);
fsm_add_event_cmp(&button_fsm, trans, &gpio_level, 1, eval_eq);
fsm_add_transition(&button_fsm, &trans, BUTTON_STATE_RELEASED, BUTTON_STATE_SINGLE);
fsm_add_event_timeout(&button_fsm, trans, BUTTON_WAIT_DOUBLE_MS);
fsm_add_transition(&button_fsm, &trans, BUTTON_STATE_RELEASED, BUTTON_STATE_DOUBLE);
fsm_add_event_cmp(&button_fsm, trans, &gpio_level, 0, eval_eq);
fsm_add_transition(&button_fsm, &trans, BUTTON_STATE_SINGLE, BUTTON_STATE_IDLE);
fsm_add_transition(&button_fsm, &trans, BUTTON_STATE_DOUBLE, BUTTON_STATE_IDLE);
fsm_add_event_cmp(&button_fsm, trans, &gpio_level, 1, eval_eq);
fsm_add_transition(&button_fsm, &trans, BUTTON_STATE_PRESSED, BUTTON_STATE_LONG);
fsm_add_event_timeout(&button_fsm, trans, BUTTON_WAIT_LONG_MS);
fsm_add_transition(&button_fsm, &trans, BUTTON_STATE_LONG, BUTTON_STATE_IDLE);
fsm_add_event_cmp(&button_fsm, trans, &gpio_level, 1, eval_eq);
/* Register states callbacks */
fsm_register_state_actions(&button_fsm, BUTTON_STATE_IDLE, NULL, on_check_gpio, NULL);
fsm_register_state_actions(&button_fsm, BUTTON_STATE_DEBOUNCE, NULL, on_check_gpio, NULL);
fsm_register_state_actions(&button_fsm, BUTTON_STATE_PRESSED, NULL, on_check_gpio, NULL);
fsm_register_state_actions(&button_fsm, BUTTON_STATE_RELEASED, NULL, on_check_gpio, NULL);
fsm_register_state_actions(&button_fsm, BUTTON_STATE_SINGLE, on_press_single, NULL, NULL);
fsm_register_state_actions(&button_fsm, BUTTON_STATE_DOUBLE, on_press_double, on_check_gpio, NULL);
fsm_register_state_actions(&button_fsm, BUTTON_STATE_LONG, on_press_long, on_check_gpio, NULL);
/* Inifinite loop */
for (;;) {
/* Execute the button FSM every TICK_MS ms */
fsm_run(&button_fsm);
dev_delay_ms(TICK_MS);
}
}
Explanation- The FSM continuously checks the button state every 10ms.
- Debounce filters out false triggers caused by mechanical bounce.
- Transitions clearly represent real-world button interactions.
- Actions are executed explicitly based on detected button patterns.
I (24) boot: ESP-IDF v5.4.1 2nd stage bootloader
I (24) boot: compile time Jul 13 2025 11:08:02
I (24) boot: chip revision: v0.4
I (25) boot: efuse block revision: v1.2
I (28) boot.esp32c3: SPI Speed : 80MHz
I (32) boot.esp32c3: SPI Mode : DIO
I (36) boot.esp32c3: SPI Flash Size : 2MB
I (39) boot: Enabling RNG early entropy source...
I (44) boot: Partition Table:
I (46) boot: ## Label Usage Type ST Offset Length
I (53) boot: 0 nvs WiFi data 01 02 00009000 00006000
I (59) boot: 1 phy_init RF data 01 01 0000f000 00001000
I (66) boot: 2 factory factory app 00 00 00010000 00100000
I (72) boot: End of partition table
I (76) esp_image: segment 0: paddr=00010020 vaddr=3c020020 size=08d0ch ( 36108) map
I (89) esp_image: segment 1: paddr=00018d34 vaddr=3fc8ba00 size=012ach ( 4780) load
I (91) esp_image: segment 2: paddr=00019fe8 vaddr=40380000 size=06030h ( 24624) load
I (102) esp_image: segment 3: paddr=00020020 vaddr=42000020 size=176c4h ( 95940) map
I (121) esp_image: segment 4: paddr=000376ec vaddr=40386030 size=057e4h ( 22500) load
I (125) esp_image: segment 5: paddr=0003ced8 vaddr=50000200 size=0001ch ( 28) load
I (129) boot: Loaded app from partition at offset 0x10000
I (130) boot: Disabling RNG early entropy source...
I (146) cpu_start: Unicore app
I (155) cpu_start: Pro cpu start user code
I (155) cpu_start: cpu freq: 160000000 Hz
I (155) app_init: Application information:
I (155) app_init: Project name: multi_function_button
I (160) app_init: App version: 3994c4e-dirty
I (164) app_init: Compile time: Jul 13 2025 11:12:29
I (169) app_init: ELF file SHA256: a378d3a9c...
I (174) app_init: ESP-IDF: v5.4.1
I (177) efuse_init: Min chip rev: v0.3
I (181) efuse_init: Max chip rev: v1.99
I (185) efuse_init: Chip rev: v0.4
I (189) heap_init: Initializing. RAM available for dynamic allocation:
I (195) heap_init: At 3FC8DB90 len 00032470 (201 KiB): RAM
I (201) heap_init: At 3FCC0000 len 0001C710 (113 KiB): Retention RAM
I (207) heap_init: At 3FCDC710 len 00002950 (10 KiB): Retention RAM
I (213) heap_init: At 5000021C len 00001DCC (7 KiB): RTCRAM
I (219) spi_flash: detected chip: generic
I (222) spi_flash: flash io: dio
W (225) spi_flash: Detected size(4096k) larger than the size in the binary image header(2048k). Using the size in the binary image header.
I (237) sleep_gpio: Configure to isolate all GPIO pins in sleep state
I (243) sleep_gpio: Enable automatic switching of GPIO sleep configuration
I (250) main_task: Started on CPU0
I (250) main_task: Calling app_main()
I (250) gpio: GPIO[9]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0
Single click!
Double click!
Double click!
Double click!
Double click!
Long click!
Long click!
Double click!
Single click!
ConclusionThis FSM library simplifies embedded firmware development, offering clarity, reliability, and efficiency. By focusing on resource-constrained environments, it significantly improves application stability and predictability, essential qualities for robust embedded solutions.
Comments