evc_upgrade.c
parse_evc_hex_content)4.5 收发时序与超时重试4.6 与 uCOS-II 的配合4.7 关键全局变量的读写5. 对外接口6. 依赖关系7. 备注与风险点源文件路径:
midware/evc/evc_upgrade.c所属模块:midware/evc(交流充电桩 EVC / EVC-II 固件远程升级) 代码行数:784
1. 文件职责
本文件负责把云端下发的充电桩固件(HEX 文件)通过 UART3(外部 485 总线)逐包烧写进充电桩,是"云端固件下载 → 充电桩烧写"这条链路的最后一环。云端先把固件存进 SDRAM(g_u8_fw_sdram1),本文件从 SDRAM 里读 ASCII 格式的 Intel HEX,用 hex_parse() 解析、再按 224 字节(EVC_MAX_PRG_LENGTH = 112*2)为单位重新拼包,最后按升级协议组帧、必要时做 AES-256 加密后下发。
它由 midware/interface/interface.c:341 驱动:当 is_upgrade_evc()(即 Get_Status_Is_Updated_From_Server(UPDT_EVC))为真时,接口任务会完全让出串口 3,改为每轮调用一次 evc_upgrad_handle() 并 OSTimeDlyHMSM(0,0,0,50),直到升级结束。升级标志由云端特殊命令经 net/server.c:2458 的 set_evc_upgrad_flag() 设置(参数是桩号,"0" 表示全部)。一代桩(g_t_evc_run_info)与二代桩(g_t_evc_ii_run_info[4])共用本文件,只是每代的运行时状态结构不同。
进度上报由 get_percent_upgrade_evc() / get_upgrade_file_offset_evc() 对外提供,前者被 HMI(hmi/ui/ui_main.c:895)画百分比,后者被云端上报(net/ems.c:6719)。
2. 关键数据结构与宏
| 名称 | 类型 | 说明 |
|---|---|---|
EVC_UPGRADE_T | 结构体(evc_upgrade.h:41-53,#pragma pack(1)) | 本文件的全部升级状态:u8_upgrad_flag[4]/u8_upgrad_err_cnt[4](每桩的使能与重试计数)、u32_addr_flash_base(HEX 04 记录给出的基址高 16 位)、u32_addr_flash(当前包起始烧写地址)、u32_addr_flash_offset/u32_old_addr_offset(HEX 记录内偏移与上一包偏移)、u32_parse_length(已消费的 HEX 文件字节数,即进度)、u8_upgrad_file_buf(指向发送缓存第 18 字节的数据区指针)、u16_length(当前包长)、u8_evc_ii_encryption_flag[4](各桩是否启用加密) |
HEX_FILE_T | 结构体(common/hex_file_parse.h:32-41) | HEX 解析结果:u8_data_len/u16_addr/u8_code_type/u16_data_buf[32]/u8_sum_check/u8_ascll_len |
EVC_UPGRADE_STATE_E | 枚举(evc_upgrade.h:18-28) | 升级主状态机:EVC_UPGRADE_INIT、EVC_PRAVITE_KEY、EVC_BOOT_ENTER、EVC_HANDSHAKE、EVC_ERASE_FLASH、EVC_WRITE_FLASH、EVC_SEND_KEY_WORD、EVC_UPGRADE_END |
EVC_UPGRADE_CODE_E | 枚举(evc_upgrade.h:30-38) | 下发的命令码:EVC_APP_JUMP=0、EVC_HAND_SHAKE=1、EVC_ERASE_APP=2、EVC_WRITE_APP=3、EVC_CHECK_FILE=4、EVC_BOOT_JUMP=5 |
PARSE_STATE_E | 枚举(hex_file_parse.h:14-21) | hex_parse() 与 parse_evc_hex_content() 的返回:PARSE_OK=0(已拼好一包)、PARSE_ADDR=1(收到地址/数据记录,需继续)、PARSE_END=2(收到 01 结束记录)、DATA_ERR/PARSE_ERR |
HEX_CODE | 枚举(hex_file_parse.h:23-29) | GET_DATA_CODE=0x00、GET_END_CODE=0x01、GET_BASE_ADDRESS_CODE=0x04、GET_LINEAR_ADDRESS_CODE=0x05 |
EVC_UPGRADE_TEMP_DATA_LEN | 宏(=80) | hex_parse() 单次读取的 ASCII 字节窗口长度 |
EVC_MAX_PRG_LENGTH | 宏(=112\*2 = 224) | 单包固件数据最大字节数,同时决定 0xFF 填充长度 |
MAX_EVC_II_NUM | 宏(evc_ii.h:14 = 4) | 二代桩最大数量,也是 u8_upgrad_flag[] 的长度 |
EVC_BUF_LEN | 宏(evc.h:14 = 256) | g_evc_send_buf/g_evc_recv_buf 长度 |
s_evc_upgrade_send_buf | static u8* | 直接指向 g_evc_send_buf(一代桩通信缓存,与一代桩共用);有效组帧区仅前 256 字节 |
s_evc_upgrade_recv_buf | static u8* | 指向 g_evc_recv_buf |
s_evc_upgrade_send_aes_buf | static u8[270] | AES 加密后的发送缓存(16 字节对齐填充后最大 32+224+16 ≈ 272,实配 270,见第 7 节) |
s_evc_upgrade_tab | static EVC_UPGRADE_T | 全局升级状态实例 |
s_hex_file_data | static HEX_FILE_T | 文件级 HEX 解析结果(注意 parse_evc_hex_content() 与主状态机共享它) |
g_u8_fw_sdram1 | 全局(sdram.c) | 固件缓冲区,本文件从 &g_u8_fw_sdram1[u32_parse_length] 处按 80 字节窗口解析 |
PUBLIC_KEY | 宏/常量(evc_ii.c) | AES-256 公钥,get_evc_ii_pravite_key() 用它换回真实密钥 |
3. 函数清单
| 函数名 | 作用域 | 说明 |
|---|---|---|
evc_upgrade_send_cmd | 全局 | 按升级协议组帧(0xFA/0x03 + 地址 + 0xAB + 长度 + 数据 + CRC16-IBM),若该桩启用加密则先 evc_ii_aes_encrypt() 再补 CRC,最后 uart3_send_data() 发出(第 45 行) |
evc_upgrade_recv_data | 全局 | 阻塞等待 UART3 异步接收数据,校验 CRC16-IBM,加密桩走 AESDecrypt() 解包后二次校验内层 CRC,超时返回 *len = 0(第 88 行) |
evc_upgrade_send_code | 全局 | 下发"纯命令"帧(数据域为 00×6 + code + 00×3),校验应答的地址/0x0B/命令码/结果字段,返回 TRUE/FALSE(第 178 行) |
evc_upgrade_send_data | 全局 | 下发"写 Flash"帧(EVC_WRITE_APP),前缀 10 字节携带烧写地址与长度(单位:16 位字),后跟 u16_length 字节固件数据(第 220 行) |
evc_upgrade_send_key_word | 全局 | 下发"跳转应用程序"帧(EVC_APP_JUMP),数据域含目标地址 0x0308FFFC 与魔术字 "OK"(第 262 行) |
parse_evc_hex_content | static | 从 SDRAM 逐条解析 HEX,按地址连续性把多条记录拼成不超过 224 字节的一包;返回 PARSE_OK/PARSE_ADDR/PARSE_END/错误码(第 311 行) |
evc_upgrad_handle | 全局 | 升级主状态机,每轮由 interface.c 调用一次;负责按桩号重试下发、驱动 HEX 解析与分包发送、结束清理(第 433 行) |
set_evc_upgrad_flag | 全局 | 由云端特殊命令设置要升级哪几台桩:flag > MAX_EVC_II_NUM 时全部清零,flag == 0 时全体置位,否则只置 flag-1(第 694 行) |
get_evc_upgrad_flag | static | 遍历 u8_upgrad_flag[],只要还有一台桩待升级就返回 TRUE(第 735 行) |
is_upgrade_evc | 全局 | 转封 Get_Status_Is_Updated_From_Server(UPDT_EVC),供 interface.c 与 HMI 判断是否处于升级流程(第 755 行) |
get_percent_upgrade_evc | 全局 | 返回 u32_parse_length * 100 / get_length_file_updating(UPDT_EVC),即已解析字节数占比(第 767 行) |
get_upgrade_file_offset_evc | 全局 | 返回 u32_parse_length,供云端上报断点续传偏移(第 780 行) |
共 13 个函数(3 个 static:parse_evc_hex_content、get_evc_upgrad_flag,加上文件头部第 38 行的前置声明 static u8 get_evc_upgrad_flag(void);,该声明本身不是定义)。
4. 核心逻辑解析
4.1 串口协议帧格式
发送帧(明文) 由 evc_upgrade_send_cmd() 组装(evc_upgrade.c:51-63),格式固定为 8 字节头 + 变长数据域 + 2 字节 CRC:
| 偏移 | 长度 | 内容 |
|---|---|---|
| 0 | 1 | 0xFA 起始码 |
| 1 | 1 | 0x03 固定字节(协议版本/类型) |
| 2 | 1 | 从机地址 addr = id + 1(id 为 0 基桩号) |
| 3 | 1 | 0x00 保留 |
| 4 | 1 | 0xAB 升级通道标识 |
| 5 | 1 | 0x00 保留 |
| 6-7 | 2 | 数据域长度 len,小端(先低字节) |
| 8 | len | 数据域(由调用者填充) |
| 8+len | 2 | CRC16-IBM,小端 |
CRC 计算范围为头 8 字节 + 数据域(不含 CRC 自身),函数 GetCrcCode_IBM() 来自 common/crc.c。
加密帧:当 s_evc_upgrade_tab.u8_evc_ii_encryption_flag[addr-1] == TRUE 时,明文整帧(含 CRC)经 evc_ii_aes_encrypt() 包装成
<addr> <len_hi> <len_lo> <remainder+2> <AES-256-ECB 密文...>
再在这段外层帧尾部追加一次 GetCrcCode_IBM() 的 2 字节 CRC,然后整体发出。注意外层帧与明文帧是两套不同的 CRC 域,接收侧也要校验两层(见 evc_upgrade_recv_data)。
接收帧(明文) 与发送帧同构([0]=0xFA、[2]=addr、[4]=0x0B 表示 ACK、[10]=被回显的命令码、[12..13]=结果码小端,0 表示成功)。接收帧(加密) 则是 <addr> <len_hi> <len_lo> <remainder+2> <密文>,解密后得到明文帧。
4.2 升级状态机
evc_upgrad_handle() 用一个 static u8 s_u8_upgrade_state(初值 EVC_UPGRADE_INIT)保存状态,每轮只执行一步,靠 interface.c 的 50 ms 轮询推进。迁移图:
EVC_UPGRADE_INIT ──(无条件)──► EVC_PRAVITE_KEY
│ 对每个待升级桩最多重试 3 次 get_evc_ii_pravite_key(i)
│ 成功 ⇒ u8_evc_ii_encryption_flag[i] = TRUE
▼
EVC_BOOT_ENTER
│ 最多 5 次 evc_upgrade_send_code(i, EVC_BOOT_JUMP=5)
│ 失败 5 次 ⇒ u8_upgrad_flag[i] = FALSE
│ 全部桩都已放弃(get_evc_upgrad_flag()==FALSE) ⇒ EVC_UPGRADE_END
▼
EVC_HANDSHAKE
│ 最多 3 次 evc_upgrade_send_code(i, EVC_HAND_SHAKE=1)
▼
EVC_ERASE_FLASH
│ 最多 3 次 evc_upgrade_send_code(i, EVC_ERASE_APP=2)
▼
EVC_WRITE_FLASH ◄─────────────┐
│ 内层 for(;;) 持续调 parse_evc_hex_content()│
│ · PARSE_OK ⇒ 跳出,向每桩最多 3 次 evc_upgrade_send_data(i)
│ (失败间隔 OSTimeDlyHMSM 250 ms)
│ · PARSE_END ⇒ 跳到 EVC_SEND_KEY_WORD
│ · PARSE_ADDR⇒ 继续解析
│ · 其它(错误) ⇒ 连续 30 次 ⇒ EVC_UPGRADE_END
│ 全部桩放弃 ⇒ EVC_UPGRADE_END
└────────────────────────────┘
(下一轮回到 WRITE_FLASH 继续下一包)
EVC_SEND_KEY_WORD
│ 每桩最多 3 次 evc_upgrade_send_key_word(i)(EVC_APP_JUMP=0)
▼
EVC_UPGRADE_END
│ memset(&s_evc_upgrade_tab,0,...)
│ g_t_evc_run_info.evc_flag_u.BIT.comm_flag = FALSE
│ g_t_evc_ii_run_info[i].evc_ii_flag_u.BIT.comm_flag = FALSE
│ Set_Status_Is_Updated_From_Server(UPDT_EVC, FALSE)
└─► 回到 EVC_UPGRADE_INIT
default 分支把状态强制打到 EVC_UPGRADE_END,起到兜底复位作用。
关键点:状态机是"逐个桩串行、互不阻塞"的——每轮对所有 u8_upgrad_flag[i] == TRUE 的桩各下发一次;某台桩连续失败到计数上限就把它踢出(u8_upgrad_flag[i] = FALSE),但不影响其它桩。只有当一台都不剩时(get_evc_upgrad_flag() == FALSE)才整体进入 END。这意味着"部分成功"是可达状态,而 EVC_UPGRADE_END 无条件清 UPDT_EVC,云端无法从本文件区分成功与失败。
4.3 命令帧数据域布局
evc_upgrade_send_code(id, code)(第 178-212 行)——数据域 10 字节:
| 数据域偏移 | 内容 |
|---|---|
| 0-5 | 0x00 × 6 |
| 6 | code(命令码) |
| 7-9 | 0x00 × 3 |
| 10.. | 若 u16_length != 0 再补 u16_length 字节(升级过程中此值在 EVC_WRITE_FLASH 之后会残留,见第 7 节) |
发出后 evc_upgrade_recv_data(..., 5000) 等 5 s,判据为 recv_buf[2] == id+1 && recv_buf[4] == 0x0B && recv_buf[10] == code && (recv_buf[12] | recv_buf[13] << 8) == 0。
evc_upgrade_send_data(id)(第 220-254 行)——数据域 10 字节前缀 + u16_length 字节固件数据:
| 数据域偏移 | 内容 |
|---|---|
| 0-1 | 0x00 × 2 |
| 2-3 | (u32_addr_flash >> 16) & 0xffff 的大端(高 16 位字) |
| 4-5 | u32_addr_flash & 0xffff 的大端(低 16 位字) |
| 6 | EVC_WRITE_APP(3) |
| 7 | 0x00 |
| 8-9 | u16_length / 2 小端 —— 长度以 16 位字为单位(满包 224 字节 → 112 字) |
| 10..10+u16_length-1 | 固件数据(固件数据实际从 s_evc_upgrade_send_buf[18] 开始写,即头 8 字节 + 前缀 10 字节之后) |
超时 2000 ms 发送、3000 ms 接收,判据同上但 recv_buf[10] 应为 EVC_WRITE_APP。
evc_upgrade_send_key_word(id)(第 262-298 行)——数据域 14 字节:
00 00 | 03 08 | FC FF | 00 | 00 | 02 00 | 'O' 'K' | 00 00
└高字┘ └低字┘ cmd 长度=2字 魔术字
按与 send_data 相同的"高字先、低字后"约定,目标地址为 0x0308FFFC,写入 2 个字(4 字节:'O'、'K'、00、00),命令码 EVC_APP_JUMP(0)。这是告诉桩内 Bootloader"固件已写完,跳到 App"的握手。超时 200/1000 ms。
4.4 HEX 解析与分包(parse_evc_hex_content)
这是本文件最难的一段(第 311-424 行)。它的输入是 SDRAM 里的 ASCII HEX 文本,游标是 s_evc_upgrade_tab.u32_parse_length,输出是"是否已凑满一包可直接发送的数据"。
l_u8_flash_data = &g_u8_fw_sdram1[u32_parse_length],调hex_parse(&s_hex_file_data, l_u8_flash_data, 80)解析一条记录。- 解析失败时用
strstr()在窗口里找"\n"/"\r"/":",把游标推过该分隔符(+1);都找不到就整窗跳过 80 字节。这是对"记录跨窗口断裂"的容错。 GET_BASE_ADDRESS_CODE(04 记录):把u16_data_buf[0]存进u32_addr_flash_base;若正好处于"半包"状态(s_u8_data_len != 0)则丢弃半包并返回PARSE_OK(先把手上的发出去),否则返回PARSE_ADDR。GET_DATA_CODE(00 记录):核心拼包逻辑u32_addr_flash_offset = s_hex_file_data.u16_addr;若与u32_old_addr_offset相同,l_u8_parse_len = 0(同一地址的重复记录,不计长度);- 若本次偏移比上次大 0x10 以上(说明中间有空洞),且已积累半包,则先把半包发出去(返回
PARSE_OK); s_u8_data_len == 0时判定为"新包开始":把u8_upgrad_file_buf指向s_evc_upgrade_send_buf[18]、整块用0xFF填 224 字节、计算本包起始地址u32_addr_flash = (u32_addr_flash_base << 16) | u32_addr_flash_offset;s_u8_data_len = (当前绝对地址 - 本包起始地址) + l_u8_parse_len,即包内已填字节数;- 若未超 224:
u16_length = s_u8_data_len,并把u16_data_buf[]里的 16 位字按 大端 逐字节写入u8_upgrad_file_buf[s_u8_data_len - l_u8_parse_len + i*2](高字节在前)——即固件以"高字节在前的 16 位小端机数据"形式搬运(ARM 的 HEX 就是按 16 位字排放的); - 若超 224:
s_u8_data_len = 0并返回PARSE_OK把上一包发出去(本记录下次重解)。
GET_LINEAR_ADDRESS_CODE(05 记录):直接PARSE_ADDR跳过,不做处理——即不支持线性地址记录,只支持 04 基址记录。GET_END_CODE(01 记录):若手上无半包则PARSE_END(全部完成),否则清半包返回PARSE_OK(先发最后一包,结束标志下一轮再处理)。
主状态机对返回值的处理(第 588-618 行):PARSE_OK → 跳出解析循环去发送;PARSE_END → 游标 += u8_ascll_len、转到 EVC_SEND_KEY_WORD;PARSE_ADDR → 游标 += u8_ascll_len、继续解析;其它 → s_u8_parse_cnt++,累计超过 30 次才放弃升级。
4.5 收发时序与超时重试
| 环节 | 发送超时 | 接收超时 | 重试上限 | 失败后动作 |
|---|---|---|---|---|
取 AES 私钥 get_evc_ii_pravite_key() | 500 ms(内部) | 500 ms(内部) | 3 | 不置 encryption_flag(走明文) |
Boot 跳转 EVC_BOOT_JUMP | 200 ms | 5000 ms | 5 | u8_upgrad_flag[i] = FALSE |
握手 EVC_HAND_SHAKE | 200 ms | 5000 ms | 3 | u8_upgrad_flag[i] = FALSE |
擦除 EVC_ERASE_APP | 200 ms | 5000 ms | 3 | u8_upgrad_flag[i] = FALSE |
写数据 EVC_WRITE_APP | 2000 ms | 3000 ms | 3(失败间隔 250 ms) | u8_upgrad_flag[i] = FALSE |
结束跳转 EVC_APP_JUMP | 200 ms | 1000 ms | 3 | 无(不置失败标志) |
所有等待都基于 OSTimeGet()(OS_TICKS_PER_SEC = 1000,即 tick = ms),并显式处理 tick 回绕(else if (OSTimeGet() < l_u32_recv_tick) l_u32_recv_tick = OSTimeGet();)。
evc_upgrade_recv_data() 的循环(第 99-169 行)值得细看:它在每一轮都会重新写 *len——包括把 *len = 0 写进超时/校验失败/无数据的路径,只有两条路径写非零值并 return。这意味着调用者在函数返回后必须同时看 l_u16_recv_len > 0 与各字段,单看 > 0 不够(当前实现就是这么做的)。
4.6 与 uCOS-II 的配合
- 不创建任务,不占用信号量/邮箱。本文件完全靠
interface.c的接口任务同步调用evc_upgrad_handle()推进,属于"被周期性驱动"的被动模块。 - 唯一的主动让出是失败重试之间的
OSTimeDlyHMSM(0, 0, 0, 250)(evc_upgrade.c:635);其余evc_upgrade_recv_data()内部是纯自旋等待,最坏情况一次调用占用 CPU 5 s(EVC_BOOT_JUMP等 5000 ms),期间不释放。因为升级期间串口 3 已被独占,这个自旋不会影响其它外设,但会拖长整个接口任务的循环周期。 evc_upgrade_send_cmd()每次发送前都调uart3_clear_buffer()清空接收缓冲,避免上一轮的残留应答被误判。
4.7 关键全局变量的读写
| 变量 | 本文件的读/写 | 触发条件 |
|---|---|---|
s_evc_upgrade_tab.u8_upgrad_flag[] | 读(决定对哪些桩操作)、写(置 FALSE 踢出失败桩) | set_evc_upgrad_flag() 由 server.c:2458 写入;本文件在重试耗尽时清位 |
s_evc_upgrade_tab.u32_parse_length | 读写 | 解析推进、PARSE_END 时补 u8_ascll_len、EVC_UPGRADE_END 时被 memset 清零 |
s_evc_upgrade_tab.u16_length | 写(解析时)、读(组帧时) | 每凑满一包更新一次 |
g_t_evc_run_info.evc_state_u.BIT.chgsts | 写 0x0A | get_evc_enable_flag() == TRUE 时,每轮都写,作为"升级中"状态给 HMI/云端 |
g_t_evc_ii_run_info[i].evc_ii_state_u.BIT.chgsts | 写 0x0A | 对每台 u8_upgrad_flag[i] == TRUE 的二代桩每轮都写 |
g_t_evc_run_info.evc_flag_u.BIT.comm_flag、g_t_evc_ii_run_info[i].evc_ii_flag_u.BIT.comm_flag | 写 FALSE | 仅在 EVC_UPGRADE_END,表示升级结束、可以恢复常规通信 |
g_u8_fw_sdram1[] | 只读 | 每轮从 u32_parse_length 处读取 80 字节 HEX 窗口 |
UPDT_EVC 状态 | 读(is_upgrade_evc)、写 FALSE(结束时) | set_evc_upgrad_flag() 的调用方(server.c)负责置 TRUE |
aesInfo[](common/aes.c:10) | 读(AESDecrypt) | 加密桩解包时按 buf[0]-1 索引 |
g_t_evc_ii_single_config[i].u8_enable | 读 | set_evc_upgrad_flag(0) 时决定哪些桩进升级队列 |
5. 对外接口
本文件导出的函数
| 函数 | 声明位置 | 调用方 |
|---|---|---|
evc_upgrad_handle | evc_upgrade.h:56 | midware/interface/interface.c:343(接口任务,is_upgrade_evc() 为真时每轮调用,后跟 50 ms 延时) |
set_evc_upgrad_flag | evc_upgrade.h:57 | net/server.c:2458(收到 UPDT_EVC 特殊命令,取 FIELD_NAME_SPECIAL_CMD_PARA3 字段 atoi 作为桩号) |
is_upgrade_evc | evc_upgrade.h:58 | midware/interface/interface.c:341、hmi/ui/ui_main.c:889 |
get_percent_upgrade_evc | evc_upgrade.h:59 | hmi/ui/ui_main.c:895(LCD 画 3 位百分比数字) |
get_upgrade_file_offset_evc | evc_upgrade.h:60 | net/ems.c:6719(云端上报断点偏移,为 0 时保留原值) |
evc_upgrade_send_cmd / evc_upgrade_recv_data / evc_upgrade_send_code / evc_upgrade_send_data / evc_upgrade_send_key_word | 仅在 .c 内定义,.h 中没有声明 | 本文件内部互调;因未声明为 static,理论上可被外部链接,但工程中无外部调用 |
本文件不导出任何全局变量(s_evc_upgrade_send_buf、s_evc_upgrade_recv_buf、s_evc_upgrade_send_aes_buf、s_evc_upgrade_tab、s_hex_file_data 全部为 static)。
本文件调用的外部函数/变量
| 外部符号 | 来源模块 |
|---|---|
uart3_clear_buffer / uart3_send_data / uart3_is_recv_async_data / uart3_get_data_len / uart3_read_data_async | hardware/usart3/usart3.c(UART3 外部 485) |
GetCrcCode_IBM | common/crc.c(CRC16-IBM) |
hex_parse / HEX_FILE_T / PARSE_* | common/hex_file_parse.c |
AESDecrypt / AESInit / aesInfo[] | common/aes.c(AES-256 ECB) |
evc_ii_aes_encrypt / get_evc_ii_pravite_key | midware/evc/evc_ii.c:2173 / :470 |
g_evc_send_buf / g_evc_recv_buf | midware/evc/evc.c:25-26 |
get_evc_enable_flag / g_t_evc_run_info | midware/evc/evc.c / evc.h |
g_t_evc_ii_run_info[] / g_t_evc_ii_single_config[] | midware/evc/evc_ii.c / evc_ii.h |
g_u8_fw_sdram1 | hardware/sdram/sdram.c |
Get_Status_Is_Updated_From_Server / Set_Status_Is_Updated_From_Server / get_length_file_updating / UPDT_EVC | net/upd_download.c / net/ems.h |
OSTimeGet / OSTimeDlyHMSM | uCOS-II |
6. 依赖关系
依赖
evc_upgrade.c
├─ evc_upgrade.h ─► includes.h, evc.h(EVC_BUF_LEN、g_evc_send_buf/recv_buf、
│ g_t_evc_run_info、get_evc_enable_flag), evc_ii.h(MAX_EVC_II_NUM、
│ g_t_evc_ii_run_info/…_single_config、evc_ii_aes_encrypt、get_evc_ii_pravite_key)
├─ upd_download.h (UPDT_EVC 状态、get_length_file_updating、Set/Get_Status_Is_Updated_From_Server)
├─ evc_download.h / evc.h / evc_ii.h
├─ ems.h / server.h(UPDT_* 枚举、ENUpdateType)
├─ usart3.h (UART3 收发)
├─ sdram.h (g_u8_fw_sdram1)
├─ hex_file_parse.h(hex_parse、HEX_FILE_T、PARSE_*)
├─ crc.h (GetCrcCode_IBM)
└─ aes.h (AESDecrypt、aesInfo[])
└─ UCOSII(OSTimeGet / OSTimeDlyHMSM)
被依赖(谁调用本文件)
midware/interface/interface.c— 主驱动者,在get_evc_enable_flag() || get_evc_ii_enable_flag()分支中把 485 口交给升级流程net/server.c— 云端特殊命令解析后调set_evc_upgrad_flag()net/ems.c— 上报升级进度偏移hmi/ui/ui_main.c— HMI 显示升级百分比- 同目录的
evc.c/evc_ii.c通过evc_ii_aes_encrypt/get_evc_ii_pravite_key/aesInfo[]与本文件构成"通信 + 升级"共用同一套加密与串口资源
对应头文件文档见 docs/knowledge/代码解析/midware/evc/evc_upgrade.h.md。
7. 备注与风险点
- 文件头注释写错(
evc_upgrade.c:2):@file :evc.c,实际是evc_upgrade.c;@date :2017/05/18与evc_upgrade.h的2017/09/26也不一致,属复制模板未改。
- 加密接收时
buf[0]的索引越界(高危)(evc_upgrade.c:137):s_evc_upgrade_tab.u8_evc_ii_encryption_flag[buf[0]-1]假设buf[0]是从机地址。这个假设在 AES 外层帧下成立(外层帧首字节确实是addr),但明文帧的首字节是0xFA,0xFA - 1 = 249,远超u8_evc_ii_encryption_flag[4]的长度 → 越界读。越界读到的字节若非 0,代码会错误地走进 AES 解密分支,用错误的buf[4]作为密文起点、用buf[1]<<8 | buf[2](对明文帧来说是0x03和addr)当作长度,行为不可预测。这段代码是从evc_ii.c:154的同名逻辑复制过来的,但evc_ii.c的协议首字节恰好是地址,复制到本文件的 0xFA 协议后就失配了。建议:把判据改成"先看buf[0]是否为0xFA,是则按明文处理;否则按buf[0]查加密表"。
- AES 解密输出缓冲区只有 40 字节(高危)(
evc_upgrade.c:95、:140):static u8 l_u8_recv_data[40],但AESDecrypt(&aesInfo[buf[0]-1], l_u8_recv_data, &buf[4], ((buf[1]<<8 | buf[2]) + buf[3]))的第四参数是"密文长度",其值由接收到的字节决定(buf[1]<<8 | buf[2]最大 65535)。收满 224 字节写 Flash 应答时,((len-2)&0xff) + 2 + remainder可达 258,会直接冲垮这个 40 字节栈缓冲区。evc_ii.c:109用的是[90],本文件被缩小到 40 且未做长度校验,属实际的栈溢出风险。
- AES 发送缓冲区 270 字节可能不够(
evc_upgrade.c:30):s_evc_upgrade_send_aes_buf[270]。加密帧 = 4 字节外层头 +ceil(明文长度/32)*32密文 + 2 字节外层 CRC。最坏情况:明文 = 8 头 + 10 前缀 + 224 数据 + 2 CRC = 244 字节,补齐到 256,加 4 + 2 = 262,仍小于 270,安全余量仅 8 字节。一旦EVC_MAX_PRG_LENGTH或前缀被加大就会溢出,是典型的"靠常量对齐撑住"的脆弱设计。
l_u16_len = buf[1]<<8 |buf[2] + 2;的运算符优先级(evc_upgrade.c:139,evc_ii.c:153同样):+优先于|,实际语义是(buf[1]<<8) | (buf[2]+2),与显然的意图((buf[1]<<8) | buf[2]) + 2不同。凑巧的是发送侧evc_ii_aes_encrypt()把len_hi写成了(u8)((len-2) & 0xff) >>8(先截断成 u8 再右移 8 位,恒为 0),所以buf[1]永远是 0,两种写法结果相同。这是"两个 bug 互相抵消"——一旦有人修正了evc_ii_aes_encrypt的高字节生成,这里立刻会算错长度。
evc_ii_aes_encrypt()的外层长度字段被写坏(evc_ii.c:2190):enc_data[1] = (u8)((len-2) & 0xff) >>8;应为(u8)(((len-2) >> 8) & 0xff)。本文件既然使用该函数,就在依赖这个错误行为,修正时必须两处同步。
get_percent_upgrade_evc()可能除零(evc_upgrade.c:769):分母get_length_file_updating(UPDT_EVC)在固件长度尚未写入时可能为 0。HMI 每帧都会调它(ui_main.c:895),若在"is_upgrade_evc()为真但长度还没到"的窗口里刷新,会发生除以零。Cortex-M4 整数除零默认不触发异常(返回 0),所以表现为进度恒 0,隐患不大但意图不清。
evc_upgrade_recv_data()里if(buf[0] == 0) return;(evc_upgrade.c:128-131):直接返回且不写*len。当前所有调用方都在调用前把l_u16_recv_len初始化成 0,所以侥幸正确;如果将来有人复用这个函数而忘了初始化,会读到垃圾长度。evc_ii.c的对应实现有同样的写法。
evc_upgrade_send_code()会带走上一包的u16_length(evc_upgrade.c:195):l_u16_send_len += s_evc_upgrade_tab.u16_length;在send_code里也会执行。u16_length只在parse_evc_hex_content()中被写。首次升级时它是 0(结构体被memset),但状态机走到EVC_UPGRADE_END后只清结构体、不清s_u8_data_len(见第 11 条),下一轮若出现异常时序,u16_length可能带着上一文件的残值被拼进BOOT_JUMP/HAND_SHAKE/ERASE_APP帧,导致帧长异常。设计上send_code不应该关心u16_length。
- 解析游标的推进与
u8_ascll_len强耦合(evc_upgrade.c:326-340、:599、:606):解析失败时用strstr()找分隔符推进,解析成功时用s_hex_file_data.u8_ascll_len推进。但strstr()是在可打印 ASCII 文本上找\n/\r/:——若固件缓冲区里恰好有非文本字节,strstr会一直扫到缓冲区内第一个 0 才停,可能跨过大量数据。另外\n的判断排在\r之前,而 HEX 文件用 CRLF,+1会停在\n上,下一轮再按\r找一次,实际会多消耗一次循环;功能上不出错(下一轮会命中strstr(...,":")分支),但每次失败要消耗两个周期。
s_u8_data_len是"跨调用残留"的 static,且EVC_UPGRADE_END不清它(evc_upgrade.c:316、:671):evc_upgrad_handle()结束时memset(&s_evc_upgrade_tab, 0, sizeof(EVC_UPGRADE_T))只清结构体,parse_evc_hex_content()内部那个static u8 s_u8_data_len不在其中。若一次升级中途被u8_upgrad_flag[]全部清零而结束(半包未发),残留的非零s_u8_data_len会让下一次升级的parse_evc_hex_content()误判"已在新包中",跳过u32_addr_flash的计算与 224 字节0xFF填充,直接往残留偏移继续写——跨文件的脏数据风险。建议在EVC_UPGRADE_END或EVC_UPGRADE_INIT里显式清零。
- 不支持线性地址记录(05 记录)(
evc_upgrade.c:407-410):GET_LINEAR_ADDRESS_CODE被无条件忽略。若充电桩固件是用"05 扩展线性地址"而非"04 扩展段地址"生成的 HEX(IAR/Keil 的可配置项不同),u32_addr_flash_base永远保持 0 或上一次 04 记录的值,烧写地址会全错,且当前实现不会报错,只会静默地把固件写到错误地址。这是最容易导致"升级成功但设备变砖"的一点。
- 失败惩罚只针对单台桩、没有整体上限:
EVC_WRITE_FLASH中若某台桩反复"发送成功但应答校验失败",u8_upgrad_err_cnt[i]会被清零(第 629 行),永远达不到"踢出"条件,循环会一直重发同一包。真正兜底的是解析错误计数s_u8_parse_cnt > 30,它对"发送卡死"无效。也就是说,应答内容错误(而非超时)这一路径缺少退出条件,可能无限循环。
EVC_CHECK_FILE(4) 命令码定义了但从未使用:EVC_UPGRADE_CODE_E里有EVC_CHECK_FILE,全文件没有任何地方下发它。看起来是预留的"文件校验"步骤未实现。
#define SOCK_ANY_PORT_NUM式的"变量命名遗留":s_evc_upgrade_tab.u8_upgrad_flag(拼写少了e)、EVC_PRAVITE_KEY(应为PRIVATE)、s_u8_upgrade_statevss_u8_upgrade_sate(后者出现在kelong_upgrade.c)等拼写瑕疵在升级模块内多处出现,不影响功能但增加了跨文件检索难度。
evc_upgrad_handle()的EVC_PRAVITE_KEY状态里get_evc_ii_pravite_key()对一代桩也调用(evc_upgrade.c:468):该函数内部按id索引g_t_evc_ii_run_info[id]与aesInfo[id]。当get_evc_enable_flag() == TRUE(一代桩)时u8_upgrad_flag[0]被置位,于是会对一代桩调用二代桩的取密钥流程,实际会往g_t_evc_ii_run_info[0]写数据、用二代桩的私有协议发文。一代/二代共用同一段状态机但协议不同,这里的混用是由set_evc_upgrad_flag()的"一代桩固定用索引 0"约定掩盖的,语义上并不干净。
uart3_clear_buffer()在每个发送前调用(evc_upgrade.c:72、:77):多桩并行升级时,这一句会丢掉其它桩刚回的应答。当前状态机对每台桩是"发一台、收一台"的严格顺序(for循环里逐个send_*()并阻塞等待),所以恰好安全;若将来改成非阻塞并发,这里会成为丢应答的根源。