资讯动态

UDS刷写日志离线分析:从CAN帧到NRC定位的工程实践

发布时间:2026/9/15 21:56:04 来源:尧图企业网站定制
前段时间同事扔给我一份刷写日志整整 4 万多行让我帮忙看下为什么刷到 60% 的时候整车控制器报错。我一开始也头大文件里全是十六进制字节CAN ID 混在一起乍一看就是天书。后来把 ISO-TP 层和 UDS 层一层层剥开用我自己维护的一款开源 UDS/ISO-TP 刷写日志离线分析工具跑了一遍几十秒内就定位到了一条 0x31 例程控制返回 0x72 的负响应。这件事让我想认真聊聊这类工具背后的分析方法和实现思路。所谓刷写日志离线分析工具就是不依赖诊断仪、不连接实车纯粹通过解析抓取的 CAN 总线日志文件把复杂的刷写过程还原成人类能看懂的时间线、服务序列和错误码报告。它适合三类人做嵌入式 Bootloader 开发的工程师、负责产线 EOL 或售后刷写问题的测试工程师以及经常要帮客户排查刷写失败问题的技术支持。接下来我会从协议分层、解析实现、时序重建、工具选型到实战避坑把整个思路完整讲一遍。1. 刷写日志到底难在哪先搞清楚我们要解决什么问题1.1 刷写不是一个“传文件”动作很多人以为 ECU 刷写就是把一个 bin 文件通过总线发过去像 U 盘拷贝一样。实际上一次完整的 UDS 刷写是一个有状态的多阶段会话先切换会话模式关闭相关通信确认编程条件然后解锁安全等级写入指纹信息通过 34/36/37 服务传输数据最后执行例程校验并复位 ECU。任何一个环节失败都会导致整个刷写流程中断。理解这一点非常重要因为离线分析工具的解析逻辑完全围绕刷写阶段来组织。如果你只盯着数据帧不看前后状态就无法判断一条 0x7F 负响应到底是安全等级不够还是数据校验失败亦或是服务不被支持。所以我在设计工具时第一件事不是写解析器而是把刷写流程拆成可识别的事件序列然后让每个 UDS 服务都映射到对应的阶段和状态。1.2 日志文件形态各异统一格式是第一道坎实际工作中拿到的日志远不止一种格式。Vector 的 ASC 和 BLF 最常见但不同的采集设备导出的 ASC 文件字段顺序都可能不一样PCAN 导出的是 CSV 或 TXT还有一些 OEM 的工具会输出带时间戳的私有文本格式甚至把多个 ECU 的日志混在一个文件里。这就带来一个很现实的问题分析工具不能只支持一种格式。我在项目里做了一层格式抽象把 ASC、CSV、TXT 甚至部分 BLF 都统一转换成内部 RawFrame 对象每个对象只保留时间戳、通道、CAN ID、数据长度和数据字节。后续的 ISO-TP 和 UDS 解析完全不关心原始文件来自哪里这样每新增一种日志格式只需要写一个很小的 parser。1.3 离线分析的真正价值可复现、可批量、可自动化在线诊断工具当然也能看服务响应但它要求实车、诊断仪、供电环境都就位出了问题还得现场抓包效率很低。离线分析工具的价值在于你可以把现场抓到的日志拿回来慢慢看定位问题后再回到环境里复现也可以一次性批量扫描几十份日志把 NRC 出现频次、刷写时长、失败阶段统计出来形成自动化回归测试。我在实际项目中就遇到过这样的情况同一款 ECU产线上偶尔出现刷写失败但问题不是必现的。用离线工具批跑了一周积累的日志才发现失败总是集中在某个特定的软件版本里而且都发生在 34 服务扩展地址之后。这个结论在线下反复测试都复现不出来但离线分析给了很明确的指向。2. ISO-TP 层把裸总线字节流还原成完整报文2.1 为什么绕不开分段机制标准 CAN 数据帧的数据场最长只有 8 字节而实际有效负载还要去掉 1 字节的 PCI协议控制信息所以单帧最多携带 7 字节应用数据。可 UDS 的服务动辄几十上百字节比如 34 服务请求带地址和长度信息36 服务一次只能发 7 字节0x31 例程控制的参数也可能超过 7 字节。所以 ISO-TPISO 15765-2必须把长报文拆成多帧传输。很多做应用层的人容易忽略这一点刷写日志里看到的一连串相同 CAN ID 的帧并不是重复报文而是同一个 UDS 消息的分段。如果不按照 ISO-TP 的规则重组你看到的就是一堆无法解读的碎片。离线分析工具的第一层核心任务就是把这些碎片重新拼成完整的 ISO-TP 报文。ISO-TP 定义了四种帧类型单帧SF、首帧FF、流控帧FC和连续帧CF。单帧里 1 字节 PCI 高四位为 0剩余 7 字节直接承载应用数据首帧 PCI 高四位为 1后跟 12 位总长度最多表示 4095 字节的大报文流控帧由接收方发出用于告诉发送方“我准备好了你可以继续发”其中包含块大小BS和最小间隔时间STmin连续帧则按顺序携带剩余数据。2.2 重组算法不能只按 CAN ID 简单拼接重组 ISO-TP 报文的核心逻辑不复杂但细节很多。每个逻辑连接通常由源地址、目标地址和地址模式物理寻址或功能寻址共同标识。收到首帧后创建重组缓冲区记录期望的总长度收到连续帧后按顺序填入缓冲区并校验帧序号。这里最容易被坑的是连续帧的序号。连续帧 PCI 的低四位是计数器从 1 开始循环使用 0~15也就是说 1、2、3...15、0、1、2...这样递增。如果解析器用简单的累加遇到回绕就会出错。另外流控帧的 BS 表示发送方最多连续发送多少个 CF 后要等待新的流控帧STmin 表示两个连续帧之间的最小间隔时间。离线分析时虽然不需要真正控制节奏但要解析这些参数因为刷写工具经常因为流控参数配置不合理导致总线拥塞或超时。我在工具里实现了一个IsoTpReassembler类以 (channel, tx_id, rx_id) 为 key 维护多个重组上下文。收到 SF 直接产出消息收到 FF 则初始化缓冲区收到 FC 更新发送状态收到 CF 填充数据填满后按照协议计算完整 payload 并产出。每个上下文还记录首帧时间戳和最后一片时间戳方便后续算传输耗时。2.3 日志时间戳微秒还是毫秒差距很大CAN 日志的时间戳通常来自抓包设备的硬件时钟精度可以到微秒但不同工具导出的单位不一样。ASC 文件一般是秒和微秒分开的字段CSV 可能是浮点秒有的厂商文本格式竟然是毫秒。如果解析器把单位搞错重组的时序会彻底乱掉尤其影响流控超时判断。我的做法是在格式解析阶段就把所有时间戳统一转换成微秒整型并保留硬件时间戳与系统时间戳的映射。后面做超时分析、时序重建时全部基于微秒计算。建议大家在写解析器时任何时间相关字段都显式标注单位不要用无单位浮点数否则后续维护一定会踩坑。3. UDS 服务层翻译从 SID 到可读诊断动作3.1 刷写相关的核心服务ISO-TP 重组出来的是一个个完整的 UDS 消息接下来要做的是解析 SID、子功能、数据参数并翻译成可读的诊断动作。UDS 服务号很多但刷写场景里高频出现的其实就十几个。SID服务名称刷写中的典型用途0x10诊断会话控制切换默认/编程/扩展会话0x11ECU 复位刷写完成后复位0x14清除诊断信息刷写前清 DTC0x19读取诊断信息读 DTC 快照0x22按 ID 读数据读软件版本/硬件版本0x27安全访问种子与密钥解锁0x28通信控制关闭/开启通信0x2E按 ID 写数据写 VIN、写指纹0x31例程控制启动编程条件检查、校验0x34请求下载开始数据传输0x36传输数据按块传输固件内容0x37请求传输退出结束数据传输0x3E测试器在线保持会话活跃0x85控制 DTC 设置刷写期间关闭 DTC表格只是索引真正重要的是理解服务之间的依赖关系。比如 34 服务必须在编程会话下通常还要先通过 27 服务解锁否则 ECU 会返回 0x33安全访问被拒绝。离线分析工具会把每个服务的响应状态记录下来形成服务链方便快速定位是哪一环断了。3.2 负响应结构和 NRC 解读当 ECU 不认可某个请求时会返回 0x7F 请求的 SID 1 字节 NRC。NRC 是刷写失败分析里最有价值的信息。很多新手只看“返回 7F 就是失败”但没去查 NRC 具体含义导致问题定位不准。NRC含义常见触发场景0x10一般拒绝请求条件不满足0x12子功能不支持会话模式不对0x13消息长度错误参数数量或格式不对0x22条件不满足未满足刷写前置条件0x24请求序列错误没走正常顺序0x31请求超出范围地址、长度超出 Flash 范围0x33安全访问被拒绝密钥错误或未解锁0x35密钥无效安全等级不匹配0x36超过重试次数解锁尝试次数过多0x37请求时间延迟超时超时未收到后续请求0x70上传下载不可接受编程会话未激活0x71传输数据暂停接收缓冲区满0x72编程错误Flash 擦写失败、校验失败0x73块序号错误36 服务块计数不对0x78请求被接收但仍在处理需要等待后续响应0x72 是刷写日志里最常见的失败码之一也是最容易被误判的。它可能表示 Flash 驱动问题、写入地址越界、校验失败、甚至供电不稳。单看 NRC 很难确定根因但配合前后文就能缩小范围如果 0x72 出现在 36 服务发送了大量数据之后大概率是写入或校验阶段出错如果出现在 31 服务启动例程时可能是例程执行超时或依赖条件不满足。3.3 解析服务参数时的细节每个服务都有各自的参数布局解析时不能只看 SID。比如 34 服务请求的格式是34 数据格式标识符 地址与长度格式标识符 内存地址 内存大小地址和长度的字节数是根据格式标识符动态解析的。OEM 变体很多有的是 4 字节地址、4 字节长度有的是 2 字节地址、2 字节长度。如果工具写死了偏移位置换一个 ECU 就会报错。36 服务则是36 块序列计数器 数据块块计数从 1 开始每块加 1。我在解析时会把块序号转换成长度信息并且检查序号是否连续。如果日志中出现跳号往往说明有丢帧或发送方逻辑异常。另外0x2E 写数据服务经常被用来写指纹信息其2E DID 数据的格式中 DID 是两字节不同 OEM 对 DID 的定义完全不同。离线工具可以提供 DID 映射配置让用户自己定义哪些 DID 代表软件版本、硬件版本、序列号、刷写时间等这样报告里就能直接显示可读信息。4. 时序重建把几十万行日志拼成一眼能看懂的刷写流程4.1 按服务特征识别刷写阶段重组并翻译出所有 UDS 消息后下一步是重建刷写流程的时序。这里我用的是“阶段识别”的思路根据服务特征和状态机把整个刷写过程切成预编程、编程、后编程三个阶段然后进一步识别出每次会话切换、安全解锁、数据传输等关键事件。预编程阶段通常包含 10 01切到默认会话、10 03切到扩展会话、85 02关闭 DTC、28 03关闭非诊断通信、3E 00保持在线等。进入编程阶段前会连续出现 27 服务的安全访问交互之后是 2E 写指纹、34/36/37 传输固件、31 01 02 03 等例程控制。后编程阶段则是 10 02切到编程会话后的复位或 10 01回默认会话、14 清 DTC、19 读 DTC、11 01 复位等。工具识别出这些阶段后会生成一张时间线把每个阶段用不同的标记展示。比如预编程是灰色传输数据是蓝色例程校验是橙色负响应是红色。一旦刷写失败红色的 7F 节点会非常显眼基本可以做到“一眼定位”。4.2 服务链与超时窗口单纯按时间排 UDS 消息还不够还需要把请求和响应配对形成服务链。物理寻址通常是请求 0x7E0、响应 0x7E8 这种一对一的配对关系功能寻址则是请求发到 0x7DF多个 ECU 各自响应这种情况下不能按“收到一个响应就匹配完成”来处理而应该等待一段窗口期。超时窗口的判断也很关键。UDS 协议里ECU 收到请求后默认要在 50ms 内给出响应某些慢服务允许扩展延迟响应前会先发 0x7F SID 0x78请求被接收但仍在处理。离线分析工具要识别这种 0x78 中间响应并在最终响应之前不判定超时。很多第三方工具在这里做得不好把 0x78 当成普通负响应导致刷写流程分析错误。我在实现阶段识别时维护了一个状态机当前会话模式、安全等级、传输状态是否已请求下载、当前块计数、例程状态。每收到一条 UDS 请求或响应就根据状态机更新状态并打上阶段标签。这样即便日志里包含多个 ECU 的消息也能按逻辑连接分别维护各自的状态机。4.3 失败归因从 NRC 往前推 100ms定位刷写失败有一套很实用的经验法则先找到最近一条 0x7F 负响应确认 NRC然后往前找 100ms 到 500ms 窗口内的所有相关帧看失败前最后一条成功的服务是什么再判断是该服务本身失败还是前序服务没完成导致条件不满足。举个例子有一次日志里反复出现31 01 02 03启动例程返回 0x22条件不满足从表面看是例程没通过。但往前翻了几百条记录发现刷写工具根本没发 27 服务的安全解锁请求只是在广播会话切换后直接尝试启动例程。这种情况下 NRC 是 0x22 而不是 0x33很容易误导人。工具如果能把“安全等级状态”自动关联进报告就能直接提示“当前安全等级未解锁不建议启动该例程”。这类关联分析才是离线工具的真正价值。5. 开源工具链选型不是每个环节都要自己造轮子5.1 CAN 日志解析层的选择开源社区里已经有不少现成的轮子没必要全部从零写。CAN 日志解析层面can-utils提供了candump等命令行工具但它主要面向在线抓取和简单文本输出Wireshark自带的 ISO-TP dissector 可以解析 ASC 和 pcap 文件还能图形化查看 ISO-TP 重组但批量分析和生成自定义报告的能力比较弱。我在项目里最初尝试过直接调 Wireshark 的tshark命令行导出解析结果优点是省力缺点是日志格式兼容性受限于 Wireshark 的 dissector而且对私有格式的支持很差。后来决定自己写解析层但参考了 Wireshark 的协议字段设计思路保证后续可以对接。如果你想快速验证一段日志内容用tshark -r file.asc -V看 ISO-TP 的解码结果是很好的起步方式但如果你想做自动化的批量分析工具还是需要自建一层解析。5.2 Python 生态python-can 与 udsoncan 的边界python-can是 Python 生态里最常用的 CAN 工具库支持读取 ASC、BLF 等格式也支持多种硬件接口。但要注意python-can的 BLF 读取能力依赖canio或者原生支持某些老的日志版本会读取失败。我建议将它作为日志读取的“可选后端”之一而不是唯一路径。udsoncan是一个非常完整的 UDS 在线诊断库支持发包、收包、服务封装但它针对的是在线交互场景并没有为离线日志分析做设计。你不能直接拿udsoncan去解析一份日志文件因为它默认所有服务都是一问一答的在线对话。不过可以借鉴它对服务的参数定义和编解码逻辑把其中的消息类抽出来复用避免自己重写一套 34/36/37 服务的参数编解码。我的最终架构是格式解析层用自研代码加python-can兜底ISO-TP 重组层完全自研UDS 服务编解码参考udsoncan的参数定义报告生成层用纯 Python 生成 JSON、CSV 和独立 HTML 文件。整个项目以 CLI 方式运行也提供了一组可导入的 Python API方便接入其他自动化测试框架。5.3 中间数据模型的设计离线分析工具最容易犯的错误是把“日志格式解析”和“业务分析”耦合在一起。如果 RawFrame 直接塞给报告模块后面每扩展一种日志格式或分析方法都会牵一发动全身。我设计了四层数据模型RawFrame原始帧包含时间戳、通道、CAN ID、数据。IsoTpMessage重组后的完整消息包含源地址、目标地址、时间戳、payload。UdsMessageUDS 层的请求或响应包含 SID、子功能、参数解析结果、NRC。SessionEvent刷写阶段和高层语义事件包含阶段标签、状态机快照、关联 UDS 消息。每一层只消费上一层的输出。比如 UDS 层不关心原始 CAN 帧的 DLC 是多少ISO-TP 层不关心 SID 的含义。这样每一层都可以独立测试也方便别人基于这个项目扩展自己的分析规则。5.4 二次开发的扩展点开源工具最终能不能被大家用起来取决于扩展点设计得好不好。我在项目里预留了几个关键扩展点日志格式解析器注册表、DID 映射表、NRC 补充说明字典、服务序列阶段识别规则。每一个都是简单的 Python 字典或函数注册用户加一个厂商私有协议时不用改动核心代码只要新增一个解析规则文件就行。举个例子某厂商在 34 服务之前多加了一个 2E 写刷写使能标志的步骤未做使能直接发 34 会返回 0x24。默认的阶段识别规则识别不到这个私有服务但通过配置用户可以把该 2E 请求标记为“刷写使能”并设定它必须在 34 服务之前完成。这样报告里就能自动标出“刷写使能未设置”这类提示。6. 实测避坑录我在解析真实刷写日志时踩过的坑6.1 时间戳单位混用导致时序乱掉我第一次拿真实 BLF 日志做回归测试时发现报告的时序完全对不上有些响应竟然出现在请求之前。排查了半天问题出在 BLF 内部的 time stamp 是以 10 纳秒为单位的 tick我用脚本读取时当成微秒直接除以 1000导致所有时间戳缩放了 100 倍。后来我统一在格式解析层做一次时间基准归一化并且针对不同格式分别写单测。这件事给我的教训是任何来源的日志都要先打印前几帧的时间戳间隔肉眼确认数量级是否合理。比如两个连续 CAN 帧之间通常间隔几毫秒如果看到间隔是几微秒或几百秒大概率是单位解析错了。6.2 多 ECU 日志混杂与同一 CAN ID 不同节点的干扰产线日志经常是一个文件包含多个 ECU 甚至多个通道的数据不同 ECU 可能使用相同的 CAN ID 范围尤其是在功能寻址广播时一个请求会带出好几个响应。如果解析器只按 CAN ID 分组必然会把不同节点的数据混在一起。解决思路是引入“逻辑节点”概念每个 ECU 节点由通道号和诊断物理请求/响应 ID 对唯一标识。比如节点 A 是ch1 0x7E0/0x7E8节点 B 是ch2 0x7E0/0x7E8虽然 CAN ID 相同但通道不同视为不同节点。功能寻址响应则通过响应 ID 的不同来区分节点。这样时间线就能按节点分层展示避免互相干扰。6.3 协议变体和私有服务的兼容不同 OEM 的刷写时序差异非常大有的在 34 服务前需要先发特定 DID 长度信息有的把 31 例程的例程 ID 定义成私有值。如果你的工具只按标准协议解析碰到私有服务就会漏报或误报。最稳妥的做法是未知服务不强行解析只输出原始 hex 和 SID 号同时标出“未知服务”并允许用户通过配置补充说明。我在项目里增加了“协议配置文件”机制用户可以针对特定 ECU 定义一个 JSON里面写明私有 DID、私有例程 ID、允许的服务序列等。分析报告会优先使用配置文件里的解释未匹配的才回退到标准定义。实测下来这种方式能覆盖绝大多数 OEM 变体同时保持核心代码简洁。6.4 性能优化与超大日志处理一份完整的产线刷写日志动辄几万帧有些使用 CAN FD 加高波特率抓取的文件甚至超过 20 万帧。如果解析时把全部帧一次性载入内存再做重组内存占用会非常夸张而且分析速度慢得让人抓狂。我的优化思路是使用生成器按帧读取边读边喂给 ISO-TP 重组器重组器完成一个完整消息就立刻产出交给 UDS 解析器处理处理完的消息直接写入中间结果文件。这样任何时刻内存里只保留当前正在重组的少量上下文几万帧的日志处理时间可以压到几秒内。另外多通道日志可以用多进程按通道并行解析最后再合并报告。时间戳排序也是一个隐藏性能点。某些日志文件里帧并不严格按时间递增比如导出工具合并多个通道时会出现小范围的乱序。我做分析前会先做一次稳定排序同时对时间戳做去重和单调化处理避免报告中出现类似“上一帧时间晚于下一帧”的诡异现象。6.5 异常流控和丢帧场景离线日志里经常能看到只有请求没有响应的情况原因可能是总线丢帧、ECU 掉电、或者日志采集本身丢包。分析工具不能遇到这种场景就崩溃而应该明确标记为“无响应”并继续解析后续内容。另外流控帧缺失但连续帧仍然出现的情况也见过这通常说明抓包工具漏采了部分帧重组器要能容忍这种缺失并给出警告而不是静默丢弃。这类异常场景正是离线分析工具比人肉看日志强的地方它能自动统计丢帧率、缺失响应数量、超时次数然后在报告开头给一个健康度总览。我后来给项目加了一个“异常摘要”模块专门把这些问题用列表打出来测试同事看到摘要就能决定是否重新抓包省下大量沟通时间。说句实在话这个工具本身并不复杂最难的始终是把各家的日志格式、私有协议和千奇百怪的刷写时序都兼容进来。我后来把常见日志格式的解析器做成了插件式谁在项目中遇到新格式按模板补一个解析函数就能解决。如果你也想做类似的工具我建议不要急着堆功能先把 ISO-TP 重组和 NRC 关联这两块做扎实再逐步扩展厂商私有协议。最后再分享一个小技巧分析刷写失败日志永远先看最近一条 0x7F然后再看它前面 100ms 内到底发生了什么。大部分刷写失败都不是“突然失败”而是前置条件没满足。把这条经验写成规则放进工具里比任何复杂的算法都管用。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价