当前位置: 首页 > news >正文

嵌入式裸机菜单库:无GUI框架的静态树形菜单实现

1. 项目概述

simple_raw_menu是一个面向嵌入式 LCD 显示场景的轻量级、无依赖菜单界面库。其设计哲学明确指向“raw”——即绕过 GUI 框架(如 LVGL、emWin)、不引入图形抽象层(如 FatFs、STemWin)、不依赖操作系统服务(如 FreeRTOS 消息队列或互斥量),仅使用底层硬件驱动(如 GPIO、SPI/I2C LCD 控制器)和 C 标准库(stdio.hstring.hstdlib.h)完成菜单逻辑与像素级绘制。

该库并非通用 UI 引擎,而是为资源受限的 MCU(如 STM32F0/F1、nRF52832、ESP32-S2、GD32E230 等 Flash < 64KB、RAM < 16KB 的平台)定制的菜单解决方案。典型应用场景包括:

  • 工业 HMI 小屏参数配置界面(如温控器、PLC 手持调试器);
  • 电池供电的便携设备主菜单(如电子秤、手持扫码仪);
  • 教学实验板上的交互式演示系统(如基于 SSD1306 OLED 的课程设计);
  • Bootloader 阶段的固件选择菜单(无文件系统、无动态内存分配)。

其核心价值在于确定性可预测性:所有内存占用在编译期静态分配,无malloc()/free()调用,无递归调用栈风险,无隐式中断上下文切换开销。菜单项数量、层级深度、字符串长度均通过宏定义约束,便于开发者在链接阶段精确控制 BSS/HEAP 占用。

2. 设计原理与架构解析

2.1 “Raw” 的本质:从抽象到裸机的回归

现代嵌入式 GUI 库常通过多层抽象隐藏硬件细节:

  • LVGL 抽象出lv_disp_drv_t显示驱动、lv_indev_drv_t输入驱动;
  • emWin 封装GUI_DEVICEGUI_TOUCH
  • Qt for MCUs 则构建完整的事件循环与信号槽机制。

simple_raw_menu反其道而行之,将抽象降至最低:

  • 显示层:直接操作 LCD 帧缓冲区(Frame Buffer)或调用lcd_draw_pixel(x, y, color)lcd_draw_string(x, y, str, font)等由用户实现的底层函数;
  • 输入层:仅接收MENU_KEY_UP/MENU_KEY_DOWN/MENU_KEY_SELECT/MENU_KEY_BACK四个逻辑按键事件,由用户通过 GPIO 中断或轮询key_scan()返回;
  • 内存模型:菜单树结构采用静态数组 + 索引引用,而非指针链表,避免运行时内存碎片与空指针风险。

这种设计使库体积极小(典型.text段 < 2KB),且完全规避了 GUI 库中常见的“重绘闪烁”、“输入延迟”、“内存泄漏”三大痛点。

2.2 菜单数据结构:静态数组驱动的树形拓扑

菜单系统以menu_item_t结构体为基本单元,定义如下:

typedef struct { const char *name; // 菜单项显示文本(存储于 Flash) void (*handler)(void); // 选中后执行的回调函数(可为空) uint8_t flags; // 标志位:MENU_ITEM_FLAG_SUBMENU / MENU_ITEM_FLAG_EXECUTABLE uint8_t child_count; // 子菜单项数量(0 表示叶节点) uint8_t *child_indices; // 指向子菜单项在全局 menu_items[] 中的索引数组(Flash 地址) } menu_item_t;

关键设计点解析:

  • child_indicesuint8_t*类型,指向一个const uint8_t child_idx[] = {0, 2, 5};形式的常量数组。此设计避免了指针数组(menu_item_t**)带来的额外 4 字节/项内存开销,在 8 位 MCU 上尤为关键;
  • flags字段复用单字节实现状态标记:MENU_ITEM_FLAG_SUBMENU(值为 0x01)表示该项为父菜单,点击后展开子项;MENU_ITEM_FLAG_EXECUTABLE(值为 0x02)表示该项为可执行命令(如“重启系统”),点击后触发handler;二者可共存(值为 0x03),实现“进入子菜单并执行初始化”的复合行为;
  • 所有menu_item_t实例必须声明为static const并置于 Flash 区域,确保只读性与零 RAM 占用。

完整菜单树通过全局数组const menu_item_t menu_items[]构建,例如一个三级菜单:

// 定义子菜单项 static const uint8_t menu_settings_items[] = {3, 4}; // 索引 3、4 对应 "Brightness" 和 "Volume" static const uint8_t menu_main_items[] = {1, 2}; // 索引 1、2 对应 "Settings" 和 "About" // 全局菜单项数组(顺序即为索引号) const menu_item_t menu_items[] = { [0] = {"Main Menu", NULL, MENU_ITEM_FLAG_SUBMENU, 2, (uint8_t*)menu_main_items}, // 根节点 [1] = {"Settings", NULL, MENU_ITEM_FLAG_SUBMENU, 2, (uint8_t*)menu_settings_items}, [2] = {"About", about_handler, MENU_ITEM_FLAG_EXECUTABLE, 0, NULL}, [3] = {"Brightness", brightness_handler, MENU_ITEM_FLAG_EXECUTABLE, 0, NULL}, [4] = {"Volume", volume_handler, MENU_ITEM_FLAG_EXECUTABLE, 0, NULL}, };

此结构在编译时生成紧凑的只读数据段,运行时通过索引查表实现 O(1) 时间复杂度的菜单跳转。

2.3 状态机引擎:无栈导航逻辑

菜单导航不依赖递归或动态栈,而是通过menu_state_t结构维护当前上下文:

typedef struct { const menu_item_t *current_menu; // 当前显示的菜单(指向 menu_items[] 中某元素) uint8_t current_index; // 当前高亮项在 current_menu->child_indices 中的偏移 uint8_t history_depth; // 历史菜单层级数(用于 BACK 键回溯) uint8_t history_stack[8]; // 存储历史菜单项索引(最大 8 层,可配置) } menu_state_t;

导航流程严格遵循有限状态机(FSM):

  • UP/DOWN 键:仅修改current_index,范围限制在[0, current_menu->child_count)
  • SELECT 键:若current_menu->child_count > 0,则将current_menu更新为&menu_items[current_menu->child_indices[current_index]],同时压栈history_stack[history_depth++] = current_menu_index
  • BACK 键:若history_depth > 0,则history_depth--current_menu = &menu_items[history_stack[history_depth]]current_index = 0

该 FSM 完全消除函数调用栈深度不确定性,即使在 128 字节栈空间的 Cortex-M0+ 上亦可稳定运行。

3. 核心 API 接口详解

3.1 初始化与主循环接口

函数签名功能说明参数详解
void menu_init(const menu_item_t *root)初始化菜单引擎,设置根菜单root: 指向根菜单项的指针(通常为&menu_items[0]
void menu_process_key(menu_key_t key)处理单次按键事件key: 枚举值MENU_KEY_UP/DOWN/SELECT/BACK
void menu_render(void)触发菜单重绘(需用户在 LCD 刷新周期内调用)无参数

menu_render()是唯一需要用户主动调用的绘制入口。其实现逻辑为:

  1. 计算当前菜单可见区域(通常为 4~6 行);
  2. 遍历current_menu->child_indices[0..min(child_count, visible_lines)]
  3. 对每个子项调用lcd_draw_string(x, y + i*line_height, menu_items[idx].name, font)
  4. current_index对应行绘制高亮标识(如"> "前缀或反色背景)。

3.2 用户回调与扩展钩子

库提供两个关键回调接口供用户注入业务逻辑:

// 菜单项选中时调用(当 flags 含 MENU_ITEM_FLAG_EXECUTABLE) extern void (*menu_on_item_selected)(const menu_item_t *item); // 菜单渲染前调用(可用于动态更新文本,如显示实时传感器值) extern void (*menu_on_pre_render)(void);

典型应用示例(动态显示电池电量):

void battery_level_handler(void) { // 执行充电管理操作 } void dynamic_text_update(void) { static char batt_str[16]; uint8_t level = get_battery_level_percent(); snprintf(batt_str, sizeof(batt_str), "Battery: %d%%", level); // 替换 menu_items[3].name(假设索引 3 为电池项)——需在初始化前完成 // 实际中建议使用全局变量 + menu_on_pre_render 实现 } // 在 main() 中注册 menu_on_pre_render = dynamic_text_update;

3.3 配置宏定义

所有可调参数通过menu_config.h中的宏控制,确保编译期优化:

宏定义默认值作用说明
MENU_MAX_HISTORY_DEPTH8最大菜单历史层级,决定history_stack数组大小
MENU_VISIBLE_LINES4LCD 单页显示的菜单行数,影响menu_render()绘制范围
MENU_LINE_HEIGHT12每行文字高度(像素),需与所用字体匹配
MENU_HIGHLIGHT_PREFIX"> "高亮行前缀字符串(可设为"\x01"使用自定义图标)
MENU_USE_FLASH_STRINGS1是否启用__attribute__((section(".flash_strings")))将字符串存入 Flash

4. 硬件驱动集成实践

4.1 LCD 驱动适配要点

simple_raw_menu不绑定任何 LCD 控制器,用户需实现以下基础函数(声明于menu_driver.h):

// 必须实现 void lcd_clear(void); // 清屏 void lcd_draw_string(uint16_t x, uint16_t y, const char *str, const font_t *font); void lcd_draw_pixel(uint16_t x, uint16_t y, uint16_t color); // 可选实现(用于高亮效果) void lcd_fill_rect(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color);

以 SSD1306 OLED(I2C 接口)为例,lcd_draw_string的关键实现:

void lcd_draw_string(uint16_t x, uint16_t y, const char *str, const font_t *font) { const uint8_t *glyph; uint16_t char_x = x; while (*str && char_x < SSD1306_WIDTH) { glyph = font_get_glyph(font, *str); // 获取字符点阵数据 if (glyph) { ssd1306_draw_bitmap(char_x, y, glyph, font->width, font->height, 1); char_x += font->width + font->spacing; } str++; } }

工程提示font_t结构体应包含widthheightspacingget_glyph函数指针,支持多字体混排。实际项目中可预编译 ASCII 字模到 Flash,避免运行时解码开销。

4.2 按键扫描与去抖策略

库期望menu_process_key()被高频调用(≥ 100Hz),因此推荐使用定时器中断轮询:

// 10ms 定时器中断服务程序 void TIM2_IRQHandler(void) { static uint16_t key_state = 0xFFFF; // 初始全 1(未按下) uint16_t raw = read_gpio_keys(); // 读取 GPIO 状态(低电平有效) // 简单硬件去抖:连续 3 次采样一致才确认 key_state = (key_state << 1) | raw | 0x8000; // 移位寄存器 if ((key_state & 0xFFFF) == 0x0000) { // 全 0 表示稳定按下 menu_process_key(MENU_KEY_SELECT); key_state = 0xFFFF; // 重置 } TIM2->SR = 0; // 清中断标志 }

对于带硬件消抖的 MCU(如 STM32G0 的 GPIO Latch 功能),可直接使用边沿触发中断,进一步降低 CPU 占用。

5. 典型应用代码示例

5.1 STM32 HAL + SSD1306 OLED 完整实现

#include "main.h" #include "menu.h" #include "ssd1306.h" #include "fonts.h" // 菜单项定义(全部位于 Flash) static const uint8_t menu_system_items[] = {1, 2}; static const uint8_t menu_main_items[] = {0}; const menu_item_t menu_items[] = { [0] = {"System", NULL, MENU_ITEM_FLAG_SUBMENU, 2, (uint8_t*)menu_system_items}, [1] = {"Reboot", system_reboot, MENU_ITEM_FLAG_EXECUTABLE, 0, NULL}, [2] = {"Factory Reset", factory_reset, MENU_ITEM_FLAG_EXECUTABLE, 0, NULL}, }; // 用户实现的 LCD 驱动 void lcd_clear(void) { ssd1306_Fill(Black); } void lcd_draw_string(uint16_t x, uint16_t y, const char *str, const font_t *font) { ssd1306_SetCursor(x, y); ssd1306_WriteString((char*)str, Font_7x10, White); } // 主函数 int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_I2C1_Init(); ssd1306_Init(); // 初始化 OLED menu_init(&menu_items[0]); // 设置根菜单 while (1) { // 按键处理(假设使用 HAL_GPIO_ReadPin) if (HAL_GPIO_ReadPin(KEY_UP_GPIO_Port, KEY_UP_Pin) == GPIO_PIN_RESET) menu_process_key(MENU_KEY_UP); if (HAL_GPIO_ReadPin(KEY_DOWN_GPIO_Port, KEY_DOWN_Pin) == GPIO_PIN_RESET) menu_process_key(MENU_KEY_DOWN); if (HAL_GPIO_ReadPin(KEY_SEL_GPIO_Port, KEY_SEL_Pin) == GPIO_PIN_RESET) menu_process_key(MENU_KEY_SELECT); if (HAL_GPIO_ReadPin(KEY_BACK_GPIO_Port, KEY_BACK_Pin) == GPIO_PIN_RESET) menu_process_key(MENU_KEY_BACK); menu_render(); // 每次循环重绘(可加帧率限制) HAL_Delay(50); } }

5.2 与 FreeRTOS 协同工作模式

在 RTOS 环境下,推荐将菜单逻辑封装为独立任务,避免阻塞其他任务:

void menu_task(void *pvParameters) { menu_init(&menu_items[0]); for(;;) { // 从队列获取按键事件(由按键任务或中断发送) menu_key_t key; if (xQueueReceive(key_queue, &key, portMAX_DELAY) == pdTRUE) { menu_process_key(key); } // 定期重绘(避免频繁刷新 LCD) vTaskDelay(pdMS_TO_TICKS(100)); menu_render(); } } // 创建任务 xTaskCreate(menu_task, "MENU", configMINIMAL_STACK_SIZE * 3, NULL, tskIDLE_PRIORITY + 2, NULL);

此时menu_on_item_selected回调中可安全调用xQueueSend()向其他任务发送指令,实现模块化设计。

6. 性能与资源占用分析

在 STM32F030F4P6(16MHz,16KB Flash,4KB RAM)平台上实测数据:

模块占用(字节)说明
.text(代码)1,842含所有菜单逻辑与状态机
.rodata(只读数据)320菜单项数组 + 字符串常量
.bss(RAM)48menu_state_t+ 静态变量
总计2,210Flash,48 RAM

对比同类方案:

  • LVGL 最小配置(仅 Core):Flash ≥ 28KB,RAM ≥ 4KB;
  • 自研简易菜单(动态内存):Flash ~3KB,但 RAM 波动达 1.2KB(malloc碎片);
  • simple_raw_menu以确定性代价换取极致精简,适合对 BOM 成本敏感的量产项目。

7. 常见问题与调试技巧

7.1 菜单无法响应按键

检查顺序:

  1. 确认menu_process_key()被正确调用(添加 LED 闪烁调试);
  2. 验证按键电平逻辑(MENU_KEY_*定义是否与硬件一致);
  3. 检查menu_items[]child_indices数组是否越界(如menu_main_items[2]引用索引 5,但menu_items仅定义到索引 4);
  4. 使用menu_state_t全局变量在调试器中观察current_menucurrent_index是否异常。

7.2 文字显示乱码或错位

根源必为字体适配问题:

  • 确认font_twidth/height与点阵数据实际尺寸一致;
  • 检查lcd_draw_stringchar_x增量是否包含font->spacing
  • OLED 屏幕需注意坐标系原点(SSD1306 为左上角,ST7735 可能为左下角)。

7.3 多级菜单返回异常

典型原因是history_stack溢出或history_depth未正确维护。强制在menu_process_key(MENU_KEY_BACK)开头添加断言:

assert(state.history_depth > 0 && "BACK pressed on root menu!");

并在开发阶段启用MENU_DEBUG宏,输出每步状态到 UART,快速定位栈操作错误。

该库已在多个工业客户项目中稳定运行超 3 年,最长连续运行时间达 17,000 小时无重启。其生命力源于对嵌入式本质的坚守:用最朴素的数据结构,解决最实际的交互问题。

http://www.jsqmd.com/news/546773/

相关文章:

  • 2026生产进度管理系统精选推荐:自动化产线、数字工厂与车间设备数据采集方案解析
  • Django REST framework的应用场景
  • FMQL系列SOC的PS侧UART功能使用说明2
  • 咱们今天来唠唠机器人轨迹规划那点事儿。不少小伙伴在玩机械臂的时候总会遇到关节空间和笛卡尔空间轨迹规划的抉择困难症,这俩货到底有什么区别?直接上硬核代码
  • 复合餐饮定制融合型番茄火锅底料推荐指南:调味料品牌推荐/钵钵鸡调料/餐调味料/黄焖鸡调料/中餐底料/串串香火锅底料/选择指南 - 优质品牌商家
  • 嵌入式轻量级3D数学库mmath:面向MCU的定点/浮点向量矩阵运算
  • 【PolarCTF2026年春季挑战赛】sql_search
  • 软件测试学习第一期
  • OpenClaw轻量部署:Qwen3-VL:30B-4bit量化版飞书助手搭建
  • Matlab处理tdms数据踩坑实录:从‘无法识别’到完美绘图的5个关键步骤
  • 2026招生财务教务一体化平台品牌推荐榜:校园一站式管理平台/校园大数据分析平台/职业院校 一体化管理平台/选择指南 - 优质品牌商家
  • STM32负载平衡监控系统设计与实现
  • STM32激光充电系统设计与实现
  • 薛定谔的交付:既上线又未上线的功能模块
  • 5步实现Switch控制器PC全功能适配:从连接到精通的设备适配指南
  • ssm+java2026年毕设司库管理系统【源码+论文】
  • 【docker】WSL2+docker_desktop+GPU环境配置避坑指南
  • 告别加班!3个Word神技巧,文档处理快人一步
  • 多项式朴素贝叶斯
  • 「理性认知」和「本能恐惧」在打架
  • AT89C52单片机驱动共阴数码管实现方法
  • Ark-Pets的模型资源管理革新:从下载困境到智能分发的实践之路
  • STM32智能水产养殖监控系统设计与实现
  • RTX4090D显存优化:OpenClaw+Qwen3-32B-Chat批量处理千页PDF
  • ssm+java2026年毕设私教预约系统【源码+论文】
  • 终极AI角色扮演指南:5分钟搭建你的专属虚拟伙伴
  • MySQL核心知识点整合(数据库操作+数据引擎+B+树索引+数据类型)
  • TMSpeech终极指南:5分钟掌握Windows离线语音识别与实时字幕生成
  • 抖音视频高效批量处理与智能管理工具实战指南
  • 【深度学习 | 论文精读】从“子空间拆解”到“社交图谱”:多模态情感分析:MISA