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

bb_update.c

源文件路径:midware/meter/bb_update.c 所属模块:midware/meter(电表/备用箱中间件) 代码行数:688

1. 文件职责

本文件实现 BackupBox(BB,备用箱)内置 MCU 的固件串口升级流程。BB 是 AlphaESS 备用电源箱,其内部还有一颗独立 MCU,EMS 侧需要把新的 .hex 固件经 UART1(RS485/串口)通过一套"复位 → 进 Bootloader → 改波特率 → 擦除 → 分块写 Flash → 校验 Flash → 写关键字 → 结束"的私有协议烧写进去。

固件原始数据来自服务器下发,已由 net/ 层的下载模块落到 SDRAM 缓冲区 g_u8_fw_sdram1(S19/HEX 文本格式),本文件负责把 HEX 文本逐行解析成二进制、按片拼凑成一次可发送的负载,再按帧发送并等待 Bootloader 的固定应答。

驱动方式:midware/meter/meter.c:5738 的 task_meter() 任务在检测到 Is_Updated_From_Server(UPDT_Meter) 为真、且电网侧电表型号为 BACKUPBOX_5K/10K/23K 时,每轮循环调用一次 bb_update();bb_update() 内部通过 OSTimeDlyHMSM() 自阻塞按毫秒级节拍推进状态机,是典型的"单任务状态机 + 阻塞等待应答"实现。

进程内还对外提供升级进度(GetPercentUpgradeBB())与已传字节偏移(GetUpgradeFileOffsetBB())供 midware/meter/meter.c 与 net/ems.c 上报云端。

2. 关键数据结构与宏

本文件宏

名称类型说明
USART_TX_TIMEOUT宏1000,串口发送超时(ms),传给 usart1_send_data()
USART_RX_TIMEOUT宏1000,接收基准超时(ms);实际接收等待用 USART_RX_TIMEOUT*10 = 10s
BB_APP_FLASH_ADDR_HEAD宏0x003D8000,BB 应用区起始地址(第一个 0xF000 半区起点)
BB_APP_FLASH_ADDR_HALF宏0x003E7000,应用区中点(第二个半区起点)
BB_APP_FLASH_ADDR_END宏0x003F6000,应用区结束;实际未被引用(:446 用裸常量写了同一个值)
BB_APP_FLASH_HALF_LEN宏0xF000,每个校验半区的字节长度
BB_APP_KEYWORD_ADDR宏0x003F3FF0,升级关键字(0x6D83)存放地址,写入即触发 Bootloader 跳转
BB_BOOT_FLASH_ADDR_END宏0x003F7FFF,HEX 解析越界判断上界(超过则视为解析结束)

本文件用到的头文件定义

名称类型说明
BB_INSTRUCTIONS枚举(bb_update.h:25)REPLY_FRAME / SEQUENCE_FRAME / COMMAND_DATA_FRAME,三种帧型;本文件未直接使用(仅语义注释)
BB_STATE枚举(bb_update.h:32)升级状态机:RESET_INSTRUCTIONS、BOOT_INSTRUCTIONS、ADJUST_RATE、ERASE_FLASH、WRITE_FLASH、CHECK_FLASH、SEND_KEY_WORD、UPGRADE_END、UPGRADE_INIT、RECV_DATA(RECV_DATA 未使用)
struct BB_UPDATE_CONTENT结构体(bb_update.h:47,#pragma pack(1))升级上下文:文件长度、Flash 基址/偏移、拼包缓冲 u8_buffer[256]、通信序列号 u8_loop_addr、校验码 u16_check_code、状态 u8_insteructions_state、序列帧字节数 u32_amount_sequence_frame 等
struct BB_CHECK结构体(bb_update.h:63 #pragma pack(1))Flash 校验累加:u8_time 校验轮次,u16_check_length[10] 参与累加的字节数、u16_check_code[10] 累加和(只用到下标 0/1/2)
MAX_PRG_LENGTH宏(bb_update.h:21)112*2 = 224,单帧最大负载字节数(112 个 16bit 字)
RECV_SEQUENCE_FRAME / RECV_COMMAND_DATA_FRAME宏(bb_update.h:19-20)0x06 / 0x01,接收侧应答帧类型判别值(本文件未直接引用)
HEX_FILE_T结构体(common/hex_file_parse.h:31)HEX 单行解析结果:u8_data_len、u16_addr、u8_code_type、u16_data_buf[32]、u8_ascll_len
PARSE_STATE_E枚举(common/hex_file_parse.h:15)PARSE_OK / PARSE_ADDR / PARSE_END / DATA_ERR / PARSE_ERR
HEX_CODE枚举(common/hex_file_parse.h:24)GET_DATA_CODE=0x00、GET_END_CODE=0x01、GET_BASE_ADDRESS_CODE=0x04、GET_LINEAR_ADDRESS_CODE=0x05

本文件全局变量

名称类型说明
g_bb_updatestruct BB_UPDATE_CONTENT(非 static,:36)升级主上下文
g_check_codestruct BB_CHECK(非 static,:37)Flash 校验累加器
s_hex_file_datastatic HEX_FILE_T(:39)HEX 解析工作区,hex_parse() 的输出
s_u32_parse_lengthstatic u32(:41)已成功发送的 HEX 文本字节数,用于算进度
g_u8_bb_recv_buf[20]u8(非 static,:42)应答帧接收缓冲
g_u8_reset_instructions0[9]帧模板复位指令,全 0(9 字节,实际只发前 9/8 字节)
g_u8_boot_instructions[8]帧模板"PSLAVER\r" = 50 53 4C 41 56 45 52 0D,Bootloader 跳转/进入口令
g_u8_adjust_rate[1]帧模板{0x41},触发 BB 侧波特率自适应
g_u8_reply_frame[8]帧模板{0x06,0x99,0x66,seq,0x00,0x08,0x00,0x00},回复帧,下标 3 为通信序列号
g_u8_sequence_frame[8]帧模板{0x01,0x66,0x99,seq,len_h,len_l,0x00,0x00},序列帧,下标 4/5 为后续"指令与数据帧"总字节数
g_u8_erase_flash[20]帧模板擦除指令,[0..1]=00 0A,[10..11]=00 0E,尾部 FF FF
g_u8_endcode[20]帧模板结束指令,[0..1]=00 0D,尾部 FF FF
g_u8_write_flash[256]帧模板写 Flash / 写关键字复用帧,[0..1]=00 0B,[2..5] 为目标地址,[8..9] 为字数,[20..243] 为负载
g_u8_check_flash[20]帧模板校验指令,[0..1]=00 0C,[2..5] 地址,[8..9] 长度,[16..17] 期望校验和,[18..19]=FF FF

3. 函数清单

函数名作用域说明
clear_bb_update_datastatic清零 g_bb_update/g_check_code,清 UPDT_Meter 服务器升级标志,清串口缓冲
parse_bb_hex_contentstatic从 g_u8_fw_sdram1[offset] 起解析一行 HEX,把数据拼进 u8_buffer,返回 PARSE_OK/PARSE_ADDR/PARSE_END/PARSE_ERR
bb_send_datastatic清缓冲后 usart1_send_data() 发送一段数据,恒返回 TRUE
deal_bb_update_datastatic阻塞接收最多 18 字节应答,按固定字节模式判定是否应答成功
bb_update全局升级主状态机入口,每次调用推进一个状态
GetPercentUpgradeBB全局返回 BB 升级百分比 = s_u32_parse_length*100/get_length_file_updating(UPDT_Meter)
GetUpgradeFileOffsetBB全局返回已传输的 HEX 文本字节数 s_u32_parse_length

4. 核心逻辑解析

4.1 主状态机(bb_update(),:305)

状态存放在 g_bb_update.u8_insteructions_state,switch 每调用一次只执行一个 case(除 WRITE_FLASH 内部有 while(1))。

UPGRADE_INIT ──► RESET_INSTRUCTIONS ──(延时后)──► BOOT_INSTRUCTIONS
                                                      │
                                            (延时 2s)  ▼
                                                  ADJUST_RATE
                                                      │
                                            (延时 1s)  ▼
                            ┌────────────────────► ERASE_FLASH
                            │(应答成功)                  │
                            │                          ▼
                            │                     WRITE_FLASH ◄─┐
                            │                       │  (while(1) 内循环分块发送)
                            │            解析到关键字地址 ▼
                            │                     CHECK_FLASH
                            │                       │ (s_u8_check_time==2)
                            │                       ▼
                            │                   SEND_KEY_WORD
                            │                       │
                            │                       └──► CHECK_FLASH (重复)
                            │                              │ (s_u8_check_time>=10)
                            │                              ▼
                            └──────────────────────── UPGRADE_END

各状态动作与迁移条件:

应答成功时 s_u8_check_time++;==2 时转 SEND_KEY_WORD;>=10 时转 UPGRADE_END(隐含最多校验 10 轮的重试上限)。应答失败时不递增、也不做任何处理,本轮直接结束,下一轮重发同一档校验请求。

总超时看门狗:函数末尾 bb_update.c:661 判断 abs(s_u32_tick_timeout - OSTimeGet()) >= 1000*60(60 秒),超时强制回 UPGRADE_INIT。各状态在收到有效应答后都会刷新 s_u32_tick_timeout,因此该定时器实质是"整流程无响应 60 秒即重来"。

4.2 HEX 解析与拼包(parse_bb_hex_content(),:126)

调用 hex_parse(&s_hex_file_data, &g_u8_fw_sdram1[offset], 80) 解析 SDRAM 中的一行文本(固定长度 80 字节)。

4.3 发送主循环(WRITE_FLASH 分支,:374)

while(1) 内两段:

第一段:解析(仅当上一轮没有接收错误 s_u8_recv_err_flag == FALSE) 调用 parse_bb_hex_content(s_u32_parse_length + l_u32_parse_length_temp),用两个游标区分"已发送的文本长度"和"本轮已累积但未发送的文本长度":

第二段:发送(当 s_u8_parse_flag == PARSE_OK 或本轮接收出错)

  1. 发回复帧:g_u8_reply_frame[3] = u8_loop_addr,发送后 u8_loop_addr++。
  2. 发序列帧:长度域 s_u16_length = 28 + g_bb_update.u32_amount_sequence_frame,g_u8_sequence_frame[3] = u8_loop_addr,[4]/[5] 为大端长度。
  3. 发指令与数据帧(复用 g_u8_write_flash):
    • [2..5] = s32_addr_flash 的 big-endian 四字节(注意顺序是 >>24, >>16, >>8, >>0)。
    • [8..9] = 字数 = u32_amount_sequence_frame / 2(大端)。
    • memcpy(g_u8_write_flash+20, g_bb_update.u8_buffer, 256) 把拼好的负载拷入。
    • 发送长度 = s_u16_length - 8(序列帧 8 字节在总长里被扣除,因为序列帧已单独发过)。
  4. Flash 校验和累加(仅当 s32_addr_flash 与上次不同 u32_old_flash_addr):按地址落点把本帧数据累加进 g_check_code.u16_check_code[0](前半区)或 [1](后半区),并递增对应 u16_check_length。
  5. 调用 deal_bb_update_data() 等应答:
    • 成功:刷新 s_u32_tick_timeout,清 s_u8_recv_err_flag,s_u32_parse_length += l_u32_parse_length_temp(推进进度游标),清零临时游标和 s_u8_send_err_cnt。
    • 失败:清 s_u8_recv_err_flag(故意置 FALSE,让下一轮重新解析同一段数据),清 l_u32_parse_length_temp,s_u8_send_err_cnt++;连续超过 3 次 → 置 UPGRADE_INIT 并 return。
    • 无论成败都 return(一次调用只发一帧)。
  6. 若解析结果为 PARSE_END(说明已到关键字地址),计算最终校验和并转 CHECK_FLASH:
    • check_code[2] = check_code[1] + 0x6D83
    • check_code[0] += (0xF000 - check_length[0]) * 0xFFFF
    • check_code[1] += (0xF000 - check_length[1]) * 0xFFFF
    • check_code[2] += ((0xF000 - check_length[1]) - 1) * 0xFFFF

即先用 0xFFFF(Flash 擦除后的空白字)把不足整半区的部分补齐,再算和,以匹配 BB 侧对整块区域求和的口径。

4.4 应答判定(deal_bb_update_data(),:240)

也就是说这是模式匹配而非 CRC 校验:BB 的 Bootloader 应答内容固定,代码只校验其中 14 个字节的常量特征。

4.5 通信协议帧格式汇总

帧长度字节布局
复位指令9 / 8全 0x00
Boot 进入850 53 4C 41 56 45 52 0D(ASCII "PSLAVER\r")
波特率自适应141
回复帧806 99 66 <seq> 00 08 00 00
序列帧801 66 99 <seq> <len_H> <len_L> 00 00;len = 28 + 负载字节数(擦除/校验/结束时固定 00 1C=28,写关键字时为 30)
擦除指令帧2000 0A 00 00 00 00 00 00 00 00 00 0E 00 00 00 00 00 00 FF FF
写 Flash 帧8 + 负载00 0B <addr BE 4B> <填充> <字数 BE 2B> ... <负载> FF FF,负载从下标 20 开始
校验 Flash 帧2000 0C <addr BE 4B> ... <len BE 2B> <crc BE 2B> FF FF
结束指令帧2000 0D 00 00 ... FF FF

无 CRC 字段——校验依赖帧内固定字节特征与 Flash 内容校验和。

4.6 与 uCOS-II 的配合

4.7 关键全局变量的读写

5. 对外接口

导出(供外部调用)

名称声明位置调用方
void bb_update(void)bb_update.h:73midware/meter/meter.c:5738(task_meter() 内)
u8 GetPercentUpgradeBB(void)bb_update.h:74midware/meter/meter.c:6844、:6857(GetPercentUpgradeMeter())
u32 GetUpgradeFileOffsetBB(void)bb_update.h:75net/ems.c:6706(上报 g_t_upgrade_run_info.u32_upgrade_file_offset)
struct BB_UPDATE_CONTENT g_bb_update无头文件声明仅本文件使用(外部无引用)
struct BB_CHECK g_check_code无头文件声明仅本文件使用(外部无引用)
u8 g_u8_bb_recv_buf[20]无头文件声明仅本文件使用(外部无引用)

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

来自 bb_update.h → includes.h 的公共定义,以及:

名称来源
hex_parse()、HEX_FILE_T、PARSE_*、GET_*_CODEcommon/hex_file_parse.h
usart1_clear_buffer()、usart1_send_data()、usart1_get_data_len()、usart1_read_data_async()hardware/usart1/usart1.h
OSTimeGet()、OSTimeDlyHMSM()、memset/memcpyuCOS-II / includes.h
Set_Status_Is_Updated_From_Server()net/upd_download.h:39
get_length_file_updating()net/upd_download.h:37
g_u8_fw_sdram1net/server.h:258(SDRAM 固件缓冲区)
UPDT_Meternet/upd_download.h(ENUpdateType)
TRUE/FALSEincludes.h

6. 依赖关系

7. 备注与风险点

  1. 校验和地址累加存在明确的算术错误(高优先级)——bb_update.c:436-451:
       u32 l_u32_flash_addr = g_bb_update.s32_addr_flash;
       for(u8 i = 0;i < l_u8_flash_len;i++)
       {
           l_u32_flash_addr += i - 10;   // i 为 u8,i<10 时 i-10 为负数
           ...
       }

i - 10 在 i < 10 时为负整数,与 u32 相加会回绕,且该值在循环中累加(等价于 addr + Σ(i-10)),导致 l_u32_flash_addr 相对真实写地址整体漂移约 5KB(224 字节负载时偏移 0x13E8)。因此前半区/后半区的边界判断(>=BB_APP_FLASH_ADDR_HEAD && <BB_APP_FLASH_ADDR_HALF)会落在错误位置,一段跨越半区边界的固件可能在两侧都漏计或重复计入校验和。正确写法应是 l_u32_flash_addr = g_bb_update.s32_addr_flash + i 或用独立下标。这一点直接决定 CHECK_FLASH 上报给 BB 的期望校验和是否正确。

  1. CHECK_FLASH 的第三次校验地址与语义不自洽——:546-562 的 else 分支地址仍用 BB_APP_FLASH_ADDR_HALF、长度仍为 0xF000,但期望值用 check_code[2],而 check_code[2] = check_code[1] + 0x6D83(:481)。同时 check_code[2] 的补齐项用的是 (0xF000 - check_length[1]) - 1(:484),与 [0]/[1] 的补齐口径不一致。0x6D83 是关键字值的魔数,这里属于针对 BB Bootloader 具体实现的"对表"写法,缺少注释,一旦 BB 侧固件版本变化即失效。
  2. CHECK_FLASH 在 SEND_KEY_WORD 之后不会被复位——SEND_KEY_WORD 成功回到 CHECK_FLASH 时 s_u8_check_time 已是 2,于是会再走 8 轮(2→10)第三次校验才结束。看起来 :573 的判断本意是"只在第一/二档校验完成后写关键字",但这个多出来的循环靠 >=10 兜底,属于设计含糊处。
  3. ERASE_FLASH 无重试上限——:364-370 只在成功时迁移,失败时原地重发;只有外层 60 秒总超时可以解困,但每次进入该状态都会重新发帧且不刷新 s_u32_tick_timeout(仅成功时刷新),因此 60s 后会被强制打断。
  4. deal_bb_update_data() 使用固定字节模式而非 CRC——:274-291 硬编码了 14 个字节的特征值,且 g_u8_bb_recv_buf 大小为 20 但循环以 18 字节为终止条件,读取下标最大 17,无越界;但若 BB 应答长度变化(例如错误码不同),将永远被判为失败。
  5. u8_buffer[256] 被整块 memcpy 到 g_u8_write_flash+20(:430),写入范围 [20, 276),而 g_u8_write_flash 数组只有 256 字节——越界 20 字节。由于实际发送长度仅为 s_u16_length-8(最大 244),越界部分不会被发出,但会破坏 g_u8_write_flash 之后的相邻全局数据(链接顺序决定受害对象)。这是真实的缓冲区溢出隐患。
  6. WRITE_FLASH 的 while(1) 缺少重入保护:若 parse_bb_hex_content 反复返回 PARSE_ADDR(HEX 文件异常长且没有 PARSE_OK 触发点),该 while 会在单次调用内长时间占住 CPU,延时只在 deal_bb_update_data() 里出现,外层 60s 看门狗也检查不到(检查在 switch 之后)。
  7. s_u32_parse_length 在 PARSE_END 路径不更新(:479-488 未累加 l_u32_parse_length_temp),所以升级成功时的进度百分比可能停在小于 100 的值。
  8. 魔法数字密集::481 的 0x6D83、:482-484 的 0xFFFF/-1、:504/:593 的 28/30、:431 的 s_u16_length-8、:267 的 18 字节长度,均无宏定义。
  9. BB_APP_FLASH_ADDR_END(:27)定义了但从未使用,:446 直接写了字面量 0x003F6000,修改宏不会生效——典型的版本遗留。
  10. g_check_code.u8_time(:197)只自增、从不读取,是死代码。
  11. g_u8_reset_instructions0 声明为 [9] 但第一次按 9 字节、第二次按 8 字节发送,属有意为之(差异复位),但注释里写的是 {0x00×9},可读性差。
  12. s_hex_file_data 与 g_u8_fw_sdram1[offset] 的 HEX 行固定按 80 字节解析(:130),隐含固件 HEX 每行文本长度恒为 80;若打包工具换行格式变化会解析失败。
  13. GetPercentUpgradeBB() 未做除零保护(:675):get_length_file_updating(UPDT_Meter) 在文件长度未知时为 0,会触发除零异常。