From 9773cb5a0aa60ed7add5ecf0d33dee85e2a0f19f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=AF=E4=BD=B3?= <13101321+jfen5577@user.noreply.gitee.com> Date: Thu, 18 Dec 2025 21:24:08 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CMakeLists.txt | 52 ++ api/menu.h | 152 ++++ api/menu_config.h | 67 ++ examples/menu_example.c | 304 ++++++++ internal/menu_core.h | 108 +++ internal/menu_data.h | 64 ++ internal/menu_def.h | 69 ++ port/menu_port.c | 98 +++ port/menu_port.h | 50 ++ src/core/menu_core.c | 515 +++++++++++++ src/lang/menu_lang.c | 123 ++++ src/param/menu_param.c | 302 ++++++++ src/utils/menu_utils.c | 77 ++ update.md | 819 +++++++++++++++++++++ 开发文档.md | 1555 +++++++++++++++++++++++++++++++++++++++ 15 files changed, 4355 insertions(+) create mode 100644 CMakeLists.txt create mode 100644 api/menu.h create mode 100644 api/menu_config.h create mode 100644 examples/menu_example.c create mode 100644 internal/menu_core.h create mode 100644 internal/menu_data.h create mode 100644 internal/menu_def.h create mode 100644 port/menu_port.c create mode 100644 port/menu_port.h create mode 100644 src/core/menu_core.c create mode 100644 src/lang/menu_lang.c create mode 100644 src/param/menu_param.c create mode 100644 src/utils/menu_utils.c create mode 100644 update.md create mode 100644 开发文档.md diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..9489030 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,52 @@ +cmake_minimum_required(VERSION 3.10) + +# Project name +project(menu_component C) + +# Set C standard +set(CMAKE_C_STANDARD 99) +set(CMAKE_C_STANDARD_REQUIRED ON) + +# Force using MinGW Makefiles if on Windows +if(WIN32) + if(NOT DEFINED CMAKE_GENERATOR) + set(CMAKE_GENERATOR "MinGW Makefiles" CACHE INTERNAL "") + endif() + # Set default compiler if not specified + if(NOT DEFINED CMAKE_C_COMPILER) + find_program(MINGW_GCC gcc) + if(MINGW_GCC) + set(CMAKE_C_COMPILER ${MINGW_GCC} CACHE INTERNAL "") + endif() + endif() +endif() + +# Compiler flags +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra -Werror -Os") + +# Include directories +include_directories( + api + internal + src/core + src/param + src/lang + src/utils + port +) + +# Source files +set(SOURCES + src/core/menu_core.c + src/param/menu_param.c + src/lang/menu_lang.c + src/utils/menu_utils.c + port/menu_port.c + examples/menu_example.c +) + +# Add example executable +add_executable(menu_example ${SOURCES}) + +# Debug build configuration +set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -g") diff --git a/api/menu.h b/api/menu.h new file mode 100644 index 0000000..2bf2ebc --- /dev/null +++ b/api/menu.h @@ -0,0 +1,152 @@ +/** + * @file menu.h + * @brief 菜单组件对外暴露的核心接口(用户唯一需要包含的头文件) + * @note 工业级嵌入式菜单组件 - 对外API层 + */ +#ifndef MENU_H +#define MENU_H + +#include "menu_config.h" +#include +#include + +/************************** 全局类型导出 **************************/ +/** + * @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 \ No newline at end of file diff --git a/api/menu_config.h b/api/menu_config.h new file mode 100644 index 0000000..f0d73b8 --- /dev/null +++ b/api/menu_config.h @@ -0,0 +1,67 @@ +/** + * @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 \ No newline at end of file diff --git a/examples/menu_example.c b/examples/menu_example.c new file mode 100644 index 0000000..8ef6667 --- /dev/null +++ b/examples/menu_example.c @@ -0,0 +1,304 @@ +/** + * @file menu_example.c + * @brief 菜单组件使用示例 + * @note 工业级嵌入式菜单组件 - 使用示例 + */ +#include "menu.h" +#include "menu_port.h" +#include + +// 条件编译:处理不同平台的键盘输入函数 +#if defined(_WIN32) +#include // Windows平台 +#else +// MinGW或其他平台的模拟实现 +#include +#include +#include + +int _kbhit(void) +{ + struct termios oldt, newt; + int ch; + int oldf; + + tcgetattr(STDIN_FILENO, &oldt); + newt = oldt; + newt.c_lflag &= ~(ICANON | ECHO); + tcsetattr(STDIN_FILENO, TCSANOW, &newt); + oldf = fcntl(STDIN_FILENO, F_GETFL, 0); + fcntl(STDIN_FILENO, F_SETFL, oldf | O_NONBLOCK); + + ch = getchar(); + + tcsetattr(STDIN_FILENO, TCSANOW, &oldt); + fcntl(STDIN_FILENO, F_SETFL, oldf); + + if(ch != EOF) + { + ungetc(ch, stdin); + return 1; + } + + return 0; +} + +int _getch(void) +{ + struct termios oldt, newt; + int ch; + + tcgetattr(STDIN_FILENO, &oldt); + newt = oldt; + newt.c_lflag &= ~(ICANON | ECHO); + tcsetattr(STDIN_FILENO, TCSANOW, &newt); + + ch = getchar(); + + tcsetattr(STDIN_FILENO, TCSANOW, &oldt); + + return ch; +} +#endif + +/************************** 菜单节点ID定义 **************************/ +#define MENU_ID_MAIN 100 ///< 主菜单 +#define MENU_ID_SETTINGS 200 ///< 设置菜单 +#define MENU_ID_DISPLAY 201 ///< 显示设置 +#define MENU_ID_LANGUAGE 202 ///< 语言设置 +#define MENU_ID_PARAMETER 300 ///< 参数设置 +#define MENU_ID_ABOUT 400 ///< 关于 + +/************************** 参数ID定义 **************************/ +#define PARAM_ID_BRIGHTNESS 1000 ///< 亮度参数 +#define PARAM_ID_CONTRAST 1001 ///< 对比度参数 + +/************************** 菜单回调函数 **************************/ +/** + * @brief 主菜单进入回调 + * @param node_id 菜单节点ID + * @return 错误码 + */ +static MenuErrCode menu_cb_main(MenuNodeId node_id) +{ + (void)node_id; // 未使用的参数 + printf("Enter Main Menu\r\n"); + return MENU_OK; +} + +/** + * @brief 设置菜单进入回调 + * @param node_id 菜单节点ID + * @return 错误码 + */ +static MenuErrCode menu_cb_settings(MenuNodeId node_id) +{ + (void)node_id; // 未使用的参数 + printf("Enter Settings Menu\r\n"); + return MENU_OK; +} + +/** + * @brief 显示设置进入回调 + * @param node_id 菜单节点ID + * @return 错误码 + */ +static MenuErrCode menu_cb_display(MenuNodeId node_id) +{ + (void)node_id; // 未使用的参数 + printf("Enter Display Settings\r\n"); + return MENU_OK; +} + +/** + * @brief 语言设置进入回调 + * @param node_id 菜单节点ID + * @return 错误码 + */ +static MenuErrCode menu_cb_language(MenuNodeId node_id) +{ + (void)node_id; // 未使用的参数 + printf("Enter Language Settings\r\n"); + return MENU_OK; +} + +/** + * @brief 参数设置进入回调 + * @param node_id 菜单节点ID + * @return 错误码 + */ +static MenuErrCode menu_cb_parameter(MenuNodeId node_id) +{ + (void)node_id; // 未使用的参数 + printf("Enter Parameter Settings\r\n"); + return MENU_OK; +} + +/** + * @brief 关于菜单进入回调 + * @param node_id 菜单节点ID + * @return 错误码 + */ +static MenuErrCode menu_cb_about(MenuNodeId node_id) +{ + (void)node_id; // 未使用的参数 + printf("Enter About Menu\r\n"); + printf("Industrial Menu Component v1.0\r\n"); + printf("Highly Portable & Modular\r\n"); + return MENU_OK; +} + +/************************** 辅助函数 **************************/ +/** + * @brief 按键扫描函数(Windows平台示例) + * @return 按键事件类型 + */ +static MenuEventType key_scan(void) +{ + if (_kbhit()) + { + int key = _getch(); + switch (key) + { + case 'w': + case 'W': + case 72: // 上箭头 + return MENU_EVENT_KEY_UP; + case 's': + case 'S': + case 80: // 下箭头 + return MENU_EVENT_KEY_DOWN; + case 13: // 回车键 + return MENU_EVENT_KEY_ENTER; + case 'b': + case 'B': + case 27: // ESC键 + return MENU_EVENT_KEY_BACK; + default: + break; + } + } + return MENU_EVENT_NONE; +} + +/************************** 主函数 **************************/ +int main(void) +{ + printf("Industrial Menu Component Example\r\n"); + printf("==================================\r\n"); + printf("Controls:\r\n"); + printf(" W/Up Arrow - Up\r\n"); + printf(" S/Down Arrow - Down\r\n"); + printf(" Enter - Enter\r\n"); + printf(" B/ESC - Back\r\n"); + printf(" Q - Quit\r\n"); + printf("==================================\r\n\r\n"); + + /************************** 初始化菜单组件 **************************/ + MenuErrCode err = menu_init(); + if (err != MENU_OK) + { + printf("Menu initialization failed: %d\r\n", err); + return -1; + } + printf("Menu initialized successfully\r\n\r\n"); + + /************************** 注册菜单节点 **************************/ + // 注册主菜单 + err = menu_register_node(0, MENU_ID_MAIN, "Main Menu", menu_cb_main, NULL); + if (err != MENU_OK) + { + printf("Register Main Menu failed: %d\r\n", err); + return -1; + } + + // 注册设置菜单 + err = menu_register_node(MENU_ID_MAIN, MENU_ID_SETTINGS, "Settings", menu_cb_settings, NULL); + if (err != MENU_OK) + { + printf("Register Settings Menu failed: %d\r\n", err); + return -1; + } + + // 注册显示设置菜单 + err = menu_register_node(MENU_ID_SETTINGS, MENU_ID_DISPLAY, "Display Settings", menu_cb_display, NULL); + if (err != MENU_OK) + { + printf("Register Display Menu failed: %d\r\n", err); + return -1; + } + + // 注册语言设置菜单 + err = menu_register_node(MENU_ID_SETTINGS, MENU_ID_LANGUAGE, "Language", menu_cb_language, NULL); + if (err != MENU_OK) + { + printf("Register Language Menu failed: %d\r\n", err); + return -1; + } + + // 注册参数设置菜单 + err = menu_register_node(MENU_ID_MAIN, MENU_ID_PARAMETER, "Parameters", menu_cb_parameter, NULL); + if (err != MENU_OK) + { + printf("Register Parameter Menu failed: %d\r\n", err); + return -1; + } + + // 注册关于菜单 + err = menu_register_node(MENU_ID_MAIN, MENU_ID_ABOUT, "About", menu_cb_about, NULL); + if (err != MENU_OK) + { + printf("Register About Menu failed: %d\r\n", err); + return -1; + } + + /************************** 注册参数 **************************/ +#if MENU_CONFIG_ENABLE_PARAM + // 注册亮度参数(范围0-100,默认50,步长1) + err = menu_param_register(MENU_ID_DISPLAY, PARAM_ID_BRIGHTNESS, + MENU_PARAM_TYPE_UINT8, 0.0f, 100.0f, 50.0f, 1.0f); + if (err != MENU_OK) + { + printf("Register Brightness Param failed: %d\r\n", err); + return -1; + } + + // 注册对比度参数(范围0-100,默认50,步长1) + err = menu_param_register(MENU_ID_DISPLAY, PARAM_ID_CONTRAST, + MENU_PARAM_TYPE_UINT8, 0.0f, 100.0f, 50.0f, 1.0f); + if (err != MENU_OK) + { + printf("Register Contrast Param failed: %d\r\n", err); + return -1; + } +#endif + + printf("Menu nodes registered successfully\r\n\r\n"); + + /************************** 主循环 **************************/ + while (1) + { + // 扫描按键 + MenuEventType event = key_scan(); + if (event != MENU_EVENT_NONE) + { + // 发送按键事件到菜单组件 + menu_post_event(event, 0); + } + + // 处理菜单主循环 + menu_main_loop(); + + // 检查退出条件 + if (_kbhit() && _getch() == 'q') + { + printf("\r\nQuit program\r\n"); + break; + } + + // 轻微延迟,避免CPU占用过高 + menu_port_delay_ms(10); + } + + return 0; +} \ No newline at end of file diff --git a/internal/menu_core.h b/internal/menu_core.h new file mode 100644 index 0000000..148286b --- /dev/null +++ b/internal/menu_core.h @@ -0,0 +1,108 @@ +/** + * @file menu_core.h + * @brief 菜单组件核心类型定义(用户无需关心) + */ +#ifndef MENU_CORE_H +#define MENU_CORE_H + +#include "menu_def.h" +#include +#include + +/************************** 核心数据结构 **************************/ +/** + * @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; ///< 是否被选中 + unsigned int 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 查找菜单节点(通过ID) + * @param node_id 节点ID + * @return 节点指针(NULL表示未找到) + */ +MenuNode* menu_core_find_node(MenuNodeId node_id); + +/** + * @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 \ No newline at end of file diff --git a/internal/menu_data.h b/internal/menu_data.h new file mode 100644 index 0000000..7cc43df --- /dev/null +++ b/internal/menu_data.h @@ -0,0 +1,64 @@ +/** + * @file menu_data.h + * @brief 菜单组件共享全局变量(用户无需关心,内部仅通过接口访问) + */ +#ifndef MENU_DATA_H +#define MENU_DATA_H + +#include "menu_core.h" + +/************************** 共享全局变量(声明,内部可见) **************************/ +/** + * @brief 菜单核心上下文(全局,仅内部访问) + */ +extern 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; + +extern 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; + +extern MenuLangStr s_menu_lang_strs[MENU_CONFIG_MAX_NODES * MENU_CONFIG_MAX_LANGS]; +extern uint8_t s_current_lang_id; +#endif // MENU_CONFIG_ENABLE_LANG + +#endif // MENU_DATA_H \ No newline at end of file diff --git a/internal/menu_def.h b/internal/menu_def.h new file mode 100644 index 0000000..feddd64 --- /dev/null +++ b/internal/menu_def.h @@ -0,0 +1,69 @@ +/** + * @file menu_def.h + * @brief 菜单组件内部宏定义和辅助函数(用户无需关心) + */ +#ifndef MENU_DEF_H +#define MENU_DEF_H + +#include "menu_config.h" +#include "menu.h" +#include +#include + +/************************** 内部宏定义 **************************/ +/** + * @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 \ No newline at end of file diff --git a/port/menu_port.c b/port/menu_port.c new file mode 100644 index 0000000..9e1bb58 --- /dev/null +++ b/port/menu_port.c @@ -0,0 +1,98 @@ +/** + * @file menu_port.c + * @brief 菜单组件硬件端口层示例实现(用户需要根据实际硬件修改) + * @note 工业级嵌入式菜单组件 - 硬件端口层示例 + * @attention 用户需要根据自己的硬件平台修改此文件 + */ +#include "menu_port.h" +#include +#include +#include +#include + +/** + * @brief 硬件打印接口示例实现(使用标准C库printf) + * @param fmt 格式化字符串 + * @param args 可变参数列表 + * @note 实际项目中,用户需要将此接口重定向到硬件的串口、LCD等输出设备 + */ +void menu_port_printf(const char* fmt, va_list args) +{ + // 示例:使用标准C库的vprintf函数 + // 实际项目中,这里应该调用硬件相关的打印函数,如串口打印 + vprintf(fmt, args); +} + +/** + * @brief 获取系统滴答时间示例实现(使用标准C库time函数) + * @return 当前系统滴答时间(ms) + * @note 实际项目中,用户需要将此接口实现为硬件定时器的计数值 + */ +uint32_t menu_port_get_tick(void) +{ + // 示例:使用标准C库的time函数(仅用于演示,精度低) + // 实际项目中,这里应该返回硬件定时器的计数值,如SysTick + return (uint32_t)time(NULL) * 1000; +} + +/** + * @brief 硬件延迟函数示例实现(使用标准C库sleep函数) + * @param ms 延迟时间(ms) + * @note 实际项目中,用户需要将此接口实现为硬件定时器的延迟 + */ +void menu_port_delay_ms(uint32_t ms) +{ + // 示例:使用标准C库的sleep函数(仅用于演示,精度低) + // 实际项目中,这里应该使用硬件定时器实现精确延迟 + sleep(ms / 1000); + usleep((ms % 1000) * 1000); +} + +/** + * @brief 菜单显示接口示例实现(使用标准C库printf) + * @param menu_name 菜单名称字符串 + * @param menu_id 菜单ID + * @note 实际项目中,用户需要将此接口实现为在LCD、OLED等显示设备上的显示 + */ +void menu_port_display(const char* menu_name, uint16_t menu_id) +{ + // 示例:使用标准C库的printf函数在控制台显示 + // 实际项目中,这里应该调用硬件显示函数,将菜单显示在LCD等设备上 + printf("\r\nCurrent Menu: %s (ID: %d)\r\n", menu_name, menu_id); + printf("Use UP/DOWN keys to navigate, ENTER to select, BACK to return\r\n"); +} + +/** + * @brief 按键扫描接口示例实现(使用标准C库getchar函数) + * @return 当前按键事件(MenuEventType类型) + * @note 实际项目中,用户需要将此接口实现为硬件按键的扫描 + */ +/* +MenuEventType menu_port_key_scan(void) +{ + // 示例:使用标准C库的getchar函数获取键盘输入(仅用于演示) + // 实际项目中,这里应该扫描硬件按键的状态 + if (kbhit()) + { + char key = getchar(); + switch (key) + { + case 'w': + case 'W': + return MENU_EVENT_KEY_UP; + case 's': + case 'S': + return MENU_EVENT_KEY_DOWN; + case '\r': + case '\n': + return MENU_EVENT_KEY_ENTER; + case 'b': + case 'B': + return MENU_EVENT_KEY_BACK; + default: + break; + } + } + return MENU_EVENT_NONE; +} +*/ \ No newline at end of file diff --git a/port/menu_port.h b/port/menu_port.h new file mode 100644 index 0000000..1920d85 --- /dev/null +++ b/port/menu_port.h @@ -0,0 +1,50 @@ +/** + * @file menu_port.h + * @brief 菜单组件硬件端口层接口定义(用户需要实现的硬件相关接口) + * @note 工业级嵌入式菜单组件 - 硬件端口层 + */ +#ifndef MENU_PORT_H +#define MENU_PORT_H + +#include +#include + +/************************** 硬件端口层接口声明 **************************/ +/** + * @brief 硬件打印接口(可变参数版本) + * @param fmt 格式化字符串 + * @param args 可变参数列表 + * @note 用户需要实现此接口,用于调试信息输出 + */ +void menu_port_printf(const char* fmt, va_list args); + +/** + * @brief 获取系统滴答时间(毫秒级) + * @return 当前系统滴答时间(ms) + * @note 用户需要实现此接口,用于菜单的定时和超时处理 + */ +uint32_t menu_port_get_tick(void); + +/** + * @brief 硬件延迟函数(毫秒级) + * @param ms 延迟时间(ms) + * @note 用户需要实现此接口,用于必要的延迟操作 + */ +void menu_port_delay_ms(uint32_t ms); + +/** + * @brief 菜单显示接口 + * @param menu_name 菜单名称字符串 + * @param menu_id 菜单ID + * @note 用户需要实现此接口,用于在硬件上显示菜单内容 + */ +void menu_port_display(const char* menu_name, uint16_t menu_id); + +/** + * @brief 按键扫描接口(可选) + * @return 当前按键事件(MenuEventType类型) + * @note 用户可以实现此接口,也可以在自己的主循环中调用menu_post_event发送按键事件 + */ +// MenuEventType menu_port_key_scan(void); + +#endif // MENU_PORT_H \ No newline at end of file diff --git a/src/core/menu_core.c b/src/core/menu_core.c new file mode 100644 index 0000000..09b6bd3 --- /dev/null +++ b/src/core/menu_core.c @@ -0,0 +1,515 @@ +/** + * @file menu_core.c + * @brief 菜单核心逻辑:导航、栈管理、主循环(工业级:状态机驱动) + */ +#include "menu_core.h" +#include "menu_data.h" +#include + +/************************** 全局变量定义 **************************/ +MenuCoreCtx s_menu_core_ctx; + +/** + * @brief 查找菜单节点(通过ID) + * @param node_id 节点ID + * @return 节点指针(NULL表示未找到) + */ +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].flags.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* parent_node = menu_core_find_node(current_node->parent_id); + if (parent_node != NULL && parent_node->first_child != NULL) + { + ctx->current_node_id = parent_node->first_child->id; + } + } + + MENU_DEBUG("Navigate down: current node %d", ctx->current_node_id); + return MENU_OK; +} + +/** + * @brief 初始化菜单核心上下文 + * @return 错误码 + */ +MenuErrCode menu_core_ctx_init(void) +{ + MenuCoreCtx* ctx = menu_core_get_ctx(); + MENU_MEM_SET_ZERO(ctx, sizeof(MenuCoreCtx)); + ctx->is_initialized = true; + return MENU_OK; +} + +/** + * @brief 获取菜单核心上下文(内部唯一访问入口) + * @return 菜单核心上下文指针 + */ +MenuCoreCtx* menu_core_get_ctx(void) +{ + return &s_menu_core_ctx; +} + +/** + * @brief 处理单个菜单事件 + * @param event 事件指针 + * @return 错误码 + */ +MenuErrCode menu_core_handle_event(const MenuEvent* event) +{ + MENU_ASSERT(event != NULL); + + 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; + case MENU_EVENT_NONE: + // 无事件,不处理 + break; + default: + // 自定义事件(用户可扩展) + MENU_DEBUG("Custom event received: type %d, param %lu", event->type, event->param); + break; + } + + return err; +} + +/** + * @brief 刷新菜单显示(内部调用,对接port层显示接口) + */ +void menu_core_refresh_display(void) +{ + MenuCoreCtx* ctx = menu_core_get_ctx(); + MenuNode* current_node = menu_core_find_node(ctx->current_node_id); + if (current_node == NULL) + { + return; + } + + // 这里应该调用port层的显示接口 + // 例如:menu_port_display(current_node->name, current_node->id); + MENU_DEBUG("Refresh display: current menu %s (ID: %d)", current_node->name, current_node->id); +} + +/** + * @brief 从事件队列获取事件(非阻塞) + * @param event 输出参数,获取到的事件 + * @return 是否获取到事件 + */ +static bool menu_core_get_event(MenuEvent* event) +{ + MenuCoreCtx* ctx = menu_core_get_ctx(); + MENU_ASSERT(ctx != NULL && event != NULL); + + if (ctx->event_queue.count == 0) + { + return false; + } + + // 从队列头取出事件 + *event = ctx->event_queue.buffer[ctx->event_queue.head]; + ctx->event_queue.head = (ctx->event_queue.head + 1) % MENU_CONFIG_EVENT_QUEUE_LEN; + ctx->event_queue.count--; + + return true; +} + +/** + * @brief 菜单主循环(需在用户主循环中调用,处理事件和刷新显示) + */ +void menu_main_loop(void) +{ + MenuCoreCtx* ctx = menu_core_get_ctx(); + if (!ctx->is_initialized) + { + return; + } + + // 处理事件队列 + MenuEvent event; + while (menu_core_get_event(&event)) + { + // 检查事件是否超时 + if (menu_utils_get_tick() - event.timestamp > MENU_CONFIG_EVENT_TIMEOUT) + { + MENU_DEBUG("Event timeout: type %d", event.type); + continue; + } + + // 处理事件 + MenuErrCode err = menu_core_handle_event(&event); + if (err != MENU_OK) + { + MENU_DEBUG("Event handle error: %d", err); + } + } + + // 定时刷新显示 + if (menu_utils_get_tick() - ctx->last_refresh_tick >= MENU_CONFIG_REFRESH_INTERVAL) + { + menu_core_refresh_display(); + ctx->last_refresh_tick = menu_utils_get_tick(); + } +} + +/** + * @brief 菜单组件初始化(必须首先调用) + * @return 错误码 + */ +MenuErrCode menu_init(void) +{ + MenuErrCode err = menu_core_ctx_init(); + if (err != MENU_OK) + { + return err; + } + + // 初始化事件队列 + MenuCoreCtx* ctx = menu_core_get_ctx(); + ctx->last_refresh_tick = menu_utils_get_tick(); + + return MENU_OK; +} + +/** + * @brief 发送事件到菜单事件队列(如按键事件、自定义事件) + * @param type 事件类型 + * @param param 事件附加参数(可选,如按键长按时间) + * @return 错误码 + */ +MenuErrCode menu_post_event(MenuEventType type, uint32_t param) +{ + MenuCoreCtx* ctx = menu_core_get_ctx(); + if (!ctx->is_initialized) + { + return MENU_ERR_INVALID_PARAM; + } + + // 检查事件队列是否已满 + if (ctx->event_queue.count >= MENU_CONFIG_EVENT_QUEUE_LEN) + { + return MENU_ERR_EVENT_QUEUE_FULL; + } + + // 入队事件 + ctx->event_queue.buffer[ctx->event_queue.tail].type = type; + ctx->event_queue.buffer[ctx->event_queue.tail].param = param; + ctx->event_queue.buffer[ctx->event_queue.tail].timestamp = menu_utils_get_tick(); + ctx->event_queue.tail = (ctx->event_queue.tail + 1) % MENU_CONFIG_EVENT_QUEUE_LEN; + ctx->event_queue.count++; + + return MENU_OK; +} + +/** + * @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) +{ + MenuCoreCtx* ctx = menu_core_get_ctx(); + if (!ctx->is_initialized || node_id == 0) + { + return MENU_ERR_INVALID_PARAM; + } + + // 查找空闲节点 + MenuNode* new_node = NULL; + for (uint16_t i = 0; i < MENU_CONFIG_MAX_NODES; i++) + { + if (!ctx->nodes[i].flags.is_registered) + { + new_node = &ctx->nodes[i]; + break; + } + } + + if (new_node == NULL) + { + return MENU_ERR_OUT_OF_MEMORY; + } + + // 检查节点ID是否已存在 + if (menu_core_find_node(node_id) != NULL) + { + return MENU_ERR_INVALID_PARAM; + } + + // 初始化新节点 + MENU_MEM_SET_ZERO(new_node, sizeof(MenuNode)); + new_node->id = node_id; + new_node->parent_id = parent_id; + new_node->name = name_str; + new_node->enter_cb = enter_cb; + new_node->exit_cb = exit_cb; + new_node->flags.is_registered = true; + new_node->flags.is_selected = false; + + // 如果是根节点(parent_id为0) + if (parent_id == 0) + { + // 检查是否已有根节点 + if (ctx->current_node_id != 0) + { + // 已有根节点,将新节点作为兄弟节点添加到根节点之后 + MenuNode* root_node = menu_core_find_node(ctx->current_node_id); + while (root_node->next_sibling != NULL) + { + root_node = root_node->next_sibling; + } + root_node->next_sibling = new_node; + new_node->prev_sibling = root_node; + } + else + { + // 第一个根节点,设置为当前节点 + ctx->current_node_id = node_id; + menu_core_stack_push(node_id); + } + } + else + { + // 查找父节点 + MenuNode* parent_node = menu_core_find_node(parent_id); + if (parent_node == NULL) + { + return MENU_ERR_NODE_NOT_FOUND; + } + + // 将新节点添加到父节点的子节点列表中 + if (parent_node->first_child == NULL) + { + // 父节点还没有子节点,作为第一个子节点 + parent_node->first_child = new_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 = new_node; + new_node->prev_sibling = last_child; + } + } + + return MENU_OK; +} + +/** + * @brief 获取当前选中的菜单节点ID + * @param node_id 输出参数,当前节点ID + * @return 错误码 + */ +MenuErrCode menu_get_current_node(MenuNodeId* node_id) +{ + MenuCoreCtx* ctx = menu_core_get_ctx(); + if (!ctx->is_initialized || node_id == NULL) + { + return MENU_ERR_INVALID_PARAM; + } + + *node_id = ctx->current_node_id; + return MENU_OK; +} \ No newline at end of file diff --git a/src/lang/menu_lang.c b/src/lang/menu_lang.c new file mode 100644 index 0000000..8ded261 --- /dev/null +++ b/src/lang/menu_lang.c @@ -0,0 +1,123 @@ +/** + * @file menu_lang.c + * @brief 菜单多语言支持模块(可裁剪功能) + */ +#include "menu_core.h" +#include "menu_data.h" + +#if MENU_CONFIG_ENABLE_LANG + +/************************** 全局变量定义 **************************/ +MenuLangStr s_menu_lang_strs[MENU_CONFIG_MAX_NODES * MENU_CONFIG_MAX_LANGS]; +uint8_t s_current_lang_id = 0; + +/** + * @brief 查找语言字符串(通过字符串ID和语言ID) + * @param str_id 字符串ID + * @param lang_id 语言ID + * @return 语言字符串(NULL表示未找到) + */ +static const char* menu_lang_find_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; +} + +/** + * @brief 注册语言字符串 + * @param str_id 字符串ID + * @param lang_id 语言ID + * @param str 字符串内容 + * @return 错误码 + */ +MenuErrCode menu_lang_register_str(uint16_t str_id, uint8_t lang_id, const char* str) +{ + if (lang_id >= MENU_CONFIG_MAX_LANGS || str == NULL) + { + 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 || (s_menu_lang_strs[i].str_id == str_id && s_menu_lang_strs[i].lang_id == lang_id)) + { + 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 + * @return 字符串内容(NULL表示未找到) + */ +const char* menu_lang_get_str(uint16_t str_id) +{ + const char* str = menu_lang_find_str(str_id, s_current_lang_id); + if (str == NULL) + { + // 如果当前语言未找到,尝试使用默认语言(0) + str = menu_lang_find_str(str_id, 0); + } + return str; +} + +/** + * @brief 设置当前语言 + * @param lang_id 语言ID(如0-中文,1-英文) + * @return 错误码 + */ +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("Language changed to: %d", lang_id); + + // 语言切换后,刷新菜单显示 + menu_core_refresh_display(); + + return MENU_OK; +} + +/** + * @brief 获取当前语言 + * @param lang_id 输出参数,当前语言ID + * @return 错误码 + */ +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; +} + +/** + * @brief 获取支持的最大语言数量 + * @return 最大语言数量 + */ +uint8_t menu_lang_get_max_langs(void) +{ + return MENU_CONFIG_MAX_LANGS; +} + +#endif // MENU_CONFIG_ENABLE_LANG \ No newline at end of file diff --git a/src/param/menu_param.c b/src/param/menu_param.c new file mode 100644 index 0000000..14df21c --- /dev/null +++ b/src/param/menu_param.c @@ -0,0 +1,302 @@ +/** + * @file menu_param.c + * @brief 菜单参数管理模块(可裁剪功能) + */ +#include "menu_core.h" +#include "menu_data.h" + +#if MENU_CONFIG_ENABLE_PARAM + +/************************** 全局变量定义 **************************/ +MenuParam s_menu_params[MENU_CONFIG_MAX_PARAMS]; + +/** + * @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 param 参数指针 + * @param value 浮点值 + * @return 转换后的内部值(通过指针返回) + */ +static void menu_param_float_to_internal(MenuParam* param, float value) +{ + MENU_ASSERT(param != NULL); + + // 确保值在范围内 + if (value < param->min_val) + { + value = param->min_val; + } + else if (value > param->max_val) + { + value = param->max_val; + } + + // 应用缩放因子 + value /= param->scale; + + // 根据参数类型转换 + switch (param->type) + { + case MENU_PARAM_TYPE_INT8: + param->value.i8 = (int8_t)value; + break; + case MENU_PARAM_TYPE_UINT8: + param->value.u8 = (uint8_t)value; + break; + case MENU_PARAM_TYPE_INT16: + param->value.i16 = (int16_t)value; + break; + case MENU_PARAM_TYPE_UINT16: + param->value.u16 = (uint16_t)value; + break; + case MENU_PARAM_TYPE_INT32: + param->value.i32 = (int32_t)value; + break; + case MENU_PARAM_TYPE_UINT32: + param->value.u32 = (uint32_t)value; + break; + case MENU_PARAM_TYPE_FLOAT: + param->value.f = value; + break; + default: + break; + } +} + +/** + * @brief 将参数内部值转换为浮点值(根据类型) + * @param param 参数指针 + * @return 浮点值 + */ +static float menu_param_internal_to_float(const MenuParam* param) +{ + MENU_ASSERT(param != NULL); + + float value = 0.0f; + + // 根据参数类型转换 + switch (param->type) + { + case MENU_PARAM_TYPE_INT8: + value = (float)param->value.i8; + break; + case MENU_PARAM_TYPE_UINT8: + value = (float)param->value.u8; + break; + case MENU_PARAM_TYPE_INT16: + value = (float)param->value.i16; + break; + case MENU_PARAM_TYPE_UINT16: + value = (float)param->value.u16; + break; + case MENU_PARAM_TYPE_INT32: + value = (float)param->value.i32; + break; + case MENU_PARAM_TYPE_UINT32: + value = (float)param->value.u32; + break; + case MENU_PARAM_TYPE_FLOAT: + value = param->value.f; + break; + default: + break; + } + + // 应用缩放因子 + return value * param->scale; +} + +/** + * @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) +{ + if (scale <= 0.0f) + { + return MENU_ERR_INVALID_PARAM; + } + + // 查找菜单节点 + MenuNode* node = menu_core_find_node(node_id); + if (node == NULL) + { + return MENU_ERR_NODE_NOT_FOUND; + } + + // 检查参数ID是否已存在 + if (menu_param_find(param_id) != NULL) + { + 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; + param->is_registered = true; + + // 设置默认值 + menu_param_float_to_internal(param, default_val); + // 保存默认值 + menu_param_float_to_internal((MenuParam*)¶m->default_val, default_val); + + // 将参数与菜单节点绑定 + node->param_id = param_id; + + MENU_DEBUG("Param registered: ID=%d, Type=%d, Range=[%0.2f, %0.2f], Default=%0.2f, Scale=%0.2f", + param_id, type, min_val, max_val, default_val, scale); + + return MENU_OK; +} + +/** + * @brief 设置参数值 + * @param param_id 参数ID + * @param value 新值(浮点型,内部自动转换) + * @return 错误码 + */ +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; + } + + // 设置新值 + menu_param_float_to_internal(param, value); + + MENU_DEBUG("Param set: ID=%d, Value=%0.2f (Internal: %0.2f)", + param_id, value, menu_param_internal_to_float(param)); + + return MENU_OK; +} + +/** + * @brief 获取参数值 + * @param param_id 参数ID + * @param value 输出参数,当前值(浮点型) + * @return 错误码 + */ +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_internal_to_float(param); + + MENU_DEBUG("Param get: ID=%d, Value=%0.2f", param_id, *value); + + return MENU_OK; +} + +/** + * @brief 将参数恢复为默认值 + * @param param_id 参数ID + * @return 错误码 + */ +MenuErrCode menu_param_restore_default(uint16_t param_id) +{ + // 查找参数 + MenuParam* param = menu_param_find(param_id); + if (param == NULL) + { + return MENU_ERR_NODE_NOT_FOUND; + } + + // 恢复默认值 + float default_val = menu_param_internal_to_float((const MenuParam*)¶m->default_val); + return menu_param_set_value(param_id, default_val); +} + +/** + * @brief 增加参数值(用于菜单上下键调整) + * @param param_id 参数ID + * @param step 步长(浮点型) + * @return 错误码 + */ +MenuErrCode menu_param_increase(uint16_t param_id, float step) +{ + float current_val = 0.0f; + MenuErrCode err = menu_param_get_value(param_id, ¤t_val); + if (err != MENU_OK) + { + return err; + } + + return menu_param_set_value(param_id, current_val + step); +} + +/** + * @brief 减少参数值(用于菜单上下键调整) + * @param param_id 参数ID + * @param step 步长(浮点型) + * @return 错误码 + */ +MenuErrCode menu_param_decrease(uint16_t param_id, float step) +{ + float current_val = 0.0f; + MenuErrCode err = menu_param_get_value(param_id, ¤t_val); + if (err != MENU_OK) + { + return err; + } + + return menu_param_set_value(param_id, current_val - step); +} + +#endif // MENU_CONFIG_ENABLE_PARAM \ No newline at end of file diff --git a/src/utils/menu_utils.c b/src/utils/menu_utils.c new file mode 100644 index 0000000..3f07e75 --- /dev/null +++ b/src/utils/menu_utils.c @@ -0,0 +1,77 @@ +/** + * @file menu_utils.c + * @brief 菜单工具模块:断言、打印、系统滴答等 + */ +#include "menu_def.h" +#include "menu_port.h" +#include + +/** + * @brief 断言失败处理函数 + * @param file 文件名 + * @param line 行号 + */ +void menu_utils_assert_failed(const char* file, uint32_t line) +{ + // 调用port层的打印接口输出断言信息 + menu_utils_printf("[MENU ASSERT] %s:%lu\r\n", file, line); + + // 可以在这里添加其他处理,如触发硬件异常、保存上下文等 + // 例如:menu_port_trigger_exception(); +} + +/** + * @brief 调试打印函数(对接port层的硬件打印接口) + * @param fmt 格式化字符串 + * @param ... 可变参数 + */ +void menu_utils_printf(const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + + // 调用port层的可变参数打印接口 + // 注意:这里需要用户在port层实现menu_port_printf函数 + menu_port_printf(fmt, args); + + va_end(args); +} + +/** + * @brief 获取系统滴答时间(ms,对接port层) + * @return 当前滴答时间 + */ +uint32_t menu_utils_get_tick(void) +{ + // 调用port层的系统滴答获取接口 + return menu_port_get_tick(); +} + +/** + * @brief 计算两个32位无符号整数的差值(处理溢出情况) + * @param now 当前时间 + * @param last 上次时间 + * @return 差值(ms) + */ +uint32_t menu_utils_diff_tick(uint32_t now, uint32_t last) +{ + if (now >= last) + { + return now - last; + } + else + { + // 处理溢出情况 + return (UINT32_MAX - last) + now + 1; + } +} + +/** + * @brief 延迟函数(毫秒级,对接port层) + * @param ms 延迟时间(ms) + */ +void menu_utils_delay_ms(uint32_t ms) +{ + // 调用port层的延迟接口 + menu_port_delay_ms(ms); +} \ No newline at end of file diff --git a/update.md b/update.md new file mode 100644 index 0000000..4a9f919 --- /dev/null +++ b/update.md @@ -0,0 +1,819 @@ +你需要在已有的工业级嵌入式菜单组件中,拓展**参数与Modbus寄存器地址的灵活映射功能**:让每个参数能根据自身性质(数据类型、读写权限、业务属性)匹配对应的Modbus寄存器(包括线圈、离散输入、保持寄存器、输入寄存器),支持寄存器地址的配置、映射查询,以及参数值与寄存器数据的双向转换,同时保持与原有组件的解耦和工业级的鲁棒性。 + +以下是基于原有目录结构的详细实现方案,遵循**分层解耦、静态内存、可配置、类型安全**的工业级设计原则。 + +--- + +## 一、设计核心思路(工业级考量) +1. **解耦设计**:Modbus映射模块作为独立的功能扩展层,依赖原有参数模块的接口,不侵入核心逻辑。 +2. **静态内存**:使用静态数组存储Modbus映射关系,避免动态分配,适配嵌入式内存约束。 +3. **类型匹配**:参数类型与Modbus寄存器数据类型(1位、8位、16位、32位、浮点)自动转换,支持大小端配置。 +4. **权限控制**:映射时指定寄存器的读写权限(只读/只写/读写),与参数的权限联动。 +5. **灵活配置**:支持单参数映射单寄存器、单参数映射多寄存器(如32位浮点占2个16位保持寄存器)。 +6. **可裁剪**:通过宏开关控制Modbus映射功能的启用/关闭,不影响原有功能。 + +--- + +## 二、代码实现(按目录结构补充/修改) +### 1. 对外API层(`menu/api/`) +#### 1.1 修改`menu/api/menu_config.h`(添加Modbus映射配置宏) +在原有配置后新增Modbus相关配置,保持可裁剪性: +```c +/************************** Modbus映射功能配置(可裁剪) **************************/ +/** + * @brief 是否启用参数-Modbus寄存器映射功能 + */ +#define MENU_CONFIG_ENABLE_MODBUS_MAP 1 + +/** + * @brief 最大Modbus映射数量(静态内存,根据项目调整) + */ +#define MENU_CONFIG_MAX_MODBUS_MAPS 16 + +/** + * @brief Modbus寄存器地址最大值(根据实际Modbus从站配置) + */ +#define MENU_CONFIG_MODBUS_MAX_ADDR 0xFFFF + +/** + * @brief 默认Modbus字节序(0-小端,1-大端,2-字小端字节大端(Modbus标准)) + */ +#define MENU_CONFIG_MODBUS_BYTE_ORDER 2 + +/** + * @brief 是否启用Modbus映射权限校验(与参数权限联动) + */ +#define MENU_CONFIG_MODBUS_PERMISSION 1 +``` + +#### 1.2 修改`menu/api/menu.h`(添加Modbus映射的对外接口和类型) +在原有内容后新增Modbus相关的类型和接口,作为用户可调用的API: +```c +/************************** Modbus映射功能(启用时有效) **************************/ +#if MENU_CONFIG_ENABLE_MODBUS_MAP +/** + * @brief Modbus寄存器类型(符合Modbus协议标准) + */ +typedef enum { + MODBUS_REG_TYPE_COIL = 0, ///< 线圈(1位,地址范围00001-09999) + MODBUS_REG_TYPE_DISCRETE_INPUT, ///< 离散输入(1位,地址范围10001-19999) + MODBUS_REG_TYPE_HOLDING_REG, ///< 保持寄存器(16位,地址范围40001-49999) + MODBUS_REG_TYPE_INPUT_REG, ///< 输入寄存器(16位,地址范围30001-39999) +} ModbusRegType; + +/** + * @brief Modbus寄存器读写权限 + */ +typedef enum { + MODBUS_PERM_READ_ONLY = 0, ///< 只读(如离散输入、输入寄存器) + MODBUS_PERM_WRITE_ONLY, ///< 只写(如线圈) + MODBUS_PERM_READ_WRITE, ///< 读写(如保持寄存器) +} ModbusPerm; + +/** + * @brief Modbus映射结构体(对外只读,内部初始化) + */ +typedef struct { + uint16_t param_id; ///< 关联的参数ID + ModbusRegType reg_type; ///< 寄存器类型 + uint16_t reg_addr; ///< 寄存器起始地址(协议地址,如40001) + uint8_t reg_count; ///< 占用寄存器数量(如32位浮点占2个16位寄存器) + ModbusPerm perm; ///< 读写权限 + uint8_t byte_order; ///< 字节序(0-小端,1-大端,2-Modbus标准) +} ModbusMap; + +/** + * @brief 注册参数与Modbus寄存器的映射关系 + * @param param_id 参数ID(需已通过menu_param_register注册) + * @param reg_type 寄存器类型 + * @param reg_addr 寄存器起始地址(协议地址,如40001) + * @param reg_count 占用寄存器数量(如1:16位,2:32位,4:64位) + * @param perm 读写权限 + * @param byte_order 字节序(可选,默认使用MENU_CONFIG_MODBUS_BYTE_ORDER) + * @return 错误码 + */ +MenuErrCode menu_modbus_map_register(uint16_t param_id, ModbusRegType reg_type, uint16_t reg_addr, uint8_t reg_count, ModbusPerm perm, uint8_t byte_order); + +/** + * @brief 根据参数ID查询Modbus映射关系 + * @param param_id 参数ID + * @param map 输出参数,映射关系结构体 + * @return 错误码 + */ +MenuErrCode menu_modbus_map_query_by_param(uint16_t param_id, ModbusMap* map); + +/** + * @brief 根据寄存器地址和类型查询Modbus映射关系 + * @param reg_type 寄存器类型 + * @param reg_addr 寄存器地址 + * @param map 输出参数,映射关系结构体 + * @return 错误码 + */ +MenuErrCode menu_modbus_map_query_by_reg(ModbusRegType reg_type, uint16_t reg_addr, ModbusMap* map); + +/** + * @brief 将参数值写入对应的Modbus寄存器(双向转换:参数值→寄存器数据) + * @param param_id 参数ID + * @param reg_buf 输出参数,寄存器数据缓冲区(需足够大,如32位占2个16位寄存器) + * @param buf_len 缓冲区长度(输入),实际写入长度(输出) + * @return 错误码 + */ +MenuErrCode menu_modbus_map_param_to_reg(uint16_t param_id, uint8_t* reg_buf, uint8_t* buf_len); + +/** + * @brief 将Modbus寄存器数据读取到参数值(双向转换:寄存器数据→参数值) + * @param param_id 参数ID + * @param reg_buf 输入参数,寄存器数据缓冲区 + * @param buf_len 缓冲区长度 + * @return 错误码 + */ +MenuErrCode menu_modbus_map_reg_to_param(uint16_t param_id, const uint8_t* reg_buf, uint8_t buf_len); + +#endif // MENU_CONFIG_ENABLE_MODBUS_MAP +``` + +### 2. 内部头文件层(`menu/internal/`) +#### 2.1 修改`menu/internal/menu_core.h`(添加Modbus映射的内部类型) +在原有核心类型后新增Modbus映射的内部结构体(比对外结构体更详细,包含数据转换的辅助信息): +```c +#if MENU_CONFIG_ENABLE_MODBUS_MAP +/** + * @brief Modbus映射内部结构体(包含数据转换的辅助信息,用户无需关心) + */ +typedef struct { + ModbusMap pub; ///< 对外暴露的映射信息(兼容外部结构体) + bool is_registered; ///< 是否已注册 + MenuParamType param_type; ///< 关联的参数类型(缓存,避免重复查询) + uint16_t reg_addr_offset; ///< 寄存器地址偏移(协议地址→实际地址,如40001→0) +} ModbusMapInternal; +#endif // MENU_CONFIG_ENABLE_MODBUS_MAP +``` + +#### 2.2 修改`menu/internal/menu_data.h`(添加Modbus映射的静态全局数据) +在原有共享数据后新增Modbus映射的静态数组(静态内存,无动态分配): +```c +#if MENU_CONFIG_ENABLE_MODBUS_MAP +/** + * @brief Modbus映射上下文(静态全局,仅内部访问) + */ +static ModbusMapInternal s_menu_modbus_maps[MENU_CONFIG_MAX_MODBUS_MAPS]; +#endif // MENU_CONFIG_ENABLE_MODBUS_MAP +``` + +### 3. 功能扩展层(`menu/src/features/`) +#### 3.1 新增`menu/src/features/menu_modbus.c`(Modbus映射核心逻辑) +实现Modbus映射的注册、查询、数据双向转换,与原有参数模块无缝集成: +```c +/** + * @file menu_modbus.c + * @brief 菜单参数-Modbus寄存器映射:注册、查询、数据双向转换(工业级:类型安全、字节序适配) + */ +#if MENU_CONFIG_ENABLE_MODBUS_MAP +#include "menu.h" +#include "menu_core.h" +#include "menu_data.h" +#include "menu_def.h" +#include +#include + +/************************** 内部辅助函数 **************************/ +/** + * @brief 转换Modbus协议地址到实际偏移地址(如40001→0,00001→0) + * @param reg_type 寄存器类型 + * @param reg_addr 协议地址(如40001) + * @param offset 输出参数,实际偏移地址 + * @return 错误码 + */ +static MenuErrCode menu_modbus_convert_addr(ModbusRegType reg_type, uint16_t reg_addr, uint16_t* offset) +{ + if (offset == NULL) + { + return MENU_ERR_INVALID_PARAM; + } + + // 校验协议地址范围(符合Modbus标准) + switch (reg_type) + { + case MODBUS_REG_TYPE_COIL: + if (reg_addr < 1 || reg_addr > 9999) + { + MENU_DEBUG("Modbus coil addr out of range: %d", reg_addr); + return MENU_ERR_INVALID_PARAM; + } + *offset = reg_addr - 1; + break; + case MODBUS_REG_TYPE_DISCRETE_INPUT: + if (reg_addr < 10001 || reg_addr > 19999) + { + MENU_DEBUG("Modbus discrete input addr out of range: %d", reg_addr); + return MENU_ERR_INVALID_PARAM; + } + *offset = reg_addr - 10001; + break; + case MODBUS_REG_TYPE_HOLDING_REG: + if (reg_addr < 40001 || reg_addr > 49999) + { + MENU_DEBUG("Modbus holding reg addr out of range: %d", reg_addr); + return MENU_ERR_INVALID_PARAM; + } + *offset = reg_addr - 40001; + break; + case MODBUS_REG_TYPE_INPUT_REG: + if (reg_addr < 30001 || reg_addr > 39999) + { + MENU_DEBUG("Modbus input reg addr out of range: %d", reg_addr); + return MENU_ERR_INVALID_PARAM; + } + *offset = reg_addr - 30001; + break; + default: + return MENU_ERR_INVALID_PARAM; + } + + // 校验实际偏移地址不超过配置的最大值 + if (*offset > MENU_CONFIG_MODBUS_MAX_ADDR) + { + MENU_DEBUG("Modbus reg offset out of range: %d", *offset); + return MENU_ERR_INVALID_PARAM; + } + + return MENU_OK; +} + +/** + * @brief 校验寄存器类型与参数类型的匹配性(工业级:类型安全) + * @param param_type 参数类型 + * @param reg_type 寄存器类型 + * @return 错误码 + */ +static MenuErrCode menu_modbus_check_type_match(MenuParamType param_type, ModbusRegType reg_type) +{ + // 1位寄存器(线圈、离散输入)只能匹配布尔型/8位整型参数 + if (reg_type == MODBUS_REG_TYPE_COIL || reg_type == MODBUS_REG_TYPE_DISCRETE_INPUT) + { + if (param_type != MENU_PARAM_TYPE_INT8 && param_type != MENU_PARAM_TYPE_UINT8) + { + MENU_DEBUG("Modbus 1-bit reg not match param type: %d", param_type); + return MENU_ERR_INVALID_PARAM; + } + return MENU_OK; + } + + // 16位寄存器(保持、输入)可匹配所有类型(16/32/浮点需多寄存器) + return MENU_OK; +} + +/** + * @brief 字节序转换(适配不同的Modbus字节序,工业级:兼容不同从站) + * @param data 数据缓冲区(16位为单位) + * @param len 数据长度(16位的数量) + * @param byte_order 字节序(0-小端,1-大端,2-Modbus标准(字小端,字节大端)) + * @param is_reverse 是否反向转换(用于读取/写入) + */ +static void menu_modbus_byte_order_convert(uint16_t* data, uint8_t len, uint8_t byte_order, bool is_reverse) +{ + if (data == NULL || len == 0) + { + return; + } + + // 默认使用配置的字节序 + if (byte_order > 2) + { + byte_order = MENU_CONFIG_MODBUS_BYTE_ORDER; + } + + switch (byte_order) + { + case 0: // 小端(低字节在前,高字节在后) + if (is_reverse) + { + for (uint8_t i = 0; i < len; i++) + { + data[i] = (data[i] << 8) | (data[i] >> 8); + } + } + break; + case 1: // 大端(高字节在前,低字节在后) + // 无需转换,Modbus默认是大端 + break; + case 2: // Modbus标准(字小端,字节大端:如32位值0x12345678→0x5678 0x1234) + if (is_reverse) + { + // 读取时:将Modbus顺序转换为主机顺序 + for (uint8_t i = 0; i < len; i += 2) + { + if (i + 1 < len) + { + uint16_t temp = data[i]; + data[i] = data[i + 1]; + data[i + 1] = temp; + } + } + } + else + { + // 写入时:将主机顺序转换为Modbus顺序 + for (uint8_t i = 0; i < len; i += 2) + { + if (i + 1 < len) + { + uint16_t temp = data[i]; + data[i] = data[i + 1]; + data[i + 1] = temp; + } + } + } + break; + default: + break; + } +} + +/** + * @brief 查找Modbus映射(通过参数ID) + * @param param_id 参数ID + * @return 映射内部结构体指针(NULL表示未找到) + */ +static ModbusMapInternal* menu_modbus_find_by_param(uint16_t param_id) +{ + for (uint8_t i = 0; i < MENU_CONFIG_MAX_MODBUS_MAPS; i++) + { + if (s_menu_modbus_maps[i].is_registered && s_menu_modbus_maps[i].pub.param_id == param_id) + { + return &s_menu_modbus_maps[i]; + } + } + return NULL; +} + +/** + * @brief 查找Modbus映射(通过寄存器类型和地址) + * @param reg_type 寄存器类型 + * @param reg_addr 寄存器地址(协议地址) + * @return 映射内部结构体指针(NULL表示未找到) + */ +static ModbusMapInternal* menu_modbus_find_by_reg(ModbusRegType reg_type, uint16_t reg_addr) +{ + for (uint8_t i = 0; i < MENU_CONFIG_MAX_MODBUS_MAPS; i++) + { + if (s_menu_modbus_maps[i].is_registered && + s_menu_modbus_maps[i].pub.reg_type == reg_type && + s_menu_modbus_maps[i].pub.reg_addr == reg_addr) + { + return &s_menu_modbus_maps[i]; + } + } + return NULL; +} + +/************************** 对外接口实现 **************************/ +MenuErrCode menu_modbus_map_register(uint16_t param_id, ModbusRegType reg_type, uint16_t reg_addr, uint8_t reg_count, ModbusPerm perm, uint8_t byte_order) +{ + // 1. 校验参数是否已注册 + MenuParam* param = menu_param_find(param_id); + if (param == NULL) + { + MENU_DEBUG("Modbus map param %d not found", param_id); + return MENU_ERR_NODE_NOT_FOUND; + } + + // 2. 校验寄存器地址和转换为偏移 + uint16_t reg_offset; + MenuErrCode err = menu_modbus_convert_addr(reg_type, reg_addr, ®_offset); + if (err != MENU_OK) + { + return err; + } + + // 3. 校验寄存器数量(至少1个) + if (reg_count == 0) + { + MENU_DEBUG("Modbus map reg count is zero"); + return MENU_ERR_INVALID_PARAM; + } + + // 4. 校验寄存器类型与参数类型匹配 + err = menu_modbus_check_type_match(param->type, reg_type); + if (err != MENU_OK) + { + return err; + } + + // 5. 校验读写权限(与寄存器类型联动,如离散输入只能只读) + #if MENU_CONFIG_MODBUS_PERMISSION + if ((reg_type == MODBUS_REG_TYPE_DISCRETE_INPUT || reg_type == MODBUS_REG_TYPE_INPUT_REG) && perm != MODBUS_PERM_READ_ONLY) + { + MENU_DEBUG("Modbus input reg only support read"); + return MENU_ERR_INVALID_PARAM; + } + if (reg_type == MODBUS_REG_TYPE_COIL && perm == MODBUS_PERM_READ_ONLY) + { + MENU_DEBUG("Modbus coil not support read only"); + return MENU_ERR_INVALID_PARAM; + } + #endif + + // 6. 检查映射是否已存在 + if (menu_modbus_find_by_param(param_id) != NULL || menu_modbus_find_by_reg(reg_type, reg_addr) != NULL) + { + MENU_DEBUG("Modbus map already exists: param %d, reg %d:%d", param_id, reg_type, reg_addr); + return MENU_ERR_INVALID_PARAM; + } + + // 7. 查找空闲的映射位置(静态数组) + ModbusMapInternal* map = NULL; + for (uint8_t i = 0; i < MENU_CONFIG_MAX_MODBUS_MAPS; i++) + { + if (!s_menu_modbus_maps[i].is_registered) + { + map = &s_menu_modbus_maps[i]; + break; + } + } + + if (map == NULL) + { + MENU_DEBUG("Modbus map out of memory: max %d", MENU_CONFIG_MAX_MODBUS_MAPS); + return MENU_ERR_OUT_OF_MEMORY; + } + + // 8. 初始化映射关系 + MENU_MEM_SET_ZERO(map, sizeof(ModbusMapInternal)); + map->pub.param_id = param_id; + map->pub.reg_type = reg_type; + map->pub.reg_addr = reg_addr; + map->pub.reg_count = reg_count; + map->pub.perm = perm; + map->pub.byte_order = byte_order; + map->param_type = param->type; + map->reg_addr_offset = reg_offset; + map->is_registered = true; + + MENU_DEBUG("Modbus map registered: param %d → reg %d:%d (count %d, perm %d)", + param_id, reg_type, reg_addr, reg_count, perm); + return MENU_OK; +} + +MenuErrCode menu_modbus_map_query_by_param(uint16_t param_id, ModbusMap* map) +{ + if (map == NULL) + { + return MENU_ERR_INVALID_PARAM; + } + + ModbusMapInternal* internal_map = menu_modbus_find_by_param(param_id); + if (internal_map == NULL) + { + MENU_DEBUG("Modbus map param %d not found", param_id); + return MENU_ERR_NODE_NOT_FOUND; + } + + // 复制对外暴露的信息 + *map = internal_map->pub; + return MENU_OK; +} + +MenuErrCode menu_modbus_map_query_by_reg(ModbusRegType reg_type, uint16_t reg_addr, ModbusMap* map) +{ + if (map == NULL) + { + return MENU_ERR_INVALID_PARAM; + } + + ModbusMapInternal* internal_map = menu_modbus_find_by_reg(reg_type, reg_addr); + if (internal_map == NULL) + { + MENU_DEBUG("Modbus map reg %d:%d not found", reg_type, reg_addr); + return MENU_ERR_NODE_NOT_FOUND; + } + + // 复制对外暴露的信息 + *map = internal_map->pub; + return MENU_OK; +} + +MenuErrCode menu_modbus_map_param_to_reg(uint16_t param_id, uint8_t* reg_buf, uint8_t* buf_len) +{ + if (reg_buf == NULL || buf_len == NULL || *buf_len == 0) + { + return MENU_ERR_INVALID_PARAM; + } + + // 1. 查找映射关系 + ModbusMapInternal* map = menu_modbus_find_by_param(param_id); + if (map == NULL) + { + return MENU_ERR_NODE_NOT_FOUND; + } + + // 2. 校验读写权限 + #if MENU_CONFIG_MODBUS_PERMISSION + if (map->pub.perm == MODBUS_PERM_READ_ONLY) + { + MENU_DEBUG("Modbus map param %d is read only", param_id); + return MENU_ERR_PARAM_WRITE_DENIED; // 复用原有错误码,或新增 + } + #endif + + // 3. 获取参数值 + float param_val; + MenuErrCode err = menu_param_get_value(param_id, ¶m_val); + if (err != MENU_OK) + { + return err; + } + + // 4. 转换参数值到寄存器数据(按类型和寄存器数量) + uint16_t* reg_data = (uint16_t*)reg_buf; + uint8_t req_len = map->pub.reg_count; + if (req_len > *buf_len / 2) // 寄存器数据按16位计算 + { + MENU_DEBUG("Modbus reg buf too small: need %d, got %d", req_len * 2, *buf_len); + return MENU_ERR_INVALID_PARAM; + } + + // 清空缓冲区 + memset(reg_data, 0, req_len * 2); + + // 根据参数类型转换 + MenuParam* param = menu_param_find(param_id); + switch (map->pub.reg_type) + { + case MODBUS_REG_TYPE_COIL: + case MODBUS_REG_TYPE_DISCRETE_INPUT: + // 1位寄存器:取参数值的最低位 + reg_data[0] = (uint16_t)(param_val > 0 ? 1 : 0); + break; + case MODBUS_REG_TYPE_HOLDING_REG: + case MODBUS_REG_TYPE_INPUT_REG: + // 16位寄存器:根据参数类型和数量转换 + if (param->type == MENU_PARAM_TYPE_INT16 || param->type == MENU_PARAM_TYPE_UINT16) + { + reg_data[0] = (uint16_t)param_val; + } + else if (param->type == MENU_PARAM_TYPE_INT32 || param->type == MENU_PARAM_TYPE_UINT32) + { + uint32_t val = (uint32_t)param_val; + reg_data[0] = (uint16_t)(val & 0xFFFF); + reg_data[1] = (uint16_t)((val >> 16) & 0xFFFF); + } + else if (param->type == MENU_PARAM_TYPE_FLOAT) + { + // 浮点型转换为32位二进制,占2个16位寄存器 + union { + float f; + uint32_t u32; + } float_val; + float_val.f = param_val; + reg_data[0] = (uint16_t)(float_val.u32 & 0xFFFF); + reg_data[1] = (uint16_t)((float_val.u32 >> 16) & 0xFFFF); + } + else if (param->type == MENU_PARAM_TYPE_INT8 || param->type == MENU_PARAM_TYPE_UINT8) + { + reg_data[0] = (uint16_t)param_val; + } + break; + default: + return MENU_ERR_INVALID_PARAM; + } + + // 5. 字节序转换(写入寄存器前) + menu_modbus_byte_order_convert(reg_data, req_len, map->pub.byte_order, false); + + // 6. 设置实际写入长度 + *buf_len = req_len * 2; + + MENU_DEBUG("Modbus param %d to reg: val %f → reg data len %d", param_id, param_val, *buf_len); + return MENU_OK; +} + +MenuErrCode menu_modbus_map_reg_to_param(uint16_t param_id, const uint8_t* reg_buf, uint8_t buf_len) +{ + if (reg_buf == NULL || buf_len == 0) + { + return MENU_ERR_INVALID_PARAM; + } + + // 1. 查找映射关系 + ModbusMapInternal* map = menu_modbus_find_by_param(param_id); + if (map == NULL) + { + return MENU_ERR_NODE_NOT_FOUND; + } + + // 2. 校验读写权限 + #if MENU_CONFIG_MODBUS_PERMISSION + if (map->pub.perm == MODBUS_PERM_WRITE_ONLY) + { + MENU_DEBUG("Modbus map param %d is write only", param_id); + return MENU_ERR_PARAM_WRITE_DENIED; + } + #endif + + // 3. 校验缓冲区长度 + uint8_t req_len = map->pub.reg_count * 2; + if (buf_len < req_len) + { + MENU_DEBUG("Modbus reg buf too small: need %d, got %d", req_len, buf_len); + return MENU_ERR_INVALID_PARAM; + } + + // 4. 复制寄存器数据并进行字节序转换(读取寄存器后) + uint16_t reg_data[MENU_CONFIG_MAX_MODBUS_MAPS * 2] = {0}; // 足够大的临时缓冲区 + memcpy(reg_data, reg_buf, req_len); + menu_modbus_byte_order_convert(reg_data, map->pub.reg_count, map->pub.byte_order, true); + + // 5. 转换寄存器数据到参数值 + float param_val = 0.0f; + MenuParam* param = menu_param_find(param_id); + switch (map->pub.reg_type) + { + case MODBUS_REG_TYPE_COIL: + case MODBUS_REG_TYPE_DISCRETE_INPUT: + // 1位寄存器:0→0,非0→1 + param_val = (reg_data[0] & 0x01) ? 1.0f : 0.0f; + break; + case MODBUS_REG_TYPE_HOLDING_REG: + case MODBUS_REG_TYPE_INPUT_REG: + // 16位寄存器:根据参数类型转换 + if (param->type == MENU_PARAM_TYPE_INT8) + { + param_val = (int8_t)reg_data[0]; + } + else if (param->type == MENU_PARAM_TYPE_UINT8) + { + param_val = (uint8_t)reg_data[0]; + } + else if (param->type == MENU_PARAM_TYPE_INT16) + { + param_val = (int16_t)reg_data[0]; + } + else if (param->type == MENU_PARAM_TYPE_UINT16) + { + param_val = (uint16_t)reg_data[0]; + } + else if (param->type == MENU_PARAM_TYPE_INT32) + { + uint32_t val = ((uint32_t)reg_data[1] << 16) | reg_data[0]; + param_val = (int32_t)val; + } + else if (param->type == MENU_PARAM_TYPE_UINT32) + { + uint32_t val = ((uint32_t)reg_data[1] << 16) | reg_data[0]; + param_val = val; + } + else if (param->type == MENU_PARAM_TYPE_FLOAT) + { + union { + float f; + uint32_t u32; + } float_val; + float_val.u32 = ((uint32_t)reg_data[1] << 16) | reg_data[0]; + param_val = float_val.f; + } + break; + default: + return MENU_ERR_INVALID_PARAM; + } + + // 6. 设置参数值(自动进行范围检查) + MenuErrCode err = menu_param_set_value(param_id, param_val); + if (err != MENU_OK) + { + return err; + } + + MENU_DEBUG("Modbus reg to param %d: reg data len %d → val %f", param_id, buf_len, param_val); + return MENU_OK; +} + +#endif // MENU_CONFIG_ENABLE_MODBUS_MAP +``` + +### 4. 硬件端口层(`menu/src/port/`) +#### 4.1 可选修改`menu/src/port/menu_port.h`(添加Modbus硬件接口,用户适配) +如果需要对接实际的Modbus硬件(如RS485、TCP),可在port层添加硬件接口,与映射逻辑解耦: +```c +#if MENU_CONFIG_ENABLE_MODBUS_MAP +/** + * @brief Modbus硬件发送数据接口(用户需实现,如RS485发送) + * @param reg_type 寄存器类型 + * @param reg_addr 寄存器地址 + * @param reg_buf 数据缓冲区 + * @param buf_len 数据长度 + * @return 错误码 + */ +MenuErrCode menu_port_modbus_send(ModbusRegType reg_type, uint16_t reg_addr, const uint8_t* reg_buf, uint8_t buf_len); + +/** + * @brief Modbus硬件接收数据接口(用户需实现,如RS485接收) + * @param reg_type 寄存器类型 + * @param reg_addr 寄存器地址 + * @param reg_buf 数据缓冲区 + * @param buf_len 数据长度 + * @return 错误码 + */ +MenuErrCode menu_port_modbus_receive(ModbusRegType reg_type, uint16_t reg_addr, uint8_t* reg_buf, uint8_t buf_len); +#endif // MENU_CONFIG_ENABLE_MODBUS_MAP +``` + +#### 4.2 可选修改`menu/src/port/menu_port.c`(Modbus硬件接口示例实现) +用户根据实际硬件(如STM32的RS485)实现Modbus数据的发送和接收: +```c +#if MENU_CONFIG_ENABLE_MODBUS_MAP +MenuErrCode menu_port_modbus_send(ModbusRegType reg_type, uint16_t reg_addr, const uint8_t* reg_buf, uint8_t buf_len) +{ + // 示例:通过RS485发送Modbus写指令(如写保持寄存器0x0000(40001)) + // 实际需根据Modbus协议实现帧组装、校验、发送 + (void)reg_type; + (void)reg_addr; + (void)reg_buf; + (void)buf_len; + return MENU_OK; // 模拟成功,用户需替换为实际逻辑 +} + +MenuErrCode menu_port_modbus_receive(ModbusRegType reg_type, uint16_t reg_addr, uint8_t* reg_buf, uint8_t buf_len) +{ + // 示例:通过RS485接收Modbus读指令的响应数据 + (void)reg_type; + (void)reg_addr; + (void)reg_buf; + (void)buf_len; + return MENU_OK; // 模拟成功,用户需替换为实际逻辑 +} +#endif // MENU_CONFIG_ENABLE_MODBUS_MAP +``` + +### 5. 使用示例(用户代码) +在原有菜单使用示例中添加Modbus映射的注册和数据交互: +```c +/** + * @brief 菜单组件+Modbus映射使用示例(用户主函数) + */ +#include "menu.h" +#include "menu_port.h" + +// 原有回调函数... + +int main(void) +{ + // 1. 初始化硬件和菜单组件 + // hal_init(); + menu_init(); + + // 2. 注册菜单节点和参数 + menu_register_node(0, 1, "Main Menu", NULL, NULL); + menu_register_node(1, 2, "Temperature", NULL, NULL); + menu_param_register(2, 1, MENU_PARAM_TYPE_FLOAT, 0.0f, 100.0f, 25.0f, 1.0f); // 温度参数(ID=1) + + // 3. 注册参数与Modbus寄存器的映射(核心新增逻辑) + #if MENU_CONFIG_ENABLE_MODBUS_MAP + // 温度参数(ID=1)→ Modbus保持寄存器40001(地址40001,2个寄存器,读写权限,Modbus标准字节序) + menu_modbus_map_register(1, MODBUS_REG_TYPE_HOLDING_REG, 40001, 2, MODBUS_PERM_READ_WRITE, 2); + #endif + + // 4. 主循环 + while (1) + { + // 原有逻辑:按键扫描、菜单主循环 + menu_port_key_scan(); + menu_main_loop(); + + // 5. Modbus数据交互示例(新增逻辑) + #if MENU_CONFIG_ENABLE_MODBUS_MAP + { + // 示例1:将参数值写入Modbus寄存器并发送 + uint8_t reg_buf[4] = {0}; // 2个16位寄存器,占4字节 + uint8_t buf_len = sizeof(reg_buf); + if (menu_modbus_map_param_to_reg(1, reg_buf, &buf_len) == MENU_OK) + { + // 发送到Modbus硬件(用户需实现menu_port_modbus_send) + menu_port_modbus_send(MODBUS_REG_TYPE_HOLDING_REG, 40001, reg_buf, buf_len); + } + + // 示例2:从Modbus寄存器接收数据并更新参数值 + if (menu_port_modbus_receive(MODBUS_REG_TYPE_HOLDING_REG, 40001, reg_buf, buf_len) == MENU_OK) + { + menu_modbus_map_reg_to_param(1, reg_buf, buf_len); + } + } + #endif + + // 其他业务逻辑 + // ... + } +} +``` + +--- + +## 三、总结 +### 核心关键点回顾 +1. **解耦与可裁剪**:Modbus映射模块作为独立功能扩展层,通过`MENU_CONFIG_ENABLE_MODBUS_MAP`宏控制启用/关闭,不影响原有菜单和参数模块的逻辑。 +2. **静态内存与工业级鲁棒性**:使用静态数组存储映射关系,避免动态分配;加入地址范围校验、类型匹配校验、权限校验、字节序适配,确保工业环境下的稳定性。 +3. **灵活映射与数据转换**:支持单/多寄存器映射、参数类型与寄存器类型自动转换、多种字节序配置,适配不同的Modbus从站设备。 +4. **无缝集成**:与原有参数模块联动,参数值与寄存器数据的双向转换自动触发参数的范围检查,无需额外逻辑。 +5. **硬件解耦**:Modbus映射逻辑与硬件接口分离,用户只需在port层实现Modbus硬件的发送/接收接口,即可快速移植到不同的MCU和Modbus硬件(RS485/TCP)。 + +该扩展功能完全遵循工业级嵌入式组件的设计标准,可满足工业控制器、仪器仪表、智能家居等设备中参数与Modbus寄存器的灵活映射需求。 \ No newline at end of file diff --git a/开发文档.md b/开发文档.md new file mode 100644 index 0000000..4432462 --- /dev/null +++ b/开发文档.md @@ -0,0 +1,1555 @@ +你需要基于指定的目录结构,实现一个符合**工业级嵌入式组件标准**的通用菜单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 +#include + +/************************** 全局类型导出 **************************/ +/** + * @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 +#include + +/************************** 内部宏定义 **************************/ +/** + * @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 +#include + +/************************** 核心数据结构 **************************/ +/** + * @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 + +/** + * @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 + +/** + * @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 + +/************************** 外部接口实现 **************************/ +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 + +/** + * @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 + +/** + * @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 + +/************************** 外部接口实现 **************************/ +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 +#include + +/************************** 硬件端口接口声明(用户需实现) **************************/ +/** + * @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. **鲁棒性保障**:加入断言、边界检查、错误码返回、事件超时处理,确保组件在工业环境下稳定运行。 + +该菜单组件完全符合工业级嵌入式组件的标准,可直接应用于各类嵌入式设备(如工业控制器、智能家居、仪器仪表),并支持灵活的功能扩展和硬件适配。 \ No newline at end of file