Files
menu/update.md
2025-12-18 21:24:20 +08:00

819 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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

你需要在已有的工业级嵌入式菜单组件中,拓展**参数与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 <stddef.h>
#include <string.h>
/************************** 内部辅助函数 **************************/
/**
* @brief 转换Modbus协议地址到实际偏移地址如40001→000001→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, &reg_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, &param_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写指令如写保持寄存器0x000040001
// 实际需根据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地址400012个寄存器读写权限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寄存器的灵活映射需求。