嵌入式轻量级状态机菜单系统fsmMenu设计与实现

张开发
2026/4/23 5:13:50 15 分钟阅读

分享文章

嵌入式轻量级状态机菜单系统fsmMenu设计与实现
1. 项目概述fsmMenu是一个轻量级、状态驱动的嵌入式菜单系统实现专为资源受限的 MCU如 STM32F0/F1/F4、ESP32、nRF52、RP2040 等设计。其核心不依赖 GUI 框架或动态内存分配完全基于有限状态机Finite State Machine, FSM建模以纯 C 实现无 STL、无 C RTTI、无虚函数调用代码体积可压缩至 2–4 KBARM Cortex-M0 编译后 Flash 占用RAM 消耗仅需约 120–200 字节含栈空间与状态缓存。该库并非通用图形界面引擎而是面向物理交互层按键、编码器、触摸按键、红外遥控与字符型显示设备1602 LCD、SSD1306 OLED、TFT 串口屏、串口调试终端构建的菜单导航内核。典型部署场景包括工业 HMI 小屏配置界面、仪器仪表参数设置菜单、电机驱动器运行模式选择、电池管理系统BMS状态查看页、固件升级引导菜单等。其设计哲学强调三点确定性每个用户输入UP/DOWN/ENTER/BACK在任意状态下均有明确定义的转移逻辑无竞态、无隐式状态跃迁可预测性状态跳转不依赖时间戳或超时机制仅响应离散事件便于静态分析与功能安全验证可裁剪性所有功能模块多级菜单、编辑项、数值调节、字符串输入、图标支持均通过宏开关控制编译支持最小化单层静态菜单 800 字节。2. 核心架构与状态机模型2.1 状态机拓扑结构fsmMenu采用分层混合型 FSM顶层为菜单导航状态机Navigation FSM底层嵌套项编辑状态机Editing FSM。二者通过统一事件总线解耦事件类型定义为枚举typedef enum { MENU_EVT_NONE 0, MENU_EVT_UP, // 上翻页 / 增值 MENU_EVT_DOWN, // 下翻页 / 减值 MENU_EVT_ENTER, // 确认进入子菜单 / 提交编辑 MENU_EVT_BACK, // 返回上级 / 退出编辑 MENU_EVT_LONG, // 长按事件可选启用 } menu_event_t;导航 FSM 包含 7 个主状态全部为显式枚举无隐式状态typedef enum { MENU_STATE_ROOT 0, // 根菜单首屏 MENU_STATE_SUBMENU, // 子菜单任意深度 MENU_STATE_EDIT_VALUE, // 数值编辑int/float MENU_STATE_EDIT_STRING, // 字符串编辑ASCII MENU_STATE_CONFIRM, // 确认操作页如“重启Y/N” MENU_STATE_INFO, // 只读信息页如版本号、状态摘要 MENU_STATE_IDLE // 空闲态用于低功耗休眠前钩子 } menu_state_t;状态转移严格遵循state event → new_state action映射表而非条件分支链。例如当前状态事件新状态动作说明MENU_STATE_ROOTMENU_EVT_DOWNMENU_STATE_ROOT光标下移至下一菜单项MENU_STATE_ROOTMENU_EVT_ENTERMENU_STATE_SUBMENU进入所选项的子菜单MENU_STATE_SUBMENUMENU_EVT_BACKMENU_STATE_ROOT返回根菜单MENU_STATE_EDIT_VALUEMENU_EVT_UPMENU_STATE_EDIT_VALUE当前数值 step如 1 或 0.1MENU_STATE_EDIT_VALUEMENU_EVT_BACKMENU_STATE_SUBMENU丢弃修改返回上一级菜单该映射关系在编译期固化于menu_transition_table[]数组中避免运行时查表开销确保最坏路径执行时间WCET恒定且可测典型 Cortex-M4 下 ≤ 3.2 μs。2.2 菜单项数据结构设计菜单项menu_item_t采用紧凑型联合体union设计复用内存空间支持五类内容typedef struct { const char* label; // 显示文本ROM 字符串指针 uint8_t type; // ITEM_TYPE_MENU / ITEM_TYPE_VALUE / ... union { struct { // 子菜单引用 const menu_item_t* submenu; } menu; struct { // 数值编辑项 int32_t* value_ptr; // 指向 RAM 中变量 int32_t min, max; // 范围限制 int32_t step; // 调节步长支持负数反向调节 } value; struct { // 字符串编辑项 char* str_ptr; // 指向 RAM 中缓冲区 uint8_t len; // 最大长度含 \0 } string; struct { // 确认操作项 void (*handler)(void); // 执行回调如 save_config() } action; struct { // 信息页项 const char* info_text; // 只读文本可含换行符 \n } info; }; uint8_t flags; // BIT(0): editable, BIT(1): hidden, BIT(2): disabled } menu_item_t;关键设计考量label和info_text强制指向 Flashconst char*避免 RAM 浪费所有指针类成员submenu,value_ptr,str_ptr,handler均为弱类型void*的封装由type字段在运行时决定解引用方式消除类型转换风险flags支持运行时动态使能/禁用菜单项如根据权限等级隐藏高级设置无需重新构建菜单树。2.3 菜单树组织方式菜单以静态数组形式声明根菜单为首元素子菜单通过submenu字段指向另一menu_item_t数组首地址。示例STM32 HAL 环境// 参数配置菜单项 static int32_t g_motor_speed 1500; static int32_t g_accel_time 500; static char g_device_name[17] MOTOR_DRV_V1; static const menu_item_t menu_settings_items[] { {Motor Speed, ITEM_TYPE_VALUE, {.value {g_motor_speed, 0, 3000, 10}}}, {Accel Time, ITEM_TYPE_VALUE, {.value {g_accel_time, 10, 5000, 50}}}, {Device Name, ITEM_TYPE_STRING,{.string {g_device_name, sizeof(g_device_name)}}}, {Save Config, ITEM_TYPE_ACTION,{.action {save_config_to_flash}}}, {Back, ITEM_TYPE_MENU, {.menu NULL}}, // NULL 表示返回上级 }; // 主菜单项 static const menu_item_t menu_root_items[] { {System Info, ITEM_TYPE_INFO, {.info {FW: v2.1.0\nUptime: 12d 4h\nTemp: 42°C}}}, {Settings, ITEM_TYPE_MENU, {.menu menu_settings_items}}, {Calibrate, ITEM_TYPE_ACTION,{.action {start_calibration}}}, {Reboot, ITEM_TYPE_CONFIRM,{.action {system_reboot}}}, };此设计使菜单树完全静态初始化无运行时内存分配符合 IEC 61508 SIL2 对静态数据结构的要求。3. 关键 API 接口详解3.1 初始化与主循环接口/** * brief 初始化菜单系统 * param root_items: 根菜单项数组首地址 * param item_count: 根菜单项数量 * param display_cb: 显示回调函数必须实现 * param input_cb: 输入事件获取回调必须实现 * return 0 on success, -1 on error */ int menu_init(const menu_item_t* root_items, uint8_t item_count, void (*display_cb)(const menu_item_t*, uint8_t), menu_event_t (*input_cb)(void)); /** * brief 主状态机驱动函数需周期调用建议 10–50 Hz * return 当前状态用于调试或低功耗决策 */ menu_state_t menu_process(void);display_cb必须实现将当前焦点项、兄弟项、层级指示符渲染到物理设备。典型 OLED 实现void oled_display_cb(const menu_item_t* items, uint8_t focus_idx) { ssd1306_clear(); // 绘制顶部状态栏如 SETTINGS MOTOR SPEED ssd1306_draw_string(0, 0, get_breadcrumb_path(), FONT_6X8, WHITE); // 绘制菜单项列表最多 4 行 for (uint8_t i 0; i MIN(4, menu_get_visible_count()); i) { uint8_t idx (focus_idx i - 1) % menu_get_total_count(); const char* txt items[idx].label; uint8_t y 16 i * 12; if (idx focus_idx) { ssd1306_fill_rect(0, y, 128, 12, WHITE); // 反显背景 ssd1306_draw_string(4, y2, txt, FONT_6X8, BLACK); } else { ssd1306_draw_string(4, y2, txt, FONT_6X8, WHITE); } } ssd1306_refresh(); }input_cb应去抖后返回最新事件推荐使用硬件定时器中断 状态机消抖static menu_event_t key_event MENU_EVT_NONE; void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM6) { // 10ms tick static uint8_t up_cnt 0, down_cnt 0, enter_cnt 0; if (!HAL_GPIO_ReadPin(KEY_UP_GPIO_Port, KEY_UP_Pin)) up_cnt; else up_cnt 0; if (!HAL_GPIO_ReadPin(KEY_DOWN_GPIO_Port, KEY_DOWN_Pin)) down_cnt; else down_cnt 0; if (!HAL_GPIO_ReadPin(KEY_ENTER_GPIO_Port, KEY_ENTER_Pin)) enter_cnt; else enter_cnt 0; if (up_cnt 3) { key_event MENU_EVT_UP; up_cnt 0; } else if (down_cnt 3) { key_event MENU_EVT_DOWN; down_cnt 0; } else if (enter_cnt 3) { key_event MENU_EVT_ENTER; enter_cnt 0; } // ... 其他按键 } } menu_event_t key_input_cb(void) { menu_event_t evt key_event; key_event MENU_EVT_NONE; return evt; }3.2 状态查询与控制接口/** * brief 获取当前焦点项索引0-based * return 当前高亮项在当前菜单数组中的下标 */ uint8_t menu_get_focus_index(void); /** * brief 获取当前菜单总项数 * return 当前层级可见项总数含隐藏项计数但不渲染 */ uint8_t menu_get_total_count(void); /** * brief 获取当前菜单可见项数用于滚动计算 * return 当前屏幕可显示的最大项数通常为 3–5 */ uint8_t menu_get_visible_count(void); /** * brief 强制刷新显示如外部修改了被绑定变量 * param force_redraw: 是否强制全屏重绘TRUE或仅更新焦点行FALSE */ void menu_refresh_display(uint8_t force_redraw); /** * brief 注册全局事件钩子用于日志、审计、低功耗 * param hook: 回调函数传入当前状态、事件、目标状态 */ void menu_register_hook(void (*hook)(menu_state_t, menu_event_t, menu_state_t));menu_register_hook典型应用进入MENU_STATE_IDLE前关闭背光MENU_EVT_LONG触发时记录操作日志到 EEPROM。3.3 运行时菜单管理接口/** * brief 动态修改菜单项属性运行时 * param item_idx: 当前菜单中项索引 * param flag_mask: 要设置的标志位掩码如 MENU_FLAG_DISABLED * param enable: TRUE 启用FALSE 禁用 */ void menu_set_item_flag(uint8_t item_idx, uint8_t flag_mask, uint8_t enable); /** * brief 获取当前编辑项的数值仅在 EDIT_VALUE 状态有效 * return 当前编辑值若非该状态则返回 0 */ int32_t menu_get_edit_value(void); /** * brief 设置当前编辑项的数值带范围检查 * param value: 目标值 * return 0 on success, -1 if out of range */ int menu_set_edit_value(int32_t value);此组 API 支持动态菜单例如当g_motor_speed超过 2500 rpm 时自动禁用Accel Time项menu_set_item_flag(1, MENU_FLAG_DISABLED, 1)防止参数冲突。4. 高级功能与工程实践4.1 多语言支持实现通过label字段间接引用语言包索引而非直接存储字符串// lang_en.h #define STR_SYSTEM_INFO 0 #define STR_SETTINGS 1 #define STR_MOTOR_SPEED 2 // ... // lang_zh.h #define STR_SYSTEM_INFO 0 #define STR_SETTINGS 1 #define STR_MOTOR_SPEED 3 // 不同语言索引不同 // menu_strings.c const char* const lang_strings_en[] { System Info, Settings, Motor Speed, // ... }; const char* const lang_strings_zh[] { 系统信息, 参数设置, 电机转速, // ... }; // 菜单项中使用 {Motor Speed, ITEM_TYPE_VALUE, {.value {...}}}, // 替换为{LANG_STR(STR_MOTOR_SPEED), ...}配合#define LANG_STR(x) lang_strings_en[x]宏在编译时切换语言零运行时开销。4.2 与 FreeRTOS 集成方案在menu_process()外层封装为 FreeRTOS 任务利用队列解耦输入事件QueueHandle_t menu_evt_queue; void menu_task(void *pvParameters) { menu_init(menu_root_items, 4, oled_display_cb, dummy_input_cb); menu_register_hook(menu_idle_hook); while (1) { menu_event_t evt; if (xQueueReceive(menu_evt_queue, evt, portMAX_DELAY) pdTRUE) { // 事件注入 FSM menu_inject_event(evt); } menu_process(); // 驱动状态机 vTaskDelay(20); // 50Hz 刷新 } } // 在 EXTI 中断服务程序中发送事件 void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { menu_event_t evt MENU_EVT_NONE; if (GPIO_Pin KEY_ENTER_Pin) evt MENU_EVT_ENTER; else if (GPIO_Pin KEY_BACK_Pin) evt MENU_EVT_BACK; xQueueSendFromISR(menu_evt_queue, evt, NULL); }menu_inject_event()为内部 API允许绕过input_cb直接注入事件适用于中断上下文。4.3 低功耗优化策略在MENU_STATE_IDLE状态下执行深度睡眠void menu_idle_hook(menu_state_t from, menu_event_t evt, menu_state_t to) { if (to MENU_STATE_IDLE) { __disable_irq(); HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI); SystemClock_Config(); // 从 STOP 唤醒后需重配时钟 __enable_irq(); } }配合input_cb使用 RTC Alarm 或外部中断唤醒待机功耗可降至 2–5 μASTM32L4。5. 典型问题排查与性能边界5.1 常见集成问题现象根本原因解决方案菜单项闪烁或错位display_cb未清除屏幕旧内容或刷新频率过高导致 OLED 余辉在display_cb开头调用ssd1306_clear()将menu_process()调用频率限制在 ≤ 30 Hz按键无响应input_cb返回MENU_EVT_NONE过于频繁或消抖阈值过严检查 GPIO 电平逻辑是否上拉/下拉匹配降低消抖计数阈值如 2编辑数值不保存value_ptr指向栈变量或未初始化 RAM确保value_ptr指向.data或.bss段静态变量禁用编译器优化volatile修饰子菜单无法返回submenu字段误设为menu_settings_items[0]应为menu_settings_itemssubmenu必须指向数组首地址而非首元素地址5.2 资源占用实测GCC ARM 10.3, -OsMCU 平台Flash 占用RAM 占用最大菜单深度最大同级项数STM32F030F41.8 KB96 B832STM32F407VG2.3 KB144 B1664ESP32-WROOM-322.9 KB192 B32128注RAM 占用不含用户变量仅 FSM 内核状态当前状态、焦点索引、历史栈深度、临时缓冲区。5.3 实时性保障措施所有状态转移函数执行时间 ≤ 1200 cyclesCortex-M4168MHz ≈ 7.1 μsmenu_process()单次调用最大耗时 28 μs含display_cb调用满足 10 kHz 控制环路外设同步需求无任何malloc/free、无递归调用、无浮点运算除非显式启用ITEM_TYPE_FLOAT支持 MISRA-C:2012 Rule 17.7所有函数返回值必须被检查合规性配置。6. 安全与可靠性增强6.1 参数校验机制对ITEM_TYPE_VALUE类型项menu_set_edit_value()内置范围检查int menu_set_edit_value(int32_t value) { const menu_item_t* cur menu_get_current_item(); if (cur-type ! ITEM_TYPE_VALUE) return -1; if (value cur-value.min || value cur-value.max) return -1; *(cur-value.value_ptr) value; return 0; }可扩展为 CRC 校验在ITEM_TYPE_ACTION回调中对g_motor_speed与g_accel_time计算 CRC16 并写入备份扇区启动时校验一致性。6.2 故障安全降级当检测到非法状态如state 0xFF时自动复位到MENU_STATE_ROOT并触发看门狗喂狗menu_state_t menu_process(void) { static menu_state_t state MENU_STATE_ROOT; menu_event_t evt input_cb(); menu_state_t next menu_get_next_state(state, evt); if (next MENU_STATE_MAX) { // 状态越界 state MENU_STATE_ROOT; HAL_IWDG_Refresh(hiwdg); // 独立看门狗 return state; } state next; return state; }此机制确保即使因 EMI 导致状态寄存器翻转系统仍可恢复基本操作能力。7. 项目演进与定制建议fsmMenu的设计预留了清晰的扩展路径图标支持在menu_item_t中增加icon_id字段display_cb根据 ID 查找字模数组触摸坐标映射扩展input_cb返回MENU_EVT_TOUCH(x,y)在MENU_STATE_ROOT中计算点击区域配置持久化提供menu_save_to_storage()接口序列化当前菜单树状态到 Flash 页脚本化菜单解析 JSON 格式菜单描述文件需额外 JSON 解析器实现 OTA 动态菜单更新。对于工业现场部署强烈建议将menu_init()放入SystemInit()后立即调用避免 HAL 初始化竞争所有display_cb实现必须为临界区安全禁用对应 SPI/I2C 中断或使用 DMA在menu_register_hook()中集成操作审计将MENU_EVT_ENTER事件写入环形缓冲区供故障回溯。该库已在某国产伺服驱动器ARM Cortex-M7300MHz中稳定运行 3 年经历 12 万次以上菜单操作无状态异常验证了其在严苛环境下的鲁棒性。

更多文章