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

hex_file_parse.c

源文件路径:common/hex_file_parse.c 所属模块:common(通用工具库) 代码行数:89

1. 文件职责

本文件是 Intel HEX(.hex)固件文件的单行解析器,只有一个函数 hex_parse()。它把 SDRAM 中一段以 ':' 开头的 Intel HEX 文本行拆解成「数据长度、装载地址、记录类型、数据字节、校验和、整行 ASCII 长度」,填入调用方提供的 HEX_FILE_T。在数据流上它处在「升级固件文本 → 目标设备私有协议帧」的中转环节:midware/evc/evc_upgrade.c(EVC 充电桩)、midware/meter/bb_update.c(电表)、midware/inverter/kelong/kelong_upgrade.c(科隆逆变器)三个升级任务各自把 .hex 固件写进 SDRAM 后逐行调用本函数,再按自己的协议重新组装并下发。函数无静态状态、不访问全局变量、不引用任何 OS 或硬件 API,是纯 CPU 字符串解析,被调用的任务上下文同步执行。

2. 关键数据结构与宏

名称类型/宏说明
hex_parse全局函数PARSE_STATE_E hex_parse(HEX_FILE_T *hex_data, u8 *buf, u8 len)(hex_file_parse.c:27)
PARSE_STATE_E枚举返回状态:PARSE_OK(0)/PARSE_ADDR(1)/PARSE_END(2)/DATA_ERR(3)/PARSE_ERR(4)(定义在 hex_file_parse.h:14)
HEX_CODE枚举Intel HEX 记录类型码:GET_DATA_CODE(0x00)/GET_END_CODE(0x01)/GET_BASE_ADDRESS_CODE(0x04)/GET_LINEAR_ADDRESS_CODE(0x05)
HEX_FILE_T结构体(packed)解析结果容器,含 u8_data_len / u16_addr / u8_code_type / u16_data_buf[32] / u8_sum_check / u8_ascll_len;本文件只写不读,全部由调用方消费
p局部 char *指向行内 ':' 的位置(strstr(buf, ":") 结果)
sum_ckeck_temp局部 u8累加式校验和中间值(拼写 ckeck 有误)
l_u32_temp_data1/2/3局部 u32sscanf 的临时承接变量,避免直接以 u8/u16 为写入目标
0x100 / 0xff / 0x100 -魔法数字校验和的二进制补数运算:sum = 0xFF & (0x100 - sum)
9魔法数字数据域相对 ':' 的字符偏移(':'+LL(2)+AAAA(4)+TT(2) = 9)
13魔法数字u8_ascll_len 的基础长度:':'(1)+LL(2)+AAAA(4)+TT(2)+CC(2)+CRLF(2) = 13

本文件不定义结构体、枚举或 #define,全部类型来自 hex_file_parse.h。

3. 函数清单

函数名作用域说明
hex_parse全局解析一行 Intel HEX 记录到 HEX_FILE_T,成功返回 PARSE_OK 并回填 u8_ascll_len,失败返回 DATA_ERR/PARSE_ERR

全文件只有这 1 个函数(hex_file_parse.c:27~:88),无 static 函数、无辅助函数、无回调,所有流程都写在这一个 62 行的函数体里。

4. 核心逻辑解析

4.1 输入帧格式(Intel HEX 单行)

: LL AAAA TT D0 D1 ... D(N-1) CC \r\n
  ↑  ↑    ↑   ↑             ↑
  |  |    |   |             └─ 校验和(1 字节,2 个 ASCII 字符)
  |  |    |   └─ 数据域(N 字节,2N 个 ASCII 字符)
  |  |    └─ 记录类型(1 字节)
  |  └─ 16 位装载地址(2 字节)
  └─ 数据长度 N(1 字节)

所有字段都是 ASCII 十六进制文本(大写/小写均可,sscanf("%X") 不区分大小写)。整行 ASCII 长度为 9 + 2N + 2 + 2 = 13 + 2N。

4.2 执行流程(hex_file_parse.c:27~:88)

步骤行号动作失败返回
1:34~:41入参检查:buf == NULL 或 len == 0DATA_ERR
2:42~:46p = strstr((char *)buf, ":") 定位记录起始DATA_ERR(找不到 ':')
3:48~:54sscanf(p, ":%02X%04X%02X", ...) 解析 LL/AAAA/TT,要求返回值为 3,随后写入 u8_data_len / u16_addr / u8_code_typePARSE_ERR
4:56~:59校验和累加种子:LL + AAAA_hi + AAAA_lo + TT—
5:61~:65sscanf(p + 9 + data_len*2, "%02X\r\n", ...) 读取 CC 到 u8_sum_check,要求返回值为 1PARSE_ERR
6:67~:77循环 i < u8_data_len/2:每次 sscanf(p + 9 + i*4, "%04X", ...) 读 4 个 ASCII 字符,作为 16 位值(大端:高字节是前两个字符)存入 u16_data_buf[i],同时把这两个字节累加进校验和PARSE_ERR
7:78~:87sum_ckeck_temp = 0xff & (0x100 - sum_ckeck_temp) 求二进制补数,与 u8_sum_check 比对;相等则 u8_ascll_len = 13 + u8_data_len*2 并返回 PARSE_OK,否则返回 PARSE_ERRPARSE_ERR

4.3 校验和算法

期望校验和 CC = (0x100 - (LL + AAAA_hi + AAAA_lo + TT + Σ 数据字节)) & 0xFF

即「所有字节(含长度、地址、类型、数据)之和的二进制补数」,是 Intel HEX 标准算法。判定方式是重新计算左式再与文件里的 CC 做等值比较(hex_file_parse.c:78~:79),而不是「把 CC 也加进去看是否为 0」。

由于累加只覆盖 u8_data_len/2 个完整字(2 字节),当 u8_data_len 为奇数时,最后一个数据字节不会进入累加,除非极端巧合,否则校验必然不通过并返回 PARSE_ERR。因此本实现事实上只支持偶数长度的数据记录(Intel HEX 实际也是按偶数长度产生,风险可接受,但接口没有明说)。

4.4 字节序约定(调用方必须遵守)

u16_data_buf[i] 保存的是「第 2i 个字符、第 2i+1 个字符」组成的 16 位大端值。调用方要还原原始字节必须两次移位,工程内 3 个调用点写法完全一致:

buf[k]     = (u8)(s_hex_file_data.u16_data_buf[i] >> 8) & 0xFF;
buf[k + 1] = (u8)(s_hex_file_data.u16_data_buf[i]) & 0xFF;
调用点数据域消费方式记录类型处理
midware/evc/evc_upgrade.c:396~:397按 l_u8_parse_len/2 循环还原进 u8_upgrad_file_buf:346 基址、:356 数据、:407 线性地址、:411 结束
midware/meter/bb_update.c:191~:192按 s32_len_buff/2 循环还原进 u8_buffer:136 基址、:146 数据、:203 结束(0x05 未处理,落入 :201 返回 PARSE_ADDR)
midware/inverter/kelong/kelong_upgrade.c:484~:485按 u8_data_len/2 循环还原进 u8_upgrade_buf[i][4..],并把 u8_data_len/u16_addr/u8_code_type/u8_sum_check 原样重排成 4 字节头 + 数据 + 1 字节校验的格式不分支,直接转发 u8_code_type

4.5 行推进机制(为什么需要 u8_ascll_len)

本函数一次只解析一行,且只在成功时回填整行 ASCII 长度(u8_ascll_len),调用方据此把读取偏移推进到下一行:

解析失败时没有长度可用,调用方只能自己 strstr 找 '\r'/'\n'/':' 估算下一行起点(如 midware/evc/evc_upgrade.c:323~:338、midware/meter/bb_update.c 的同类分支),并在连续失败若干次后放弃(kelong_upgrade.c 用 l_u8_parse_err_cnt 计数)。因此本文件不包含任何状态机、重试或超时逻辑——状态机完全由调用方持有,本函数是无状态的一步转换。

4.6 与 uCOS-II / 定时的关系

4.7 关键全局变量的读写

本文件不读写任何全局变量:输入来自 buf,输出写入 hex_data 指向的结构体(3 个调用点使用的都是该文件内的 static HEX_FILE_T s_hex_file_data 或函数内局部变量)。文件里的 p、sum_ckeck_temp、l_u32_temp_data1/2/3 全部是栈上局部变量。

5. 对外接口

导出(供外部使用):

名称使用方
hex_parse()midware/evc/evc_upgrade.c:321(窗口长度 EVC_UPGRADE_TEMP_DATA_LEN = 80)、midware/meter/bb_update.c:130(窗口 80)、midware/inverter/kelong/kelong_upgrade.c:455(窗口 80)
HEX_FILE_T / PARSE_STATE_E / HEX_CODE由 midware/evc/evc_upgrade.c、midware/meter/bb_update.c、midware/inverter/kelong/kelong_upgrade.c 使用;见 hex_file_parse.h.md 第 5 节

本文件不导出任何全局变量,不注册回调。

被调用(本文件调用的外部函数/符号):

名称来源
strstrC 标准库 string.h(hex_file_parse.c:13 用 #include "string.h" 引入)
sscanfC 标准库 stdio.h,经 includes.h(hex_file_parse.h:12)间接引入
u8 / u16 / u32 / PARSE_STATE_E / HEX_CODE / HEX_FILE_Tcommon/hex_file_parse.h

6. 依赖关系

依赖:

被依赖:

7. 备注与风险点

  1. u8_data_len 是外部可控值,却没有上界检查 → u16_data_buf[32] 可被越界写:hex_file_parse.c:67 的循环上界直接取记录里的 LL 字段(最大 0xFF = 255),每次写 hex_data->u16_data_buf[i](2 字节)。当 LL > 64 时会写到 u16_data_buf 之后,覆盖 u8_sum_check、u8_ascll_len 以及结构体之外的内存(3 个调用点的结构体都是 static 或栈变量,越界会破坏相邻变量甚至栈)。数据来源是服务器下发的固件 .hex 文本,属外部不可信输入。接口上唯一的长度参数 len 只被用来判断是否为 0(hex_file_parse.c:34),完全可以用来做 len >= 9 + 2*LL 与 LL <= 64 的校验,但没有做。
  2. sscanf 的格式串 "%02X\r\n" 并不校验 CRLF:在 scanf 语义里,格式串中的空白字符(含 '\r'、'\n')表示「跳过任意数量的空白」,而不是按字面匹配。所以这一行既不校验行尾就是 CRLF,也可能跨行继续读取后面的内容。hex_file_parse.c:61、:65 处的写法看上去像在检查行尾,实际只是跳过空白,容易让维护者产生错误的安全感。
  3. 所有 sscanf 都在未确认行长的情况下前移指针:p + 9 + data_len*2、p + 9 + i*4 等偏移只依赖文件里声明的长度字段,不依赖实际缓冲边界。由于调用方传入的是 SDRAM 中的整块固件映像(后面还有后续行文本),越读时通常不会立刻崩,而是把下一行的字符当成本行数据解析,产生静默的错误解析结果;只有当读到 SDRAM 映像末尾之外时才可能触发硬件异常。
  4. (int *)&l_u32_temp_data1 形式的强转写入很脆:%02X/%04X 会按 int(4 字节)写入目标地址。当前 3 个临时变量都是 u32,因此安全;但一旦有人为了"省内存"把它们改成 u16/u8,就会立刻变成栈越界写(这也是把临时变量声明为 u32 的原因,但没有注释说明,属隐含约定)。
  5. 失败路径上结构体是"半写状态":u8_data_len / u16_addr / u8_code_type 在第 3 步就被写入(hex_file_parse.c:52~:54),之后才可能因校验和不匹配返回 PARSE_ERR。也就是说调用方不能用返回码之外的信息判断结构体是否有效,但工程内的调用方确实在错误分支里只检查返回码(如 alphaess/evc_upgrade.c:322),暂时安全;隐患在于后来者可能直接在 PARSE_ERR 分支里读 u8_code_type 做判断,从而读到脏数据。
  6. 失败时无法推进行偏移,导致调用方重复实现"找下一行":u8_ascll_len 只在 PARSE_OK 路径回填(hex_file_parse.c:81),失败时调用方只能自己 strstr 找 '\r'/'\n'/':'(midware/evc/evc_upgrade.c:323~:338、midware/meter/bb_update.c 同类分支),这段"容错推进"逻辑在两个文件里各写一遍,行为还不完全一致,是后续固件升级卡死/跳行类问题的常见来源。更合理的做法是让 hex_parse 在失败时也回填一个「本行长度」或直接返回已消费的字符数。
  7. 大量魔法数字:9(数据域偏移)、13(整行基础长度)、0x100/0xff(校验和取反)、2(每字节 2 字符)、4(每个 u16 4 字符)、32(u16_data_buf 容量,在头文件里)都散落在表达式中,没有命名宏。尤其 13 只在成功路径用一次,与 9 的关系(13 = 9 + 2(CC) + 2(CRLF))没有任何注释。
  8. 拼写错误已固化到接口:sum_ckeck_temp(ckeck)、u8_ascll_len(ascll)两处拼写错误分别在实现与头文件里,ascll 版本已扩散到 4 个调用点,修改需跨模块同步。
  9. #include "string.h" 用引号包含标准头:在 Keil/ARMCC 下能正常解析,但风格与工程内其它文件不一致(crc.h 用 "chip.h",convert.h 用 <includes.h>)。本文件实际上只用到 strstr,不需要 string.h 之外的任何东西。
  10. hex_parse 与 parsing_upgrade_file(common/s19.c)是两套并列的"固件行解析器",前者解析 Intel HEX、后者解析 Motorola S19,二者的错误码枚举还同名不同值(PARSE_OK/PARSE_END/DATA_ERR),详见 hex_file_parse.h.md 备注 2 与 s19.h.md 备注。