modbus.c
modbus.h)2.3 本文件内嵌的局部静态3. 函数清单4. 核心逻辑解析4.1 RTU 从站接收状态机(handle_modbus_ret_recv_data)4.2 地址合法性校验(is_start_addr_and_num_err)4.3 读映射 get_para_from_reg_addr4.4 写映射 Set_Para_to_Reg_Addr4.5 TCP 命令分派(response_modbus_tcp_cmd)4.6 固件升级流程(0x18 → 0x19 → 0x20 → 0x21 → 0x22)4.7 TCP Socket 状态机(ems_to_modbus_tcp)4.8 Sunspec 协议复用4.9 周期性充放电时间表校验算法4.10 定时/调度时序与 uCOS-II 配合4.11 关键全局变量的读写与触发条件汇总5. 对外接口5.1 本文件导出的函数5.2 本文件导出的全局变量5.3 本文件调用的外部函数/变量(按模块)6. 依赖关系7. 备注与风险点7.1 缓冲区越界风险(高)7.2 协议/语义正确性问题(中高)7.3 并发与共享资源风险(中)7.4 代码质量与维护性(中低)源文件路径:
midware/interface/modbus/modbus.c所属模块:midware/interface/modbus(Modbus 通信接口层) 代码行数:10221
1. 文件职责
本文件是 EMS 主控的 Modbus 从站协议栈实现,同时承载 Modbus RTU(UART3/RS485)与 Modbus TCP(W5500 Socket3,端口 502)两条通道,并对外暴露"寄存器号 ↔ 系统变量"的完整映射。它处在数据流的南向接口位置:外部监控/上位机/云端通过 Modbus 读写寄存器,本文件把它们翻译成对 g_t_logic_info、g_t_ems_info、g_t_bat_info、g_t_meter、g_t_ct、dsp_meter_calibration_data、g_t_dispatch_t 等模块变量的读写,或反向把这些变量的当前值打包成寄存器值回复。RTU 侧由 midware/interface/interface.c 的 task_modbus_rtu 任务在 OSSemPend(g_t_uart3_rcv_sem, …) 收到串口帧后调用 handle_modbus_ret_recv_data() 驱动;TCP 侧由 midware/ethernt/ethernet.c 的网络任务在链路就绪后调用 ems_to_modbus_tcp() 驱动 Socket 状态机。此外,本文件还实现了基于 Modbus TCP 的固件升级服务端(0x18/0x19/0x20/0x21/0x22 五个私有功能码)以及周期性充放电时间表(28 段)的合法性校验与落盘触发。文件体量的大头是两个巨型 switch(读分派 1211 个 case、写分派 493 个 case),真正的控制逻辑只占尾部约 2000 行。
2. 关键数据结构与宏
2.1 本文件定义的全局/静态变量
| 名称 | 类型 | 说明 |
|---|---|---|
buff_Recv_modbus | static u8[LEN_RECV_MODBUS] | RTU 接收组帧缓冲,256 字节,仅本文件可见 |
buff_send_modbus | u8[LEN_RECV_MODBUS](全局导出) | RTU 发送组帧缓冲,256 字节;midware/inverter/pvcharger.c 复用同一缓冲做 PV 充电机主机应答 |
buff_Recv_modbus_tcp | static u8[2*1024],定位在 SDRAM_MODBUS_REV_BUF_ADDR | TCP 接收缓冲 2048 字节,由 hardware/sdram/sdram.h 指定物理地址 |
buff_send_modbus_tcp | static u8[5*1024],定位在 SDRAM_MODBUS_SEND_BUF_ADDR | TCP 发送缓冲 5120 字节,物理地址固定 |
g_u8_read_write_timecount | u8(全局导出) | "当前正在读/写第几个周期性时间段",取值 0~27;<14 表示 control1 组,≥14 表示 control2 组(下标减 14) |
g_u8_save_time_extend1_flag | u8(全局导出) | control1 组有新合法时间段被接受时置位,供落盘判断 |
g_u8_save_time_extend2_flag | u8(全局导出) | control2 组同上 |
modbus_rtu_info | MODBUS_REQ_T(全局导出) | RTU 最近一帧成功解析出的从站地址/功能码/起始地址/数量 |
modbus_tcp_info | MODBUS_TCP_TABLE(全局导出) | TCP 最近一帧的 MBAP 头 + 寄存器请求 |
modbus_tcp_upgrade_table | MODBUS_TCP_UPGRADE_T | TCP 升级会话上下文(升级标志/对象/类型) |
g_t_modbus_config | MODBUS_CONFIG_INFO_T | Modbus 从站配置(地址、波特率、Sunspec 使能与地址、Sunspec 版本),带 CRC 存 Flash |
g_t_config_save_flag | CONFIG_SAVE_FLAG_T | 13 个"参数已变更需落盘"标志,本文件大量置位 |
g_t_time_control_run | TIME_CONTROL_RUN_TAB_T | 周期性时间段的暂存(staging)区,写寄存器先落到这里,校验通过后才拷进 g_t_logic_info.t_config_extend1_data/2_data |
g_t_ate_ctrol | ATE_CTROL_T | ATE 测试控制字(链路状态、LED 控制、CAN 透传测试) |
2.2 使用到的宏/常量(定义于 modbus.h)
| 名称 | 类型 | 说明 |
|---|---|---|
LEN_RECV_MODBUS | 宏 | 256,RTU 缓冲长度 |
PVCHARGR_BROADCAST_ADDR | 宏 | 0,PV 充电机广播地址;本文件所有"广播帧不回复"判断都基于它 |
FUNC_READ_REGISTER | 宏 | 0x03 |
FUNC_WRITE_SINGLE_REGISTER | 宏 | 0x06 |
FUNC_WRITE_REGISTER | 宏 | 0x10 |
FUNC_QUERRY_UPGRADE_REGISTER | 宏 | 0x18,返回升级前置 JSON |
FUNC_CHECK_UPGRADE_REGISTER | 宏 | 0x19,校验升级信息 |
FUNC_UPDATE_SYSTEM_REGISTER | 宏 | 0x20,下发升级配置(PDU 30 字节) |
FUNC_WRITE_FIRMWARE_CONTENT_REGISTER | 宏 | 0x21,写固件分片 |
FUNC_QUERY_UPGRADE_PROGRESS_REGISTER | 宏 | 0x22,查升级进度 |
MODBUS_TCP_PDU_ADDR | 宏 | 7,TCP 报文中功能码所在下标;所有 TCP 字段偏移都写成 MODBUS_TCP_PDU_ADDR + n |
TCP_PDU_CONFIG_LENGTH | 宏 | 0x1E(30),升级配置报文长度字段必须等于它 |
MD5_MAX_BYTE / STRING_ZERO | 宏 | 16 / "00000000",MD5 长度与"未填写"判定 |
UPOBJECT / FIRM_VERSION / FACTORY_MODEL | 宏 | 升级 JSON 三个键名 |
MAX_ONES_PER_BIT | 宏 | 4,同一星期位允许的最大充/放电段数 |
MODBUS_EX_* | 枚举 | 协议异常码(MODBUS_EX_NONE_E、MODBUS_EX_ILLEGAL_DATA_ADDRESS_E、MODBUS_EX_ILLEGAL_DATA_VALUE_E 等) |
REG_TREU_E / REG_ILLEGAL_DATA_ADDRESS_E | 枚举 | REG_MOSBUS_ERR_E 中的内部判定码,本文件大量作为返回值 |
SUNSPEC_STRU_NUM | 宏(来自 sunspec_modbus.h) | 783,Sunspec 结构体数组长度,作为 sunspec_modbus_data_get/set 的边界参数 |
REG_MODBUS_E | 枚举(来自 modbus.h) | 全部寄存器地址常量,REG_MAX_MODBUS_E ≈ 0x2351 作为全局地址上界 |
MODBUS_UPGRADE_OBJECT_E / UPGRADE_CONFIG_ERR_E | 枚举 | 升级对象(EMS/APP2/BMU/LMU/ISO/INV_MASTER/INV_SLAVE/INV_ARM/BMS)与升级配置错误码 |
SDRAM_MODBUS_REV_BUF_ADDR / SDRAM_MODBUS_SEND_BUF_ADDR | 宏(hardware/sdram/sdram.h) | TCP 收发缓冲的物理地址 |
SOCK_TCP_SERVER / MEC_SERVER_PORT | 宏(midware/ethernt/ethernet.h) | 3 / 502,Modbus TCP 用 Socket 号与端口 |
BMS_FIRM_DOWNLOAD / INVC_FIRM_DOWNLOAD / INV_ARM_DOWNLOAD / INFO_MAX | 宏(net/server.h) | 固件 MD5 校验的类型参数 |
SPECIAL_CMD_UPDATE_EMS / _EMS_APP2 / _BMS / _INV / _INV_ARM | 宏(net/server.h) | 升级 JSON 中 "Upgrade object" 的期望取值 |
MODBUS_RTU_DEBUG | 宏(未在任何头文件中定义) | 第 8408 行 #if (MODBUS_RTU_DEBUG == 1) 使用;未定义时按 0 处理,故超时打印被裁掉 |
2.3 本文件内嵌的局部静态
| 名称 | 类型 | 说明 |
|---|---|---|
g_u8_set_logic_config_flag | static u8(Set_Para_to_Reg_Addr 函数内,第 5345 行) | 只在写明一个周期性时间段字段时置 TRUE,函数返回前检查并清零,然后调用 time_charge_discharge_save_check()。与 g_t_config_save_flag.u8_set_logic_config_flag 同名不同物,极易误读 |
s_u8_socket_status / s_u8_socket_set_flag | static(ems_to_modbus_tcp 内) | 记录 Socket 上次状态以抑制重复日志;以及 listen 成功后的 keep-alive 一次性设置标志 |
3. 函数清单
| 函数名 | 作用域 | 说明 | |
|---|---|---|---|
is_start_addr_and_num_err | static | 判断从站地址与"起始地址 + 数量"是否落在某个已声明的寄存器段窗口内,返回 REG_TREU_E 或 REG_ILLEGAL_DATA_ADDRESS_E | |
get_para_from_reg_addr | 全局 | 读分派:按 (device_addr, reg_addr_modbus) 从系统变量取 1 个 16 位寄存器值(1211 个 case) | |
Set_Para_to_Reg_Addr | static | 写分派:按 (device_addr, reg_addr_modbus, value) 把 1 个 16 位值写入系统变量/调用设置接口(493 个 case) | |
modbus_send_data | 全局 | 清 UART3 接收缓冲后调用 uart3_send_data(buf, len, 1000) 发送 RTU 帧 | |
modbus_data_is_recv_async | 全局 | 包装 uart3_is_recv_async_data2(),查询是否有异步待取字节 | |
modbus_get_data_byte | 全局 | 包装 uart3_read_data_async(),取 1 字节 | |
handle_Modbus_response | 全局(未在头文件声明) | 组 0x03 读响应帧:地址 + 0x03 + 字节数 + 逐寄存器数据 + CRC | |
handle_modbus_write_single_response | 全局(未声明) | 组 0x06 写单寄存器响应(回显地址与值),并执行 Set_Para_to_Reg_Addr | |
Handle_Modbus_Write_Response | 全局(未声明) | 组 0x10 写多寄存器响应(回显起始地址与数量),并逐寄存器执行写入 | |
Handle_Modbus_Exception | 全局(未声明) | 组异常响应:地址 + (功能码\ | 0x80) + 异常码 + CRC |
handle_modbus_ret_recv_data | 全局 | RTU 从站主状态机:轮询 UART3 异步字节,按状态机逐字节组帧、校验 CRC、校验地址范围、执行读/写并回复 | |
check_upgrade_infomation_for_cmd | 全局(未声明) | 对单个升级目标校验 JSON 中的对象名/固件版本/机型,返回 6 位错误位图 | |
check_upgrade_infomation | 全局(未声明) | 从 TCP 缓冲抽取 JSON,按目标对象选择固件版本/机型来源后调用上者 | |
set_update_system_config | 全局(未声明) | 解析 0x20 升级配置 PDU:升级标志、厂商标识 "311"、目标对象、机型码、MD5、文件长度,并初始化升级上下文 | |
write_firmware_content | 全局(未声明) | 解析 0x21 固件分片:校验包长与偏移,按目标对象把数据写入 SPI Flash 对应固件区 | |
response_modbus_tcp_cmd | static | TCP 命令分派与响应组帧(0x03/0x06/0x10/0x18/0x19/0x20/0x21/0x22 共 8 个分支) | |
handle_modbus_tcp_recv_data | 全局(未声明) | 从 Socket 接收 MBAP 头 + PDU,解析字段、做地址范围校验,再调用 response_modbus_tcp_cmd | |
ems_to_modbus_tcp | 全局 | Modbus TCP Socket 状态机(SOCK_INIT/SOCK_ESTABLISHED/SOCK_CLOSE_WAIT/SOCK_CLOSED),并周期性推送电网采样曲线 | |
set_flag_modbus_force_update | 全局 | 设置 modbus_tcp_upgrade_table.u8_update_flag | |
get_flag_modbus_force_update | 全局 | 读取该标志(ethernet.c/net/ems.c 用它屏蔽服务器侧升级) | |
is_time_period_overlap | 全局 | 归一化起止顺序后判断两个时间段是否重叠 | |
is_timeperiod_charge_insert | 全局 | 校验新充电时间段是否可插入(起 < 止,且不与已有的其它段重叠) | |
is_timeperiod_discharge_insert | 全局 | 放电时间段版本,逻辑同上 | |
count_chargebits_inposition | 全局 | 统计指定星期位上已使能的充电段数(排除当前正在写入的那一段) | |
count_dischargebits_inposition | 全局 | 统计指定星期位上已使能的放电段数 | |
chargebit_over_check | 全局 | 逐星期位校验"已有 + 新增"是否超过 MAX_ONES_PER_BIT | |
dischargebit_over_check | 全局 | 放电版本,逻辑同上 | |
time_charge_discharge_save_check | 全局 | 比较暂存区与已存区,合法性通过后把 10 个字段逐个拷入 g_t_logic_info.t_config_extend1_data/2_data 并置落盘标志 | |
modbus_sunspec_en_set | 全局 | 单独设置 Sunspec 使能位并置 u8_set_modbus_config_flag | |
modbus_sunspec_addr_set | 全局 | 单独设置 Sunspec 地址(禁 0、禁等于主站地址)并置落盘标志 | |
modbus_sunspec_en_addr_set | 全局 | 由 16 位复合值(高字节使能、低字节地址)同时设置,并触发 sunspec_modbus_data_init() |
4. 核心逻辑解析
4.1 RTU 从站接收状态机(handle_modbus_ret_recv_data)
入口与超时:函数一进来用 start_temp_tick = OSTimeGet() 记时;主 while(1) 每轮先判 (OSTimeGet() - start_temp_tick) >= 300。由于 OS_TICKS_PER_SEC = 1000(UCOSII/uCOS-CONFIG/os_cfg.h:51),即整帧接收窗口 300 ms,超时直接 return,不回异常帧。没有待取字节时 continue 空转(忙等,不让出 CPU,见风险点)。
状态机(E_MODBUS_Recv_Meter_State):
| 当前状态 | 触发条件 | 动作 | 迁移 |
|---|---|---|---|
ADDR_M | 字节 == g_t_modbus_config.u8_address_modbus,或 == u16_sunspec_addr 且 u8_sunspec_en == TRUE,或 == 0(广播)且 pvcharger_read_machine_mode() == PVCHARGR_FROM_E | 清 buff_Recv_modbus、idx = 0、写入首字节 | → FUNC_M |
FUNC_M | 字节 ∈ {0x03, 0x06, 0x10} | buff_Recv_modbus[idx++] | → REGISTER_BITS_8_15,register_temp = 0 |
FUNC_M | 其它功能码 | 不记录 | → ADDR_M(静默丢弃) |
REGISTER_BITS_8_15 | 无条件 | 记起始地址高字节 | → REGISTER_BITS_0_7 |
REGISTER_BITS_0_7 | 地址在允许窗口内(见下) | 若功能码是 0x06,令 data_len_temp = 2 | 0x06 → DATA_RECVD;否则 → REGISTER_NUM_8_15 |
REGISTER_BITS_0_7 | 地址越界 | 置 l_u8_exception_code = MODBUS_EX_ILLEGAL_DATA_ADDRESS_E | → EXCEPTION_E |
REGISTER_NUM_8_15 / REGISTER_NUM_0_7 | register_temp + register_num_temp - 1 仍在窗口内 | 记录数量 | 0x03 → CRC_Bits_0_7(跳过字节数域);0x10 → LEN_0_7 |
REGISTER_NUM_0_7 | 越界 | 置异常码 | → EXCEPTION_E |
LEN_0_7(仅 0x10) | byte == register_num_temp * 2 | data_len_temp = byte | → DATA_RECVD |
LEN_0_7 | 字节数不匹配 | l_u8_exception_code = MODBUS_EX_ILLEGAL_DATA_VALUE_E | → EXCEPTION_E |
DATA_RECVD | 0x06:data_len_temp + 4 <= idx;其它:data_len_temp + 7 <= idx | 持续收集数据字节 | → CRC_Bits_0_7 |
CRC_Bits_0_7 | 无条件 | 暂存 CRC 低字节 | → CRC_Bits_8_15 |
CRC_Bits_8_15 | crc_16 == GetCrcCode_Modbus(buff_Recv_modbus, idx) | 填充 modbus_rtu_info(0x06 时强制 num_reg = 0x01),Sunspec 帧调用 sunspec_refresh_flag_set(),再调 is_start_addr_and_num_err | 合法 → 执行读/写并 return;非法 → 发异常帧 |
CRC_Bits_8_15 | CRC 不匹配 | 置 MODBUS_EX_ILLEGAL_DATA_VALUE_E | → EXCEPTION_E |
EXCEPTION_E | 无条件 | 调 Handle_Modbus_Exception(modbus_rtu_info, code) 后 return | — |
窗口判定内联在两处,条件为三者之一:主站地址且 register_temp < REG_MAX_MODBUS_E;Sunspec 地址且使能且 REG_SUNSPEC_START ≤ addr < REG_MAX_SUNSPEC_MODBUS;广播地址 0 且 PV 充电机模式为从机且地址落在 REG_PV_CHARGER_CONFIG_START_E ~ REG_PV_CHARGER_CONFIG_MAX_E。数量域用 register_temp + num - 1 判上界。
执行阶段(CRC 通过且范围合法):
- 0x03 →
handle_Modbus_response(modbus_rtu_info) - 0x06 →
handle_modbus_write_single_response(modbus_rtu_info, &buff_Recv_modbus[4])(值在帧内下标 4~5) - 0x10 →
Handle_Modbus_Write_Response(modbus_rtu_info, &buff_Recv_modbus[7])(数据在帧内下标 7 起,前面是 addr/reg/num/len)
响应组帧格式(RTU,低字节先发 CRC):
- 读响应:
从站地址 | 0x03 | num_reg*2 | data_hi,data_lo ... | CRC_lo,CRC_hi;数据由get_para_from_reg_addr()逐寄存器取值,Sunspec 分支改用sunspec_modbus_data_get(reg, SUNSPEC_STRU_NUM)。 - 写单响应:
从站地址 | 0x06 | 起始地址高,低 | 值高,低 | CRC(标准回显);执行Set_Para_to_Reg_Addr后再组帧。 - 写多响应:
从站地址 | 0x10 | 起始地址高,低 | 数量高,低 | CRC;执行时循环Set_Para_to_Reg_Addr(addr + i, data[2i]<<8 | data[2i+1])。 - 异常响应:
从站地址 | 功能码|0x80 | 异常码 | CRC。 - 写 0x06/0x10 收尾的统一动作:若
g_t_config_save_flag.u8_set_time_flag被置位(系统时间寄存器被写),立即清零并调Set_RTC(&SYSTEM_DATETIME),即"写时间寄存器后马上写 RTC"。 - 广播 (地址 0) 一律不回复:三个响应函数都在组帧前
if (req_master.u8_local_addr == PVCHARGR_BROADCAST_ADDR) return;。注意handle_Modbus_response的该判断在取值之前,因此广播读被完全丢弃;而写路径在执行之后判断,所以广播写"生效但不应答",符合 RTU 广播语义。
4.2 地址合法性校验(is_start_addr_and_num_err)
外层判"从站地址匹配",内层逐个 if/else if 比较 16 个地址段窗口,每段内部的判定模板是:
if (段起始 <= addr && addr < 段结束)
if (段起始 <= addr + num - 1 && addr + num - 1 < 段结束) return REG_TREU_E;
return REG_ILLEGAL_DATA_ADDRESS_E;
覆盖的段包括:电表 REG_MAX_METER_E、EMS 指令组 0x1150~0x1177、电池、EV、电池 SN、逆变器、ALPHAESS、逆变器信息、HHE、系统 REG_SYS_MAX_E、Safety、ATE、校准、DEBUG、SYS_CUSTOM、SOC 校准、AUX、GC 油机、PV 充电机;另有一条 Sunspec 独立分支(要求 u8_sunspec_en == TRUE)。都不匹配则返回 REG_ILLEGAL_DATA_ADDRESS_E。
关键特性:该函数只校验"地址落在某个已声明段内",不校验该地址是否真的被
get_para_from_reg_addr/Set_Para_to_Reg_Addr实现了case。因此写入段内未定义的寄存器会走到default: break;,被默默忽略,但仍回正常的 0x06/0x10 成功响应。
4.3 读映射 get_para_from_reg_addr
单一巨型 switch(reg_addr_modbus),1211 个 case,每个 case 只负责一个 16 位寄存器并 break;default 时返回初值 l_u16_ret = 0。典型取值模式:
- 直接透传:
l_u16_ret = g_t_logic_info.t_config_data.X - 32 位量的高低字拆读:
>> 16/& 0xFFFF(如meter_read_eoutput(GRID_METER, PHASE_T)、PV_Capacity_Storage) - 缩放取值:
(meter_read_voltage(GRID_METER, PHASE_A) / 10) & 0xFFFF(电压缩小 10 倍以适配 16 位)、meter_read_current(...) / 10 - 组合字:
REG_TIME_PERIOD_CONTROL_FLAG_E返回(Time_Allow_Dischg_Enable << 1) | Time_Force_Chg_Enable,与写侧的三值映射互为逆运算 - 复合编码:
REG_EMS_SUNSPEC_SET_E = (u8_sunspec_en == TRUE) << 8 | (u16_sunspec_addr & 0x00FF),与写侧modbus_sunspec_en_addr_set(value)成对 - 版本字符串解析:
sscanf(EMS_SOFT_VERSION_SAFTY, "V%c.%c%c%c", ...)后按字节返回 - 周期性时间段读:先读/写
REG_NEW_TIME_GROUP_PERIOD_READ_WRITE_E(返回g_u8_read_write_timecount)选定槽位,再连续读同组其它寄存器;g_u8_read_write_timecount < 14时取t_config_extend1_data.logic_time_charge_dicharge_control1[idx],否则取t_config_extend2_data.logic_time_charge_dicharge_control2[idx - 14] - 干接点
REG_EMS_DO0_E/DO1_E/DI0_E/DI1_E恒定返回 0(占位未实现) - 校验标志位
REG_EMS_GET_FLAG_E的实现被注释掉(第 320 行),恒返回 0
buffer/device_addr 参数只在函数开头用于"地址是否匹配"判断(device_addr == u8_address_modbus 或 device_addr == 0 且地址落在 PV 充电机段),进入 switch 后 device_addr 不再参与分支——即同编号寄存器对不同从站地址返回同一份数据,PV 充电机的差异只体现在地址窗口上。
4.4 写映射 Set_Para_to_Reg_Addr
结构完全对应读侧,switch 有 493 个 case,四种写法:
- 直接赋值:
g_t_logic_info.t_config_data.X = value;随后置位对应的g_t_config_save_flag.*。 - 32 位高低字分两次拼装(读-改-写):
X = (value << 16) | (X & 0xffff); / X = (X & 0xffff0000) | value;
这是非原子的读-改-写,若主站只写了一个字,另一个字来自上次的缓存值(见风险点)。
- 调用模块接口:如
meter_set_meter_ct_enable(GRID_METER, value == TRUE)、bms_set_mos_enable(value >> 8)、inverter_set_three_unbalance_mode(value)、inverter_write_safety(value)、modbus_sunspec_en_addr_set(value)、Set_RTC(&SYSTEM_DATETIME)等。 - 带范围钳制的赋值,典型模式:
- 小时:
if ((u8)value > 23) value = 0; - 分钟:
if ((u8)value > 59) value = 0; - 周期性段起止分钟:
if (value < (24*60)) {...} - 充/放截止 SOC:
if (value <= 100 && value >= 10) - 充/放功率:
if (value <= 10000 && value >= 1) - 星期使能掩码:
if (value <= 127) - 时间段总数:
if (value < 28) g_u8_read_write_timecount = value; - 时间控制模式:内层
switch(value)把 0/1/2/3 映射为Time_Force_Chg_Enable与Time_Allow_Dischg_Enable的四种组合,default归零。
- 小时:
注意这些钳制都是静默丢弃/归零,不返回异常码给主站。
- 特殊副作用:
REG_MODBUS_BAUD_RATE_E写入且值变化时立即uart3_init(9600/115200/256000)切波特率;REG_RESET_MODE_E内层switch支持1=energy_reset()、2=meter_reset()、4=factory_reset()、8=先 OSTimeDlyHMSM(0,0,5,0) 再 system_hard_reset();REG_ATE_EMS_RESTART_DSP==TRUE调inverter_reset()。
- 函数尾部固定动作(第 8146~8152 行):若函数内静态
g_u8_set_logic_config_flag == 1,清零并调用time_charge_discharge_save_check()。也就是说"周期性时间段合法性校验 + 落盘"只在这条路径上触发一次。
4.5 TCP 命令分派(response_modbus_tcp_cmd)
TCP 报文 = 6 字节 MBAP 头(事务标识 2 + 协议标识 2 + 长度 2)+ 单元标识符(下标 6)+ PDU(下标 7 起)。响应一律先回填 transaction_id、protocol_id 以对齐请求。各功能码:
| 功能码 | 分支行为 | 响应格式 | |
|---|---|---|---|
0x03 | 循环 get_para_from_reg_addr 取 num_reg 个寄存器 | 头 + len = num_reg*2 + 3 + 单元 + 0x03 + num_reg*2 + 数据 → send(SOCK_TCP_SERVER, ...) | |
0x06 | 先组帧(回显 addr + 值),再 Set_Para_to_Reg_Addr(...);若 u8_set_time_flag 置位则 Set_RTC | 头(len=0x0006) + 单元 + 0x06 + 地址 2B + 值 2B | |
0x10 | 循环 `Set_Para_to_Reg_Addr(addr + i, buf[2i]<<8 \ | buf[2i+1])`;同样处理 RTC | 头(len=0x0006) + 单元 + 0x10 + 起始地址 2B + 数量 2B |
0x18 | 按目标对象用 sprintf 拼一段 JSON:{"Upgrade object":"…","Firmware Version":"…","Factory model":"…"},版本/机型分别来自 get_ems_hard_version()/get_ems_hard_type()、bms_read_hard_version()/bms_read_battery_model()、inverter_read_hw_ver()/inverter_read_system_modle() | 头 + len = strlen(json)+3 + 单元 + 0x18 + 对象码 + JSON 原文(无 CRC) | |
0x19 | check_upgrade_infomation() 校验,返回值 0 表示通过 | 通过:len=0x0C,0x55 0x19 <对象> 0x20 0x01 0x33 0x31 0x31 后接 0x01 0x00 <主版本> <次版本>;失败:len=0x03,0x55 0x99 <错误位图> | |
0x20 | set_update_system_config() | 成功:单元 0x20 0x06;失败:单元 0xA0 0x15 | |
0x21 | write_firmware_content(),成功后若已收满则做 MD5 收尾 | 成功:单元 0x21 0x06;失败:单元 0xA1 (0x15 + ret) | |
0x22 | 查询升级进度 | 头部长度写 0x04:单元 0x22 <对象> <百分比>;无升级进行中时 0x03:`单元 (0x22\ | 0x80) 0x15` |
0x19 的版本解析采用固定格式假设:l_u8_master_vsrsion = hard_version[1] - '0'、l_u8_slave_version = (hard_version[3]-'0')*10 + (hard_version[4]-'0'),即要求字符串形如 Vd.dd。
4.6 固件升级流程(0x18 → 0x19 → 0x20 → 0x21 → 0x22)
- 查询(0x18):主站先问目标设备的当前固件版本与机型。
- 校验(0x19):
check_upgrade_infomation()从buff_Recv_modbus_tcp[MODBUS_TCP_PDU_ADDR+2+16]起按"遇到字节0xFF即结束"截取 JSON,长度上限 256;再按对象码选择期望的cmdStr / firmVer / model交给check_upgrade_infomation_for_cmd()。后者用json_get_value()取三个键,逻辑是"取不到 → 置位;取到但memcmp不等 → 置位",返回 6 位位图(bit0/1 对象、bit2/3 版本、bit4/5 机型;奇数位表示"键缺失",偶数位表示"值不匹配")。版本比较长度是strlen(firmVer) - minusNum,其中minusNum = 1(当对象是 EMS 或 APP2 时),即 EMS/APP2 故意不比较固件版本号的最后一个字符。 - 配置(0x20):
set_update_system_config()要求u16_data_len == 0x1E,逐字段解析:PDU+1 升级标志(必须START_UPGRADE_E)、PDU+2~4 厂商标识(必须0x33 0x31 0x31即 ASCII"311")、PDU+5 对象码、PDU+6 机型码(BMU 必须0x1A,LMU/ISO 必须0x19)、PDU+7~8 硬版本两字节(两者不能同时为 0,否则视为错误并据此格式化"V%d.%02d"写入g_stEMSInfo.pstCurrUpt->as8HardVer)、PDU+9~24 共 16 字节 MD5(用sprintf(..., "%02X")逐字节转 32 位十六进制存as8MD5Code,并检查前 8 字符不等于"00000000")、PDU+25~28 四字节大端文件总长度(不能为 0)。全部通过后设置pstCurrUpt->bIsUpdateForce = TRUE、u32LenFile、u32OffsetFile = 0,调set_updt_status_current(UPDT_STS_UPDATING)。对象到槽位的映射为 EMS/APP2→UPDT_EMS,BMS/BMU→UPDT_BMS,INV_MASTER→UPDT_INV1,INV_SLAVE→UPDT_INV2,INV_ARM→UPDT_INV3。 - 写分片(0x21):
write_firmware_content()要求 PDU+0 ==0x21,读 PDU+1~2 分片长度、PDU+3~6 大端文件偏移(返回码 1 = 长度不符,2 = 偏移不符)。数据起点是&buff_Recv_modbus_tcp[14](= MBAP 6 字节 + 单元 1 + PDU 头部 7 字节)。然后按对象调save_fw_to_spi_flash(data, len, UPDT_xxx)并increase_current_offset_file_updt(len);EMS 与 APP2 用set_save_firm_in_app_flash_flag(TRUE/FALSE)区分写入 APP1/APP2 分区。每次成功响应后判断get_current_point_offset_file_updt() >= pstCurrUpt->u32LenFile,若已收满则按对象调calculate_ems_flash_md5()或calculate_dev_FW_Flash_MD5(UPDT_xxx, g_u8_fw_sdram1/2, <类型>)做整包校验,随后清空升级上下文(bIsUpdateForce、bIsGotUpdtFileInfo、UPDT_STS_NONE、u8_update_flag、u8_update_object、u8_update_type、pstCurrUpt = NULL),置bIsUpdatedFromServer = TRUE。 - 进度(0x22):PDU+1 为 0 时按"当前正在升级谁"自动判断(依次问
IsUpgradeBMS()、inverter_read_upgrade_flag_master/slave/arm()),否则按指定对象返回;百分比来自GetPercentUpgradeBMS()或inverter_read_upgrade_percent_master/slave/arm()。
set_flag_modbus_force_update / get_flag_modbus_force_update 操作同一个 modbus_tcp_upgrade_table.u8_update_flag,被 midware/ethernt/ethernet.c:1468 和 net/ems.c:4658 用来在"Modbus 正在强制升级"时屏蔽服务器下发的升级,避免两条升级通道打架。
4.7 TCP Socket 状态机(ems_to_modbus_tcp)
由 midware/ethernt/ethernet.c:1035 在网络任务中每轮调用,使用 SOCK_TCP_SERVER = 3、端口 MEC_SERVER_PORT = 502:
getSn_SR() 状态 | 动作 |
|---|---|
SOCK_INIT | listen(SOCK_TCP_SERVER);成功则置 s_u8_socket_set_flag,下一轮用 setsockopt(SOCK_TCP_SERVER, SO_KEEPALIVEAUTO, &l_u8KeepAliveCnt=12) 打开自动 Keep-Alive(12 个周期) |
SOCK_ESTABLISHED | 若 getSn_IR() & Sn_IR_RECV:清中断标志、l_u16_len = getSn_RX_RSR(...),长度 > 0 时置 g_stEMSInfo.b_modbus_Socket = TRUE 并调 handle_modbus_tcp_recv_data(SOCK_TCP_SERVER);随后无条件检查电网采样曲线发送标志 |
SOCK_CLOSE_WAIT | close(SOCK_TCP_SERVER) |
SOCK_CLOSED | socket(SOCK_TCP_SERVER, Sn_MR_TCP, 502, Sn_MR_ND) 重新打开,并置 b_modbus_Socket = FALSE |
电网采样曲线主动推送:当 g_grid_sampling_curve_t.u8_sampling_curve_en == TRUE 且 u8_sampling_curve1_send_flag(或 2)为 TRUE 时,组一帧"MBAP 头 + 0xAA 0x55 + cnt 组 ×2 个 16 位大端值 + Modbus CRC"直接 send,然后清零计数与发送标志。注意该帧并非 Modbus PDU 格式,却复用了 MBAP 头字段并附加了 Modbus CRC。
接收侧(handle_modbus_tcp_recv_data):先 memset 清 2048 字节缓冲;轮询 getSn_RX_RSR() < 6 直到收到 6 字节头或超过 1000 tick(=1 s);recv(..., 6) 取 MBAP 头,若不足 6 字节直接返回;解析 l_u16DataLen 并钳制到 sizeof(buff_Recv_modbus_tcp) - 6;再循环 recv 直到凑满 l_u16DataLen,累计超 1000 tick 则 break,未收满即返回。随后解析 transaction_id / protocol_id / data_len / 单元标识符,只接受"单元号 == 主站地址"或"单元号 == Sunspec 地址且使能"的报文;功能码只识别 0x03/0x06/0x10,其中 0x06 强制 num_reg = 1、0x10 从下标 10~11 取数量并把数据指针置为 &buff_Recv_modbus_tcp[13];地址校验同样走 is_start_addr_and_num_err,失败时回一条 功能码|0x80 + REG_ILLEGAL_DATA_ADDRESS_E 的异常帧。
4.8 Sunspec 协议复用
Sunspec 与私有寄存器表共用同一套收发链路,靠"单元标识符 + 地址窗口"分流:
- 判定:
u8_sunspec_addr == 帧地址 && u8_sunspec_en == TRUE - 地址窗口:
REG_SUNSPEC_START (0x9C40) ~ REG_MAX_SUNSPEC_MODBUS (0xA052) - 读:
sunspec_modbus_data_get(reg, SUNSPEC_STRU_NUM);写:sunspec_modbus_data_set(reg, SUNSPEC_STRU_NUM, value) - 每收到一个 CRC 正确的 Sunspec 帧就调
sunspec_refresh_flag_set()刷新通信时间戳 - 使能/地址的配置入口是寄存器
REG_EMS_SUNSPEC_SET_E = 0x07F0:写入 16 位值,高字节 = 使能、低字节 = 地址,交给modbus_sunspec_en_addr_set()拆解。使能或地址发生变化时,会置g_u8appConfigFlagExtend2 = FALSE、Set_Setup_Config_Extend2_Status(FALSE)(标记扩展配置未就绪,促使重新上报),并在使能时调用sunspec_modbus_data_init() - 读回值由
(u8_sunspec_en == TRUE) << 8 | (u16_sunspec_addr & 0x00FF)拼装,与写侧对称
4.9 周期性充放电时间表校验算法
数据模型:共 28 个时间段槽位,前 14 个存 t_config_extend1_data.logic_time_charge_dicharge_control1[],后 14 个存 t_config_extend2_data.logic_time_charge_dicharge_control2[]。当前操作槽位由全局 g_u8_read_write_timecount(0~27)决定,凡涉及数组下标处都写成 idx < 14 ? idx : idx - 14 的三元形式。
流程(主站先写 REG_NEW_TIME_GROUP_PERIOD_READ_WRITE_E 选槽 → 再依次写该段的起止分钟、截止 SOC、功率、星期掩码):
- 每次写入落到暂存区
g_t_time_control_run,并置函数内静态g_u8_set_logic_config_flag = TRUE。 Set_Para_to_Reg_Addr返回前若该标志为 1,清标志并调time_charge_discharge_save_check()。time_charge_discharge_save_check()先用memcmp比较暂存段与已存段的LOGIC_TIME_CONTROL_TAB_T:- 对当前槽位所在的组(
idx < 14用 extend1,否则用 extend2)做memcmp,相同则什么也不做(幂等写入不重复落盘)。 - 若不同,则分别尝试充电与放电两条独立通道:
is_timeperiod_{charge,discharge}_insert(...) && {charge,discharge}bit_over_check(...),都通过才把暂存段的字段逐个拷贝到g_t_logic_info.t_config_extend{1,2}_data对应槽位,并置g_u8_save_time_extend{1,2}_flag = TRUE。充电侧拷贝的是charger_start_time_minutes / charger_end_time_minutes / charger_cut_soc / charger_grid_power / time_charge_week_day;放电侧拷贝discharger_start_time_minutes / discharger_end_time_minutes / discharger_cut_soc / discharger_power / time_discharge_week_day。 - 同时比较并同步
weekday_charge_discahrge_en(全局总开关),变化时置u8_set_logic_config_flag。 - 最后若
g_u8_save_time_extend1_flag/2_flag为真,清标志并置g_t_config_save_flag.u8_set_logic_config_flag2/..._flag3,交由落盘流程持久化。
- 对当前槽位所在的组(
重叠判定(is_time_period_overlap):先做"起止归一化"(若 start > end 就交换,从而容忍跨零点时间段),再返回 !((e_end <= n_start) || (e_start >= n_end)),即区间半开相交。
插入判定(is_timeperiod_charge_insert / ..._discharge_insert):
if (当前 start >= 当前 end) return FALSE; // 起点必须早于终点
for (i = 0; i < timecount(=14); i++)
if (i != 当前槽位)
若与 exist1.control1[i] 重叠 或 与 exist2.control2[i] 重叠 → return FALSE;
return TRUE;
即新段必须与"已存的两组数组中除自己以外的所有同序号段"都不重叠。
位数统计与上限(count_{charge,discharge}bits_inposition + {charge,discharge}bit_over_check):
count_*:对给定的position(0~6,对应周一到周日位;position >= 7直接返回FALSE)统计两组数组中该位为 1 的段数,并跳过当前正在写入的那一段(因为它尚未落盘):g_u8_read_write_timecount < 14时跳过control1中的当前槽,control2全部计入;- 否则
control1全部计入,跳过control2中的当前槽。
*_over_check:对i = 0..6逐个星期位计算current_bitcount + new_bitcount,任一超过MAX_ONES_PER_BIT(4)即返回FALSE。也就是每天每个方向最多 4 个时间段。
4.10 定时/调度时序与 uCOS-II 配合
- RTU 驱动:
midware/interface/interface.c的task_modbus_rtu使用OSSemPend(g_t_uart3_rcv_sem, 100, &err)阻塞等待 UART3 接收信号量,OS_ERR_NONE时才调handle_modbus_ret_recv_data();随后若 Sunspec 使能且sunspec_refresh_flag_get() != 0则调sunspec_modbus_data_refresh()。Pend 超时期 100(tick,=100 ms)用于避免切换到充电桩功能后任务卡死。因此本文件不创建任务、不使用信号量/邮箱,只被任务直接调用,是纯被动函数库。 - 帧内时序:RTU 用
OSTimeGet()实现 300 tick 的整帧接收超时;TCP 用 1000 tick 的头接收超时和 1000 tick 的整包接收超时。所有超时都以"软件轮询 + tick 比较"实现,未使用硬件定时器或看门狗。 - TCP 驱动:
midware/ethernt/ethernet.c:1035在网络任务中每轮调ems_to_modbus_tcp(),由 W5500 中断寄存器Sn_IR_RECV触发实际收包;发送采样曲线在同一轮内完成。 - 落盘时序:本文件只置
g_t_config_save_flag的位,Flash 写入由其它模块(data/system_config/system_config.c)在配置任务中完成,二者通过共享结构体标志做松耦合。唯一例外是系统时间:写时间寄存器后handle_*会立即(同一次调用内)调用Set_RTC(),不等待落盘任务。 - 看门狗与超时:
handle_modbus_ret_recv_data内部的while(1)在无字节时continue忙等(最长 300 tick),期间不让出 CPU,在高优先级任务中会挤占同级/低级任务(见风险点)。
4.11 关键全局变量的读写与触发条件汇总
| 变量 | 本文件写点 | 本文件读点 | 触发条件 |
|---|---|---|---|
g_t_modbus_config | u8_address_modbus、u8_modbus_baud_rate(含 uart3_init)、Sunspec 位(经 modbus_sunspec_* 系列) | 几乎每个函数的地址/使能判定 | 写 REG_MODBUS_ADDRESS_E / REG_MODBUS_BAUD_RATE_E / REG_EMS_SUNSPEC_SET_E |
g_t_config_save_flag.* | 13 个位全被置位 | u8_set_time_flag 在三个响应函数中读取 | 任何写寄存器分支;u8_set_time_flag 触发 Set_RTC |
g_u8_read_write_timecount | 写 REG_NEW_TIME_GROUP_PERIOD_READ_WRITE_E(值 < 28 时) | 读同寄存器返回;所有周期性段读写分支 | 主站选段 |
g_t_time_control_run | 写暂存段所有字段 | memcmp 与逐字段拷贝 | 主站写 REG_NEW_TIME_* 系列 |
g_t_logic_info | 通过写分派间接修改(时间段、馈网百分比、PV 容量、时间控制模式、南澳调度、油机等) | 读分派大量读取 | 主站读写对应寄存器 |
g_t_config_extend1/2_data | time_charge_discharge_save_check 校验通过后逐字段写入 | 读分派、count_*/is_* 校验函数 | 周期性时间段写入且校验通过 |
modbus_rtu_info | CRC 通过时填充 | 异常响应、读/写执行 | 每帧有效 RTU 帧 |
modbus_tcp_info | handle_modbus_tcp_recv_data 解析填充 | response_modbus_tcp_cmd 全部响应;ems_to_modbus_tcp 采样曲线帧头 | 每帧有效 TCP 报文 |
modbus_tcp_upgrade_table | set_update_system_config、write_firmware_content 收尾 | response_modbus_tcp_cmd 的 0x21 分支、get_flag_modbus_force_update | 升级流程 |
g_stEMSInfo.pstCurrUpt 及 stUpt[...] | 升级配置/收尾 | 升级配置/进度 | 0x20/0x21/0x22 |
g_grid_sampling_curve_t | u8_sampling_curve{1,2}_en(写校准段)、发送后清 u16_sampling_curve{1,2}_cnt 与 u8_sampling_curve{1,2}_send_flag | 帧组包 | 采样曲线使能 + 发送标志置位 |
5. 对外接口
5.1 本文件导出的函数
| 导出函数 | 声明位置 | 主要调用方 |
|---|---|---|
handle_modbus_ret_recv_data | modbus.h | midware/interface/interface.c:367(task_modbus_rtu) |
ems_to_modbus_tcp | modbus.h | midware/ethernt/ethernet.c:1035 |
set_flag_modbus_force_update / get_flag_modbus_force_update | modbus.h | midware/ethernt/ethernet.c:1468,1482、net/ems.c:4658 |
modbus_sunspec_en_addr_set | modbus.h | net/server.c:1497 |
modbus_sunspec_en_set / modbus_sunspec_addr_set | modbus.h | 本文件内部与外部配置流程 |
time_charge_discharge_save_check | modbus.h | 本文件内部(Set_Para_to_Reg_Addr 尾部) |
is_time_period_overlap 及 4 个时间段校验函数 | modbus.h | 本文件内部(也可供 HMI/服务器复用) |
get_para_from_reg_addr / modbus_send_data / modbus_data_is_recv_async / modbus_get_data_byte | modbus.h | midware/inverter/pvcharger.c:126,136(PV 充电机主机侧复用),以及本文件内部 |
handle_Modbus_response / handle_modbus_write_single_response / Handle_Modbus_Write_Response / Handle_Modbus_Exception / handle_modbus_tcp_recv_data / check_upgrade_infomation / check_upgrade_infomation_for_cmd / set_update_system_config / write_firmware_content | 无头文件原型 | 仅本文件内部调用(具备外部链接但未被声明) |
5.2 本文件导出的全局变量
| 变量 | 声明位置 | 使用方 |
|---|---|---|
buff_send_modbus | modbus.h extern | midware/inverter/pvcharger.c |
g_t_modbus_config | modbus.h extern | interface.c、ethernet.c、data/system_config/system_config.c、net/server.c |
g_t_config_save_flag | modbus.h extern | 全项目配置落盘流程 |
g_t_time_control_run | modbus.h extern | hmi/key.c、net/server.c、midware/inverter/inverter.c、data/system_config/system_config.c |
g_t_ate_ctrol | modbus.h extern | ATE/板测流程 |
g_u8_read_write_timecount、g_u8_save_time_extend1_flag、g_u8_save_time_extend2_flag、modbus_rtu_info、modbus_tcp_info、modbus_tcp_upgrade_table | 仅本文件定义,未在头文件 extern(隐式全局) | 跨文件可见但无原型/无 extern 声明,属"事实导出" |
5.3 本文件调用的外部函数/变量(按模块)
| 模块 | 使用的外部接口 |
|---|---|
hardware/uart(usart3.h) | uart3_clear_buffer、uart3_send_data、uart3_is_recv_async_data2、uart3_read_data_async、uart3_init |
common/crc(crc.h) | GetCrcCode_Modbus |
common/json(json.h) | json_get_value |
midware/ethernt(socket.h/ethernet.h) | getSn_SR、getSn_IR、setSn_IR、getSn_RX_RSR、socket、listen、close、setsockopt、recv、send、SOCK_TCP_SERVER、MEC_SERVER_PORT、Sn_MR_TCP、Sn_MR_ND、Sn_IR_RECV、SOCK_INIT/ESTABLISHED/CLOSE_WAIT/CLOSED、SO_KEEPALIVEAUTO、SOCK_OK |
midware/meter(meter.h、ct.h、energy) | meter_read_eoutput/einput/voltage/current/frequent/actpower、meter_set/get_meter_ct_enable、meter_set/get_meter_ct_rate、meter_set/get_meter_or_ct、meter_set_connect_status、meter_clear_run_data_flag、meter_reset、energy_read_pv_inverter_output、energy_read_storage_pv_input、energy_reset、g_t_meter、g_t_ct、dsp_meter_calibration_data、g_grid_sampling_curve_t |
midware/bms(bms.h) | bms_read_hard_version、bms_read_battery_model、bms_set_mos_enable、bms_set_mos_open_enable、bms_set_ready_enable、IsUpgradeBMS、GetPercentUpgradeBMS、g_t_bat_info |
midware/inverter(inverter.h、pvcharger.h) | inverter_read_hw_ver、inverter_read_system_modle、inverter_set_dc_double_charge_power、inverter_set_italy_anti_backflow、inverter_write_safety、inverter_set/get_three_unbalance_mode、inverter_set_phase_negation、inverter_read_upgrade_flag_{master,slave,arm}、inverter_read_upgrade_percent_{master,slave,arm}、inverter_reset、inverter_set_n_n_short、inverter_read_n_n_short、pvcharger_read_machine_mode、pvcharger_from_machine_recv_sync、g_t_inv_info、g_modbus_pvchargr_info、g_inverter_pvchargr_info |
logic(logic.h) | g_t_logic_info(t_config_data、t_config_extend1_data、t_config_extend2_data、南澳调度字段) |
dispatch(dispatch.h) | g_t_dispatch_t(t_stsystemcmdinfo、t_freq_dispatch_rundata) |
data(data.h) | g_t_ems_info、g_t_system_base_info、SYSTEM_DATETIME、Set_RTC、energy_reset、meter_reset、factory_reset |
net(server.h、ems.h、*_download.h) | g_stEMSInfo(含 stUpt[]、pstCurrUpt、b_modbus_Socket)、set_updt_status_current、UPDT_STS_*、UPDT_EMS/BMS/INV1/INV2/INV3、save_fw_to_spi_flash、increase_current_offset_file_updt、get_current_point_offset_file_updt、set_save_firm_in_app_flash_flag、calculate_ems_flash_md5、calculate_dev_FW_Flash_MD5、IsSystemDownload、g_u8_fw_sdram1/2、SPECIAL_CMD_UPDATE_*、Set_Setup_Config_Extend2_Status、g_u8appConfigFlagExtend2 |
system_config | Set_Setup_Config_Extend2_Status 的配套配置状态 |
sunspec_business(sunspec_business.h) | sunspec_modbus_data_init、sunspec_modbus_data_get、sunspec_modbus_data_set、sunspec_refresh_flag_set |
debug(debug.h) | ShowData、SERIAL_DEBUG_LOG_OUT |
system_aux、dry_node、evc、evc_ii、board_test、battery_upgrade、alphaess_comm_usart、alphaess_comm_can、ems_boot_handle、app_led、upd_download、ems_download、bms_download、inv_download、meter_download | AUX 通道配置、干接点、充电桩、板测、电池升级、AlphaESS 自定义串口/CAN 通信、EMS Boot 处理、LED 显示、固件下载辅助 |
6. 依赖关系
依赖(本文件 include / 使用)
modbus.h、usart3.h、crc.h、socket.h、ethernet.h、json.h、meter.h、bms.h、inverter.h、logic.h、dispatch.h、data.h、system_aux.h、ems.h、system_config.h、server.h、ct.h、board_test.h、upd_download.h、ems_download.h、bms_download.h、inv_download.h、meter_download.h、battery_upgrade.h、alphaess_comm_usart.h、dry_node.h、evc.h、evc_ii.h、ems_boot_handle.h、sunspec_business.h、alphaess_comm_can.h、app_led.h;以及间接依赖 hardware/sdram/sdram.h(缓冲物理地址)、hardware/uart(串口驱动)、hardware/flash(配置结构体对齐)、UCOSII(OSTimeGet、OSTimeDlyHMSM、OS_TICKS_PER_SEC)。
被依赖(用到本文件导出符号的文件)
midware/interface/interface.c—task_modbus_rtu调用handle_modbus_ret_recv_data(),并读写g_t_modbus_configmidware/ethernt/ethernet.c— 调用ems_to_modbus_tcp()、get/set_flag_modbus_force_update()midware/inverter/pvcharger.c— 复用get_para_from_reg_addr()、modbus_send_data()、buff_send_modbusnet/server.c— 调用modbus_sunspec_en_addr_set(),使用g_t_time_control_runnet/ems.c— 调用get_flag_modbus_force_update()data/system_config/system_config.c— 初始化g_t_modbus_config(ADDR_MODBUS/ADDR_SUNSPEC_MODBUS),消费g_t_config_save_flag、g_t_time_control_runhmi/key.c— 使用g_t_time_control_runmidware/inverter/inverter.c— 使用g_t_time_control_runmidware/interface/modbus/sunspec_business.c/sunspec_modbus.c— 共用MODBUS_REQ_T、Sunspec 地址窗口与全局缓冲约定
7. 备注与风险点
7.1 缓冲区越界风险(高)
- RTU 组帧缓冲无下标上限:
handle_modbus_ret_recv_data的DATA_RECVD状态对每次到达的字节无条件执行buff_Recv_modbus[idx++],而buff_Recv_modbus只有LEN_RECV_MODBUS = 256字节。进入数据态时idx已为 7(地址 1 + 功能码 1 + 地址 2 + 数量 1 + 或长度 1),LEN_0_7只要求byte == register_num_temp * 2,即data_len_temp最大 254(register_num_temp≤ 127),最后还会写 2 字节 CRC 到CRC_Bits_*(注意 CRC 两字节并不写入buff_Recv_modbus,只在局部变量crc_16中)。数据字节最多写到下标7 + 254 - 1 = 260> 255,仍可能越界约 5 字节。同时idx的类型是u32,无任何钳制。 - RTU 响应缓冲无上限:
handle_Modbus_response写入buff_send_modbus(256 字节),长度 =3 + num_reg*2 + 2。is_start_addr_and_num_err只做"落在某个段窗口内"的判定,而REG_MAX_MODBUS_E ≈ 0x2351,理论上可接受num_reg达数千,足以溢出 256 字节缓冲。buff_send_modbus还被pvcharger.c共享,溢出会污染其它模块数据。 - TCP 0x03 读响应的字节数字段被截断:
buff_send_modbus_tcp[l_u16_len_para++] = modbus_tcp_info.t_reg.num_reg * 2;左侧是u8,num_reg * 2是int。当num_reg > 127时字节数字段被静默截断(如 200 → 400 → 144),主站会收到自相矛盾的响应。 - TCP 0x03 / 0x10 的循环长度未做与缓冲长度的一致性校验:
response_modbus_tcp_cmd中for (i = 0; i < num_reg; i++)写入 5120 字节的buff_send_modbus_tcp,或从 2048 字节的buff_Recv_modbus_tcp读取2*num_reg字节。num_reg仅受地址段窗口约束(最宽段可达数千),存在双向越界(读越界 + 写越界)。 handle_modbus_tcp_recv_data只对整包长度做了钳制:l_u16DataLen被钳到sizeof(buff_Recv_modbus_tcp) - 6 = 2042,且recv的写入偏移是buff_Recv_modbus_tcp + l_u8HeadLen + l_u16RecvLen,l_u16RecvLen最多累加到l_u16DataLen,理论上界 6 + 2042 = 2048,恰好贴合;但l_u16RecvLen是u16,若recv返回异常值会累加溢出为小值而继续循环。PDU 内部字段(如PDU+25~28的文件长度、PDU+1~2的分片长度)没有与u16_data_len交叉校验,check_upgrade_infomation还会从PDU+2+16起扫描至PDU+2+16+255(下标约 280),此时若u16_data_len小于 30 会读到缓冲区尾部残留数据(缓冲区每次进来都memset,故读到的是 0,不会越界)。
7.2 协议/语义正确性问题(中高)
- 对 CRC 错误的帧也回异常响应:Modbus RTU 规范要求 CRC 失败时不应答(否则会与主站重试混淆)。本文件在
CRC_Bits_8_15判定 CRC 不等时置EXCEPTION_E,随后发出功能码|0x80 + 0x03的异常帧。 - 异常帧可能带错误的从站地址:
Handle_Modbus_Exception(modbus_rtu_info, …)使用的是上一次成功解析的modbus_rtu_info。当异常来自地址越界路径(REGISTER_BITS_0_7/REGISTER_NUM_0_7状态)时,本次帧的地址/功能码尚未写入modbus_rtu_info,于是异常响应会回旧地址 + 旧功能码,主站无法正确关联。 is_start_addr_and_num_err不校验case是否存在:段内未实现的寄存器写入会default: break;后静默成功。例如写REG_EMS_GET_VOLT_R_E(0x1174,只读组)返回 0x06 成功响应但无任何效果——上位机无法区分"写成功"与"寄存器不存在"。num_reg == 0时判定为"非法地址"而非"非法数据":addr + num - 1在u16下回绕为0xFFFF,不满足上界条件,返回REG_ILLEGAL_DATA_ADDRESS_E。异常码语义偏差。is_start_addr_and_num_err的运算符优先级依赖:(A == x) || (A == 0) && (addr… )依赖&&高于||即等价于A==x || (A==0 && …)。当前语义正确但写成两行同一缩进,后续修改极易引入错误。- 地址上界校验只查第一段:外层
if/else if链在地址命中的第一个段就return,不做多段交叉判定;这是正确设计,但意味着若两个段地址范围发生交叠(枚举插删导致),后一个段将永远不可达。 REG_MODBUS_BAUD_RATE_E写入值无范围检查:内层switch有default: break;,因此写入 0xFFFF 不会切波特率,但会把u8_modbus_baud_rate存成 0xFF 并落盘,重启后按非预期值初始化。REG_MODBUS_ADDRESS_E无取值范围/冲突检查:可直接改成 0(与 PV 充电机广播地址冲突)或与u16_sunspec_addr相同(导致 Sunspec 分流条件与私有表条件同时成立,实际走 Sunspec 分支)。modbus_sunspec_addr_set只在"值变化"时才落盘,但modbus_sunspec_en_addr_set的变化判断是"与"关系:if ((l_u8_en != u8_sunspec_en) || (l_u8_addr != u16_sunspec_addr))用u8与u16比较(整型提升后仍正确),随后无条件调modbus_sunspec_en_set(l_u8_en)。u8_sunspec_addr是u16,但这个入口只取低字节,故 Sunspec 从站地址实际上被限制在 0~255,而modbus_sunspec_addr_set本身接受u16——两处接口的参数宽度不一致。0x20的对象槽位映射不完整:set_update_system_config支持 EMS/APP2/BMS(BMU)/INV_MASTER/INV_SLAVE/INV_ARM 六个分支,但UPGRADE_OBJECT_LMU_E/UPGRADE_OBJECT_ISO_E只在机型码校验里出现,没有对应的g_stEMSInfo.pstCurrUpt赋值,落到else返回CONFIG_UPGRADE_OBJECT_ERR_E;而check_upgrade_infomation与0x22分支也没有 LMU/ISO 的处理,枚举里定义了但流程上不可用。write_firmware_content的失败码直接编码进协议:失败响应是单元 0xA1 (0x15 + ret),ret ∈ {1,2,3,4},于是响应字节为 0x16~0x19。这种"用偏移量当错误码"的写法把内部返回码暴露为线上协议,且与成功码0x06不在同一编码空间,容易与标准 Modbus 异常码混淆。- 0x19 版本解析假设固定字符串格式:
hard_version[1] - '0'与hard_version[3]/[4] - '0'假定形如Vd.dd。若实际版本是V10.02或V1.2,会得到错误的版本号;只做了strlen > 0的长度检查,未检查长度 ≥ 5。 check_upgrade_infomation_for_cmd的版本比较长度减 1:对 EMS/APP2 传minusNum = 1,即不比较固件版本字符串最后一个字符。这是为解决"带后缀版本号"的兼容处理,但会让任意末字符差异都判为合法,属宽放校验。
7.3 并发与共享资源风险(中)
buff_send_modbus被跨文件共享:modbus.c与midware/inverter/pvcharger.c使用同一个 256 字节发送缓冲,两个上下文(RTU 从站应答 与 PV 充电机主机轮询)若在不同任务中并发,会相互覆盖。g_u8_read_write_timecount是"会话级"游标但用全局变量实现:主站需要先写选段寄存器再写数据寄存器,两条命令之间若被另一个主站(RTU 与 TCP 同时接入,或 Sunspec)插入一次选段写入,就会把数据写到错误槽位,且没有超时复位机制。update系列的读-改-写非原子:(value << 16) | (X & 0xffff)这种 32 位量的高低字分两次写入,若通信中断或只写了一个字,X会长期处于"半新半旧"状态;time_charge_discharge_save_check的memcmp也会把这种中间态当作一次真实变更。- 忙等不让出 CPU:
handle_modbus_ret_recv_data的while(1)在modbus_data_is_recv_async()为假时continue(最坏 300 tick 不让出 CPU);handle_modbus_tcp_recv_data的两个等待循环同样只比较 tick,不调用OSTimeDly。在高优先级通信任务中会造成同优先级任务饥饿。 - 没有信号量保护共享配置结构体:
g_t_logic_info、g_t_config_save_flag、g_t_time_control_run被task_modbus_rtu、网络任务、HMI 按键任务和配置落盘任务同时访问,本文件未加任何互斥。虽然多数操作是单字节写,但 32 位量(如PV_Capacity_Storage)与数组段(control1[])的拷贝存在撕裂风险。
7.4 代码质量与维护性(中低)
- 命名遮蔽:
Set_Para_to_Reg_Addr内的函数级静态g_u8_set_logic_config_flag与g_t_config_save_flag.u8_set_logic_config_flag、以及本文件大量使用的g_t_config_save_flag.u8_set_logic_config_flag名字高度相似却完全无关。第 8147 行的if(g_u8_set_logic_config_flag == 1)很容易被误读为"检查落盘标志"。 - 9 个非 static 函数缺少头文件原型(
handle_Modbus_response、handle_modbus_write_single_response、Handle_Modbus_Write_Response、Handle_Modbus_Exception、handle_modbus_tcp_recv_data、check_upgrade_infomation、check_upgrade_infomation_for_cmd、set_update_system_config、write_firmware_content),污染全局命名空间且失去编译期类型检查。另有 6 个全局变量(modbus_rtu_info、modbus_tcp_info、modbus_tcp_upgrade_table、g_u8_read_write_timecount、g_u8_save_time_extend1_flag、g_u8_save_time_extend2_flag)同样没有extern声明。 handle_modbus_tcp_recv_data(u8 net_type)的参数net_type完全未使用(内部直接用SOCK_TCP_SERVER常量),属遗留签名。get_para_from_reg_addr中大量声明未使用的局部变量:l_s8_ems_soft_ver[16]、l_s8_sys_safty_ver[4](后者仅在 safety 版本分支用到)、l_u8_sn_temp_id、l_u8_sn_temp_byte、l_u16_sn_byte_temp、l_u32_string1~3,是重构残留。REG_EMS_GET_FLAG_E的实现被注释掉(第 320 行// l_u16_ret = dsp_meter_calibration_data.u16_calibration_enable;),读该校准标志恒为 0,但地址合法性校验仍把它当作合法上界。- 干接点读寄存器恒返回 0(
REG_EMS_DO0_E/DO1_E/DI0_E/DI1_E),写侧未实现对应分支,属未完成功能占位。 ems_to_modbus_tcp中的采样曲线帧复用modbus_tcp_info的transaction_id/protocol_id:如果此前从未收到过 TCP 报文,这两个字段是全零(modbus_tcp_info未显式初始化,作为全局变量在 BSS 段为 0);若收到过,则是上一帧的值,与本次推送无关。且该帧带 Modbus CRC 却不是 Modbus PDU,规范性存疑。- 魔法数字密集:300 / 1000(tick 超时)、12(keep-alive)、
0x33 0x31 0x31(ASCII "311" 厂商标识)、0x1A/0x19(BMU / LMU-ISO 机型码)、28(时间段槽位总数)、14(两组各 14 段)、7(星期位数)、100/10(SOC 上下限)、10000(功率上限)、127(星期掩码)、24*60(分钟上限)、0xAA 0x55(采样曲线帧标记)、0x55/0x99(升级协议标记)、0x15(升级失败码基址)、0x0C/0x06/0x03/0x04(各响应长度字段)。除MAX_ONES_PER_BIT外几乎没有具名常量,是主要维护负担。 #if (DEBUG == 1) & (DEBUG_TCP_CLIENT_STATE == 1)使用按位与&而非逻辑与&&:当前写法能工作(两侧都是 0/1),但属于误用,若某侧宏定义为非 0/1 值会得到意外结果。同类写法出现在第 8608 行#if (DEBUG == 1) & (DEBUG_MODBUS == 1)。MODBUS_RTU_DEBUG未定义:第 8408 行的#if (MODBUS_RTU_DEBUG == 1)在任何头文件里都找不到定义,未定义标识符在#if中按 0 处理,因此 RTU 超时打印永远被裁掉——该调试开关实际上是失效的(与DEBUG_MODBUS并存,功能重叠)。#pragma pack(1)覆盖 5000 行的巨型 switch:结构体紧凑对齐与业务逻辑无关,但MODBUS_REQ_T的紧凑性是 RTU 直接memcpy解析的前提;一旦有人把#pragma pack移到别处,会出现非常隐蔽的字段错位。- 两个巨型
switch的缩进层级已达 3~4 层(函数体 → 外层if→switch→case内if),单文件 10221 行、单个switch1211 个case,edit/review/diff成本极高;建议按地址段拆分为多个static子函数或表驱动({addr, get_fn, set_fn}表)。 time_charge_discharge_save_check的字段拷贝是手写枚举:只拷贝 5 个充电字段 + 5 个放电字段,没有拷贝charger_*之外的字段(若LOGIC_TIME_CONTROL_TAB_T后续新增成员,这里必须同步修改,否则新字段永远无法落盘)。且memcmp比较的是整个结构体,而拷贝是部分字段——若结构体含 padding 字节,memcmp可能因未初始化 padding 而误判"有变更",从而频繁触发校验与落盘。