EMS 知识库
首页 / knowledge/代码解析/midware/interface/modbus

modbus.c

源文件路径: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_modbusstatic u8[LEN_RECV_MODBUS]RTU 接收组帧缓冲,256 字节,仅本文件可见
buff_send_modbusu8[LEN_RECV_MODBUS](全局导出)RTU 发送组帧缓冲,256 字节;midware/inverter/pvcharger.c 复用同一缓冲做 PV 充电机主机应答
buff_Recv_modbus_tcpstatic u8[2*1024],定位在 SDRAM_MODBUS_REV_BUF_ADDRTCP 接收缓冲 2048 字节,由 hardware/sdram/sdram.h 指定物理地址
buff_send_modbus_tcpstatic u8[5*1024],定位在 SDRAM_MODBUS_SEND_BUF_ADDRTCP 发送缓冲 5120 字节,物理地址固定
g_u8_read_write_timecountu8(全局导出)"当前正在读/写第几个周期性时间段",取值 0~27;<14 表示 control1 组,≥14 表示 control2 组(下标减 14)
g_u8_save_time_extend1_flagu8(全局导出)control1 组有新合法时间段被接受时置位,供落盘判断
g_u8_save_time_extend2_flagu8(全局导出)control2 组同上
modbus_rtu_infoMODBUS_REQ_T(全局导出)RTU 最近一帧成功解析出的从站地址/功能码/起始地址/数量
modbus_tcp_infoMODBUS_TCP_TABLE(全局导出)TCP 最近一帧的 MBAP 头 + 寄存器请求
modbus_tcp_upgrade_tableMODBUS_TCP_UPGRADE_TTCP 升级会话上下文(升级标志/对象/类型)
g_t_modbus_configMODBUS_CONFIG_INFO_TModbus 从站配置(地址、波特率、Sunspec 使能与地址、Sunspec 版本),带 CRC 存 Flash
g_t_config_save_flagCONFIG_SAVE_FLAG_T13 个"参数已变更需落盘"标志,本文件大量置位
g_t_time_control_runTIME_CONTROL_RUN_TAB_T周期性时间段的暂存(staging)区,写寄存器先落到这里,校验通过后才拷进 g_t_logic_info.t_config_extend1_data/2_data
g_t_ate_ctrolATE_CTROL_TATE 测试控制字(链路状态、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_flagstatic 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_flagstatic(ems_to_modbus_tcp 内)记录 Socket 上次状态以抑制重复日志;以及 listen 成功后的 keep-alive 一次性设置标志

3. 函数清单

函数名作用域说明
is_start_addr_and_num_errstatic判断从站地址与"起始地址 + 数量"是否落在某个已声明的寄存器段窗口内,返回 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_Addrstatic写分派:按 (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_cmdstaticTCP 命令分派与响应组帧(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 = 20x06 → 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_7register_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 * 2data_len_temp = byte→ DATA_RECVD
LEN_0_7字节数不匹配l_u8_exception_code = MODBUS_EX_ILLEGAL_DATA_VALUE_E→ EXCEPTION_E
DATA_RECVD0x06: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_15crc_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_15CRC 不匹配置 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 通过且范围合法):

响应组帧格式(RTU,低字节先发 CRC):

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。典型取值模式:

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,四种写法:

  1. 直接赋值:g_t_logic_info.t_config_data.X = value; 随后置位对应的 g_t_config_save_flag.*。
  2. 32 位高低字分两次拼装(读-改-写):

X = (value << 16) | (X & 0xffff); / X = (X & 0xffff0000) | value; 这是非原子的读-改-写,若主站只写了一个字,另一个字来自上次的缓存值(见风险点)。

  1. 调用模块接口:如 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) 等。
  2. 带范围钳制的赋值,典型模式:
    • 小时: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 归零。

注意这些钳制都是静默丢弃/归零,不返回异常码给主站。

  1. 特殊副作用: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()。
  1. 函数尾部固定动作(第 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)
0x19check_upgrade_infomation() 校验,返回值 0 表示通过通过:len=0x0C,0x55 0x19 <对象> 0x20 0x01 0x33 0x31 0x31 后接 0x01 0x00 <主版本> <次版本>;失败:len=0x03,0x55 0x99 <错误位图>
0x20set_update_system_config()成功:单元 0x20 0x06;失败:单元 0xA0 0x15
0x21write_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)

  1. 查询(0x18):主站先问目标设备的当前固件版本与机型。
  2. 校验(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 故意不比较固件版本号的最后一个字符。
  3. 配置(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。
  4. 写分片(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。
  5. 进度(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_INITlisten(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_WAITclose(SOCK_TCP_SERVER)
SOCK_CLOSEDsocket(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 与私有寄存器表共用同一套收发链路,靠"单元标识符 + 地址窗口"分流:

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、功率、星期掩码):

  1. 每次写入落到暂存区 g_t_time_control_run,并置函数内静态 g_u8_set_logic_config_flag = TRUE。
  2. Set_Para_to_Reg_Addr 返回前若该标志为 1,清标志并调 time_charge_discharge_save_check()。
  3. 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):

4.10 定时/调度时序与 uCOS-II 配合

4.11 关键全局变量的读写与触发条件汇总

变量本文件写点本文件读点触发条件
g_t_modbus_configu8_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_datatime_charge_discharge_save_check 校验通过后逐字段写入读分派、count_*/is_* 校验函数周期性时间段写入且校验通过
modbus_rtu_infoCRC 通过时填充异常响应、读/写执行每帧有效 RTU 帧
modbus_tcp_infohandle_modbus_tcp_recv_data 解析填充response_modbus_tcp_cmd 全部响应;ems_to_modbus_tcp 采样曲线帧头每帧有效 TCP 报文
modbus_tcp_upgrade_tableset_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_tu8_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_datamodbus.hmidware/interface/interface.c:367(task_modbus_rtu)
ems_to_modbus_tcpmodbus.hmidware/ethernt/ethernet.c:1035
set_flag_modbus_force_update / get_flag_modbus_force_updatemodbus.hmidware/ethernt/ethernet.c:1468,1482、net/ems.c:4658
modbus_sunspec_en_addr_setmodbus.hnet/server.c:1497
modbus_sunspec_en_set / modbus_sunspec_addr_setmodbus.h本文件内部与外部配置流程
time_charge_discharge_save_checkmodbus.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_bytemodbus.hmidware/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_modbusmodbus.h externmidware/inverter/pvcharger.c
g_t_modbus_configmodbus.h externinterface.c、ethernet.c、data/system_config/system_config.c、net/server.c
g_t_config_save_flagmodbus.h extern全项目配置落盘流程
g_t_time_control_runmodbus.h externhmi/key.c、net/server.c、midware/inverter/inverter.c、data/system_config/system_config.c
g_t_ate_ctrolmodbus.h externATE/板测流程
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_configSet_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_downloadAUX 通道配置、干接点、充电桩、板测、电池升级、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)。

被依赖(用到本文件导出符号的文件)

7. 备注与风险点

7.1 缓冲区越界风险(高)

  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,无任何钳制。
  2. 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 共享,溢出会污染其它模块数据。
  3. 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),主站会收到自相矛盾的响应。
  4. 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 仅受地址段窗口约束(最宽段可达数千),存在双向越界(读越界 + 写越界)。
  5. 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 协议/语义正确性问题(中高)

  1. 对 CRC 错误的帧也回异常响应:Modbus RTU 规范要求 CRC 失败时不应答(否则会与主站重试混淆)。本文件在 CRC_Bits_8_15 判定 CRC 不等时置 EXCEPTION_E,随后发出 功能码|0x80 + 0x03 的异常帧。
  2. 异常帧可能带错误的从站地址:Handle_Modbus_Exception(modbus_rtu_info, …) 使用的是上一次成功解析的 modbus_rtu_info。当异常来自地址越界路径(REGISTER_BITS_0_7 / REGISTER_NUM_0_7 状态)时,本次帧的地址/功能码尚未写入 modbus_rtu_info,于是异常响应会回旧地址 + 旧功能码,主站无法正确关联。
  3. is_start_addr_and_num_err 不校验 case 是否存在:段内未实现的寄存器写入会 default: break; 后静默成功。例如写 REG_EMS_GET_VOLT_R_E(0x1174,只读组)返回 0x06 成功响应但无任何效果——上位机无法区分"写成功"与"寄存器不存在"。
  4. num_reg == 0 时判定为"非法地址"而非"非法数据":addr + num - 1 在 u16 下回绕为 0xFFFF,不满足上界条件,返回 REG_ILLEGAL_DATA_ADDRESS_E。异常码语义偏差。
  5. is_start_addr_and_num_err 的运算符优先级依赖:(A == x) || (A == 0) && (addr… ) 依赖 && 高于 || 即等价于 A==x || (A==0 && …)。当前语义正确但写成两行同一缩进,后续修改极易引入错误。
  6. 地址上界校验只查第一段:外层 if/else if 链在地址命中的第一个段就 return,不做多段交叉判定;这是正确设计,但意味着若两个段地址范围发生交叠(枚举插删导致),后一个段将永远不可达。
  7. REG_MODBUS_BAUD_RATE_E 写入值无范围检查:内层 switch 有 default: break;,因此写入 0xFFFF 不会切波特率,但会把 u8_modbus_baud_rate 存成 0xFF 并落盘,重启后按非预期值初始化。
  8. REG_MODBUS_ADDRESS_E 无取值范围/冲突检查:可直接改成 0(与 PV 充电机广播地址冲突)或与 u16_sunspec_addr 相同(导致 Sunspec 分流条件与私有表条件同时成立,实际走 Sunspec 分支)。
  9. 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——两处接口的参数宽度不一致。
  10. 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 的处理,枚举里定义了但流程上不可用。
  11. write_firmware_content 的失败码直接编码进协议:失败响应是 单元 0xA1 (0x15 + ret),ret ∈ {1,2,3,4},于是响应字节为 0x16~0x19。这种"用偏移量当错误码"的写法把内部返回码暴露为线上协议,且与成功码 0x06 不在同一编码空间,容易与标准 Modbus 异常码混淆。
  12. 0x19 版本解析假设固定字符串格式:hard_version[1] - '0' 与 hard_version[3]/[4] - '0' 假定形如 Vd.dd。若实际版本是 V10.02 或 V1.2,会得到错误的版本号;只做了 strlen > 0 的长度检查,未检查长度 ≥ 5。
  13. check_upgrade_infomation_for_cmd 的版本比较长度减 1:对 EMS/APP2 传 minusNum = 1,即不比较固件版本字符串最后一个字符。这是为解决"带后缀版本号"的兼容处理,但会让任意末字符差异都判为合法,属宽放校验。

7.3 并发与共享资源风险(中)

  1. buff_send_modbus 被跨文件共享:modbus.c 与 midware/inverter/pvcharger.c 使用同一个 256 字节发送缓冲,两个上下文(RTU 从站应答 与 PV 充电机主机轮询)若在不同任务中并发,会相互覆盖。
  2. g_u8_read_write_timecount 是"会话级"游标但用全局变量实现:主站需要先写选段寄存器再写数据寄存器,两条命令之间若被另一个主站(RTU 与 TCP 同时接入,或 Sunspec)插入一次选段写入,就会把数据写到错误槽位,且没有超时复位机制。
  3. update 系列的读-改-写非原子:(value << 16) | (X & 0xffff) 这种 32 位量的高低字分两次写入,若通信中断或只写了一个字,X 会长期处于"半新半旧"状态;time_charge_discharge_save_check 的 memcmp 也会把这种中间态当作一次真实变更。
  4. 忙等不让出 CPU:handle_modbus_ret_recv_data 的 while(1) 在 modbus_data_is_recv_async() 为假时 continue(最坏 300 tick 不让出 CPU);handle_modbus_tcp_recv_data 的两个等待循环同样只比较 tick,不调用 OSTimeDly。在高优先级通信任务中会造成同优先级任务饥饿。
  5. 没有信号量保护共享配置结构体: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 代码质量与维护性(中低)

  1. 命名遮蔽: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) 很容易被误读为"检查落盘标志"。
  2. 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 声明。
  3. handle_modbus_tcp_recv_data(u8 net_type) 的参数 net_type 完全未使用(内部直接用 SOCK_TCP_SERVER 常量),属遗留签名。
  4. 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,是重构残留。
  5. REG_EMS_GET_FLAG_E 的实现被注释掉(第 320 行 // l_u16_ret = dsp_meter_calibration_data.u16_calibration_enable;),读该校准标志恒为 0,但地址合法性校验仍把它当作合法上界。
  6. 干接点读寄存器恒返回 0(REG_EMS_DO0_E/DO1_E/DI0_E/DI1_E),写侧未实现对应分支,属未完成功能占位。
  7. ems_to_modbus_tcp 中的采样曲线帧复用 modbus_tcp_info 的 transaction_id/protocol_id:如果此前从未收到过 TCP 报文,这两个字段是全零(modbus_tcp_info 未显式初始化,作为全局变量在 BSS 段为 0);若收到过,则是上一帧的值,与本次推送无关。且该帧带 Modbus CRC 却不是 Modbus PDU,规范性存疑。
  8. 魔法数字密集: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 外几乎没有具名常量,是主要维护负担。
  9. #if (DEBUG == 1) & (DEBUG_TCP_CLIENT_STATE == 1) 使用按位与 & 而非逻辑与 &&:当前写法能工作(两侧都是 0/1),但属于误用,若某侧宏定义为非 0/1 值会得到意外结果。同类写法出现在第 8608 行 #if (DEBUG == 1) & (DEBUG_MODBUS == 1)。
  10. MODBUS_RTU_DEBUG 未定义:第 8408 行的 #if (MODBUS_RTU_DEBUG == 1) 在任何头文件里都找不到定义,未定义标识符在 #if 中按 0 处理,因此 RTU 超时打印永远被裁掉——该调试开关实际上是失效的(与 DEBUG_MODBUS 并存,功能重叠)。
  11. #pragma pack(1) 覆盖 5000 行的巨型 switch:结构体紧凑对齐与业务逻辑无关,但 MODBUS_REQ_T 的紧凑性是 RTU 直接 memcpy 解析的前提;一旦有人把 #pragma pack 移到别处,会出现非常隐蔽的字段错位。
  12. 两个巨型 switch 的缩进层级已达 3~4 层(函数体 → 外层 if → switch → case 内 if),单文件 10221 行、单个 switch 1211 个 case,edit/review/diff 成本极高;建议按地址段拆分为多个 static 子函数或表驱动({addr, get_fn, set_fn} 表)。
  13. time_charge_discharge_save_check 的字段拷贝是手写枚举:只拷贝 5 个充电字段 + 5 个放电字段,没有拷贝 charger_* 之外的字段(若 LOGIC_TIME_CONTROL_TAB_T 后续新增成员,这里必须同步修改,否则新字段永远无法落盘)。且 memcmp 比较的是整个结构体,而拷贝是部分字段——若结构体含 padding 字节,memcmp 可能因未初始化 padding 而误判"有变更",从而频繁触发校验与落盘。