29 KiB
29 KiB
你需要在已有的工业级嵌入式菜单组件中,拓展参数与Modbus寄存器地址的灵活映射功能:让每个参数能根据自身性质(数据类型、读写权限、业务属性)匹配对应的Modbus寄存器(包括线圈、离散输入、保持寄存器、输入寄存器),支持寄存器地址的配置、映射查询,以及参数值与寄存器数据的双向转换,同时保持与原有组件的解耦和工业级的鲁棒性。
以下是基于原有目录结构的详细实现方案,遵循分层解耦、静态内存、可配置、类型安全的工业级设计原则。
一、设计核心思路(工业级考量)
- 解耦设计:Modbus映射模块作为独立的功能扩展层,依赖原有参数模块的接口,不侵入核心逻辑。
- 静态内存:使用静态数组存储Modbus映射关系,避免动态分配,适配嵌入式内存约束。
- 类型匹配:参数类型与Modbus寄存器数据类型(1位、8位、16位、32位、浮点)自动转换,支持大小端配置。
- 权限控制:映射时指定寄存器的读写权限(只读/只写/读写),与参数的权限联动。
- 灵活配置:支持单参数映射单寄存器、单参数映射多寄存器(如32位浮点占2个16位保持寄存器)。
- 可裁剪:通过宏开关控制Modbus映射功能的启用/关闭,不影响原有功能。
二、代码实现(按目录结构补充/修改)
1. 对外API层(menu/api/)
1.1 修改menu/api/menu_config.h(添加Modbus映射配置宏)
在原有配置后新增Modbus相关配置,保持可裁剪性:
/************************** 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:
/************************** 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映射的内部结构体(比对外结构体更详细,包含数据转换的辅助信息):
#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映射的静态数组(静态内存,无动态分配):
#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映射的注册、查询、数据双向转换,与原有参数模块无缝集成:
/**
* @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→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层添加硬件接口,与映射逻辑解耦:
#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数据的发送和接收:
#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映射的注册和数据交互:
/**
* @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
// 其他业务逻辑
// ...
}
}
三、总结
核心关键点回顾
- 解耦与可裁剪:Modbus映射模块作为独立功能扩展层,通过
MENU_CONFIG_ENABLE_MODBUS_MAP宏控制启用/关闭,不影响原有菜单和参数模块的逻辑。 - 静态内存与工业级鲁棒性:使用静态数组存储映射关系,避免动态分配;加入地址范围校验、类型匹配校验、权限校验、字节序适配,确保工业环境下的稳定性。
- 灵活映射与数据转换:支持单/多寄存器映射、参数类型与寄存器类型自动转换、多种字节序配置,适配不同的Modbus从站设备。
- 无缝集成:与原有参数模块联动,参数值与寄存器数据的双向转换自动触发参数的范围检查,无需额外逻辑。
- 硬件解耦:Modbus映射逻辑与硬件接口分离,用户只需在port层实现Modbus硬件的发送/接收接口,即可快速移植到不同的MCU和Modbus硬件(RS485/TCP)。
该扩展功能完全遵循工业级嵌入式组件的设计标准,可满足工业控制器、仪器仪表、智能家居等设备中参数与Modbus寄存器的灵活映射需求。