ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

车载CAN通信与UDS诊断协议栈:从底层驱动到上层服务完整实战

车载CAN通信与UDS诊断协议栈:从底层驱动到上层服务完整实战 做车载软件开发最怕的就是底层和上层各干各的。搞驱动的只管报文收发做诊断的只管服务实现结果到了台架联调波形不对怪协议栈协议栈不对怪驱动来回扯皮。这篇文档把车载底层 CAN 通信和上层 UDS 诊断协议串起来讲以我实际做过的控制器项目为蓝本从 CAN 硬件驱动、位时序配置、总线波形排查到 UDS 诊断栈、ISO-TP 传输层再到两者如何对接、如何联调完整走一遍。适合车载嵌入式软件工程师、车载测试工程师以及正准备从应用层转到底层开发的读者参考。1. 项目整体定位与协议栈设计思路1.1 这个项目到底要解决什么问题很多人以为 CAN 通信就是调个库发发报文UDS 诊断就是收个请求回个响应。真上了车规项目这两块的坑比想象中多得多。底层的位时序配错了总线波特率明明看着是 500k一挂上整车网络就疯狂报错帧上层的会话状态没管好诊断仪一进来就卡在 security access 过不去整车下线检测直接超时。这个项目的目标很明确在一个标准 MCU 平台上从零搭建一套满足车载要求的通信栈实现两层能力。底层是把 CAN 控制器驱动起来能稳定收发明文报文能通过波形确认通信质量上层是基于 ISO 14229 实现 UDS 诊断服务包括会话切换、安全访问、读写数据、DTC 管理。两层之间通过 ISO-TP 传输层衔接让诊断仪发过来的多帧请求能正确重组响应能正确拆分。这套东西做完直接收益就是硬件端的报文质量和协议端的服务响应都可控。测试工程师拿诊断仪连上就能操作开发工程师根据错误码和波形就能定位不用再靠猜。1.2 分层设计为什么驱动、传输层、服务层必须分开我最早做这个项目时图省事把 UDS 服务处理直接写进了 CAN 接收中断里结果一个长报文多帧传输就把整个中断卡死低优先级报文全丢。后来老老实实按 AUTOSAR 的思路虽然没有直接上 AUTOSAR 全套但分层原则是借鉴的。整体分四层CAN 驱动层负责收发帧、波特率配置、滤波、错误管理ISO-TP 层负责单帧和多帧的重组拆分把任意长度诊断数据映射到 CAN 帧上UDS 服务层负责解析诊断请求、执行服务、生成响应应用层负责最终的数据读写和业务控制。层与层之间通过回调函数解耦CAN 接收到了诊断报文驱动层把帧抛给 ISO-TPISO-TP 攒够一个完整请求再抛给 UDSUDS 处理完结果再一层层封装回去。这样设计的好处实测下来有三点。一是排查问题快底层波形乱不影响上层逻辑协议交互异常直接抓 ISO-TP 层帧。二是可移植性强换 MCU 平台只改驱动层上层逻辑不用动。三是便于测试每一层都可以独立用 PCAN 或 CANoe 灌数据验证。很多自己写协议栈的团队前两点感受最深。1.3 技术选型与硬件环境控制器平台选的是 STM32F103内置 bxCAN 控制器配合 TJA1050 高速收发器。这套组合在车载后装和部分前装项目中非常常见资料多、工具链成熟、成本低特别适合做协议栈原型验证。不过要说明前装项目里 STM32F103 的 bxCAN 只有两个邮箱处理大量报文时容易瓶颈我用它主要是因为验证协议逻辑方便后续量产再往更高性能平台迁。工具链方面调试阶段用 USB-CAN 分析仪抓报文配合 CANScope 看波形。总线仿真和 UDS 交互测试用 CANoe虽然贵但做诊断自动化测试和一致性测试时确实省时间。示波器选 100MHz 以上带宽、至少两通道CAN 总线波形测量够用。开发环境就标准 Keil MDK代码分层之后编译很快。软件架构上驱动层用寄存器直操不用 HAL 库的复杂封装方便控制位时序细节。这个选择让后面排查采样点问题时省了很多力气因为位时序每个寄存器值都是可预期的。2. CAN 底层通信帧、仲裁与波特率2.1 CAN 数据帧结构与 ID 优先级CAN 2.0 数据帧的基本结构每一个做车载的工程师都得刻在脑子里。标准帧从 SOF 开始接着是仲裁场ID 加 RTR、控制场IDE、DLC、数据场、CRC、ACK 和 EOF。扩展帧只是把 ID 从 11 位扩到 29 位结构上多出 SRR、IDE、RTR 这些位。关键是理解仲裁机制多个节点同时发报文时CAN 总线的显性电平逻辑 0会覆盖隐性电平逻辑 1每个节点在发送 ID 的同时回读总线电平。如果发隐性位却读到了显性位说明有更高优先级的报文在抢总线当前节点立即停止发送转入接收模式。所以 CAN 的 ID 越小优先级越高。这个机制直接决定了 ID 分配策略。车身控制里制动、转向这类安全相关报文必须分配小 ID保证在总线拥塞时能抢到优先权。我见过有项目把空调控制报文排到 0x100 以内把底盘报文排到 0x700 以后结果一拥堵空调拼命抢总线底盘报文丢了还没人发现。这种低级错误就是没搞懂 CAN 仲裁的典型后果。2.2 位时间与采样点BS1、BS2、SJW 到底怎么配CAN 的波特率不能简单理解为每秒多少位它由位时间决定。一个位时间分成同步段、传播段、相位缓冲段 1、相位缓冲段 2 四段每段由若干时间量子 TQ 组成。TQ 来自 CAN 控制器时钟分频后的时基一个位时间的总 TQ 数决定波特率采样点位置则由相位缓冲段 1 和 2 的比例决定。STM32F103 上的配置里同步段固定为 1 TQ相位缓冲段 1 和 2 由 BS1、BS2 寄存器设置。以 500kbps 为例CAN 外设时钟 36MHz设 BRP 分频为 4则每个 TQ 为 111ns整位时间需要 18 个 TQ。我最终配的是 BS112、BS25、SJW1采样点位置在同步段加 BS1 之后也就是 (112)/18约 72%。这个采样点位置在 -40℃ 到 85℃ 的台架测试里表现稳定。SJW 很多人不重视它决定了控制器能容忍多大的时钟偏差和相位漂移用于重新同步。常规取 1 到 2 个 TQ 就够了取大了虽然容忍偏差能力强但容易把真正的电平跳变误判成时钟漂移反而引入噪声。只有总线非常长、节点时钟精度差时才需要适当增大 SJW。注意不同 MCU 的位时间寄存器命名不同有的把传播段单独列出来有的合并进 BS1。换平台移植时不能只看波特率相同就完事必须按位时间结构重新算采样点。2.3 验收滤波器的匹配逻辑ACCCode/ACCMask 的坑CAN 控制器基本都有硬件滤波功能用来决定哪些报文进入接收 FIFO哪些直接丢弃。这个功能在报文量大的时候特别有用能大幅降低 MCU 中断频率。老式 SJA1000 时代的叫法是 ACCCode 和 ACCMask。ACCCode 是期望接收的报文 ID 起始值ACCMask 是掩码。注意这个掩码语义和 STM32 正好相反SJA1000 里掩码位为 1 表示对应位必须匹配为 0 表示该位不关心而 STM32 的掩码模式下掩码位为 1 表示不关心为 0 表示必须匹配。很多从 SJA1000 迁移到 STM32 的工程师死在这上面。实际配置时我用的是 STM32 滤波器组 0掩码模式。比如要接收 0x123 和 0x120 两个基地址下的系列报文就把掩码低 8 位设为 0高 3 位设为 1只匹配 ID 高 3 位。这样既过滤掉了无关报文又保留了同一功能族的连续 ID。测试时发现如果在滤波器里不加接收所有报文的旁路模式调试初期很容易出现总线上明明有报文MCU 就是收不到的假死现象。所以调试初期我会把滤波器设为全部通过等协议验证通过后再逐步收紧。2.4 硬件接线与终端电阻第一拨踩坑现场CAN 物理层的坑十个有八个出在终端电阻上。高速 CAN 总线要求在总线最两端各接一个 120Ω 终端电阻两个并联后总线等效电阻约 60Ω。这个 60Ω 是判断总线连接是否标准的最快速手段整车断电状态下直接用万用表量 CAN_H 和 CAN_L 之间如果测到约 60Ω终端电阻没问题如果测到只有 120Ω说明总线只有一端接了电阻如果接近 0Ω多半有节点短路或者电阻焊错。收发器选型上TJA1050 这类高速收发器支持 1Mbps信号边沿比较陡。TJA1042 相比 1050 增加了待机模式和总线唤醒功能适合常电设备。还有一点收发器的共模电压范围要覆盖整车场景否则直接接在带干扰的 12V 电源系统里总线电平很容易被拉偏造成大量错误帧。实操心得我在三块板级联测试时出现过一次诡异现象——单独测试每块板都正常三块连一起就随机丢帧。最后用万用表一量总线电阻只有 30Ω原来三块板每块都焊了 120Ω 终端电阻三端并联等效只有 40Ω算上接触电阻更低收发器驱动能力被拖垮了。记住终端电阻只在总线物理两端放中间节点不接。3. CAN 波形分析如何判断总线通信质量3.1 波形上必须看的几个指标用示波器测 CAN 波形很多人只关心能不能看到方波其实信息远不止这些。CAN 总线在隐性状态时CAN_H 和 CAN_L 都处于 2.5V 附近差分电压约 0V显性状态时CAN_H 拉高到 3.5VCAN_L 拉低到 1.5V差分电压约 2V。从波形上要读的东西包括几个。位时间一个显性位加上后续隐性位的完整长度应该和配置的波特率严格对应500kbps 下位时间就是 2μs差 10% 以上就要怀疑时钟配置。采样点波形边沿跳变后等到采样点位置再看电平是否稳定这能间接判断当前节点的采样点配得对不对。边沿质量正常波形上升沿和下降沿都比较陡如果边沿坡度很缓说明总线负载过重、终端电阻异常或者收发器驱动能力不足。还有一个容易忽略的是波形上的振铃和过冲。振铃严重时在采样点位置可能读到错误电平导致偶发错误帧。这种情况在石油钻探、重型机械这类电磁环境恶劣的应用里尤其常见光靠软件滤波没用得从硬件层面处理。3.2 典型波形异常与成因CAN 波形异常我归纳下来主要有三类。第一类是电平整体偏低或偏高。比如隐形电平不在 2.5V 附近而是掉到 2V 以下往往是收发器电源不稳或者总线对地漏电。这种异常比较隐蔽报文偶尔还能通但一旦温度变化就出问题。第二类是边沿过缓。显性到隐性的跳变拉得很长像正弦波一样。常见原因是终端电阻阻值不对或者总线上节点电容过大。CAN 总线的分布电容是客观存在的节点越多线缆越长电容越大。这就是为什么总线设计规范里对单段线缆长度、节点数量有严格限制的原因。第三类是采样点附近出现毛刺。示波器上看一个位周期的后半段本来应该是稳定的隐性电平结果中间插进一个很窄的显性毛刺。这可能是电磁干扰耦合进来的也可能是某个节点发送了错误帧。抓波形时一定要开启示波器的单次触发模式把触发条件设为错误帧对应的电平跳变才能稳定抓到这种偶发问题。提示判断波形好不好别只看一帧。把示波器设置为余晖模式连续采集几百帧观察每个位跳变的抖动范围。如果跳变沿在几十 ns 的区间内抖动说明总线工作正常如果抖动范围达到几百 ns那就必须查终端电阻、总线拓扑和收发器了。3.3 用示波器和 CANScope 实测的操作步骤我在台架上测波形的基本步骤这里写出来供参考。第一步将示波器探头接在 CAN_H 和 CAN_L 之间用差分测量这样能直接看到 CAN 差分信号。如果没有差分探头就分别用两个通道测 CAN_H 和 CAN_L用数学通道做减法。第二步设置触发。触发源选差分通道触发电平设为 1V触发方式用单次因为总线空闲时是隐性电平持续低电平状态只有报文来了才有跳变。第三步调整时基。500kbps 下一个完整数据帧标准帧大概在 130μs 左右加上帧间隔用 50μs/div 比较合适。如果只关注单个位时间就把时基切到 500ns/div。第四步抓完波形后测量关键参数。用光标量位时间、用示波器的上升下降时间测量功能看边沿、观察余晖模式下的抖动范围。把这些参数和第二章提到的理论值对照基本能判断总线质量。CANScope 这种专业工具比示波器方便的地方在于它能直接解码 CAN 报文把波形和帧内容对应起来还能统计错误帧率、总线负载率。做一致性测试时CANScope 可以自动测量各个时段的长度并和标准比对省去很多手工读数的时间。如果项目预算允许建议作为常备工具。3.4 一致性测试中经常查的参数做过整车网络测试的应该知道CAN 总线一致性测试是每个 ECU 量产前必须过的关卡。参数包括位时间、采样点位置、发送起始位延时、输出信号边沿时间、循环延迟、帧间空间、错误帧恢复时间等。这里面最容易被设计坑到的就是采样点位置。整车厂对采样点通常有明确范围比如 70% 到 90%。不同 ECU 的采样点不一致短距离通信没感觉一旦总线长度拉长或者节点时钟漂移通信质量立刻下降。我做过一个项目两个供应商的 ECU 采样点一个设在 65%一个设在 80%在实验室 0.5 米线缆下完全正常装车后 5 米线缆就偶发错误帧。最后协调供应商把两个 ECU 的采样点都调到 75% 附近才稳定下来。另外发送起始位延时也要测。这个参数指的是总线从忙碌状态变空闲后节点经过多长时间开始发送报文。规范里有明确上下限测这个参数能确认节点的位定时恢复逻辑是否正常。4. UDS 诊断协议栈设计会话、服务与安全4.1 UDS 服务的整体框架UDSUnified Diagnostic Services定义在 ISO 14229 中是车载诊断的标准应用层协议。它的服务 ID 分门别类0x10 是诊断会话控制0x11 是 ECU 复位0x22 是按数据标识符读取数据0x2E 是写入数据0x27 是安全访问0x31 是例程控制0x19 是读取 DTC 信息0x14 是清除 DTC0x85 是控制 DTC 设置。设计协议栈时我没有把每个服务都做成独立的巨型函数而是按服务 ID 分发到一个统一处理框架。请求进来后先校验会话状态再校验子功能是否支持然后校验参数长度最后才执行具体服务。这个校验顺序很关键直接照 ISO 14229 的错误码优先级来做能避免很多协议交互歧义。一个标准的正响应格式是SID 0x40加服务附加数据。比如 0x10 01 请求进入默认会话正响应是 0x50 01。负响应统一是 0x7F后面跟请求的 SID 和 NRC 错误码。这套规则要刻在脑子里写协议栈和测试用例时都离不开。4.2 会话管理与子功能响应规则诊断会话控制是 UDS 最基础的服务。标准会话 0x01 是默认会话上电即处于该状态0x02 是编程会话用于 Bootloader 刷新0x03 是扩展会话大部分诊断服务需要在扩展会话下才能执行。会话管理里最核心的是一个超时机制。ISO 14229 规定如果 ECU 在 P2 时间内没有收到新的诊断请求就要回到默认会话。整车厂对 P2 的取值通常有要求一般是 25ms 到 50msP2* 是 5000ms 左右。这个超时在协议栈里必须做一个独立定时器不能靠应用层轮询。我用一个 1ms 的 tick 累积器超时时间到就自动把会话状态切回默认同时清掉安全解锁状态。子功能响应规则上0x10 服务带抑制正响应位bit 6如果置 1ECU 不能发正响应但可以发负响应。这个位在 0x11、0x31、0x85 里也有。实现时要注意如果请求的子功能带抑制位那么 ECU 虽然不发正响应但服务本身要执行。我在调试某次下线检测时遇到明明没收到正响应可设备却复位了的现象就是诊断仪发命令时抑制响应位被置位导致的。4.3 0x27 安全访问seed-key 的实现细节0x27 服务是 UDS 里面最容易出问题的一个因为它的安全机制和算法跟项目强绑定的。流程不复杂诊断仪发 0x27 01 请求种子ECU 返回种子诊断仪根据种子算 key发 0x27 02ECU 校验 key 匹配则解锁成功。实际开发中密钥算法是每个项目自己定的。常见的有查表法、循环移位、CRC 加盐、异或叠加。安全性要求高的项目会做成动态密钥每次 seed 都不一样防止重放攻击。我常做的一种是取 seed 的低 8 位和固定字节做异或再做三次查表置换最后和一个计数器相加。算法本身不复杂但要注意几个工程问题。第一安全状态要有重试次数限制通常连续失败 3 到 5 次ECU 必须锁定一段时间比如 10 秒期间 0x27 请求直接返回 0x36。这个次数限制在量产产线上很关键防止设备反复尝试导致 ECU 锁死。第二seed 的随机性要够如果每次都是固定序列等于没有安全防护。第三解锁状态和会话状态绑定离开扩展会话或复位后必须重新解锁。注意0x27 服务在 Bootloader 里的实现方式可能和 App 不同App 解锁成功后刷新程序到了 Bootloader 又要重新解锁。但 Bootloader 的 seed-key 算法通常和 App 共用一个模块避免两套代码维护两套密钥。开发时最好把算法单独抽出来App 和 Bootloader 共用编译。4.4 DTC 的编码与 0x19、0x14 服务实现DTCDiagnostic Trouble Code在 CAN 总线协议栈里以三字节形式存在高字节、中字节、低字节。整车厂 DTC 定义表里通常把第一字节对应 DTC 的 High Byte第二字节对应 Medium Byte第三字节对应 Low Byte但要注意和内燃机时代的五位 DTC 码的转换关系。例如常见 P 开头代码实际上要换算成十六进制三字节才能放到诊断帧里。0x19 服务是按子功能读取 DTC。0x01 是报告当前状态下的 DTC 数量0x02 是报告当前 DTC 的具体状态0x0A 是报告已确认的 DTC 及其快照。实现 0x19 时最难的是 DTC 状态掩码的管理。DTC 状态掩码有 8 个 bit比如 bit0 是 testFailedbit3 是 confirmedDTCbit6 是 testNotCompletedSinceLastClear。每次故障检测、修复确认、清除操作都要按规范更新这些位。我见过不少测试工程师对掩码含义不熟导致 0x19 02 请求返回的 DTC 状态和实际故障对不上最后发现是 ECU 里状态位更新逻辑写错了。0x14 清除 DTC 是产线和售后最常用的服务。标准中 0x14 报文的 DTC 三字节可以填 0xFFFFFF表示清除所有 DTC也可以只清除指定 DTC。实现时注意清除 DTC 不能简单地把存储清零要同时清除 DTC 状态的扩展数据记录、快照、老化计数。否则会出现 DTC 清了但快照还残留的脏数据问题。4.5 NRC 错误码响应设计NRCNegative Response Code是 UDS 诊断的拒绝理由。设计良好的 NRC 响应能让诊断仪和研发人员快速定位问题。常用 NRC 包括0x11 服务不支持、0x12 子功能不支持、0x13 报文长度错误或格式不对、0x22 条件不满足、0x24 请求顺序错误、0x31 请求超出范围、0x33 安全访问被拒绝、0x35 密钥不匹配、0x36 尝试次数超限、0x78 请求正在处理中。这里有个细节负响应的优先级要和状态机设计结合起来。比如当前处于默认会话时请求了扩展会话才能执行的服务应该返回 0x7F SID 0x7F0x7F 表示服务不被接收不对实际是 0x33 或 0x22 按场景。更常见的是当请求的服务当前不可用时返回 0x11当服务可用但参数超出范围时返回 0x31当安全未解锁时返回 0x33。顺序其实有讲究代码实现时用 if-else 串行判断先判断服务是否存在再判断子功能、长度、会话、安全状态、参数范围。0x78 响应pending 是另一个容易踩坑的点。当某个操作耗时超过 P2* 时ECU 必须先回一个 0x7F SID 0x78表示请求在处理中等处理完再发真正的正响应或负响应。注意 0x78 不是最终响应它只是占个坑。诊断仪收到 0x78 后要继续等最终响应如果一直等不到就按超时处理。写协议栈时0x78 之后的最终响应不能走常规的响应通道要由异步任务完成后主动发送这个流程在测试时很容易出问题。5. 打通上下层ISO-TP 传输层与诊断报文路由5.1 为什么一定要有 ISO-TP 这一层CAN 经典帧最多带 8 字节数据而一条 UDS 诊断请求或者响应往往超过 8 字节比如写 Bootloader 的一段数据可能是 4KB。ISO 15765-2通常说的 ISO-TP就是干这个的把超过 CAN 帧能力的长数据拆分到多个 CAN 帧里传输接收端再按顺序重组。很多初学者不理解为什么要单独分一层。其实就是一句话UDS 服务层只关心完整请求和完整响应它不关心这一段数据被拆成了 4 帧还是 40 帧。ISO-TP 层把分组和重组的工作自己消化掉UDS 层拿到的永远是完整数据。这个抽象带来的好处是上层服务逻辑不用考虑 CAN 帧边界写代码时清爽很多。ISO-TP 同时又定义了流控机制防止发送方发太快、接收方处理不过来。这对诊断刷写场景很重要——刷写时一包 2KB 的下载数据如果不能流控接收缓冲区一满就只能丢帧重传。5.2 单帧、首帧、连续帧、流控帧的格式细节ISO-TP 把数据分成四种帧类型。单帧用于传输不超过 7 字节的数据经典 CAN 下1 字节 PCI 加上最多 7 字节数据首帧是多帧传输的第一帧载荷最多 6 字节连续帧是后续的数据帧每个最多 7 字节流控帧由接收方发送用于告诉发送方可以继续发多少帧。PCI 字节的高半字节决定帧类型0x0 是单帧低 4 位是长度0x1 是首帧低 4 位是 12 位长度的最高位0x2 是连续帧低 4 位是帧序号0x3 是流控帧低 4 位是块大小BS。流控帧的 bit 4-5 是流控状态FS0 表示继续发送1 表示等待2 表示溢出。多帧传输的最大长度由首帧的 12 位长度决定也就是最多 4095 字节。经典 CAN 一帧 8 字节如果数据长度超过 7 字节且想在一条诊断消息里传完就必须走多帧。在经典 CAN 上超过 4095 字节的诊断消息是传不了的只能由应用层拆分。这也是 CAN FD 出现的原因之一——CAN FD 一帧最多 64 字节ISO-TP 在 CAN FD 上的长度字段也扩展了能支持更大的传输块。5.3 发送和接收状态机的实现ISO-TP 层最容易写崩的地方是状态机尤其是多帧传输过程中的超时和丢帧处理。发送端状态可以这样设计发送单帧时如果数据长度不超过 7 字节直接封装发送流程结束。发送多帧时先发首帧状态进入等待流控收到流控帧后根据 BS 块大小决定连续发送多少帧然后等流控如果 BS 是 0表示接收方不限制块大小发送方可以连续发送直到结束。发送过程中每次等待流控都要设置超时超时没收到必须重发首帧或者主动放弃。接收端状态相对简单空闲状态收到单帧直接转给上层收到首帧根据长度分配缓冲区状态进入等待连续帧等待连续帧时校验帧序号是否连续如果序号跳变说明中间丢帧丢弃整个消息并等待重传收满长度后组装完整数据状态回到空闲。帧序号是 4 位从 1 开始递增到 0xF 后回 1。这个序号循环很容易在写测试用例时被忽略我在自测时就遇到过序号从 0xF 回 1 时状态机报丢帧的假警。5.4 物理寻址、功能寻址与实际联调UDS 在 CAN 总线上有两种寻址方式。物理寻址是点对点通信诊断仪和某个 ECU 单独对话报文的 CAN ID 是固定的物理请求 ID 和物理响应 ID。功能寻址则是诊断仪向总线上所有 ECU 广播比如 0x7DF 就是标准的功能请求 ID所有支持诊断的 ECU 都会收到。物理寻址和功能寻址在实现上的差别主要体现在响应策略。功能寻址请求ECU 不能发正响应否则多个 ECU 同时响应会把总线塞爆。但负响应可以发因为负响应里带了 ECU 的源地址诊断仪能区分是谁拒绝的。实测中发现有些 ECU 在功能寻址时把负响应也屏蔽了导致诊断仪查不到任何反馈维护时很难定位。我的建议是功能寻址的负响应保留只屏蔽正响应这样可以方便排查无响应问题。联调阶段最常用的测试方式用 CANoe 模拟诊断仪向目标 ECU 发送 UDS 请求同时抓取总线上的报文和响应。联调时第一件事是核对源地址和目标地址。很多项目 ECU 的物理请求 ID 和物理响应 ID 是整车厂定义的如果响应 ID 写错诊断仪就会一直报请求超时。排查这个问题最快的方法是抓总线看 ECU 到底有没有在响应。6. 开发中的典型故障与排查记录6.1 整条总线不通从电阻到波特率的排查清单我在这个项目里经历最多的故障就是总线完全不通。排查顺序很重要乱试会浪费时间。第一步用万用表量 CAN_H 和 CAN_L 之间的电阻正常应在 60Ω 左右。如果测到 120Ω查是不是少了一个终端电阻如果接近 0Ω查有没有短路如果开路查 CAN 线是不是断在连接器上了。第二步上电后用示波器看波形总线空闲时 CAN_H 和 CAN_L 都应在 2.5V 附近如果某一根线不在这个范围查收发器供电和共模电平。第三步用 CAN 分析仪发标准报文看能不能收到 ACK 信号。CAN 报文发送时发送节点会在 ACK 槽等接收节点拉低电平如果波形上 ACK 是隐性说明总线上只有发送节点自己没有其他节点或者接收节点没正常工作。过了这三步基本能排除物理层问题。剩下最可能的是波特率或位时序配置不对。用示波器量一下位时间和配置值对照差 5% 以上就要复查时钟分频。6.2 偶发错误帧与 Bus-off收发异常的真实案例偶发错误帧是最难查的问题之一因为它不是必现的。我说一个真实案例项目里有一块控制器运行一两个小时后开始偶尔报发送超时重启后恢复。抓总线才发现问题不是发送超时而是控制器已经进入了 Bus-off 状态。CAN 控制器内部有发送错误计数器和接收错误计数器。发送出错一次计数器加 8接收出错加 1成功一次减 1。当发送错误计数超过 255控制器进入 Bus-off完全退出总线通信。问题在于我的代码里只检查了 CAN 发送邮箱的完成标志没有监听 Bus-off 中断所以控制器已经离线了应用层还傻傻等邮箱空闲一直等到超时。解决措施有两个层面软件上在驱动层挂接错误中断监控 TEC 和 Bus-off 状态一旦进入 Bus-off等待 128 次 11 位隐性位总线空闲条件后自动恢复并做错误计数上报硬件上查总线波形发现是某个节点的收发器在高温下边沿变缓导致采样点位置误判。后来把该节点的 SJW 从 1 调到 2问题改善明显。6.3 UDS 27 服务失败从响应码定位到密钥算法UDS 联调时 0x27 服务失败率最高。我碰到最多的是0x27 01 能拿到 seed但 0x27 02 返回 0x35 key 不匹配。这个错误码在后装 ECU 和诊断仪之间最常见的原因是两端没对算法。诊断仪厂商拿到的算法版本和 ECU 固件里烧录的版本不一致。排查方法是先看 seed 的分布。如果每次解锁请求的 seed 都一样那问题大概率出在 ECU 端没有实现随机种子生成或者算法里随机源没接对。如果 seed 每次都不同但 key 总校验失败要么是算法实现有差异字节序、位序、补位要么是 key 生成的输入参数里混进了时间戳等动态因子。第二个常见问题是重试锁定。产线测试时某个设备因为 key 计算错误连续尝试了 5 次ECU 进入 10 秒锁定后续测试用例全部返回 0x36。很多工程师以为是 ECU 挂了其实只是锁定期没过完。调试时可以在上位机里做一个自动等待锁定超时的功能或者在开发阶段把锁定时间临时调到 2 秒方便反复验证解锁流程。6.4 测试验证清单与故障速查表开发完成后我用下面的清单做验收测试形成一张速查表方便排查。故障现象优先检查项常见原因总线完全无波形万用表量 CAN_H/CAN_L 电阻终端电阻缺失或线缆断开诊断仪请求超时抓总线看响应 ID物理响应 ID 地址配置错误报文发送失败查看错误寄存器发送超时未处理、Bus-off 状态偶发错误帧示波器看边沿和采样点采样点偏移、终端电阻异常UDS 返回 0x31检查数据参数范围DID 不存在或参数越界UDS 返回 0x33确认会话和安全状态未进入扩展会话或未解锁UDS 返回 0x7F SID 0x78 后无响应检查异步任务最终响应发送逻辑丢失测试时我用 CANoe 跑了两套脚本一套是 ISO 14229 定义的诊断服务测试用例覆盖每个服务的正响应和常见负响应另一套是故障注入测试在总线上人为插入错误帧、断开节点、降低供电电压观察 ECU 是否会出现永久性 Bus-off。这两套跑下来协议栈的鲁棒性基本能立住。我个人在实际操作中的体会是CAN 和 UDS 联调项目一半的时间花在排查物理层和传输层真正写 UDS 服务逻辑的时间反而是少数。协议栈的分层、滤波器的配置、位时序的计算这些底层功底决定了上层诊断功能能不能稳定运行。做完这个项目后再回头看最值得投入的是状态机的设计——不管是 CAN 驱动的错误处理状态机还是 ISO-TP 的多帧传输状态机只要状态机逻辑清晰测试时遇到问题基本看一眼日志就能定位。最后再分享一个后续扩展的方向。经典 CAN 在诊断刷写时 8 字节载荷的瓶颈很明显CAN FD 用 64 字节数据场把刷写效率提升了一个量级。ISO-TP 在 CAN FD 上的实现除了帧格式和数据场长度的差异整体分层思路和状态机设计和经典 CAN 完全一致。在这个项目基础上往 CAN FD 迁移工作量主要在驱动层适配上层协议栈不需要动。车载以太网和 DoIP 也是同样道理分层和状态机的思想完全通用只是载体变了。先把 CAN 和 UDS 这一套基本功打牢后面新总线协议上手会快很多。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进