第一次把 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 这类新兴国产工具。三者的差异用一个表格就能看清。能力点CANoePCAN 自研脚本TSMasterUDS/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 诊断服务的语义。刷写这种强诊断交互的场景务必加载 CDDCANdela Diagnostic Description或 ODX 文件。TSMaster 加载 CDD 之后诊断控制台会自动解析出服务列表、DID、DTC、参数格式和 NRC 描述。比如我输入10 02工具界面会显示这是“Diagnostic Session Control”请求并且自动提示子功能的位置。如果输入27 01界面会告诉我这是请求种子响应里哪个字节是种子种子长度是多少。这在排查问题时的效率提升非常明显。如果没有 CDDTSMaster 也支持手动建立诊断配置把服务 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 和诊断状态机的泥潭里拉了出来但刷写是否可靠最终还是取决于流程设计和细节处理是否严谨。如果你正在搭自己的刷写流程建议按这个顺序来先手跑一遍协议链路再固化成脚本最后把所有边界情况和异常分支补齐这样产线才不至于在凌晨两点给你打电话。
