1555 lines
43 KiB
Markdown
1555 lines
43 KiB
Markdown
你需要基于指定的目录结构,实现一个符合**工业级嵌入式组件标准**的通用菜单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. **鲁棒性保障**:加入断言、边界检查、错误码返回、事件超时处理,确保组件在工业环境下稳定运行。
|
||
|
||
该菜单组件完全符合工业级嵌入式组件的标准,可直接应用于各类嵌入式设备(如工业控制器、智能家居、仪器仪表),并支持灵活的功能扩展和硬件适配。 |