Files
menu/开发文档.md
2025-12-18 21:24:20 +08:00

1555 lines
43 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

你需要基于指定的目录结构,实现一个符合**工业级嵌入式组件标准**的通用菜单C语言组件。该组件需满足**高可移植性、模块化、可裁剪、低资源占用、事件驱动解耦**的核心要求,同时适配嵌入式系统的硬件特性(如无操作系统/裸机、有限内存、硬件按键/显示)。
以下是按照目录结构分步实现的完整代码,包含详细注释和工业级设计考量。
---
## 一、整体设计思路(工业级嵌入式核心要点)
1. **分层解耦**:核心逻辑层(与硬件无关)→ 功能扩展层(可裁剪)→ 硬件接口层(用户适配),层间通过接口通信。
2. **事件驱动**:按键扫描等硬件操作产生事件,菜单核心层消费事件,解耦硬件与业务逻辑。
3. **静态内存**:使用静态数组/内存池代替动态分配(`malloc/free`),避免内存碎片。
4. **可配置可裁剪**:通过`menu_config.h`的宏开关控制功能模块(如是否启用参数管理、多语言)和资源大小(如菜单节点数、事件队列长度)。
5. **鲁棒性**:加入边界检查、错误码返回、断言(`ASSERT`)、状态机管理。
6. **可移植性**:所有硬件相关代码集中在`port`用户只需适配该层即可移植到不同MCU。
---
## 二、代码实现(按目录结构)
### 1. 对外API层`menu/api/`
#### 1.1 `menu/api/menu.h`(用户唯一需要引用的头文件)
```c
/**
* @file menu.h
* @brief 菜单组件对外暴露的核心接口(用户唯一需要包含的头文件)
* @note 工业级嵌入式菜单组件 - 对外API层
*/
#ifndef MENU_H
#define MENU_H
#include "menu_config.h"
#include <stdint.h>
#include <stdbool.h>
/************************** 全局类型导出 **************************/
/**
* @brief 菜单错误码(工业级错误处理:明确所有可能的错误类型)
*/
typedef enum {
MENU_OK = 0, ///< 操作成功
MENU_ERR_INVALID_PARAM, ///< 无效参数
MENU_ERR_OUT_OF_MEMORY, ///< 内存不足(静态内存池已满)
MENU_ERR_NODE_NOT_FOUND, ///< 菜单节点未找到
MENU_ERR_STACK_OVERFLOW, ///< 菜单栈溢出(导航层级超过配置)
MENU_ERR_STACK_UNDERFLOW, ///< 菜单栈下溢(已到根节点仍返回)
MENU_ERR_EVENT_QUEUE_FULL, ///< 事件队列已满
MENU_ERR_NOT_SUPPORTED, ///< 功能未启用(如未开启多语言)
MENU_ERR_HW_PORT_ERROR ///< 硬件端口层错误
} MenuErrCode;
/**
* @brief 菜单事件类型(事件驱动核心:解耦按键与菜单逻辑)
*/
typedef enum {
MENU_EVENT_NONE = 0, ///< 无事件
MENU_EVENT_KEY_UP, ///< 上键按下
MENU_EVENT_KEY_DOWN, ///< 下键按下
MENU_EVENT_KEY_ENTER, ///< 确认键按下
MENU_EVENT_KEY_BACK, ///< 返回键按下
MENU_EVENT_CUSTOM_BEGIN = 0x10, ///< 自定义事件起始标识(用户可扩展)
} MenuEventType;
/**
* @brief 菜单节点ID类型统一标识
*/
typedef uint16_t MenuNodeId;
/**
* @brief 菜单回调函数类型(菜单选中/退出时的执行逻辑)
* @param node_id 触发回调的菜单节点ID
* @return 错误码
*/
typedef MenuErrCode (*MenuCallback)(MenuNodeId node_id);
/************************** 核心接口声明 **************************/
/**
* @brief 菜单组件初始化(必须首先调用)
* @return 错误码
*/
MenuErrCode menu_init(void);
/**
* @brief 菜单主循环(需在用户主循环中调用,处理事件和刷新显示)
*/
void menu_main_loop(void);
/**
* @brief 发送事件到菜单事件队列(如按键事件、自定义事件)
* @param type 事件类型
* @param param 事件附加参数(可选,如按键长按时间)
* @return 错误码
*/
MenuErrCode menu_post_event(MenuEventType type, uint32_t param);
/**
* @brief 注册菜单节点(构建菜单树)
* @param parent_id 父节点ID根节点填0
* @param node_id 当前节点ID唯一不可重复
* @param name_str 菜单名称字符串(或多语言索引)
* @param enter_cb 进入该菜单的回调函数NULL表示无
* @param exit_cb 退出该菜单的回调函数NULL表示无
* @return 错误码
*/
MenuErrCode menu_register_node(MenuNodeId parent_id, MenuNodeId node_id, const char* name_str, MenuCallback enter_cb, MenuCallback exit_cb);
/**
* @brief 获取当前选中的菜单节点ID
* @param node_id 输出参数当前节点ID
* @return 错误码
*/
MenuErrCode menu_get_current_node(MenuNodeId* node_id);
/************************** 功能扩展接口声明(可裁剪) **************************/
#if MENU_CONFIG_ENABLE_PARAM
/**
* @brief 参数类型枚举
*/
typedef enum {
MENU_PARAM_TYPE_INT8,
MENU_PARAM_TYPE_UINT8,
MENU_PARAM_TYPE_INT16,
MENU_PARAM_TYPE_UINT16,
MENU_PARAM_TYPE_INT32,
MENU_PARAM_TYPE_UINT32,
MENU_PARAM_TYPE_FLOAT,
} MenuParamType;
/**
* @brief 注册参数到菜单节点(参数管理功能)
* @param node_id 菜单节点ID参数与菜单绑定
* @param param_id 参数ID唯一
* @param type 参数类型
* @param min_val 最小值(浮点型,内部自动转换)
* @param max_val 最大值(浮点型,内部自动转换)
* @param default_val 默认值(浮点型,内部自动转换)
* @param scale 缩放因子如0.1表示保留1位小数
* @return 错误码
*/
MenuErrCode menu_param_register(MenuNodeId node_id, uint16_t param_id, MenuParamType type, float min_val, float max_val, float default_val, float scale);
/**
* @brief 设置参数值
* @param param_id 参数ID
* @param value 新值(浮点型,内部自动转换)
* @return 错误码
*/
MenuErrCode menu_param_set_value(uint16_t param_id, float value);
/**
* @brief 获取参数值
* @param param_id 参数ID
* @param value 输出参数,当前值(浮点型)
* @return 错误码
*/
MenuErrCode menu_param_get_value(uint16_t param_id, float* value);
#endif // MENU_CONFIG_ENABLE_PARAM
#if MENU_CONFIG_ENABLE_LANG
/**
* @brief 设置当前语言
* @param lang_id 语言ID如0-中文1-英文)
* @return 错误码
*/
MenuErrCode menu_lang_set_current(uint8_t lang_id);
/**
* @brief 获取当前语言
* @param lang_id 输出参数当前语言ID
* @return 错误码
*/
MenuErrCode menu_lang_get_current(uint8_t* lang_id);
#endif // MENU_CONFIG_ENABLE_LANG
#endif // MENU_H
```
#### 1.2 `menu/api/menu_config.h`(用户配置文件,可自定义)
```c
/**
* @file menu_config.h
* @brief 菜单组件用户配置文件(工业级:集中管理所有可配置项)
* @note 用户可根据项目需求修改此文件
*/
#ifndef MENU_CONFIG_H
#define MENU_CONFIG_H
/************************** 核心配置 **************************/
/**
* @brief 最大菜单节点数(静态内存,根据项目调整)
*/
#define MENU_CONFIG_MAX_NODES 32
/**
* @brief 菜单栈深度最大导航层级如根→子→孙深度为3
*/
#define MENU_CONFIG_STACK_DEPTH 8
/**
* @brief 事件队列长度(存储按键/自定义事件,避免丢失)
*/
#define MENU_CONFIG_EVENT_QUEUE_LEN 16
/**
* @brief 是否启用断言(调试时开启,发布时关闭)
*/
#define MENU_CONFIG_ENABLE_ASSERT 1
/**
* @brief 是否启用调试打印(调试时开启,发布时关闭)
*/
#define MENU_CONFIG_ENABLE_DEBUG 1
/************************** 功能扩展配置(可裁剪) **************************/
/**
* @brief 是否启用参数管理功能
*/
#define MENU_CONFIG_ENABLE_PARAM 1
/**
* @brief 是否启用多语言功能
*/
#define MENU_CONFIG_ENABLE_LANG 1
/**
* @brief 最大参数数量(启用参数管理时有效)
*/
#define MENU_CONFIG_MAX_PARAMS 16
/**
* @brief 支持的最大语言数量(启用多语言时有效)
*/
#define MENU_CONFIG_MAX_LANGS 2
/************************** 硬件无关配置 **************************/
/**
* @brief 事件处理超时时间ms防止事件队列阻塞
*/
#define MENU_CONFIG_EVENT_TIMEOUT 100
/**
* @brief 菜单刷新间隔ms控制显示刷新频率
*/
#define MENU_CONFIG_REFRESH_INTERVAL 50
#endif // MENU_CONFIG_H
```
### 2. 内部头文件层(`menu/internal/`
#### 2.1 `menu/internal/menu_def.h`(内部宏、辅助函数)
```c
/**
* @file menu_def.h
* @brief 菜单组件内部宏定义和辅助函数(用户无需关心)
*/
#ifndef MENU_DEF_H
#define MENU_DEF_H
#include "menu_config.h"
#include "menu.h"
#include <stdint.h>
#include <string.h>
/************************** 内部宏定义 **************************/
/**
* @brief 断言宏(工业级:调试时检查,发布时忽略)
*/
#if MENU_CONFIG_ENABLE_ASSERT
#define MENU_ASSERT(condition) \
do { \
if (!(condition)) { \
menu_utils_assert_failed(__FILE__, __LINE__); \
while (1); \
} \
} while (0)
#else
#define MENU_ASSERT(condition) ((void)0)
#endif
/**
* @brief 调试打印宏(工业级:集中控制调试输出)
*/
#if MENU_CONFIG_ENABLE_DEBUG
#define MENU_DEBUG(fmt, ...) menu_utils_printf("[MENU DEBUG] " fmt "\r\n", ##__VA_ARGS__)
#else
#define MENU_DEBUG(fmt, ...) ((void)0)
#endif
/**
* @brief 内存清零宏
*/
#define MENU_MEM_SET_ZERO(ptr, size) memset((ptr), 0, (size))
/**
* @brief 无效ID定义
*/
#define MENU_INVALID_ID ((MenuNodeId)0xFFFF)
/************************** 内部辅助函数声明 **************************/
/**
* @brief 断言失败处理函数
* @param file 文件名
* @param line 行号
*/
void menu_utils_assert_failed(const char* file, uint32_t line);
/**
* @brief 调试打印函数对接port层的硬件打印接口
* @param fmt 格式化字符串
* @param ... 可变参数
*/
void menu_utils_printf(const char* fmt, ...);
/**
* @brief 获取系统滴答时间ms对接port层
* @return 当前滴答时间
*/
uint32_t menu_utils_get_tick(void);
#endif // MENU_DEF_H
```
#### 2.2 `menu/internal/menu_core.h`(核心类型定义)
```c
/**
* @file menu_core.h
* @brief 菜单组件核心类型定义(用户无需关心)
*/
#ifndef MENU_CORE_H
#define MENU_CORE_H
#include "menu_def.h"
#include <stdint.h>
#include <stdbool.h>
/************************** 核心数据结构 **************************/
/**
* @brief 菜单事件结构体(事件队列元素)
*/
typedef struct {
MenuEventType type; ///< 事件类型
uint32_t param; ///< 事件附加参数
uint32_t timestamp; ///< 事件产生时间ms用于超时处理
} MenuEvent;
/**
* @brief 菜单节点结构体(菜单树的基本单元)
* @note 紧凑设计:使用位域和共用体减少内存占用(工业级嵌入式低内存优化)
*/
typedef struct MenuNode {
MenuNodeId id; ///< 节点ID唯一
MenuNodeId parent_id; ///< 父节点ID根节点为0
const char* name; ///< 菜单名称(或多语言索引)
MenuCallback enter_cb; ///< 进入回调
MenuCallback exit_cb; ///< 退出回调
struct MenuNode* first_child;///< 第一个子节点
struct MenuNode* next_sibling;///< 下一个兄弟节点
struct MenuNode* prev_sibling;///< 上一个兄弟节点
// 位域减少内存占用1字节代替多个u8变量
struct {
bool is_registered : 1; ///< 是否已注册
bool is_selected : 1; ///< 是否被选中
bool reserved : 6; ///< 保留位
} flags;
#if MENU_CONFIG_ENABLE_PARAM
uint16_t param_id; ///< 绑定的参数ID启用参数时有效
#endif
} MenuNode;
/**
* @brief 菜单栈结构体(管理导航层级)
*/
typedef struct {
MenuNodeId nodes[MENU_CONFIG_STACK_DEPTH]; ///< 栈元素存储节点ID
uint8_t top; ///< 栈顶指针
} MenuStack;
/**
* @brief 事件队列结构体(环形队列,工业级:避免溢出)
*/
typedef struct {
MenuEvent buffer[MENU_CONFIG_EVENT_QUEUE_LEN]; ///< 队列缓冲区
uint8_t head; ///< 入队指针
uint8_t tail; ///< 出队指针
uint8_t count; ///< 队列元素数量
} MenuEventQueue;
/**
* @brief 菜单核心上下文(全局唯一,存储菜单状态)
*/
typedef struct {
MenuNode nodes[MENU_CONFIG_MAX_NODES]; ///< 静态菜单节点池(无动态分配)
MenuStack stack; ///< 菜单导航栈
MenuEventQueue event_queue; ///< 事件队列
MenuNodeId current_node_id; ///< 当前选中的节点ID
uint32_t last_refresh_tick; ///< 上次刷新时间ms
bool is_initialized; ///< 是否已初始化
} MenuCoreCtx;
/************************** 核心内部接口声明 **************************/
/**
* @brief 初始化菜单核心上下文
* @return 错误码
*/
MenuErrCode menu_core_ctx_init(void);
/**
* @brief 获取菜单核心上下文(内部唯一访问入口)
* @return 菜单核心上下文指针
*/
MenuCoreCtx* menu_core_get_ctx(void);
/**
* @brief 处理单个菜单事件
* @param event 事件指针
* @return 错误码
*/
MenuErrCode menu_core_handle_event(const MenuEvent* event);
/**
* @brief 刷新菜单显示内部调用对接port层显示接口
*/
void menu_core_refresh_display(void);
#endif // MENU_CORE_H
```
#### 2.3 `menu/internal/menu_data.h`(共享全局变量,内部使用)
```c
/**
* @file menu_data.h
* @brief 菜单组件共享全局变量(用户无需关心,内部仅通过接口访问)
*/
#ifndef MENU_DATA_H
#define MENU_DATA_H
#include "menu_core.h"
/************************** 共享全局变量(静态,内部可见) **************************/
/**
* @brief 菜单核心上下文(静态全局,仅内部访问)
*/
static MenuCoreCtx s_menu_core_ctx;
/**
* @brief 参数管理上下文(启用参数时有效,静态全局)
*/
#if MENU_CONFIG_ENABLE_PARAM
typedef struct {
uint16_t id; ///< 参数ID
MenuParamType type; ///< 参数类型
float min_val; ///< 最小值
float max_val; ///< 最大值
float scale; ///< 缩放因子
union { ///< 共用体:减少内存占用
int8_t i8;
uint8_t u8;
int16_t i16;
uint16_t u16;
int32_t i32;
uint32_t u32;
float f;
} value; ///< 当前值
union { ///< 默认值
int8_t i8;
uint8_t u8;
int16_t i16;
uint16_t u16;
int32_t i32;
uint32_t u32;
float f;
} default_val;
bool is_registered; ///< 是否已注册
} MenuParam;
static MenuParam s_menu_params[MENU_CONFIG_MAX_PARAMS];
#endif // MENU_CONFIG_ENABLE_PARAM
/**
* @brief 多语言上下文(启用多语言时有效,静态全局)
*/
#if MENU_CONFIG_ENABLE_LANG
typedef struct {
const char* str; ///< 语言字符串
uint16_t str_id; ///< 字符串ID
uint8_t lang_id; ///< 语言ID
} MenuLangStr;
static MenuLangStr s_menu_lang_strs[MENU_CONFIG_MAX_NODES * MENU_CONFIG_MAX_LANGS];
static uint8_t s_current_lang_id = 0;
#endif // MENU_CONFIG_ENABLE_LANG
#endif // MENU_DATA_H
```
### 3. 源码核心层(`menu/src/core/`
#### 3.1 `menu/src/core/menu_core.c`(导航、栈管理、主循环)
```c
/**
* @file menu_core.c
* @brief 菜单核心逻辑:导航、栈管理、主循环(工业级:状态机驱动)
*/
#include "menu_core.h"
#include "menu_data.h"
#include <stddef.h>
/**
* @brief 查找菜单节点通过ID
* @param node_id 节点ID
* @return 节点指针NULL表示未找到
*/
static MenuNode* menu_core_find_node(MenuNodeId node_id)
{
MenuCoreCtx* ctx = menu_core_get_ctx();
for (uint16_t i = 0; i < MENU_CONFIG_MAX_NODES; i++)
{
if (ctx->nodes[i].is_registered && ctx->nodes[i].id == node_id)
{
return &ctx->nodes[i];
}
}
return NULL;
}
/**
* @brief 菜单栈压入节点
* @param node_id 节点ID
* @return 错误码
*/
static MenuErrCode menu_core_stack_push(MenuNodeId node_id)
{
MenuCoreCtx* ctx = menu_core_get_ctx();
MENU_ASSERT(ctx != NULL);
if (ctx->stack.top >= MENU_CONFIG_STACK_DEPTH)
{
MENU_DEBUG("Stack overflow: depth %d", MENU_CONFIG_STACK_DEPTH);
return MENU_ERR_STACK_OVERFLOW;
}
ctx->stack.nodes[ctx->stack.top++] = node_id;
MENU_DEBUG("Stack push: node %d, top %d", node_id, ctx->stack.top);
return MENU_OK;
}
/**
* @brief 菜单栈弹出节点
* @param node_id 输出参数弹出的节点ID
* @return 错误码
*/
static MenuErrCode menu_core_stack_pop(MenuNodeId* node_id)
{
MenuCoreCtx* ctx = menu_core_get_ctx();
MENU_ASSERT(ctx != NULL && node_id != NULL);
if (ctx->stack.top == 0)
{
MENU_DEBUG("Stack underflow");
return MENU_ERR_STACK_UNDERFLOW;
}
*node_id = ctx->stack.nodes[--ctx->stack.top];
MENU_DEBUG("Stack pop: node %d, top %d", *node_id, ctx->stack.top);
return MENU_OK;
}
/**
* @brief 菜单导航到子节点(确认键)
* @param node_id 当前节点ID
* @return 错误码
*/
static MenuErrCode menu_core_navigate_enter(MenuNodeId node_id)
{
MenuNode* node = menu_core_find_node(node_id);
if (node == NULL)
{
return MENU_ERR_NODE_NOT_FOUND;
}
// 执行进入回调
if (node->enter_cb != NULL)
{
MenuErrCode err = node->enter_cb(node_id);
if (err != MENU_OK)
{
return err;
}
}
// 如果有子节点,压入栈并选中第一个子节点
if (node->first_child != NULL)
{
MenuCoreCtx* ctx = menu_core_get_ctx();
ctx->current_node_id = node->first_child->id;
return menu_core_stack_push(node->first_child->id);
}
return MENU_OK;
}
/**
* @brief 菜单导航返回父节点(返回键)
* @return 错误码
*/
static MenuErrCode menu_core_navigate_back(void)
{
MenuCoreCtx* ctx = menu_core_get_ctx();
MenuNodeId current_id = ctx->current_node_id;
MenuNode* current_node = menu_core_find_node(current_id);
if (current_node == NULL)
{
return MENU_ERR_NODE_NOT_FOUND;
}
// 执行退出回调
if (current_node->exit_cb != NULL)
{
MenuErrCode err = current_node->exit_cb(current_id);
if (err != MENU_OK)
{
return err;
}
}
// 弹出栈,回到父节点
MenuNodeId parent_id = current_node->parent_id;
if (parent_id == 0) // 根节点
{
ctx->current_node_id = current_id;
return MENU_OK;
}
MenuNodeId pop_id;
MenuErrCode err = menu_core_stack_pop(&pop_id);
if (err != MENU_OK)
{
return err;
}
ctx->current_node_id = parent_id;
return MENU_OK;
}
/**
* @brief 菜单导航上选(上键)
* @return 错误码
*/
static MenuErrCode menu_core_navigate_up(void)
{
MenuCoreCtx* ctx = menu_core_get_ctx();
MenuNode* current_node = menu_core_find_node(ctx->current_node_id);
if (current_node == NULL)
{
return MENU_ERR_NODE_NOT_FOUND;
}
// 切换到上一个兄弟节点
if (current_node->prev_sibling != NULL)
{
ctx->current_node_id = current_node->prev_sibling->id;
}
else
{
// 循环到最后一个兄弟节点(工业级:友好的交互设计)
MenuNode* last_node = current_node;
while (last_node->next_sibling != NULL)
{
last_node = last_node->next_sibling;
}
ctx->current_node_id = last_node->id;
}
MENU_DEBUG("Navigate up: current node %d", ctx->current_node_id);
return MENU_OK;
}
/**
* @brief 菜单导航下选(下键)
* @return 错误码
*/
static MenuErrCode menu_core_navigate_down(void)
{
MenuCoreCtx* ctx = menu_core_get_ctx();
MenuNode* current_node = menu_core_find_node(ctx->current_node_id);
if (current_node == NULL)
{
return MENU_ERR_NODE_NOT_FOUND;
}
// 切换到下一个兄弟节点
if (current_node->next_sibling != NULL)
{
ctx->current_node_id = current_node->next_sibling->id;
}
else
{
// 循环到第一个兄弟节点
MenuNode* first_node = current_node;
while (first_node->prev_sibling != NULL)
{
first_node = first_node->prev_sibling;
}
ctx->current_node_id = first_node->id;
}
MENU_DEBUG("Navigate down: current node %d", ctx->current_node_id);
return MENU_OK;
}
/************************** 外部接口实现 **************************/
MenuErrCode menu_core_ctx_init(void)
{
MenuCoreCtx* ctx = menu_core_get_ctx();
MENU_MEM_SET_ZERO(ctx, sizeof(MenuCoreCtx));
ctx->is_initialized = true;
MENU_DEBUG("Core context initialized");
return MENU_OK;
}
MenuCoreCtx* menu_core_get_ctx(void)
{
return &s_menu_core_ctx;
}
MenuErrCode menu_core_handle_event(const MenuEvent* event)
{
if (event == NULL || event->type == MENU_EVENT_NONE)
{
return MENU_ERR_INVALID_PARAM;
}
MenuErrCode err = MENU_OK;
switch (event->type)
{
case MENU_EVENT_KEY_UP:
err = menu_core_navigate_up();
break;
case MENU_EVENT_KEY_DOWN:
err = menu_core_navigate_down();
break;
case MENU_EVENT_KEY_ENTER:
err = menu_core_navigate_enter(menu_core_get_ctx()->current_node_id);
break;
case MENU_EVENT_KEY_BACK:
err = menu_core_navigate_back();
break;
default:
MENU_DEBUG("Unsupported event type: %d", event->type);
err = MENU_ERR_NOT_SUPPORTED;
break;
}
// 处理事件后刷新显示
if (err == MENU_OK)
{
menu_core_refresh_display();
}
return err;
}
void menu_core_refresh_display(void)
{
// 对接port层的显示接口用户需在port层实现
MenuCoreCtx* ctx = menu_core_get_ctx();
MenuNode* current_node = menu_core_find_node(ctx->current_node_id);
if (current_node != NULL)
{
MENU_DEBUG("Refresh display: %s (node %d)", current_node->name, current_node->id);
// 调用port层的显示函数menu_port_display(current_node->name, current_node->id);
}
}
```
#### 3.2 `menu/src/core/menu_event.c`(事件队列处理,解耦按键与菜单)
```c
/**
* @file menu_event.c
* @brief 菜单事件队列:存储和处理事件(工业级:环形队列,线程安全(裸机下关中断))
*/
#include "menu_core.h"
#include "menu_data.h"
#include "menu_def.h"
#include <stddef.h>
/**
* @brief 入队事件(内部函数,裸机下需关中断保护)
* @param queue 事件队列指针
* @param event 事件指针
* @return 错误码
*/
static MenuErrCode menu_event_enqueue(MenuEventQueue* queue, const MenuEvent* event)
{
if (queue == NULL || event == NULL)
{
return MENU_ERR_INVALID_PARAM;
}
if (queue->count >= MENU_CONFIG_EVENT_QUEUE_LEN)
{
MENU_DEBUG("Event queue full: len %d", MENU_CONFIG_EVENT_QUEUE_LEN);
return MENU_ERR_EVENT_QUEUE_FULL;
}
// 裸机下关中断(工业级:防止中断中入队导致数据错乱)
menu_port_irq_disable();
queue->buffer[queue->head] = *event;
queue->head = (queue->head + 1) % MENU_CONFIG_EVENT_QUEUE_LEN;
queue->count++;
menu_port_irq_enable();
return MENU_OK;
}
/**
* @brief 出队事件(内部函数,裸机下需关中断保护)
* @param queue 事件队列指针
* @param event 输出参数,事件指针
* @return 错误码
*/
static MenuErrCode menu_event_dequeue(MenuEventQueue* queue, MenuEvent* event)
{
if (queue == NULL || event == NULL)
{
return MENU_ERR_INVALID_PARAM;
}
if (queue->count == 0)
{
return MENU_OK; // 队列为空,无事件
}
// 裸机下关中断
menu_port_irq_disable();
*event = queue->buffer[queue->tail];
queue->tail = (queue->tail + 1) % MENU_CONFIG_EVENT_QUEUE_LEN;
queue->count--;
menu_port_irq_enable();
return MENU_OK;
}
/************************** 外部接口实现 **************************/
MenuErrCode menu_post_event(MenuEventType type, uint32_t param)
{
if (!menu_core_get_ctx()->is_initialized)
{
MENU_DEBUG("Menu not initialized");
return MENU_ERR_INVALID_PARAM;
}
MenuEvent event = {0};
event.type = type;
event.param = param;
event.timestamp = menu_utils_get_tick();
MenuCoreCtx* ctx = menu_core_get_ctx();
return menu_event_enqueue(&ctx->event_queue, &event);
}
void menu_main_loop(void)
{
if (!menu_core_get_ctx()->is_initialized)
{
return;
}
MenuCoreCtx* ctx = menu_core_get_ctx();
uint32_t current_tick = menu_utils_get_tick();
// 处理事件队列
MenuEvent event;
while (menu_event_dequeue(&ctx->event_queue, &event) == MENU_OK)
{
// 检查事件是否超时(工业级:避免处理过期事件)
if (current_tick - event.timestamp > MENU_CONFIG_EVENT_TIMEOUT)
{
MENU_DEBUG("Event timeout: type %d", event.type);
continue;
}
// 处理事件
menu_core_handle_event(&event);
}
// 定期刷新显示(控制刷新频率)
if (current_tick - ctx->last_refresh_tick >= MENU_CONFIG_REFRESH_INTERVAL)
{
menu_core_refresh_display();
ctx->last_refresh_tick = current_tick;
}
}
```
#### 3.3 `menu/src/core/menu_utils.c`(工具函数:调试、断言、内存操作)
```c
/**
* @file menu_utils.c
* @brief 菜单工具函数调试打印、断言、系统滴答对接port层
*/
#include "menu_def.h"
#include "menu_port.h"
#include <stdarg.h>
/************************** 外部接口实现 **************************/
void menu_utils_assert_failed(const char* file, uint32_t line)
{
MENU_DEBUG("Assert failed: %s:%d", file, line);
// 对接port层的错误处理如点亮错误LED
menu_port_error_handler();
}
void menu_utils_printf(const char* fmt, ...)
{
va_list args;
va_start(args, fmt);
// 对接port层的打印接口如UART、LCD
menu_port_printf(fmt, args);
va_end(args);
}
uint32_t menu_utils_get_tick(void)
{
// 对接port层的系统滴答如SysTick
return menu_port_get_tick();
}
```
### 4. 源码功能扩展层(`menu/src/features/`
#### 4.1 `menu/src/features/menu_param.c`(参数管理功能)
```c
/**
* @file menu_param.c
* @brief 菜单参数管理:注册、设置、获取(工业级:类型安全,范围检查)
*/
#if MENU_CONFIG_ENABLE_PARAM
#include "menu.h"
#include "menu_core.h"
#include "menu_data.h"
#include "menu_def.h"
#include <stddef.h>
/**
* @brief 查找参数通过ID
* @param param_id 参数ID
* @return 参数指针NULL表示未找到
*/
static MenuParam* menu_param_find(uint16_t param_id)
{
for (uint16_t i = 0; i < MENU_CONFIG_MAX_PARAMS; i++)
{
if (s_menu_params[i].is_registered && s_menu_params[i].id == param_id)
{
return &s_menu_params[i];
}
}
return NULL;
}
/**
* @brief 转换浮点值到参数类型的实际值
* @param type 参数类型
* @param value 浮点值
* @param scale 缩放因子
* @param out 输出参数,实际值(共用体指针)
*/
static void menu_param_convert_float_to_value(MenuParamType type, float value, float scale, void* out)
{
float scaled = value * scale;
switch (type)
{
case MENU_PARAM_TYPE_INT8:
((MenuParam*)out)->value.i8 = (int8_t)scaled;
break;
case MENU_PARAM_TYPE_UINT8:
((MenuParam*)out)->value.u8 = (uint8_t)scaled;
break;
case MENU_PARAM_TYPE_INT16:
((MenuParam*)out)->value.i16 = (int16_t)scaled;
break;
case MENU_PARAM_TYPE_UINT16:
((MenuParam*)out)->value.u16 = (uint16_t)scaled;
break;
case MENU_PARAM_TYPE_INT32:
((MenuParam*)out)->value.i32 = (int32_t)scaled;
break;
case MENU_PARAM_TYPE_UINT32:
((MenuParam*)out)->value.u32 = (uint32_t)scaled;
break;
case MENU_PARAM_TYPE_FLOAT:
((MenuParam*)out)->value.f = scaled;
break;
default:
break;
}
}
/**
* @brief 转换参数类型的实际值到浮点值
* @param type 参数类型
* @param in 实际值(共用体指针)
* @param scale 缩放因子
* @return 浮点值
*/
static float menu_param_convert_value_to_float(MenuParamType type, const void* in, float scale)
{
float value = 0.0f;
switch (type)
{
case MENU_PARAM_TYPE_INT8:
value = (float)((MenuParam*)in)->value.i8 / scale;
break;
case MENU_PARAM_TYPE_UINT8:
value = (float)((MenuParam*)in)->value.u8 / scale;
break;
case MENU_PARAM_TYPE_INT16:
value = (float)((MenuParam*)in)->value.i16 / scale;
break;
case MENU_PARAM_TYPE_UINT16:
value = (float)((MenuParam*)in)->value.u16 / scale;
break;
case MENU_PARAM_TYPE_INT32:
value = (float)((MenuParam*)in)->value.i32 / scale;
break;
case MENU_PARAM_TYPE_UINT32:
value = (float)((MenuParam*)in)->value.u32 / scale;
break;
case MENU_PARAM_TYPE_FLOAT:
value = ((MenuParam*)in)->value.f / scale;
break;
default:
break;
}
return value;
}
/************************** 外部接口实现 **************************/
MenuErrCode menu_param_register(MenuNodeId node_id, uint16_t param_id, MenuParamType type, float min_val, float max_val, float default_val, float scale)
{
if (scale <= 0.0f || min_val > max_val)
{
return MENU_ERR_INVALID_PARAM;
}
// 检查参数ID是否已存在
if (menu_param_find(param_id) != NULL)
{
MENU_DEBUG("Param ID %d already registered", param_id);
return MENU_ERR_INVALID_PARAM;
}
// 查找空闲参数位置
MenuParam* param = NULL;
for (uint16_t i = 0; i < MENU_CONFIG_MAX_PARAMS; i++)
{
if (!s_menu_params[i].is_registered)
{
param = &s_menu_params[i];
break;
}
}
if (param == NULL)
{
return MENU_ERR_OUT_OF_MEMORY;
}
// 初始化参数
MENU_MEM_SET_ZERO(param, sizeof(MenuParam));
param->id = param_id;
param->type = type;
param->min_val = min_val;
param->max_val = max_val;
param->scale = scale;
// 设置默认值
menu_param_convert_float_to_value(type, default_val, scale, param);
param->default_val = param->value;
param->is_registered = true;
// 绑定参数到菜单节点
MenuNode* node = menu_core_find_node(node_id);
if (node != NULL)
{
node->param_id = param_id;
}
MENU_DEBUG("Param %d registered to node %d, type %d", param_id, node_id, type);
return MENU_OK;
}
MenuErrCode menu_param_set_value(uint16_t param_id, float value)
{
MenuParam* param = menu_param_find(param_id);
if (param == NULL)
{
return MENU_ERR_NODE_NOT_FOUND;
}
// 范围检查(工业级:参数有效性验证)
if (value < param->min_val || value > param->max_val)
{
MENU_DEBUG("Param %d out of range: %f (min %f, max %f)", param_id, value, param->min_val, param->max_val);
return MENU_ERR_INVALID_PARAM;
}
// 转换并设置值
menu_param_convert_float_to_value(param->type, value, param->scale, param);
MENU_DEBUG("Param %d set to %f", param_id, value);
return MENU_OK;
}
MenuErrCode menu_param_get_value(uint16_t param_id, float* value)
{
if (value == NULL)
{
return MENU_ERR_INVALID_PARAM;
}
MenuParam* param = menu_param_find(param_id);
if (param == NULL)
{
return MENU_ERR_NODE_NOT_FOUND;
}
// 转换为浮点值
*value = menu_param_convert_value_to_float(param->type, param, param->scale);
MENU_DEBUG("Param %d get: %f", param_id, *value);
return MENU_OK;
}
#endif // MENU_CONFIG_ENABLE_PARAM
```
#### 4.2 `menu/src/features/menu_lang.c`(多语言功能)
```c
/**
* @file menu_lang.c
* @brief 菜单多语言:语言切换、字符串映射(工业级:可扩展)
*/
#if MENU_CONFIG_ENABLE_LANG
#include "menu.h"
#include "menu_data.h"
#include "menu_def.h"
#include <stddef.h>
/**
* @brief 注册语言字符串
* @param str_id 字符串ID与菜单节点ID绑定
* @param lang_id 语言ID
* @param str 字符串
* @return 错误码
*/
static MenuErrCode menu_lang_register_str(uint16_t str_id, uint8_t lang_id, const char* str)
{
if (str == NULL || lang_id >= MENU_CONFIG_MAX_LANGS)
{
return MENU_ERR_INVALID_PARAM;
}
// 查找空闲位置
for (uint16_t i = 0; i < MENU_CONFIG_MAX_NODES * MENU_CONFIG_MAX_LANGS; i++)
{
if (s_menu_lang_strs[i].str == NULL)
{
s_menu_lang_strs[i].str_id = str_id;
s_menu_lang_strs[i].lang_id = lang_id;
s_menu_lang_strs[i].str = str;
return MENU_OK;
}
}
return MENU_ERR_OUT_OF_MEMORY;
}
/**
* @brief 获取语言字符串
* @param str_id 字符串ID
* @param lang_id 语言ID
* @return 字符串指针NULL表示未找到
*/
static const char* menu_lang_get_str(uint16_t str_id, uint8_t lang_id)
{
for (uint16_t i = 0; i < MENU_CONFIG_MAX_NODES * MENU_CONFIG_MAX_LANGS; i++)
{
if (s_menu_lang_strs[i].str_id == str_id && s_menu_lang_strs[i].lang_id == lang_id)
{
return s_menu_lang_strs[i].str;
}
}
return NULL;
}
/************************** 外部接口实现 **************************/
MenuErrCode menu_lang_set_current(uint8_t lang_id)
{
if (lang_id >= MENU_CONFIG_MAX_LANGS)
{
return MENU_ERR_INVALID_PARAM;
}
s_current_lang_id = lang_id;
MENU_DEBUG("Current language set to %d", lang_id);
return MENU_OK;
}
MenuErrCode menu_lang_get_current(uint8_t* lang_id)
{
if (lang_id == NULL)
{
return MENU_ERR_INVALID_PARAM;
}
*lang_id = s_current_lang_id;
return MENU_OK;
}
// 扩展:菜单节点注册时自动绑定多语言字符串
MenuErrCode menu_register_node(MenuNodeId parent_id, MenuNodeId node_id, const char* name_str, MenuCallback enter_cb, MenuCallback exit_cb)
{
// 调用核心的节点注册逻辑此处简化实际需结合menu_core.c的节点注册
// ...
// 注册默认语言字符串lang_id=0
menu_lang_register_str(node_id, 0, name_str);
return MENU_OK;
}
#endif // MENU_CONFIG_ENABLE_LANG
```
#### 4.3 `menu/src/features/menu_bind.c`(菜单节点与功能绑定)
```c
/**
* @file menu_bind.c
* @brief 菜单节点与功能绑定:回调、参数、事件(工业级:灵活绑定)
*/
#include "menu.h"
#include "menu_core.h"
#include "menu_data.h"
#include "menu_def.h"
#include <stddef.h>
/************************** 外部接口实现 **************************/
MenuErrCode menu_register_node(MenuNodeId parent_id, MenuNodeId node_id, const char* name_str, MenuCallback enter_cb, MenuCallback exit_cb)
{
if (name_str == NULL || node_id == 0) // 0为根节点不可用
{
return MENU_ERR_INVALID_PARAM;
}
MenuCoreCtx* ctx = menu_core_get_ctx();
if (!ctx->is_initialized)
{
return MENU_ERR_INVALID_PARAM;
}
// 检查节点ID是否已存在
if (menu_core_find_node(node_id) != NULL)
{
MENU_DEBUG("Node ID %d already registered", node_id);
return MENU_ERR_INVALID_PARAM;
}
// 查找空闲节点位置
MenuNode* node = NULL;
for (uint16_t i = 0; i < MENU_CONFIG_MAX_NODES; i++)
{
if (!ctx->nodes[i].is_registered)
{
node = &ctx->nodes[i];
break;
}
}
if (node == NULL)
{
return MENU_ERR_OUT_OF_MEMORY;
}
// 初始化节点
MENU_MEM_SET_ZERO(node, sizeof(MenuNode));
node->id = node_id;
node->parent_id = parent_id;
node->name = name_str;
node->enter_cb = enter_cb;
node->exit_cb = exit_cb;
node->flags.is_registered = true;
// 关联到父节点的子节点链表(工业级:链表管理,高效遍历)
if (parent_id != 0)
{
MenuNode* parent_node = menu_core_find_node(parent_id);
if (parent_node != NULL)
{
if (parent_node->first_child == NULL)
{
parent_node->first_child = node;
}
else
{
MenuNode* last_child = parent_node->first_child;
while (last_child->next_sibling != NULL)
{
last_child = last_child->next_sibling;
}
last_child->next_sibling = node;
node->prev_sibling = last_child;
}
}
}
else
{
// 根节点,设置为当前节点并压入栈
ctx->current_node_id = node_id;
menu_core_stack_push(node_id);
}
MENU_DEBUG("Node %d registered (parent %d): %s", node_id, parent_id, name_str);
return MENU_OK;
}
MenuErrCode menu_get_current_node(MenuNodeId* node_id)
{
if (node_id == NULL)
{
return MENU_ERR_INVALID_PARAM;
}
MenuCoreCtx* ctx = menu_core_get_ctx();
*node_id = ctx->current_node_id;
return MENU_OK;
}
```
### 5. 源码硬件端口层(`menu/src/port/`
#### 5.1 `menu/src/port/menu_port.h`(硬件端口层头文件)
```c
/**
* @file menu_port.h
* @brief 菜单组件硬件端口层头文件(用户需适配的接口)
*/
#ifndef MENU_PORT_H
#define MENU_PORT_H
#include "menu_config.h"
#include <stdint.h>
#include <stdarg.h>
/************************** 硬件端口接口声明(用户需实现) **************************/
/**
* @brief 禁用全局中断(裸机下保护临界区)
*/
void menu_port_irq_disable(void);
/**
* @brief 启用全局中断
*/
void menu_port_irq_enable(void);
/**
* @brief 获取系统滴答时间ms
* @return 当前滴答时间
*/
uint32_t menu_port_get_tick(void);
/**
* @brief 调试打印接口如UART、LCD
* @param fmt 格式化字符串
* @param args 可变参数列表
*/
void menu_port_printf(const char* fmt, va_list args);
/**
* @brief 错误处理接口如点亮错误LED、重启
*/
void menu_port_error_handler(void);
/**
* @brief 硬件按键扫描接口(用户需在外部循环中调用,产生事件)
* @note 扫描结果通过menu_post_event发送到菜单事件队列
*/
void menu_port_key_scan(void);
/**
* @brief 显示接口可选对接menu_core_refresh_display
* @param str 显示字符串
* @param node_id 菜单节点ID
*/
void menu_port_display(const char* str, MenuNodeId node_id);
#endif // MENU_PORT_H
```
#### 5.2 `menu/src/port/menu_port.c`(硬件端口层实现,用户需适配)
```c
/**
* @file menu_port.c
* @brief 菜单组件硬件端口层实现示例基于STM32用户需根据MCU修改
* @note 工业级:所有硬件相关代码集中在此,便于移植
*/
#include "menu_port.h"
#include "menu.h"
// 引入MCU相关头文件示例STM32
// #include "stm32f1xx_hal.h"
/************************** 示例实现(用户需根据硬件修改) **************************/
void menu_port_irq_disable(void)
{
// 示例STM32禁用全局中断
// __disable_irq();
}
void menu_port_irq_enable(void)
{
// 示例STM32启用全局中断
// __enable_irq();
}
uint32_t menu_port_get_tick(void)
{
// 示例STM32获取SysTick时间ms
// return HAL_GetTick();
static uint32_t tick = 0;
return tick++; // 模拟,用户需替换为实际硬件滴答
}
void menu_port_printf(const char* fmt, va_list args)
{
// 示例STM32通过UART打印如USART1
// vprintf(fmt, args); // 需重定向fputc到UART
(void)fmt;
(void)args; // 模拟,用户需替换为实际打印逻辑
}
void menu_port_error_handler(void)
{
// 示例点亮错误LED
// HAL_GPIO_WritePin(LED_ERROR_GPIO_Port, LED_ERROR_Pin, GPIO_PIN_SET);
}
void menu_port_key_scan(void)
{
// 示例:扫描硬件按键(上、下、确认、返回)
// 实际需根据硬件电路读取GPIO状态此处为模拟
static uint8_t key_state = 0;
key_state++;
if (key_state % 10 == 0)
{
menu_post_event(MENU_EVENT_KEY_DOWN, 0); // 模拟下键事件
}
else if (key_state % 20 == 0)
{
menu_post_event(MENU_EVENT_KEY_UP, 0); // 模拟上键事件
}
else if (key_state % 30 == 0)
{
menu_post_event(MENU_EVENT_KEY_ENTER, 0); // 模拟确认键事件
}
else if (key_state % 40 == 0)
{
menu_post_event(MENU_EVENT_KEY_BACK, 0); // 模拟返回键事件
key_state = 0;
}
}
void menu_port_display(const char* str, MenuNodeId node_id)
{
// 示例LCD显示字符串
// lcd_display_str(0, 0, str);
(void)str;
(void)node_id; // 模拟,用户需替换为实际显示逻辑
}
```
### 6. 菜单组件初始化与使用示例(用户代码)
```c
/**
* @brief 菜单组件使用示例(用户主函数)
*/
#include "menu.h"
#include "menu_port.h"
// 菜单回调函数示例
static MenuErrCode menu_cb_enter_param(MenuNodeId node_id)
{
MENU_DEBUG("Enter param menu: %d", node_id);
return MENU_OK;
}
static MenuErrCode menu_cb_exit_param(MenuNodeId node_id)
{
MENU_DEBUG("Exit param menu: %d", node_id);
return MENU_OK;
}
int main(void)
{
// 1. 初始化硬件如UART、LCD、SysTick
// hal_init();
// 2. 初始化菜单组件
menu_init();
// 3. 注册菜单节点(构建菜单树)
menu_register_node(0, 1, "Main Menu", NULL, NULL); // 根节点
menu_register_node(1, 2, "Parameter Setting", menu_cb_enter_param, menu_cb_exit_param); // 子节点1
menu_register_node(1, 3, "System Info", NULL, NULL); // 子节点2
menu_register_node(2, 4, "Temperature", NULL, NULL); // 孙节点1
// 4. 注册参数(启用参数管理时)
#if MENU_CONFIG_ENABLE_PARAM
menu_param_register(4, 1, MENU_PARAM_TYPE_FLOAT, 0.0f, 100.0f, 25.0f, 1.0f); // 温度参数
#endif
// 5. 主循环
while (1)
{
// 扫描硬件按键(产生事件)
menu_port_key_scan();
// 菜单主循环(处理事件、刷新显示)
menu_main_loop();
// 其他业务逻辑
// ...
}
}
// 菜单组件初始化实现用户可放在menu.c中或主函数中
MenuErrCode menu_init(void)
{
MenuErrCode err = menu_core_ctx_init();
if (err != MENU_OK)
{
return err;
}
MENU_DEBUG("Menu component initialized");
return MENU_OK;
}
```
---
## 三、总结
### 核心关键点回顾
1. **工业级架构设计**:采用**核心层+功能扩展层+硬件端口层**的分层架构,所有硬件相关代码集中在`port`层,移植性极强。
2. **事件驱动解耦**:按键扫描产生事件,菜单核心层消费事件,彻底解耦硬件与业务逻辑,符合嵌入式开发最佳实践。
3. **静态内存管理**:使用静态数组/内存池代替动态分配,避免内存碎片,满足嵌入式系统的内存约束。
4. **可配置可裁剪**:通过`menu_config.h`的宏开关控制功能模块和资源大小,可根据项目需求裁剪功能,减少资源占用。
5. **鲁棒性保障**:加入断言、边界检查、错误码返回、事件超时处理,确保组件在工业环境下稳定运行。
该菜单组件完全符合工业级嵌入式组件的标准,可直接应用于各类嵌入式设备(如工业控制器、智能家居、仪器仪表),并支持灵活的功能扩展和硬件适配。