第一次把 1MB 以上的固件往 CAN FD 总线上灌的时候,我盯着传输进度条,手心全是汗。那是一批需要OTA升级的 ECU,客户指定用 TSMaster 搭 UDS 诊断刷写流程,要求在台架、产线和售后三种场景下都能稳定跑通。之前我用过 CANoe,也写过 PCAN 加 Python 的自主刷写工具,但对 TSMaster 能否扛住大文件刷写,心里其实没底。
跑完整个项目后回头看,TSMaster 在诊断协议栈、CDD 解析、C 小程序扩展和报文记录这些环节,确实把门槛压得很低。尤其是用诊断控制台手跑一遍 UDS 刷写全流程,再固化成 C 小程序自动执行,整个过程对诊断工程师、ECU bootloader 开发者、产线自动化测试人员都有参考价值。这篇文章我就从选型逻辑、协议链路、工具配置、脚本固化、大文件处理和安全隐患这几个维度和大家拆一遍。
1. 用 TSMaster 搭刷写流程,先想清楚它比别家强在哪
1.1 这个项目当时到底要什么
我接手的项目不是简单的“把 hex 文件发下去”就完事,而是有三层要求。
第一是台架阶段的协议验证,要看 ECU 的 bootloader 响应是否符合 OEM 规范,包括会话切换、安全等级、擦除时序、块序号处理这些细节。第二是产线阶段的批量刷写,要求整个过程自动化,不能靠人盯屏幕,还要有完整日志供质量追溯。第三是售后阶段的二次升级,网络环境更差,报文可能经过网关转发,时序和丢包处理必须更保守。
这三层场景叠加在一起,对工具的要求就很具体了:要有完整的 UDS 诊断协议栈,不能让我从 ISO-TP 分帧开始写;要能解析 OEM 提供的 CDD 或 ODX 诊断数据库,而不是让我对着文档手拼字节;要能灵活写脚本处理自定义的 seed-key 算法和大文件拆分;最后还得有可靠的报文记录功能,刷写失败时能复盘。
1.2 TSMaster 和其他工具的差异
我这些年接触过的刷写工具大致分成三类:CANoe 这类商业综合工具、PCAN 加自研脚本、TSMaster 这类新兴国产工具。三者的差异,用一个表格就能看清。
| 能力点 | CANoe | PCAN + 自研脚本 | TSMaster |
|---|---|---|---|
| UDS/ISO-TP 协议栈 | 内置完整 | 自己写,容易踩坑 | 内置完整 |
| CDD/ODX 解析 | 支持,但授权成本高 | 需要另外找库或手动解析 | 支持,界面直观 |
| 脚本定制 | CAPL,学习曲线陡 | Python/C 自由,但要处理底层分帧 | C 小程序 + Python 接口 |
| ECU 仿真 | CANoe 能模拟,但配置复杂 | 基本没有 | 支持诊断仿真,能模拟 ECU |
| 上手成本 | 高 | 中,但开发量大 | 低,下载即可用,支持主流 CAN 卡 |
CANoe 确实功能全面,但授权成本高,而且 CAPL 这门语言的学习曲线对新人不太友好。自研脚本虽然自由度高,可光是把 ISO-TP 的连续帧、流控帧处理好,就得耗掉不少时间,还要考虑诊断态的状态管理,容易在细节上翻车。TSMaster 内置了诊断协议栈,配上 CDD 之后,诊断控制台里能直接看到服务名、参数含义和 NRC 描述,对排查问题非常方便。C 小程序又能让我把 OEM 的 seed-key 算法、文件分包逻辑写得像普通 C 代码一样自然,这一点在后续固化流程时帮了很大忙。
1.3 先把“刷写成功”的定义写清楚
项目启动第一天,我就和团队把“刷写成功”定义成了四个可验收的指标,而不是简单看进度条走完。
第一,固件版本号能正确读回,且与应用层版本预期一致。第二,刷写完成后 DTC 能被正常清除,再用 19 服务读取时无新增故障码。第三,ECU 能正常跳转到应用区,整车网络无异常,总线错误率不超过阈值。第四,整个刷写过程有完整日志,能追溯到每一帧报文的收发时间。
这个定义挺重要,因为后续所有脚本逻辑、测试用例、产线验收标准,都是围绕这四个指标展开的。没有这个前提,就容易出现“看起来刷完、实际有问题”的情况。
2. UDS 刷写协议主干:从会话切换说到校验复位
2.1 刷写前的基础预备动作
UDS 刷写的核心链路其实不复杂,但每一步都不能含糊。我习惯把整个过程分成三个阶段:预备、传输、校验。
预备阶段从诊断会话切换开始。绝大多数 ECU 只有在编程会话下才允许擦除和写入 flash,所以第一帧请求通常是10 02,如果 ECU 在某些场景下要求先进入扩展会话,那就先发10 03再切到10 02。这一步的关键是确认响应为50 02,同时注意部分 ECU 在切会话时会切掉整车通信,导致总线上一段时间没有报文,测试时要留出足够的等待窗口。
会话切换之后是安全等级。刷写属于敏感操作,bootloader 通常不允许直接执行,必须通过27服务解锁。子功能一般是奇数请求种子、偶数发送密钥,比如27 01请求种子,27 02发送计算后的密钥。OEM 的 seed-key 算法五花八门,种子长度有 2 字节、4 字节、8 字节不等,密钥计算还往往带一个随机器或时间戳。TSMaster 里写 C 小程序做这一步非常顺手,收到种子后直接进算法函数算出密钥,再回填发送。
这里特别提醒一句:安全等级解锁往往有次数限制和超时限制。比如连续 3 次密钥错误,ECU 会拒绝后续请求,必须断电或等一段时间才能再次尝试。脚本里一定要把失败计数和时间戳处理好,否则产线误操作会把 ECU 锁死。
2.2 例程与数据传输,刷写链路里的核心节点
安全等级解锁通过后,就进入了例程和数据传输阶段。典型流程是:
31 01 FF00启动例程擦除内存,比如擦除 bootloader 指定区域。响应71 01 FF00表示擦除成功。34服务请求下载,格式类似34 00 44 [地址] [长度],其中00表示数据格式标识符(高四位压缩方法,低四位加密方法,00就是无压缩无加密),44表示地址和长度各占 4 字节。- ECU 收到
34后返回74 00 [最大块长度],这个最大块长度决定了后面36服务每次能发多少字节。 36服务传输数据。每条请求36 01 [数据...],01是块序号,从01开始,到FF后回绕到00,再继续计数。脚本里要对 256 取模,否则就等着收 NRC 0x73 吧。- 数据发完后,
37服务请求退出传输。收到77表示 ECU 确认退出,此时 flash 可能还在后台写入,不要立刻断电。 - 校验阶段通常再发一次
31 01,比如31 01 0202或31 01 FF01,让 ECU 对写入的数据做完整性和 CRC 校验。 - 最后根据 OEM 规范发
11 03软复位,让 ECU 跳转到应用区。
这里我把 31 服务划分成两块:擦除用的 RID(比如 FF00)和校验用的 RID(比如 0202、FF01)。不同厂商对 RID 的定义差别很大,具体值必须查 CDD 或 OEM 文档。TSMaster 加载 CDD 后,诊断控制台会把这些 RID 解释成可读的服务名,能省不少事。
2.3 刷写前后的 DTC 与版本信息处理
刷写流程里我习惯把 19、22、14 这几个服务和刷写动作组合在一起,形成完整的产线测试环。
刷写前先读一次 DTC 和软件版本,保留基线。常见服务是19 02读取 DTC 和22 F1 90读取版本号之类的 DID。刷写完成后,再用14 FF FF FF清除所有 DTC,然后用19服务确认 DTC 状态正常,最后读一次软件版本与预期比对。
很多人觉得这一步多余,但实际产线里靠这个动作能拦截不少问题。比如 bootloader 虽然没有报错,但应用区没有正常拉起,版本号读不回来,那就是刷写失败。早期发现总比整车下线后再返工成本低得多。
2.4 几个容易踩的 NRC 与时序细节
UDS 刷写过程中,负响应最常见的是 0x22、0x31、0x33、0x73、0x78,我把它们整理成了一张速查表。
| NRC | 含义 | 常见触发原因 |
|---|---|---|
| 0x22 | 条件不正确 | 在默认会话直接擦除/刷写 |
| 0x31 | 请求超出范围 | 地址、长度、参数不符合 ECU 要求 |
| 0x33 | 安全访问被拒绝 | seed-key 错误,或解锁次数超限 |
| 0x73 | 块序号错误 | 36 服务的块序号跳号 |
| 0x78 | 请求正确,但需要更长时间处理 | 擦除、写 flash 时耗时较长 |
0x78 这个 NRC 特别值得单独说。很多自研上位机把 0x78 当成错误直接退出,这是不对的。UDS 规定,收到 0x78 后,客户端要进入 P2* 等待窗口,继续等待服务器在稍后给出真正的正响应或负响应。TSMaster 的诊断配置里可以对 P2 和 P2* 的超时时间做设置,测试时把 P2* 调到 5 到 10 秒,避免正常耗时操作被误判为超时。
3. 在 TSMaster 里把刷写环境配置到可战斗状态
3.1 加载 CDD,别用 DBC 硬撑
很多人拿到项目第一反应是找 DBC,但 DBC 只是信号层定义,不包含 UDS 诊断服务的语义。刷写这种强诊断交互的场景,务必加载 CDD(CANdela Diagnostic Description)或 ODX 文件。
TSMaster 加载 CDD 之后,诊断控制台会自动解析出服务列表、DID、DTC、参数格式和 NRC 描述。比如我输入10 02,工具界面会显示这是“Diagnostic Session Control”请求,并且自动提示子功能的位置。如果输入27 01,界面会告诉我这是请求种子,响应里哪个字节是种子,种子长度是多少。这在排查问题时的效率提升非常明显。
如果没有 CDD,TSMaster 也支持手动建立诊断配置,把服务 ID、子功能、参数布局一个个填进去。只不过手动配置工作量大,而且容易出错,一般只用于验证某个自定义服务。我建议项目一开始就直接向 OEM 或供应商索要 CDD,别凑合。
3.2 地址映射与 CAN FD 数据段设置
CAN 上跑 UDS,地址映射直接影响能不能收到响应。
经典 CAN 11 位 ID 环境下,我常用的请求地址是 0x7E0,响应地址是 0x7E8,功能寻址 0x7DF。CAN FD 或车载以太网场景下,很多 OEM 会用 29 位 ID,比如物理请求0x18DAxxF1、物理响应0x18DAF1xx,其中 xx 是 ECU 地址。TSMaster 的诊断配置里要正确设置请求 ID、响应 ID 和功能 ID,否则发出去的请求石沉大海。
另外,CAN FD 要把数据段长度设对。TSMaster 里设置 CAN FD 数据段为 64 字节后,诊断协议栈会自动把多出的空间利用起来,一条36服务可以一次携带最多 63 字节的应用数据,刷写速度比经典 CAN 快很多。这一步不要漏了,很多新手刷写慢、超时,就是这个配置没设置对。
3.3 用诊断仿真在没有 ECU 的时候先把流程验证一遍
TSMaster 的诊断仿真功能,对开发阶段帮助特别大。我能直接在软件里创建一个 ECU 节点,为它配置 UDS 服务响应,比如10 02返回50 02、27 01返回种子、34返回最大块长度,甚至能模拟36服务后触发0x78延迟响应。
我当时的做法是先用诊断仿真把整套刷写流程在软件层面跑通,确认脚本逻辑没有明显问题后再接真实 ECU。这样做的好处是:第一,不用每次都在台架上占设备;第二,仿真环境能稳定复现各种 NRC 和超时场景,方便验证脚本的异常处理分支。尤其在做售后升级流程测试时,可以模拟网关丢包、ECU 长时间不响应等极端情况。
4. 用 C 小程序把刷写流程固化成可靠逻辑
4.1 为什么不用纯图形化序列
TSMaster 有图形化的测试序列模块,可以拖拽步骤实现线性流程,简单场景确实够用。但刷写流程绕不开文件读取、seed-key 计算、块序号维护、循环发送、异常重试这些逻辑,用图形化界面搭出来既繁琐又难维护。
所以我选择了 C 小程序。TSMaster 的 C 小程序本质是一个基于 C 语言脚本的运行环境,可以调用工具提供的报文收发、定时、文件 IO 等接口。这样我既不用处理 ISO-TP 分帧,又能用熟悉的 C 语法把 OEM 的算法和业务逻辑直接写进去。整段代码经过编译后,在 TSMaster 界面里一键运行,也可以配合测试模块做自动化。
4.2 一个典型的刷写主流程骨架
下面这段代码是刷写逻辑的骨架,不包含具体 TSMaster API 细节,主要提供一个思路。实际使用时,API 调用方式以你当前 TSMaster 版本的头文件为准。
#include <stdint.h> #include <string.h> // 发送UDS请求并等待响应,函数内部分装报文收发和超时处理 // req: 请求负载,reqLen: 请求长度 // resp: 响应缓冲区,respLen: 返回响应长度 // 返回0表示成功,非0表示错误码 int Diag_SendRequest(uint8_t ecuId, const uint8_t *req, uint32_t reqLen, uint8_t *resp, uint32_t *respLen); // 读取固件文件,解析bin/hex并填充缓冲区 // 返回数据总长度 uint32_t File_Load(const char *path, uint8_t *buf, uint32_t bufSize); // OEM seed-key算法,输入seed输出key void Security_CalcKey(const uint8_t *seed, uint8_t seedLen, uint8_t *key, uint8_t *keyLen); int Flash_Program(const char *firmwarePath, uint32_t ecuId) { uint8_t req[256], resp[512]; uint32_t respLen = 0; uint8_t seed[8], key[8]; uint8_t firmware[1024 * 1024]; uint32_t fileLen = 0; // 1. 读取固件文件 fileLen = File_Load(firmwarePath, firmware, sizeof(firmware)); if (fileLen == 0) return -1; // 2. 切换到编程会话 uint8_t sessionReq[2] = {0x10, 0x02}; if (Diag_SendRequest(ecuId, sessionReq, 2, resp, &respLen) != 0) return -2; // 检查响应是否为50 02 // 3. 请求种子 uint8_t seedReq[2] = {0x27, 0x01}; if (Diag_SendRequest(ecuId, seedReq, 2, resp, &respLen) != 0) return -3; // 根据OEM协议从resp[2:]拷贝种子到seed[],得到seedLen // 4. 计算密钥并发送 uint8_t keyLen = 0; Security_CalcKey(seed, seedLen, key, &keyLen); uint8_t keyReq[2 + 8] = {0x27, 0x02}; memcpy(&keyReq[2], key, keyLen); if (Diag_SendRequest(ecuId, keyReq, 2 + keyLen, resp, &respLen) != 0) return -4; // 5. 启动擦除例程,例如 31 01 FF00 uint8_t eraseReq[4] = {0x31, 0x01, 0xFF, 0x00}; if (Diag_SendRequest(ecuId, eraseReq, 4, resp, &respLen) != 0) return -5; // 6. 请求下载,地址和长度按OEM协议组装 // 这里以4字节地址+4字节长度为例 uint8_t downloadReq[10] = {0x34, 0x00, 0x44, 0x00, 0x00, 0x00, 0x00, // 起始地址 0x00, 0x01, 0x00, 0x00}; // 数据长度 if (Diag_SendRequest(ecuId, downloadReq, 10, resp, &respLen) != 0) return -6; // 从响应74 00 [块长度]中获取最大块长度maxBlock // 7. 循环发送36服务 uint32_t blockCounter = 0x01; uint32_t offset = 0; while (offset < fileLen) { uint32_t chunk = (fileLen - offset); if (chunk > maxBlock) chunk = maxBlock; req[0] = 0x36; req[1] = blockCounter & 0xFF; memcpy(&req[2], &firmware[offset], chunk); if (Diag_SendRequest(ecuId, req, 2 + chunk, resp, &respLen) != 0) { // 处理单块失败,建议记录日志后重试或退出 return -7; } offset += chunk; blockCounter = (blockCounter + 1) & 0xFF; } // 8. 请求退出传输 37 uint8_t exitReq[1] = {0x37}; if (Diag_SendRequest(ecuId, exitReq, 1, resp, &respLen) != 0) return -8; // 9. 校验完整性,例如31 01 0202 uint8_t checkReq[4] = {0x31, 0x01, 0x02, 0x02}; if (Diag_SendRequest(ecuId, checkReq, 4, resp, &respLen) != 0) return -9; // 10. 软复位 11 03 uint8_t resetReq[2] = {0x11, 0x03}; if (Diag_SendRequest(ecuId, resetReq, 2, resp, &respLen) != 0) return -10; return 0; }这段骨架代码把刷写动作按顺序排下来了,实际项目里还需要加上每个步骤的响应校验、超时重试、日志输出和错误码归类。重点在于,块序号用与运算取模,避免回绕时出错,这是我在早期版本脚本里吃过亏的地方。
4.3 把 seed-key 算法和文件解析封装成独立模块
刷写逻辑里有两个高风险点:一个是 seed-key 算法,另一个是固件文件解析。我都建议把它们单独封装。
seed-key 算法是 OEM 的核心保密逻辑,不同项目算法不同。我在项目里把它做成独立函数,输入种子、输出密钥,算法内部可以加盐、加移位、加查表。这样换车型时,只需要替换这个函数,主流程不用动。
固件文件解析方面,bin 文件最省事,直接二进制读出即可。hex 和 s19 文件则需要解析地址和校验。以 Intel HEX 为例,每条记录格式是:LLAAAATT[DD...]CC,其中AA是地址、TT是类型、DD是数据,还要注意扩展地址记录(类型 04)会改变后续数据的高 16 位地址。解析时最怕的是忽略扩展地址,导致刷写地址错误,轻则 NRC 0x31,重则把数据写到不该写的地方。
我自己在 C 小程序里写了一个简单的 hex 解析器,解析后按地址填充到连续缓冲区,同时记录起始地址和数据长度。这样34服务里的地址和长度直接来自解析结果,不会因为手填地址搞错。
5. 大文件刷写与现场问题的处理经验
5.1 分块长度不是越大越好
有些 ECU 的 bootloader 最大块长度是 0x100,也就是一次36服务最多带 256 字节数据。如果34响应里 maxNumberOfBlockLength 写的是 0x100,那脚本里 chunk 就取 256,不要自作主张加大。
另外,部分 ECU 对块长度还有对齐要求,比如必须是 4 或 8 的倍数。我是在解析文件后,先按 4 字节对齐规则把数据补齐,再进入循环发送。否则最后一块剩余 1 到 3 字节时,ECU 可能直接返回 0x31 或 0x72。
刷写速度也不是越快越好。CAN FD 一次能带 63 字节应用数据,理论速率很高,但 ECU 端在擦写 flash 时总线接收能力会降下来。我实测时发现,连续发送太快,ECU 会回 0x78 甚至直接丢帧。后来在每帧之间加了一个很小的间隔,比如 0.5 到 1 毫秒,刷写稳定性和成功率明显提升。这个值因 ECU 而异,最好做成可配置项。
5.2 现场调用:跨网关、多 ECU 和总线干扰
售后升级场景里,测试仪往往不是直接挂在 ECU 上,而是经过网关转发。这时候请求 ID、响应 ID 可能被网关重映射,加上网关本身有转发延迟,原来的 P2 超时时间就不够用了。碰到这种场景,先把超时时间放宽到 100ms 到 200ms,再根据实测逐步降下来。
多 ECU 同时刷写时,要注意各 ECU 的物理寻址和功能寻址不能混淆。TSMaster 里可以配置多个诊断目标,按 ECU 地址切换请求。产线批量刷写时,我一般按顺序逐台刷,避免总线上多个 bootloader 同时擦除引发瞬时过流。
总线质量也不能忽视。刷写过程中如果总线出现错误帧,轻则丢帧重传,重则导致 ECU 卡在异常状态。TSMaster 的报文统计面板可以实时看错误帧数量。我在现场遇到过一根屏蔽线压接不良导致刷写失败率飙升的案例,换了线缆就好了。
5.3 0x78 的处理和刷写失败后的恢复策略
0x78 在前面章节提过,它是“正在处理”的意思,不是错误。现场脚本里要单独开一个状态分支:收到 0x78 后,继续等待,直到收到真正的正响应或超时。尤其是擦除例程和刷写完成后的校验例程,ECU 处理时间可能长达几十秒,P2* 超时要给足。
一旦刷写失败,不要急着清错误或盲目重试。先看日志定位失败的服务和 NRC,再去判断是地址错、长度错、还是安全等级掉了。很多 ECU 在安全等级失败后会锁定一段时间,脚本里最好等锁定窗口过了再重试,否则会连续失败。
我自己的恢复策略是先切回默认会话,再重新走一遍编程会话和安全等级流程,从完整的第一步重刷一次。这样状态机回到初始位置,比在失败现场乱发服务更可靠。
6. 刷写安全视角:过年总能听见的安全问题也得考虑
6.1 刷写链路里常见的攻击面
刷写是和安全距离最近的诊断操作之一。坐在车里顺着 OBD 口就能把固件刷掉,这在车辆安全测试里已经不是新闻。常见的攻击面有这么几类。
第一种是报文监听和逆向。攻击者用总线抓包工具录制一段完整的刷写过程,得到 27 服务里的种子和密钥,再通过大量样本逆向出 seed-key 算法。这是最传统也最常见的路径。第二种是重放攻击,把录下来的原始报文重新发到总线上,直接尝试复现刷写动作。如果协议没有重放防护,ECU 可能就被“合法”报文刷了。第三种是伪造 ECU 响应或伪造刷写工具,配合各种恶意脚本,在生产环境里做中间人干扰。第四类是信息泄露,比如固件文件本身、刷写日志、CDD 文件泄露,都会大大降低攻击门槛。
6.2 在 TSMaster 里做重放与模糊测试
安全自测不能只停留在理论。TSMaster 的报文记录和回放功能可以直接用来做重放攻击验证。
我把一次完整的正常刷写过程录下来,然后原样回放到总线上,看 ECU 是否会再次执行刷写。如果没有做刷写时间戳校验、滚动码、或安全等级状态机结合的校验,ECU 很可能直接接受重放,这就是一个明确的安全漏洞。这个测试不需要额外工具,TSMaster 日志回放功能就能做。
模糊测试也可以用脚本实现。把 36 服务的块序号随机改掉、把 34 服务的地址长度随机增大、把 31 服务的 RID 换成未定义的数值,逐个发送到 ECU 上,观察 ECU 是否会异常复位、卡死或出现非预期行为。TSMaster 的 C 小程序构造这种随机帧很方便,我写过一个简单的模糊测试脚本,在台架上跑一夜,能发现不少健壮性问题。
6.3 生产端的安全防御闭环
防御层面,我总结成几个实操点。
第一,seed-key 算法不要用固定的、可逆向的简单变换,要加盐、加动态因子,密钥参数要根据每次会话变化。第二,刷写流程要带安全状态校验,比如要求先完成 10 02 和 27 解锁,才能执行后续 31 和 34,避免跨步骤注入。第三,固件文件要加密存储和传输,ECU 端解密后再写入 flash,同时配合安全启动和完整性校验,防止篡改固件被直接刷入。第四,产线和售后刷写工具要加权限管理,日志上传审计,CDD 和固件文件按权限分发。
另一个容易被忽视的细节是防回滚。很多攻击手法不是刷“新固件”,而是刷“旧版本固件”,让车辆退回到有已知漏洞的版本。刷写流程里检查版本号,并禁止降级,是成本很低的防御手段。
这些安全措施不一定全部落地,但至少在协议设计阶段就考虑进去,比上线后发现问题再打补丁省事得多。
项目收尾的时候,我最满意的一点不是刷写速度有多快,而是整个流程的“可解释性”——任何一步失败,日志里都能清楚看到是哪个服务、哪个 NRC、哪个文件偏移位置出问题。TSMaster 把我从底层 ISO-TP 和诊断状态机的泥潭里拉了出来,但刷写是否可靠,最终还是取决于流程设计和细节处理是否严谨。如果你正在搭自己的刷写流程,建议按这个顺序来:先手跑一遍协议链路,再固化成脚本,最后把所有边界情况和异常分支补齐,这样产线才不至于在凌晨两点给你打电话。