hex_file_parse.c
hex_file_parse.c:27~:88)4.3 校验和算法4.4 字节序约定(调用方必须遵守)4.5 行推进机制(为什么需要 u8_ascll_len)4.6 与 uCOS-II / 定时的关系4.7 关键全局变量的读写5. 对外接口6. 依赖关系7. 备注与风险点源文件路径:
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 | 局部 u32 | sscanf 的临时承接变量,避免直接以 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 == 0 | DATA_ERR |
| 2 | :42~:46 | p = strstr((char *)buf, ":") 定位记录起始 | DATA_ERR(找不到 ':') |
| 3 | :48~:54 | sscanf(p, ":%02X%04X%02X", ...) 解析 LL/AAAA/TT,要求返回值为 3,随后写入 u8_data_len / u16_addr / u8_code_type | PARSE_ERR |
| 4 | :56~:59 | 校验和累加种子:LL + AAAA_hi + AAAA_lo + TT | — |
| 5 | :61~:65 | sscanf(p + 9 + data_len*2, "%02X\r\n", ...) 读取 CC 到 u8_sum_check,要求返回值为 1 | PARSE_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~:87 | sum_ckeck_temp = 0xff & (0x100 - sum_ckeck_temp) 求二进制补数,与 u8_sum_check 比对;相等则 u8_ascll_len = 13 + u8_data_len*2 并返回 PARSE_OK,否则返回 PARSE_ERR | PARSE_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),调用方据此把读取偏移推进到下一行:
midware/evc/evc_upgrade.c:599、:606:u32_parse_length += s_hex_file_data.u8_ascll_len;midware/inverter/kelong/kelong_upgrade.c:476:l_u8_recv_file_offset += s_hex_file_data.u8_ascll_len;midware/meter/bb_update.c:389:l_u32_parse_length_temp += s_hex_file_data.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 / 定时的关系
- 不调用任何 OS API,不使用信号量/消息邮箱/延时;
- 无静态变量(连
static局部缓冲都没有),完全可重入; - 无动态内存分配;
- 执行时间与行长成正比(
data_len/2次sscanf,每次 4 字符),最典型的一行是 16 字节数据(sscanf8 次),开销微秒级; - 因此并发风险不在本文件,而在调用方共用的 SDRAM 固件映像与各自的解析偏移变量。
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 节 |
本文件不导出任何全局变量,不注册回调。
被调用(本文件调用的外部函数/符号):
| 名称 | 来源 |
|---|---|
strstr | C 标准库 string.h(hex_file_parse.c:13 用 #include "string.h" 引入) |
sscanf | C 标准库 stdio.h,经 includes.h(hex_file_parse.h:12)间接引入 |
u8 / u16 / u32 / PARSE_STATE_E / HEX_CODE / HEX_FILE_T | common/hex_file_parse.h |
6. 依赖关系
依赖:
common/hex_file_parse.h(hex_file_parse.c:12);string.h(hex_file_parse.c:13,写法用引号而非尖括号);- 间接依赖
UCOSII/uCOS-CONFIG/includes.h链(stdio.h/chip.h)。
被依赖:
midware/evc/evc_upgrade.c(真实使用)midware/meter/bb_update.c(真实使用)midware/inverter/kelong/kelong_upgrade.c(真实使用)midware/inverter/alphaess/alphaess_upgrade.c、midware/inverter/ginlong/ginlong_upgrade.c、midware/inverter/set/set_upgrade_s5.c(仅 include 头文件,未调用)
7. 备注与风险点
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的校验,但没有做。sscanf的格式串"%02X\r\n"并不校验 CRLF:在scanf语义里,格式串中的空白字符(含'\r'、'\n')表示「跳过任意数量的空白」,而不是按字面匹配。所以这一行既不校验行尾就是 CRLF,也可能跨行继续读取后面的内容。hex_file_parse.c:61、:65处的写法看上去像在检查行尾,实际只是跳过空白,容易让维护者产生错误的安全感。- 所有
sscanf都在未确认行长的情况下前移指针:p + 9 + data_len*2、p + 9 + i*4等偏移只依赖文件里声明的长度字段,不依赖实际缓冲边界。由于调用方传入的是 SDRAM 中的整块固件映像(后面还有后续行文本),越读时通常不会立刻崩,而是把下一行的字符当成本行数据解析,产生静默的错误解析结果;只有当读到 SDRAM 映像末尾之外时才可能触发硬件异常。 (int *)&l_u32_temp_data1形式的强转写入很脆:%02X/%04X会按int(4 字节)写入目标地址。当前 3 个临时变量都是u32,因此安全;但一旦有人为了"省内存"把它们改成u16/u8,就会立刻变成栈越界写(这也是把临时变量声明为u32的原因,但没有注释说明,属隐含约定)。- 失败路径上结构体是"半写状态":
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做判断,从而读到脏数据。 - 失败时无法推进行偏移,导致调用方重复实现"找下一行":
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在失败时也回填一个「本行长度」或直接返回已消费的字符数。 - 大量魔法数字:
9(数据域偏移)、13(整行基础长度)、0x100/0xff(校验和取反)、2(每字节 2 字符)、4(每个u164 字符)、32(u16_data_buf容量,在头文件里)都散落在表达式中,没有命名宏。尤其13只在成功路径用一次,与9的关系(13 = 9 + 2(CC) + 2(CRLF))没有任何注释。 - 拼写错误已固化到接口:
sum_ckeck_temp(ckeck)、u8_ascll_len(ascll)两处拼写错误分别在实现与头文件里,ascll版本已扩散到 4 个调用点,修改需跨模块同步。 #include "string.h"用引号包含标准头:在 Keil/ARMCC 下能正常解析,但风格与工程内其它文件不一致(crc.h用"chip.h",convert.h用<includes.h>)。本文件实际上只用到strstr,不需要string.h之外的任何东西。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备注。