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

meter_upgrade.c

源文件路径:midware/meter/meter_upgrade.c 所属模块:midware/meter(电表中间件) 代码行数:482

1. 文件职责

本文件实现外部电表(含电网侧电表、PV 侧电表、正泰 CHNT 无线电表)的固件在线升级。EMS 从服务器下载电表固件(HEX/S19 文本)落到 SDRAM 缓冲区 g_u8_fw_sdram1,本模块按厂商私有的 Modbus-兼容协议把二进制数据分块烧写到电表 MCU 的 Flash:进 Bootloader → 握手 → 擦除 → 分块写 → 退出 Bootloader。

数据流位置:net/ 层负责下载与置位 UPDT_Meter 升级标志;midware/meter/meter.c:5742/5756/5761 的 task_meter() 在标志为真时每轮调用一次 meter_upgrade_handle(),直到本模块在结束时把标志清掉。本模块是"数据流的末端执行者",也是电表总线的独占使用者(升级期间其他电表查询被跳过)。

除常规电表外,本模块还支持正泰无线模块组的多点升级:先通过 g_t_meter.g_code_stack 栈把 AP(0xE1)与其 STA 子节点(0xE2/0xE3)依次入栈,再逐个出栈升级,并在过程中切换串口波特率到 115200。

2. 关键数据结构与宏

本文件宏

名称类型说明
DEBUG_METER_UPDATE宏(.h:16)0,调试打印开关;关闭时 printf 相关代码被预处理剔除
METER_UPGRADE_MAX_DATA宏250,单帧最大固件数据字节数(主循环与尾块长度都用它)
METER_UPGRADE_BEGIN_ADDR宏0x00000000,电表侧 Flash 起始烧写地址
RECV_SEQUENCE_FRAME宏0x06,响应帧中"序列/数据帧"标识(判定接收有效时用 recv[7] 比对)
METER_UPGRADE_CMD_CODE宏0x23,本协议的命令字(请求帧 [1] 固定为该值)
METER_BUF_LEN宏1024;本文件与全工程均未使用,遗留常量
METER_UPDATE_VALID_LEN宏500;本文件与全工程均未使用,遗留常量
CHNT_WIRELESS_UPDATE_POP宏7,正泰无线升级的"子节点状态位域"常量,0b111 使 STA1/STA2 都被入栈

本文件用到的头文件定义

名称类型说明
METER_UPGRADE_STATE_E枚举(.h:27)主状态机:METER_UPGRADE_INIT(0)、METER_BOOT_ENTER(1)、METER_HAND_SHAKE(2)、METER_ERASE_FLASH(3)、METER_WRITE_FLASH(4)、METER_BOOT_EXIT(5)、METER_UPGRADE_END(6)。类型名本身未被引用,只用其枚举常量
METER_UPGRADE_CMD_E枚举(.h:38)命令码:METERCMD_BOOT_ENTER(0)、METERCMD_HAND_SHAKE(1)、METERCMD_ERASE_FLASH(2)、METERCMD_WRITE_FLASH(3)、METERCMD_BOOT_EXIT(4);类型名未被引用
METER_UPGRADE_T结构体(.h:48,#pragma pack(1))升级上下文,见下节

METER_UPGRADE_T 字段

字段类型说明
u16_lengthu32本次要发送的数据字节数(命名前缀 u16_ 但实际是 u32);每帧为 250,尾块为剩余长度
u32_addr_flashu32电表侧目标 Flash 地址,从 METER_UPGRADE_BEGIN_ADDR 起每次 +250(实际只用于组帧)
u32_addr_flash_offsetu32源侧(g_u8_fw_sdram1)已发送的字节偏移;也是升级进度的来源
u8_upgrade_file_bufu8*固件缓冲区基址,擦除成功后指向 g_u8_fw_sdram1
u16_send_file_numu16总帧数 = ceil(固件长度 / 250)
u8_upgrade_stateu8状态机当前状态(METER_UPGRADE_STATE_E 的值)
u8_meter_addru8本次升级目标的 Modbus 从站地址(电网表 0x01 / PV 表 0x02 或 0x0B / 无线节点 0xE1~0xE3)
u8_err_cntu8当前状态连续失败次数,超过各自门限即跳到 METER_UPGRADE_END
u32_sequnou32通信序列号,每次成功发送后自增;填入请求帧 [2]

本文件静态全局

名称类型说明
s_meter_upgrade_send_buf[306]static u8[306]发送帧缓冲,最大帧 13 + 250 + 2 = 265 字节,留有余量
s_meter_upgrade_recv_buf[11]static u8[11]接收帧缓冲,仅 11 字节且接收时不做长度上限检查
s_meter_upgrade_tabstatic METER_UPGRADE_T升级上下文(BSS 清零,状态初值即 METER_UPGRADE_INIT)

3. 函数清单

函数名作用域说明
meter_upgrade_recv_datastatic阻塞式接收一帧:按 timeout 轮询 USART1,做 CRC16/Modbus 校验与 4 个字段比对,结果通过 *len 返回(0 表示失败)
meter_upgrade_send_cmdstatic清串口缓冲后把 s_meter_upgrade_send_buf 按指定长度发出(发送超时固定 500ms)
meter_upgrade_send_codestatic组"控制命令帧"(15 字节,命令码放在 [10])并发送 + 等应答,返回成功/失败
meter_upgrade_send_datastatic组"写数据帧"(13 + length + 2 字节,含地址、长度与固件数据)并发送 + 等应答
chnt_wireless_add_update_codestatic按位域参数把 AP 地址与各 STA 子节点地址压入 g_t_meter.g_code_stack
chnt_wireless_add_pop_codestatic从 g_t_meter.g_code_stack 弹出一个地址(LIFO),栈空返回 0
meter_upgrade_handle全局升级主状态机入口,每次调用推进一个状态
get_percent_upgrade_meter全局返回升级百分比 = u32_addr_flash_offset * 100 / get_length_file_updating(UPDT_Meter)
get_upgrade_file_offset_meter全局返回已发送的固件字节偏移 u32_addr_flash_offset

本文件另在 :30-33 有 4 行前置声明(meter_upgrade_recv_data、meter_upgrade_send_cmd、meter_upgrade_send_code、meter_upgrade_send_data)。

4. 核心逻辑解析

4.1 主状态机(meter_upgrade_handle(),:285)

METER_UPGRADE_INIT ──确定目标地址──► METER_BOOT_ENTER
                                          │ 成功(清零 err_cnt, sequno++)
                                          ▼
                    ┌──────────────► METER_HAND_SHAKE
                    │                     │ 成功
                    │                     ▼
                    │                METER_ERASE_FLASH ──成功→ 记录文件指针/总帧数/起始地址
                    │                     │
                    │                     ▼
                    │                METER_WRITE_FLASH ◄──┐
                    │                     │ 全部帧发完      │ 每帧成功: cnt++, sequno++,
                    │                     ▼                │ offset = cnt*250, addr += 250
                    │                METER_BOOT_EXIT ───────┘
                    │                     │ 成功
                    │                     ▼
                    │              METER_UPGRADE_END
                    │                     │ 无线表: 栈中还有地址
                    └─────────────────────┤   → 直接回 METER_BOOT_ENTER(升级下一个无线节点)
                                          │ 否则: 恢复波特率/绑定 → 清 server 标志 → METER_UPGRADE_INIT

各状态动作与迁移条件

失败 err_cnt > 3 → END。

错误计数门限一览:METER_BOOT_ENTER 为 5,METER_HAND_SHAKE / METER_ERASE_FLASH / METER_WRITE_FLASH / METER_BOOT_EXIT 均为 3。没有全局超时看门狗(与 bb_update.c 的 60 秒总超时不同),但每一次收发本身有超时(控制帧 5s、数据帧 2s),因此单次调用最长阻塞约 5 秒;若某状态持续失败,最多 4~6 次即终止。

4.2 通信协议帧格式

控制命令帧(meter_upgrade_send_code(),共 15 字节)

偏移值含义
0u8_meter_addr目标从站地址
10x23(METER_UPGRADE_CMD_CODE)命令字
2u32_sequno通信序列号(低 8 位)
30x08长度域低位
40x00长度域高位(小端,值 = 8)
5–90x00 ×5保留/填充
10命令码(0=进 Boot,1=握手,2=擦除,4=退 Boot)操作码
11–120x00,0x00保留
13–14CRC16/Modbus低字节在前

写数据帧(meter_upgrade_send_data(),共 13 + length + 2 字节)

偏移值含义
0u8_meter_addr从站地址
10x23命令字
2u32_sequno序列号
3–4u16_length*2 + 8长度域,小端(:204-205);疑似"数据字节数×2 + 8"
50x00保留
6–9u32_addr_flash目标 Flash 地址,编码为 高 16 位字在前、每个 16bit 字小端:[6]=>>16, [7]=>>24, [8]=>>0, [9]=>>8
10METERCMD_WRITE_FLASH = 3操作码
11–12u16_length数据长度,小端
13 … 13+length-1固件数据从 u8_upgrade_file_buf[offset + i] 逐字节拷贝(:216-219)
末 2 字节CRC16/Modbus低字节在前

响应帧校验(meter_upgrade_recv_data(),:86-101)

一旦 CRC 校验通过,还要求全部满足:

判定说明
recv[0] == u8_meter_addr从站地址回显正确
recv[3] == 0x03固定值(协议约定的响应类型/长度标识)
recv[6] == send_buf[10]回显请求帧的操作码(即"这条响应属于哪条命令")
recv[7] == RECV_SEQUENCE_FRAME(0x06)序列/数据帧标识

四项全满足才把 *len 置为实际长度(成功);否则置 0(失败)。

说明:源码中没有给出完整响应帧的字段布局文档,代码只检查上述 4 个位置;recv[1]、recv[2]、recv[4]、recv[5]、recv[8..10] 的含义未在代码中体现。

超时与重试机制

4.3 正泰无线多节点升级(栈式地址管理)

入栈(chnt_wireless_add_update_code(),:250)

g_t_meter.g_code_stack.u8_top = 0;
memset(g_t_meter.g_code_stack.u8_codes, 0, sizeof(...));
g_t_meter.g_code_stack.u8_codes[top++] = CHNT_WIRELESS_AP_ADDR;   // 0xE1,AP
for (u8 i = 1; i < MAX_CODES; i++)                                 // MAX_CODES = 3 → i = 1,2
    if (((state >> i) & 0x01) && (top < (MAX_CODES + 1)))
        g_t_meter.g_code_stack.u8_codes[top++] = 0xE0 | (i + 1);   // 0xE2, 0xE3

g_code_stack(meter.h:313)定义 u8_codes[MAX_CODES+1] = 4 个字节、u8_top 为栈顶。以实参 CHNT_WIRELESS_UPDATE_POP = 7(0b111)调用时,位 1、位 2 命中,栈内容为 [0xE1, 0xE2, 0xE3]、top = 3。

出栈(chnt_wireless_add_pop_code(),:268)

LIFO:栈空返回 0(表示升级序列结束);否则 top--,取出 u8_codes[top] 并把该槽位清 0。

实际升级顺序:METER_UPGRADE_INIT 弹出的是 0xE3(STA2),METER_UPGRADE_END 再弹 0xE2(STA1),最后弹 0xE1(AP)——即先子节点、后 AP。

波特率切换:进入无线升级前 usart1_init(115200),全部完成后 usart1_init(9600) 恢复。恢复前后各有 2s / 30ms 延时用于等待无线模块重新绑定(meter_set_chnt_wireless_bound(0xE1, 3))。

4.4 定时/调度与 uCOS-II 配合

4.5 关键全局变量的读写

变量写方读方触发条件
s_meter_upgrade_tab.u8_upgrade_state本文件本文件BSS 初值 0 = METER_UPGRADE_INIT
s_meter_upgrade_tab.u32_addr_flash_offsetMETER_WRITE_FLASH、METER_ERASE_FLASHget_percent_upgrade_meter() / get_upgrade_file_offset_meter()每帧成功后按 cnt*250 重算
s_meter_upgrade_tab.u8_meter_addrMETER_UPGRADE_INIT、METER_UPGRADE_ENDmeter_upgrade_send_code/data、接收校验每次确定目标设备时设置
g_t_meter.g_code_stackchnt_wireless_add_update_code / chnt_wireless_add_pop_code同上仅 CHNT_WIRELESS 流程
g_u8_fw_sdram1net/ 层下载模块meter_upgrade_send_data只读,METER_ERASE_FLASH 成功后把指针绑到 u8_upgrade_file_buf
g_t_meter.t_config_data.e_grid_meter_type_old / e_pv_meter_type_old配置模块meter.c:5733-5757(判断走 BB 升级还是本模块)由调用方决定,不在本文件
UPDT_Meter 升级标志本文件(METER_UPGRADE_END 清除)task_meter() 循环条件由 net/ 层置位
OSTimeGet() 节拍uCOS-II本文件所有超时判定—

5. 对外接口

导出(供外部调用)

名称声明位置调用方
void meter_upgrade_handle(void)meter_upgrade.h:66midware/meter/meter.c:5742、:5756、:5761(task_meter())
u8 get_percent_upgrade_meter(void)meter_upgrade.h:67midware/meter/meter.c:6848、:6861、:6866(GetPercentUpgradeMeter())
u32 get_upgrade_file_offset_meter(void)meter_upgrade.h:68net/ems.c:6711(上报 g_t_upgrade_run_info.u32_upgrade_file_offset)

本文件的三个静态缓冲(s_meter_upgrade_send_buf / s_meter_upgrade_recv_buf / s_meter_upgrade_tab)均为 static,无外部链接。

调用(依赖的外部函数/变量)

名称来源
usart1_init() / usart1_clear_buffer() / usart1_send_data() / usart1_is_recv_async_data() / usart1_get_data_len() / usart1_read_data_async()hardware/usart1/usart1.h:8-13
GetCrcCode_Modbus()common/crc.h:17
get_meter_updt_object()net/meter_download.h:18(返回 GRID_METER/PV_METER/CHNT_WIRELESS/MID_METER)
get_length_file_updating() / Set_Status_Is_Updated_From_Server() / UPDT_Meternet/upd_download.h:37/39
g_u8_fw_sdram1net/server.h:258(net/sdram.h)
g_t_meter、METER_UPGRADE_T 相关地址宏midware/meter/meter.h:GRID_ALPHA_METER_ADDR(0x01)、PV_ALPHA_METER_ADDR(0x02)、PV_GRIDFORMING_EN_METER_ADDR(0x0B)、CHNT_WIRELESS_AP_ADDR(0xE1)、MAX_CODES(3)、CHNT_WIRELESS_STACK
meter_get_pv_meter_addr_change_flag()midware/meter/meter.h:482
meter_set_chnt_wireless_bound()midware/meter/meter.h:522
OSTimeGet() / OSTimeDlyHMSM()、memcpy / memset / size_tuCOS-II、标准 C 库(经 includes.h)
TRUE / FALSEincludes.h

6. 依赖关系

7. 备注与风险点

  1. 接收缓冲无长度上限检查(高优先级,栈/全局溢出)——meter_upgrade_recv_data() 的 :72-79:
       for(size_t i = 0; i < l_u16_len; i++) *(buf + i) = usart1_read_data_async();

l_u16_len 直接取自 usart1_get_data_len(),没有像 meter_recv_data()(meter.c:78)那样先按缓冲尺寸截断。而唯一调用方传入的是 11 字节的 s_meter_upgrade_recv_buf。只要总线上出现超过 11 字节的一帧(错误帧、其他设备应答、噪声、或厂商响应变长),就会越界写 11 字节之外的静态存储区。这是本文件最严重的隐患。

  1. 校验用的是全局缓冲而不是参数缓冲——:88-91 判定条件读的是 s_meter_upgrade_recv_buf[...],而数据是写进形参 buf 的。当前唯一调用点传的正好就是同一个数组,因此"能用",但函数签名与实际语义不一致,一旦被复用即出错。应改为判定 *(buf + n)。
  2. 完整响应帧布局未文档化——只校验 recv[0]、recv[3]、recv[6]、recv[7] 四个位置(:88-91),其中 recv[3]==0x03 与 recv[7]==0x06 为魔数,recv[6] 被解释为"操作码回显"。这种"四点抽样校验 + CRC"的方式在协议变更时很难定位问题,且缺乏注释。
  3. CHNT_WIRELESS_UPDATE_POP 被当作常量位域传入——:307 写死 chnt_wireless_add_update_code(CHNT_WIRELESS_UPDATE_POP),形参名却叫 u8_module_child_nodes_state,暗示它本该是"实际在线的子节点掩码"。当前实现等于无条件同时升级 STA1 与 STA2,若现场只装了其中一个节点,升级其一会失败并消耗 3 次重试后跳到 END;虽然不会导致死循环,但每次升级都会白跑一轮。
  4. u16_send_file_num 为 0 时尾块逻辑会异常——:375 判断 s_u16_file_send_cnt < u16_send_file_num - 1,两者都是 u16;若固件长度为 0(send_file_num = 0),0 - 1 回绕成 65535,条件成立,于是会以 u16_length = 250 发送 250 字节来自 g_u8_fw_sdram1 的未定义内容(实为缓冲区残留)。虽然正常下载流程不会给出 0 长度文件,但缺少防御。
  5. 两个除法/取长度接口缺少除零保护——get_percent_upgrade_meter()(:469)直接用 get_length_file_updating(UPDT_Meter) 作除数;文件长度未知时返回 0 会触发除零异常(Cortex-M4 上表现为 HardFault 或返回 0,取决于配置)。METER_ERASE_FLASH 里也多次调用同一接口(:356-358),若长度在升级中途变化会导致帧数错乱。
  6. METER_BUF_LEN、METER_UPDATE_VALID_LEN 是死宏——.h:22-23 定义了 1024 与 500,全工程检索无任何引用,属从早期大缓冲设计遗留下来的常量,容易误导(让人以为接收缓冲有 1024 字节,实际是 11)。
  7. u16_length 字段类型与命名不一致——METER_UPGRADE_T.u16_length 声明为 u32,u8_upgrade_state 用 u8 存枚举,风格与 bb_update.h 中同样的 s32_ 前缀误用问题一致,属工程通病。
  8. METER_UPGRADE_CMD_E / METER_UPGRADE_STATE_E 的类型名从未被引用——只用到了枚举常量。这样枚举实际上退化为"常量集合",失去了类型检查意义(任何整型都能赋给 u8_upgrade_state)。
  9. usart1_init(115200) 会改变共享总线波特率——无线升级期间把电表总线波特率从 9600 切到 115200,若该串口上同时挂了电网表/PV 表(实际接线可能是分时的),切换期间其它设备无法通讯。代码通过"升级期间 task_meter() 不跑常规查询"来回避冲突,但这依赖调用方严格遵循控制流,属隐式契约。
  10. METER_BOOT_EXIT 成功分支里的 err_cnt = 0 是无效赋值——:421 之后 METER_UPGRADE_END 会 memset 整个表,该赋值随即被覆盖。
  11. meter_upgrade_recv_data() 在 l_u16_len < 2 时忙等——:67-70 用 continue 跳过,但数据未被读出,若接收缓冲长期只有 1 字节,会以极小间隔空转直到超时(最多 5 秒)。虽不会死锁,但属于忙等待,未用 OSTimeDlyHMSM() 让出 CPU(对比 bb_update.c:260 有 30ms 让出)。
  12. RECV_SEQUENCE_FRAME 宏在 bb_update.h 与 meter_upgrade.h 中重复定义(都是 0x06,含义不同:前者是 BB 应答特征,后者是电表响应帧类型)。若某 .c 同时 include 两个头文件会产生宏重定义告警(当前 meter.c 两者都 include,定义值相同故编译器只报 warning 或静默)。
  13. 无线多节点升级失败会静默放弃后续节点吗?——不会:METER_UPGRADE_END 只要栈里还有地址就继续弹,因此即使某个节点升级失败(err_cnt 超限跳到 END),也会继续升级下一个节点,整体以"清 UPDT_Meter 标志"收尾,失败信息不向云端反馈(没有错误码字段),现场只能从电表版本号判断。