s19.c
midware/bms/alpha/battery_upgrade.c)4.6 静态缓冲与可重入性4.7 关键全局变量的读写5. 对外接口6. 依赖关系7. 备注与风险点源文件路径:
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 | 局部 u32 | sscanf("%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~:51 | file_addr == NULL 或 file_pack->u8_burn_data == NULL(注意:不检查 file_pack 本身) | BUF_ADDR_ERR |
| 定位记录头 | :53~:61 | strstr((char *)file_addr, "S") 找本行 'S' | S_ADDR_ERR |
| 定位行尾 | :63~:71 | strstr((char *)file_addr, "\n") 找 '\n' | END_ADDR_ERR |
| (调试) | :73~:81 | S19_DEBUG 为 1 时打印本行原文与长度 | — |
| 分支派发 | :84~:412 | 按 l_u8_s_addr[1](类型字符)分派到 S0/S1/S2/S3/S7/S8/S9,其它 → S_ADDR_ERR | S_ADDR_ERR |
每个数据分支(S0~S3)内部都走同样的 6 步:
sscanf(&s_addr[2], "%02X", ...)读长度字段;失败 →LEN_ERR(:86、:165、:239、:313);- 一致性校验:
l_u32_s19_len != (((l_u8_end_addr - 1) - &l_u8_s_addr[4]) / 2)→LEN_ERR(:95、:174、:248、:322)。含义是「长度字段必须等于'S'+类型+长度字段之后、'\r'之前的字节对数」; l_u8_check_code_temp = l_u32_s19_len;作为校验和初值(:104、:183、:257、:331);- 循环
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); memset(&s_u8_buf[len-1], 0, 4)清校验和槽,然后l_u8_check_code_temp = 0xFF - l_u8_check_code_temp;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 - 3 | memcpy(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 - 4 | memcpy(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 - 5 | memcpy(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_OK | u8_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),但:
- 它不保存跨调用的状态(每次调用都从
l_u8_check_code_temp = l_u32_s19_len重新开始),所以逻辑上不需要static; - 它是本文件唯一被多个调用共享的可变内存,一旦将来出现第二个调用者(例如另一种电池平台升级同时进行),两行解析会互相覆盖
s_u8_buf,属于不可重入接口。当前工程内battery_upgrade.c是唯一调用者,故未暴露。
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_PCK | midware/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_END | midware/bms/alpha/battery_upgrade.c:641、:646、:711 |
本文件不导出全局变量,不注册回调,不定义 static 辅助函数。
被调用(本文件调用的外部函数/符号):
| 名称 | 来源 |
|---|---|
strstr | C 标准库 string.h(经 s19.h:15 引入) |
memset / memcpy | C 标准库 string.h(同上) |
sscanf / printf | C 标准库 stdio.h(经 s19.h:14 引入;printf 调用点全部被 #if S19_DEBUG 包裹,生产版本不编译) |
NULL | stdio.h/string.h |
u8 / u32、S19_FILE_PASRE_PCK、状态枚举成员 | system/sys/chip.h(经 s19.h:13)与 common/s19.h |
6. 依赖关系
依赖:
common/s19.h(s19.c:16)——唯一的#include;- 间接依赖:
system/sys/chip.h(类型)、C 标准库<stdio.h>/<string.h>(由s19.h代为包含)。
被依赖:
midware/bms/alpha/battery_upgrade.c(唯一使用方,见第 5 节)。
7. 备注与风险点
file_pack本身没有做 NULL 检查:s19.c:45的判断是(file_addr == NULL) || (file_pack->u8_burn_data == NULL)——在file_pack == NULL时会先解引用再判断,直接触发硬件异常。当前唯一调用点传的是栈上取址(battery_upgrade.c:555、:635),不会为 NULL,所以未暴露;但作为公共解析接口,缺少这一层防护是明确的缺陷。u8_burn_data输出缓冲的容量约束完全没有保护:burn_len由长度字段推导(S1最多 252 字节),而memcpy(:218、:292、:366)不检查目标容量。调用方midware/bms/alpha/battery_upgrade.c:46分配的是u8 u8_burn_data[256],恰好覆盖 252 字节数据,但没有任何编译期断言或运行期校验来保证这个「巧合」在将来仍然成立。固件文本来自云端下载,属外部输入。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。
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。也就是说安全性建立在「源映像中紧邻的字符恰好不是十六进制数字」这一偶然条件上,而不是任何显式边界检查,属典型的「两个缺陷互相掩盖」。sscanf(...) == NULL的判据不严谨::86、:111、:130、:165、:186、:202、:239、:260、:276、:313、:334、:350共 12 处都用「返回值等于NULL(即 0)」判断失败。正确写法应为!= 1(成功转换的字段数)。当前输入是内存字符串,sscanf不会返回EOF,所以行为正确;但这一写法掩盖了「一个字段都没转换成功」与「EOF」的区别,也让人误以为sscanf返回指针。(int *)&l_u32_s19_len形式的强转:把u32 *转成int *交给%X写入。在本平台(32 位、int与unsigned int同宽)安全,但%02X会写满 4 字节,一旦将来变量类型被改成u16/u8就会栈越界写。更稳妥的写法是直接用unsigned int类型的临时变量。- 控制流深度达 6 层、四个分支近乎逐字重复:
S0/S1/S2/S3四段共约 300 行代码只有「地址宽度」与「len偏移量」两处差异,典型的可提取为「统一解析 + 按类型取地址宽度」的写法。当前形态下任何一处修正(例如上面第 3 点的memset问题)都必须改 7 遍,漏改一处就会形成行为不一致。 l_u8_s_addr/l_u8_end_addr命名与实际类型不符:声明是char *(:37~:38),命名却用u8前缀,且在使用点反复(char *)/(u8 *)互相强转(:53、:111、:145~:146)。读代码时容易误判字段宽度。S5/S6(记录计数)未支持:S-Record 规范里的S5(16 位记录计数)、S6(24 位记录计数)会落到:406的兜底分支返回S_ADDR_ERR。若上游固件用的是带计数的 S19 变体,第 2 次解析就会失败并把升级重置(battery_upgrade.c:716~:727的 3 次计数),表现为「升级反复重启」。- 文件头注释重复:
:10~:15连续出现两个Includes注释块(复制粘贴残留);:25的函数注释里参数表已过时(写的是u8* file_addr,u32 burn_addr,u8* burn_data,u16 burn_len,与实际签名(const u8*, S19_FILE_PASRE_PCK*)完全不符)。 - 调试打印会打印整行:
S19_DEBUG置 1 时:76~:80会逐个字符printf整行内容,在实时任务中开销很大,只适合离线排查。 - 编码:本文件是 UTF-8(中文注释如
:145的//一行s为标准 UTF-8 字节),与同目录convert.*、hex_file_parse.*、crc.h、s19.h一致;但同目录的crc.c是 GBK 编码(见crc.c.md备注 8),common/内部编码并不统一。