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

s19.c

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

1. 文件职责

本文件是 Motorola S-Record(.s19)固件文件的单行解析器,只有一个函数 parsing_upgrade_file()。它把 SDRAM 中的一行 S-Record 文本(S + 类型 + 长度 + 地址 + 数据 + 校验和 + 换行)拆成「本行数据类型、装载地址、有效数据长度、数据内容」,并给出校验结果,供调用方判断该行是文件头(S0)、数据行(S1/S2/S3)还是结束行(S7/S8/S9)。在数据流上它处在 BMS 电池固件升级链路的最前端:midware/bms/alpha/battery_upgrade.c 的 SEND_UPGRADE_DATA 状态下每轮调用一次本函数取一行,再把数据按 BMS 的 CAN 协议分 8 字节一包下发。函数没有任何 OS 调用,但内部使用一个 static 行缓冲,且校验逻辑完全依赖源映像中「CRLF 行结束」这一格式前提。

2. 关键数据结构与宏

名称类型/宏说明
parsing_upgrade_file全局函数u8 parsing_upgrade_file(const u8 *file_addr, S19_FILE_PASRE_PCK *file_pack)(s19.c:35)
S19_FILE_PASRE_PCK结构体出现/出参容器(定义在 s19.h:32):u8_s_addr、u8_end_addr、u8_burn_data(入)、u8_burn_len(出)、u32_burn_addr(出)
S19_FILE_PASRE_STATUS_E枚举返回值语义集:BUF_ADDR_ERR(0)…CHECK_SUM_ERR(5)、HEAD_OK(6)、PARSE_OK(7)、PARSE_END(8)
S19_DEBUG宏(= 0)调试打印开关(定义在 s19.h:17),为 0 时全部 printf 不参与编译
s_u8_buf[257]static u8 数组行解析中间缓冲(s19.c:43),静态存活、不可重入;索引对应「长度字段之后的字节序号」
l_u8_s_addr / l_u8_end_addr局部 char *分别指向本行 'S' 与 '\n'(命名用 u8 前缀但实际类型是 char *,见备注 8)
l_u32_s19_len局部 u32记录里的长度字段(覆盖「地址 + 数据 + 校验和」字节数)
l_u32_temp_data局部 u32sscanf("%02X") 的临时承接变量
l_u8_check_code_temp / l_u8_check_code局部 u8计算出的校验和 / 文件里给出的校验和
0xFF - l_u8_check_code_temp表达式SREC 校验和公式:count + addr + data 之和的反码
257魔法数字行缓冲容量,按「最大长度字段 0xFF + 2」预留
sizeof((char *)&s_u8_buf[...])表达式取的是 sizeof(char *)(4 字节),而作者显然只想清 1~2 个字节(见备注 3)

本文件不定义新的宏、枚举或结构体,全部类型来自 s19.h。

3. 函数清单

函数名作用域说明
parsing_upgrade_file全局解析一行 S-Record,返回 HEAD_OK/PARSE_OK/PARSE_END 或 6 种错误码之一

全文件只有这 1 个函数(s19.c:35~:413)。它是一个 379 行的单函数,内部用 if / else if 链按记录类型分成 7 个分支(S0、S1、S2、S3、S7、S8、S9)加 1 个兜底分支,没有拆出任何辅助函数,也没有 static 子函数或回调。

4. 核心逻辑解析

4.1 输入格式(Motorola S-Record)

S T CC AAAA... DD... DD CS \n
↑ ↑ ↑   ↑         ↑      ↑
| | |   |         |      └─ 校验和(1 字节,2 个 ASCII 字符)
| | |   |         └─ 数据域(CC-地址字节数-1 个字节)
| | |   └─ 装载地址(S0:0 字节,S1:2,S2:3,S3:4)
| | └─ 长度字段 CC:本行「地址 + 数据 + 校验和」的字节数(1 字节,2 个 ASCII 字符)
| └─ 记录类型字符('0'~'9')
└─ 固定字符 'S'

每字节都用 2 个 ASCII 十六进制字符表示,行尾为 \r\n(本实现要求 CRLF,见 4.4)。

4.2 执行流程

阶段行号动作失败返回
入参检查:45~:51file_addr == NULL 或 file_pack->u8_burn_data == NULL(注意:不检查 file_pack 本身)BUF_ADDR_ERR
定位记录头:53~:61strstr((char *)file_addr, "S") 找本行 'S'S_ADDR_ERR
定位行尾:63~:71strstr((char *)file_addr, "\n") 找 '\n'END_ADDR_ERR
(调试):73~:81S19_DEBUG 为 1 时打印本行原文与长度—
分支派发:84~:412按 l_u8_s_addr[1](类型字符)分派到 S0/S1/S2/S3/S7/S8/S9,其它 → S_ADDR_ERRS_ADDR_ERR

每个数据分支(S0~S3)内部都走同样的 6 步:

  1. sscanf(&s_addr[2], "%02X", ...) 读长度字段;失败 → LEN_ERR(:86、:165、:239、:313);
  2. 一致性校验:l_u32_s19_len != (((l_u8_end_addr - 1) - &l_u8_s_addr[4]) / 2) → LEN_ERR(:95、:174、:248、:322)。含义是「长度字段必须等于 'S'+类型+长度字段之后、'\r' 之前的字节对数」;
  3. l_u8_check_code_temp = l_u32_s19_len; 作为校验和初值(:104、:183、:257、:331);
  4. 循环 i < (l_u32_s19_len - 1):sscanf(&s_addr[4 + 2*i], "%02X", ...) 逐字节读入 s_u8_buf[i] 并累加进校验和;失败 → DATA_ERR(:109~:126、:184~:198、:258~:272、:332~:346);
  5. memset(&s_u8_buf[len-1], 0, 4) 清校验和槽,然后 l_u8_check_code_temp = 0xFF - l_u8_check_code_temp;
  6. sscanf(&s_addr[4 + 2*(len-1)], "%02X", ...) 读文件给出的校验和(失败 → CHECK_SUM_ERR),比对:相等 → 填充 file_pack 并返回成功码;不等 → CHECK_SUM_ERR。

4.3 各记录类型的分支差异

类型行号地址解析u8_burn_len数据来源返回
S0:84~:162无地址(数据域即元信息)不填不拷贝HEAD_OK
S1:163~:236`(s_u8_buf[0] << 8) \s_u8_buf[1]`(2 字节)len - 3memcpy(burn_data, &s_u8_buf[2], len-3)PARSE_OK
S2:237~:310`(s_u8_buf[0] << 16) \(s_u8_buf[1] << 8) \s_u8_buf[2]`(3 字节)len - 4memcpy(burn_data, &s_u8_buf[3], len-4)PARSE_OK
S3:311~:384`(s_u8_buf[0] << 24) \(s_u8_buf[1] << 16) \(s_u8_buf[2] << 8) \s_u8_buf[3]`(4 字节)len - 5memcpy(burn_data, &s_u8_buf[4], len-5)PARSE_OK
S7:385~:391———PARSE_END
S8:392~:398———PARSE_END
S9:399~:405———PARSE_END
其它(S4/S5/S6 等):406~:412———S_ADDR_ERR

S1/S2/S3 三个分支代码是逐字复制的(仅地址拼接行数、burn_len 的减数、memcpy 起点不同),共约 220 行重复代码;S0 分支是同样结构的第 4 份拷贝。

三种成功返回都同时把 file_pack->u8_s_addr 与 file_pack->u8_end_addr 设为指向源映像内部的指针(不是副本),调用方据此计算下一行偏移;HEAD_OK 只填这两个指针,PARSE_OK 额外填 u32_burn_addr、u8_burn_len 并拷贝数据。

4.4 校验和算法与格式前提

计算值 = 0xFF - (count + 地址字节 + 数据字节)      // u8 自然回绕,等价于按字节取反码

这是 S-Record 标准校验和(count、地址、数据各字节之和的按位取反)。累加变量 l_u8_check_code_temp 是 u8,加法溢出即自动模 256,因此不需要额外掩码。

CRLF 是硬前提:第 2 步的长度一致性校验用 (end_addr - 1) - &s_addr[4],其中 end_addr 指向 '\n',减 1 后又去掉了 '\r',得到的恰是 2*count。如果文件只用 LF 结尾,该值变成 2*count - 1,整除 2 后为 count - 1,与 count 不等 → 直接返回 LEN_ERR。也就是说只有 CRLF 行结束的 .s19 文件才能被解析,纯 LF 文件会在第一行就全部失败。

4.5 与调用方状态机的配合(midware/bms/alpha/battery_upgrade.c)

本文件自身没有状态机:它一次只解析一行、不做循环、不保存进度。进度完全由调用方的 s_bat_upgrade_tab.u32_file_address_offset(当前解析偏移)维护,调用方每轮传入 &g_u8_fw_sdram1[offset](battery_upgrade.c:635)并在得到结果后自行推进:

返回码调用方动作行号
HEAD_OKu8_file_parse_cnt = 0,偏移推进到 (u8_end_addr - g_u8_fw_sdram1) + 1(跳过 S0 头行)battery_upgrade.c:641~:645
PARSE_OK取 u32_burn_addr / u8_burn_len,按 8 字节一包调用 send_data_code();中途失败即整体回到 UPGRADE_INFO_INIT;处理完再推进偏移、u8_pck_indx++battery_upgrade.c:646~:710
PARSE_END进入 UPGRADE_END 状态,延时 1 秒后发结束码battery_upgrade.c:711~:715
其它(全部错误码)u8_file_parse_cnt++;连续 3 次失败 → UPGRADE_INFO_INIT 从头重来battery_upgrade.c:716~:727

重试策略:本文件不重试,重试是「3 次解析失败就整体初始化升级」+「发包失败 3 次就整体初始化」(battery_upgrade.c:620~:629、:654~:658)。超时同样是调用方持有:SYSTEM_STATUS_CHECK 用 abs(OSTimeGet() - s_u32_timeout_tick) > 60*1000 判断 60 秒超时(battery_upgrade.c:588)。

与 uCOS-II 的关系:本文件不调用任何 OS API(无延时、无信号量、无消息邮箱),整行解析是同步阻塞的纯 CPU 操作(一次 sscanf 逐字节解析,典型 16 字节数据行约 25 次 sscanf 调用)。任务的让出与延时(OSTimeDlyHMSM(0, 0, 1, 0)、:611、:706)全部发生在调用方,本文件不会主动让出 CPU。调用方进入 SEND_UPGRADE_DATA 之前已经通过 get_inverter_control_bat_disable_flag()(battery_upgrade.c:595)确认电池已停止工作,因此解析期间不存在并发改写 g_u8_fw_sdram1 的路径。

4.6 静态缓冲与可重入性

s_u8_buf[257] 是 static,每次调用都会清零被使用到的位置(第 5 步的 memset),但:

4.7 关键全局变量的读写

本文件不读写任何全局变量:输入是 file_addr 指向的只读映像,输出写入 file_pack;s_u8_buf 是文件内 static 局部,其余全是栈变量。因此对 g_t_logic_info、g_t_ems_info 等全局状态没有任何直接介入——BMS 升级的进度、烧写地址、失败计数全部由调用方的 s_bat_upgrade_tab(battery_upgrade.c 内的 static 表)维护。

5. 对外接口

导出(供外部使用):

名称使用方
parsing_upgrade_file()midware/bms/alpha/battery_upgrade.c:635
S19_FILE_PASRE_PCKmidware/bms/alpha/battery_upgrade.c:555(S19_FILE_PASRE_PCK l_pasre_pck = {0};,u8_burn_data 在 :633 被指向 s_bat_upgrade_tab.u8_burn_data)
返回码 HEAD_OK/PARSE_OK/PARSE_ENDmidware/bms/alpha/battery_upgrade.c:641、:646、:711

本文件不导出全局变量,不注册回调,不定义 static 辅助函数。

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

名称来源
strstrC 标准库 string.h(经 s19.h:15 引入)
memset / memcpyC 标准库 string.h(同上)
sscanf / printfC 标准库 stdio.h(经 s19.h:14 引入;printf 调用点全部被 #if S19_DEBUG 包裹,生产版本不编译)
NULLstdio.h/string.h
u8 / u32、S19_FILE_PASRE_PCK、状态枚举成员system/sys/chip.h(经 s19.h:13)与 common/s19.h

6. 依赖关系

依赖:

被依赖:

7. 备注与风险点

  1. file_pack 本身没有做 NULL 检查:s19.c:45 的判断是 (file_addr == NULL) || (file_pack->u8_burn_data == NULL)——在 file_pack == NULL 时会先解引用再判断,直接触发硬件异常。当前唯一调用点传的是栈上取址(battery_upgrade.c:555、:635),不会为 NULL,所以未暴露;但作为公共解析接口,缺少这一层防护是明确的缺陷。
  2. u8_burn_data 输出缓冲的容量约束完全没有保护:burn_len 由长度字段推导(S1 最多 252 字节),而 memcpy(:218、:292、:366)不检查目标容量。调用方 midware/bms/alpha/battery_upgrade.c:46 分配的是 u8 u8_burn_data[256],恰好覆盖 252 字节数据,但没有任何编译期断言或运行期校验来保证这个「巧合」在将来仍然成立。固件文本来自云端下载,属外部输入。
  3. memset 的长度参数写错,存在 1~2 字节越界写(高危、潜伏):四处 memset(&buf[len-1], 0, sizeof((char *)&buf[len-1]))(:127、:199、:273、:347)与三处针对 file_pack->u8_burn_data 的同类写法(:219、:293、:367)中,sizeof 取的是 char * 的大小 = 4 字节,而作者意图显然只是清掉「校验和」那 1 个字节。当 count 取到 255(S1 行携带 252 字节数据时完全合法)时:
    • memset(&s_u8_buf[254], 0, 4) 会写到 s_u8_buf[257],而数组只有 257 个元素(合法下标 0~256)→ 静态数组溢出 1 字节;
    • memset(&burn_data[254], 0, 4) 会写到 u8_burn_data[257],而调用方缓冲是 256 字节 → 越界写 2 字节(破坏 s_bat_upgrade_tab 中紧跟其后的成员)。

典型 BMS 固件的 S19 行只有 16~32 字节载荷(count 约 19~35),因此目前不会触发;但只要上游给出一次长行文件(或恶意构造的行)就会命中。修正方式是把 sizeof(...) 换成明确的 2 或 1。

  1. count == 0 会让循环上界下溢,靠内层 sscanf 失败才没有变成死循环:第 2 步的一致性校验在「'S'+类型+长度字段之后到 '\r' 之间只有 0~1 个字符」时可以被满足(例如行 S100\r\n、S100A\r\n),此时 l_u32_s19_len == 0,第 4 步的循环条件 i < (l_u32_s19_len - 1) 变成 i < 0xFFFFFFFF(int 提升为 unsigned 后恒真,i 是 u8 会回绕)——理论上是不终止的循环。实际没有炸是因为循环体内第一句 sscanf(&s_addr[4+2*i], "%02X", ...) 立刻在 '\r'/'\n'/'S' 这些非十六进制字符上失败并返回 DATA_ERR。也就是说安全性建立在「源映像中紧邻的字符恰好不是十六进制数字」这一偶然条件上,而不是任何显式边界检查,属典型的「两个缺陷互相掩盖」。
  2. sscanf(...) == NULL 的判据不严谨::86、:111、:130、:165、:186、:202、:239、:260、:276、:313、:334、:350 共 12 处都用「返回值等于 NULL(即 0)」判断失败。正确写法应为 != 1(成功转换的字段数)。当前输入是内存字符串,sscanf 不会返回 EOF,所以行为正确;但这一写法掩盖了「一个字段都没转换成功」与「EOF」的区别,也让人误以为 sscanf 返回指针。
  3. (int *)&l_u32_s19_len 形式的强转:把 u32 * 转成 int * 交给 %X 写入。在本平台(32 位、int 与 unsigned int 同宽)安全,但 %02X 会写满 4 字节,一旦将来变量类型被改成 u16/u8 就会栈越界写。更稳妥的写法是直接用 unsigned int 类型的临时变量。
  4. 控制流深度达 6 层、四个分支近乎逐字重复:S0/S1/S2/S3 四段共约 300 行代码只有「地址宽度」与「len 偏移量」两处差异,典型的可提取为「统一解析 + 按类型取地址宽度」的写法。当前形态下任何一处修正(例如上面第 3 点的 memset 问题)都必须改 7 遍,漏改一处就会形成行为不一致。
  5. l_u8_s_addr / l_u8_end_addr 命名与实际类型不符:声明是 char *(:37~:38),命名却用 u8 前缀,且在使用点反复 (char *)/(u8 *) 互相强转(:53、:111、:145~:146)。读代码时容易误判字段宽度。
  6. S5/S6(记录计数)未支持:S-Record 规范里的 S5(16 位记录计数)、S6(24 位记录计数)会落到 :406 的兜底分支返回 S_ADDR_ERR。若上游固件用的是带计数的 S19 变体,第 2 次解析就会失败并把升级重置(battery_upgrade.c:716~:727 的 3 次计数),表现为「升级反复重启」。
  7. 文件头注释重复::10~:15 连续出现两个 Includes 注释块(复制粘贴残留);:25 的函数注释里参数表已过时(写的是 u8* file_addr,u32 burn_addr,u8* burn_data,u16 burn_len,与实际签名 (const u8*, S19_FILE_PASRE_PCK*) 完全不符)。
  8. 调试打印会打印整行:S19_DEBUG 置 1 时 :76~:80 会逐个字符 printf 整行内容,在实时任务中开销很大,只适合离线排查。
  9. 编码:本文件是 UTF-8(中文注释如 :145 的 //一行s 为标准 UTF-8 字节),与同目录 convert.*、hex_file_parse.*、crc.h、s19.h 一致;但同目录的 crc.c 是 GBK 编码(见 crc.c.md 备注 8),common/ 内部编码并不统一。