
车载底层 CAN 通信与上层 UDS 诊断协议开发技术文档做车载软件开发这几年我越来越觉得 CANController Area Network控制器局域网和 UDSUnified Diagnostic Services统一诊断服务这两块内容是所有想入行或者正在做车载测试、嵌入式开发的人必须迈过去的两道坎。你光会点单片机、会点 C 语言在车载领域里其实很难走远因为车上几乎所有 ECUElectronic Control Unit电子控制单元之间的信息交互都跑在 CAN 总线上而整车下线检测、售后故障排查又基本全依赖 UDS 诊断协议。这篇文章我就围绕“车载底层 CAN 通信与上层 UDS 诊断协议开发”这个主题把我从实际项目里踩过的坑、用过的工具、总结出的方法论完整地分享出来不整虚的。这篇文章适合三类人看一是刚转行做车载测试或者嵌入式软件开发的新人你需要搞懂 CAN 和 UDS 到底是怎么工作的二是已经在做 CAN 通信、但想往诊断方向深入一点的工程师本文能帮你把 UDS 那套服务流程理顺三是准备面试车载相关岗位的朋友里面不少内容都是高频考点比如 CAN 时钟误差怎么解决、UDS 19 服务 01 子功能怎么用、27 服务种子密钥流程等等我尽量用大白话讲清楚。1. 项目背景与整体架构设计思路1.1 为什么车载开发离不开 CAN 和 UDS 这条技术链路先聊聊背景。现代汽车里面 ECU 数量早就不是几十个的概念了稍微智能一点的车型整车 ECU 数量能突破一百个。每个 ECU 都要跟其他 ECU 通信比如 BMSBattery Management System电池管理系统要把电池电压、温度、SOC 发给 VCUVehicle Control Unit整车控制器VCU 又要把扭矩请求发给 MCUMotor Control Unit电机控制器。这么多控制器之间如果全用点对点的线束连接整车线束会重得离谱、成本也压不住所以必须有一条共享的通信总线。CAN 总线就是干这个事的。CAN 总线是 Bosch 公司在 1986 年提出的一种串行通信协议最初就是为了解决汽车内部大量控制单元之间的数据交换问题。它用两条差分信号线CAN_H 和 CAN_L就能挂载几十个节点抗干扰能力强、实时性好、成本低。直到现在CAN 依然是车载网络里最主流的底层通信方式。但光是能通信还不够。车用着用着可能出毛病售后技师得知道是哪个 ECU 报的故障、能不能读到实时数据、要不要刷写新程序。这时候就需要一套统一的上层诊断协议让外部诊断仪能跟任意一个 ECU 对话。UDS 就是这套标准它定义在 ISO 14229 里跑在 CAN 总线之上通过 CAN 帧把诊断请求发出去ECU 再把诊断响应发回来。所以你会发现CAN 是“路”UDS 是“跑在路上的车”两者是一个底一个上的关系做车载开发必须把这条链路从头到尾打通。1.2 项目技术架构的分层设计方法这个项目从架构上可以分成三层来看物理层与数据链路层对应 CAN 控制器和收发器负责电平转换、帧收发、错误检测、仲裁等开发时主要关心波特率配置、终端电阻、采样点这些参数。传输层与会话层对应 ISO 15765-2通常叫 TP 层Transport Protocol 传输协议负责把超长的诊断数据拆分到多个 CAN 帧里传输并在接收端重组。应用层对应 UDS 协议本身处理各种诊断服务的请求和响应包括会话控制、安全访问、数据读取、故障码操作、例程控制等。我们在实际开发中通常也会按这个分层来组织代码底层驱动CAN Driver 传输协议层CAN TP 诊断应用层UDS Stack。这样做的好处是模块间解耦底层换芯片换了不影响上层逻辑上层增加诊断服务也不用改动传输和驱动代码。我记得第一个完整的项目里我们就直接在 NXP 的 S32K 系列芯片上做这套东西。S32K 的 FlexCAN 模块用来收发 CAN 帧上层配一个 CAN TP 模块再往上是自己写的诊断服务处理逻辑。整个架构理顺之后后续接什么新项目都很快基本就是修改服务表和路由表的事。1.3 关键技术点选型与整体开发流程开发流程上这个项目大体分为需求分析、AUTOSAR 与非 AUTOSAR 方案选型、协议栈开发与集成、台架/实车测试几个阶段。方案选型这一步很多团队容易忽略细节。如果你的项目是基于 AUTOSARAutomotive Open System Architecture汽车开放系统架构的那 CAN Driver、CanIf、CanTp、Dcm 这些模块都是现成的你主要做配置和集成如果是不带 AUTOSAR 的裸机开发或者普通 RTOS 环境就得自己实现或者移植一套协议栈。我自己的经验是小项目或者学习阶段优先考虑自己写或者用开源的小型协议栈省去 AUTOSAR 工具链那套复杂流程但是量产项目除非团队里有非常熟悉协议栈的专家否则还是走 AUTOSAR 成熟方案更稳因为诊断协议栈的边界情况太多了自己写很容易漏。这个项目里核心的关键技术点有这么几个CAN 波特率与采样点计算、CAN 时钟误差容错处理、DBC 报文矩阵设计与解析、UDS 服务分发与子功能处理、ISO 15765-2 传输层分包重组、安全访问种子密钥算法、故障码 DTC 的读写清除逻辑。每一个点我都会在后面的章节里展开讲。2. 底层 CAN 通信核心技术解析2.1 CAN 报文结构与总线仲裁机制先过一遍 CAN 报文的组成。标准 CAN 2.0A 的数据帧由帧起始SOF、仲裁域Identifier 标识符 RTR 远程发送请求位、控制域IDE、DLC 数据长度代码、数据域0~8 字节、CRC 校验域、ACK 确认位和帧结束组成。开发时最常打交道的是仲裁域里的 ID这个 ID 不只是报文的标识还决定了总线访问优先级。CAN 总线是载波监听多路访问/冲突避免CSMA/CA机制多个节点同时发报文时靠仲裁段逐位比较显性位逻辑 0会覆盖隐性位逻辑 1所以 ID 数值越小的报文优先级越高。比如动力系统的报文一般会给比舒适系统更小的 ID保证优先传输。我在做 DBCCAN database 数据库文件解析工具的时候经常用 python-can 库来读报文再结合 cantools 把 DBC 文件转成解析代码。这里提一句DBC 文件里每个信号定义了起始位、长度、字节序Intel 还是 Motorola、缩放因子和偏移量解析时最容易搞错的是 Motorola 格式也叫大端格式的位序换算很多新手在这儿栽跟头。我自己的办法是写完解析代码后先用已知报文回灌验证不要指望一把过。2.2 CAN 时钟误差的成因与重同步机制做 CAN 底层驱动或者调试总线故障时时钟误差是一个绕不开的问题。每个 CAN 节点都有自己的晶振晶振本身有精度误差同时温度变化、老化也会导致时钟漂移。当总线上某个节点的时钟频率和主节点偏差超过一定范围就会导致采样点偏移进而产生位错误、CRC 错误严重时整个网络都通信异常。CAN 协议为了解决这个问题设计了两类同步机制硬同步和重同步。硬同步发生在帧起始的 SOF 位所有节点重新开始位定时重同步发生在帧内遇到隐性到显性的跳变沿时每个节点根据自己的相位误差调整采样点位置。CAN 控制器通过同步跳转宽度SJWSynchronization Jump Width来限制每次重同步的最大调整量。项目里有一次现象非常典型某块板子单独测 CAN 收发正常但挂到台架上就疯狂报 BUS OFF。排查下来发现是板载晶振负载电容匹配不对导致实际频率偏差超过 1.5%超出了 CAN 控制器容忍范围。后来把晶振换成精度更高的有源晶振问题就消失了。所以选型时不要贪便宜用普通陶瓷谐振器一定要用晶振并且按芯片手册要求配置正确的负载电容。另外在软件里尽量把采样点配置在位的 75%~80% 位置这个区间对各种干扰的容错能力最好。2.3 CAN 波特率配置与采样点计算波特率配置本质上就是给位时间分段。CAN 的一个位时间bit time被分成四段同步段SS固定 1 个时间份额、传播时间段PTS、相位缓冲段 1PBS1、相位缓冲段 2PBS2。采样点就是 PBS1 结束的位置。计算过程举个例子。假设芯片外设时钟频率 40 MHz需要配置 500 kbps 波特率那每个位的时间就是 2 us时间份额TqTime Quantum可以做如下分配Prescaler 分频值取 4则 Tq 4 / 40 MHz 0.1 us一个位由 20 个 Tq 组成。把同步段设为 1 TqPTS 设为 7 TqPBS1 设为 8 TqPBS2 设为 4 Tq那么采样点 (1 7 8) / 20 80%。这样配置可行。反过来如果已知目标采样点比例也能反推各段长度。实际操作里我用 S32K 的 FlexCAN 或者 STM32 的 bxCAN 时都会先用逻辑分析仪抓一下实际波形测量显性位宽度看波特率和采样点对不对。不要完全信任代码注释里的计算结果因为芯片参考手册里对传播时间段和相位缓冲段的命名可能不同一个字母没对上结果就完全不同。2.4 CAN FD 与车载以太网对底层设计的补充影响现在的新车型越来越多地引入 CAN FDCAN with Flexible Data-rate和车载以太网。CAN FD 相比经典 CAN数据段波特率可以更高通常 2 Mbps 甚至 5 Mbps一帧最多能带 64 字节数据对诊断刷写这种大块数据传输非常友好。做底层开发时如果工程里同时用到经典 CAN 和 CAN FD要注意控制器是否支持混合模式以及终端电阻匹配在高速率下是否有更高的要求。CAN FD 的仲裁段和数据段波特率可以不同这意味着采样点配置要分别计算。另外CAN FD 在总线错误处理上有个显著区别经典 CAN 发错时所有节点都会响应错误帧而 CAN FD 只有错误节点自己发错误标志这部分在实际调试时容易被表象误导需要特别留意。如果项目还涉及 UDS over Ethernet通常叫 DoIPDiagnostic over Internet Protocol基于 IP 的诊断协议那底层就完全变了不再是 CAN 帧而是一整个 TCP/IP 协议栈。DoIP 的优势是带宽大适合远程诊断和 OTA 刷写但它在安全接入、端口管理上比 CAN 诊断复杂不少。后续如果做跨域控制器开发比如智能座舱或自动驾驶域控DoIP 几乎是必选项这一点可以在第 7 节展开讲。3. CAN 通信开发实操与驱动实现要点3.1 CAN 控制器初始化与收发流程我先讲一下最基础的 CAN 驱动初始化步骤以 STM32 的 bxCAN 为例使能相关时钟GPIO 时钟和 CAN 外设时钟。配置 GPIO 复用引脚CAN_RX 和 CAN_TX 引脚要配置为复用功能。配置 CAN 工作模式正常模式或环回模式。调试初期建议先用环回模式Loopback不需要外部节点就能确认控制器本身收发正常。设置波特率和位时间参数。配置过滤器经典 CAN 的报文过滤在硬件层做可以按 ID 范围过滤也可以按掩码匹配。如果没有正确配置过滤器会导致收不到任何报文这是新手最容易犯的错。使能中断发送中断、接收中断、错误中断、总线关闭中断分别处理。发送流程应用层把一帧数据打包成 CAN_TxHeaderTypeDef然后调用 HAL_CAN_AddTxMessage等待发送完成中断后在回调里释放发送缓冲。接收流程CAN 控制器收到有效帧后触发接收中断在 HAL_CAN_RxFifo0MsgPendingCallback 中调用 HAL_CAN_GetRxMessage 把数据取出来再交给上层协议栈。项目中比较容易被忽略的一点是CAN 控制器的发送邮箱数量是有限的bxCAN 是 3 个邮箱如果上层短时间内投递大量报文发送邮箱会满此时调用 AddTxMessage 会返回错误。正确的做法是做一个发送队列把待发送报文缓存到内存里然后在发送完成中断里逐个补发。我见过有人图省事不写队列结果总线负载一高诊断请求周期性丢失排查了半天。3.2 基于 S32K FlexCAN 的工程实践S32K 的 FlexCAN 跟 STM32 的 bxCAN 差别不小。FlexCAN 使用的是消息缓冲区MBMessage Buffer机制每个 MB 可以独立配置成发送或接收缓冲区数量多、灵活性高。开发时你需要初始化 FlexCAN 模块选择时钟源、设置分频、设置位时间。分配 MB比如 MB0~MB7 用于接收MB8~MB15 用于发送。配置中断每个 MB 都有独立中断也可以通过 FIFO 模式接收。使能 FlexCAN 的协议引擎启动模块。我在实际项目里习惯把 FlexCAN 的接收 FIFO 打开。接收 FIFO 的好处是硬件自动缓冲收到的报文即使应用层暂时来不及处理也不容易丢帧。但要注意过滤表还是要配否则 FIFO 缓存里全是无关报文很快就会被塞满。S32K 的时钟配置也比较讲究。FlexCAN 模块时钟可以来自 SIRCSlow Internal RC 慢速内部时钟、FIRC、SOSCSystem Oscillator 系统振荡器或者 PLL不同时钟源在 CAN 波特率配置时会得到不同的 Prescaler 范围。开发时我建议先从 SOSC 这种稳定性高的时钟源开始确认整车网络通信正常后再去优化功耗相关的时钟切换不要一上来就搞复杂的时钟树。3.3 DBC 报文解析与波形验证方法整车开发中每个 ECU 的 CAN 报文信号定义都会维护在 DBC 文件里。DBC 可以理解为 CAN 通信的“数据库”里面描述了每条报文的周期、ID、数据字节、信号布局。常用工具除了 Vector CANdb还可以用开源的 cantools 库。用 cantools 解析 DBC 报文很简单import cantools import can db cantools.database.load_file(vehicle.dbc) bus can.interface.Bus(channelcan0, bustypesocketcan) for msg in bus: if msg.arbitration_id db.get_message_by_name(VCU_Status).frame_id: data db.decode_message(msg.arbitration_id, msg.data) print(data)代码里 db.decode_message 返回一个字典键是信号名比如 vcu_speed、soc 等。开发前期我强烈建议准备一套可复现的测试脚本每收到一帧报文就打印再跟 DBC 信号的值人工比对确认解析正确。波形验证方面示波器或逻辑分析仪是必需品。我调试波特率时经常用逻辑分析仪抓 CAN_H 与 CAN_L 的差分波形测量最短显性位的时间。如果时钟配置正确最短显性位宽度应该正好等于 1/fdfd 为数据段波特率。如果测出来偏宽或偏窄就能反过来校准 Prescaler 和位时间参数。这个方法在任何一款 MCU 上都通用不用依赖厂商调试工具。4. 上层 UDS 诊断协议开发与实现细节4.1 UDS 协议栈框架与寻址模式UDS 是 ISO 14229 定义的应用层诊断协议。它跑在不同传输协议之上CAN、LIN、以太网等但在 CAN 上最常用的载体是 ISO 15765-2CAN TP。UDS 的核心思路是“请求-响应”外部诊断仪发出请求ECU 执行后返回响应。诊断仪和 ECU 之间是一问一答的模式。寻址方式分为物理寻址和功能寻址。物理寻址是点对点通信请求帧的目标地址是某个具体 ECU 的诊断地址只有这个 ECU 响应功能寻址是广播式的比如给所有 ECU 发“进入扩展会话”的请求ID 通常是 0x7DF所有支持该服务的 ECU 都会响应。这个差异在开发测试中时要特别注意用功能寻址发“读取 DID”请求会收到多个 ECU 的响应数据解析不能只按单 ECU 处理。从开发实现角度看UDS 协议栈可以拆成三层CAN 驱动负责收发原始帧CAN TP 层负责处理长报文的分包传输将 4095 字节的最大诊断消息分成多个单帧/连续帧并在接收端重组UDS 层负责解析请求报文里的 SIDService Identifier 服务标识符和子功能调用对应的处理函数再组响应报文。4.2 UDS 常用服务功能拆解19 27 31 34 36 37开发诊断功能时最常用到的 UDS 服务是以下几个诊断会话控制0x10包括默认会话、编程会话和扩展会话。很多诊断服务只在非默认会话下才能执行所以收到请求后先切会话是标准操作。比如 0x10 03 进入扩展会话ECU 如果支持则回 0x50 03。安全访问0x27涉及种子和密钥的交换用于保护写入类操作。先发请求种子比如 0x27 01ECU 返回一个种子值诊断仪用特定算法计算密钥0x27 02发回ECU 校验一致后解锁。注意安全访问有失败计数器和延迟时间连续输错密钥会锁死一段时间。例程控制0x31执行 ECU 内部特定程序比如擦除 Flash、检查通信、学习转向角传感器等。典型流程是 0x31 01 开始例程、0x31 02 停止例程、0x31 03 查询例程结果。例程编号通常是 2 字节或 3 字节的 DID 地址。请求下载0x34、传输数据0x36、请求退出传输0x37这三个服务合起来构成完整的 Flash 刷写流程。先通过 0x34 告诉 ECU 要下载的数据长度和内存地址ECU 回一块最大传输字节数比如 64 字节之后诊断仪分成多个 0x36 请求把数据块发过去最后用 0x37 结束传输。读取 DID0x22和写入 DID0x2E用来读写 ECU 内部数据标识符Data Identifier比如 VIN、软硬件版本号、标定参数、采集的电压温度等。读取 DID 是售后诊断里最常用的服务没有之一。读取/清除故障码0x19、0x140x19 有很多子功能其中 01 表示按状态掩码读取故障码02 表示读取特定故障码的快照数据04 表示读取故障码扩展数据。清除故障码用 0x14一般格式是 0x14 FF FF FF FF 清除所有 DTC。实际项目里UDS 服务的返回码NRCNegative Response Code要格外重视。ECU 不支持请求的服务时要返回 0x7F SID NRC。常见的 NRC 有 0x10一般拒绝、0x11服务不支持、0x12子功能不支持、0x13报文长度或格式错误、0x22条件不满足、0x31请求超出范围、0x33安全访问被拒绝、0x78请求正在处理需要诊断仪等待。在协议栈里我建议把 NRC 处理做成一张映射表方便上层直接查。4.3 会话管理、安全访问与 DTC 管理的核心流程UDS 的状态管理是整个诊断逻辑里最容易出问题的地方。每个 ECU 必须知道自己当前处在什么会话里因为不同会话能执行的服务集合不同。默认会话0x01下只能执行基本读操作扩展会话0x03下能执行写操作和例程控制编程会话0x02下主要执行刷写相关服务。会话还有超时机制一般在非默认会话停留超过一定时间通常是 5 秒ECU 要自动回到默认会话。这个超时时间在实车上往往被测试团队专门拿出来考协议栈里一定要做成可配置的参数。DTCDiagnostic Trouble Code 诊断故障码的管理是我建议单独写成一个模块的。DTC 的状态信息不是简单的 0/1而是由多个状态位构成比如当前存在、历史存在、当前失败、上次失败、测试未完成等0x19 服务可以按不同子功能读取。开发时我习惯用一组 bit 掩码表示每个 DTC 的状态需要上报时再按 UDS 要求的格式填充成响应报文。清除 DTC 时要考虑条件——很多 ECU 只有在特定条件下才允许清除比如车速为 0、点火开关 ON如果条件不满足要返回 0x22 条件不满足。4.4 基于 CAN TP 的 UDS 报文分包传输原理当 UDS 报文超过 8 字节时就需要 CAN TP 来分包。ISO 15765-2 定义了四种帧类型单帧SFSingle Frame数据长度 7 字节如果使用扩展地址则更少直接一帧发完。首帧FFFirst Frame数据长度超过 7 字节时发送第一帧包含 12 位总长度信息。连续帧CFContinuous Frame后续数据帧每帧最多 7 字节数据带序列号。流控帧FCFlow Control接收方告诉发送方每次可以连续发多少个连续帧以及两个连续帧之间的最小间隔时间。实际刷写 ECU 时一次请求下载的数据大小可能到 1 MB分包和重组都是在 CAN 驱动之上自动完成的。我在实现 CAN TP 模块时踩过一个比较典型的坑接收方向发送方发完流控帧后如果连续帧之间的间隔控制得不够有些 CAN 控制器在 FIFO 溢出的情况下会静默丢帧而上层还在干等重组完成最后导致整个诊断会话超时。解决办法是增加 CAN 接收 FIFO 深度并且在应用层做超时重传机制不要完全依赖 CAN 控制器的错误处理。5. 上层诊断与底层 CAN 的衔接方式5.1 从 UDS 服务到 CAN 帧的完整数据流我画一下我自己平时脑中的完整数据流诊断仪发送一个 0x22 F1 90 的请求要读取 VIN 码的前几个字节。这个请求先经过 UDS 层封装成诊断消息包含 SID 和参数CAN TP 层根据长度决定用单帧还是多帧发送最终交给 CAN 驱动打包成 CAN 报文发出。ECU 收到这个报文后CAN 驱动先确认收帧CAN TP 层把分片重组UDS 层解析出 SID0x22、DID0xF190查表找到 VIN 码存储位置将数据组装成响应报文再按照 CAN TP 的分包规则逐帧发回给诊断仪。数据流向说起来简单但真在代码里实现时最麻烦的是异步并发。比如 ECU 正在执行一个耗时的例程控制期间又收到一个读取 DID 的请求协议栈必须保证不能因为正在响应上一个请求就忽略新请求。我的做法是让诊断模块维护一个简单状态机空闲状态、等待 CAN TP 发送完成状态、等待应用处理完成状态。只有状态机处于空闲时新请求才会被接受否则适当地返回 0x78 请求处理中让诊断仪稍后重试。5.2 诊断报文与底层驱动的接口设计接口设计的核心是解耦。我在代码里定义了一组操作函数指针比如CAN_Transmit(uint32_t id, uint8_t* data, uint8_t len)CAN_Receive(uint32_t* id, uint8_t* data, uint8_t* len)CANTP_Send(uint8_t* data, uint16_t len)CANTP_Receive(uint8_t* data, uint16_t* len)所有上层 UDS 逻辑只跟 CANTP_Send/CANTP_Receive 打交道不直接操作 CAN 驱动。这样做的好处是后续要从 CAN 切到 LIN 或者以太网只需要替换底层传输接口的实现UDS 层代码完全不用变动。我还习惯在接口层加一个环形缓冲区存放接收到的诊断报文。CAN 接收中断里只负责把数据拷贝进环形缓冲区然后置一个事件标志主循环里检测到事件后调用 CAN TP 和 UDS 流程处理。这种方式比直接在中断里做完整协议解析要安全得多不会因为中断耗时过长导致丢帧。5.3 上层应用如何正确映射诊断结果UDS 服务执行后结果不只是 0x00成功或 NRC还可能携带大量数据。比如 0x19 02 读取 DTC 快照响应里会包含 DTC 编号、DTC 状态、快照记录编号、DID 列表和数据。上层应用比如仪表盘上的故障灯逻辑不能只是简单判断“有故障码”还必须正确解析 DTC 的状态位区分“当前故障”和“历史故障”。我见过一个低级但不罕见的 bug上层代码把 0x19 01 响应的 DTC 状态字节直接当作布尔值导致只要历史故障存在故障灯就常亮不灭。正确的做法是按位解析状态比如 bit01 表示测试失败当前故障bit41 表示历史存在但不一定是当前故障。这些细节做不到位排查问题会浪费大量时间。6. 开发工具链与实测环境搭建6.1 常用的 CAN 上位机工具与总线分析仪开发 CAN 通信和 UDS 诊断手里没有几把趁手的工具不行。我常用的是CANoeVector功能最全支持 CAPL 脚本、DBC 加载、UDS 诊断控制是车厂和 Tier1 的标配工具。缺点就是贵个人学习不建议直接上。PCAN-View / PCAN-ExplorerPEAK便宜实用适合日常看报文和简单诊断。CANable / 兼容虚拟串口的 USB-CAN 工具适合个人开发开源的 cantools Python 就能配合做自动化测试。周立功 ZCANPRO国产工具里不错的界面顺手支持 CAN FD。如果你是刚入门我建议直接从 PCAN 或者国产 USB-CAN 开始配合 Wireshark 的 CAN 解析插件能直观看到每一帧的 ID、长度、数据还能统计总线负载率和错误帧。等需要做复杂仿真和自动化测试时再考虑上 CANoe。6.2 搭建一套可复现的 UDS 自动化测试脚本自动化测试是保证诊断协议栈质量的关键。我自己习惯用 Python 写一套轻量级测试框架核心代码类似import can import cantools import uds bus can.interface.Bus(channelPCAN_USBBUS1, bustypepcan) ecu uds.Client(bus, request_id0x7E0, response_id0x7E8) # 进入扩展会话 resp ecu.request(0x10, 0x03) assert resp.sid 0x50 and resp.data[0] 0x03 # 读取 VIN resp ecu.request(0x22, 0xF1, 0x90) print(resp) # 安全访问示例 seed ecu.request(0x27, 0x01).data[0] key calculate_key(seed) resp ecu.request(0x27, 0x02, key) assert resp.sid 0x67这套脚本跑起来后基本能在一个晚上把常用的 UDS 服务全部回归一遍。我特别建议在自动化脚本里加随机性测试随机间隔发送诊断请求、随机切换会话、随机输入非法参数很多协议栈的隐藏问题就是在这种压力测试下暴露出来的。6.3 台架与实车联调时的注意事项台架测试时要确保 CAN 网络里没有其他 ECU 干扰最好用独立电源避免地电位差异影响通信。实车联调时需要注意的点更多一定要先确认整车上电状态不要让诊断仪误触发了高压部件相关例程另外实车 CAN 网络负载率高诊断响应时间往往会比台架慢脚本里的超时时间不能设得太短。有一次我在实车上做刷写测试刷到一半总线出现大量错误帧后来发现是 OBD 口上同时挂了好几台诊断设备节点太多终端电阻被破坏导致了反射。后来规定测试时只允许挂一个诊断仪其他设备全部拔掉问题就没了。这个教训让我记住了一件事现场排查 CAN 问题第一步永远先检查物理连接再看波形最后才怀疑软件协议栈。7. 常见问题与排查技巧实录7.1 CAN 总线 BUS OFF 与错误帧的定位思路CAN 总线出现 BUS OFF 时节点会暂时离开总线不再参与通信。排查思路我一般按下面的顺序用总线分析仪看错误帧的类型是位错误、填充错误、CRC 错误还是 ACK 错误。检查波特率是否一致最快速的方法是抓波形测一个位的宽度对比各节点配置。检查终端电阻总线上应该在两端各有一个 120 欧的电阻万用表测 CAN_H 和 CAN_L 之间的电阻应该约 60 欧。检查是否有节点在总线空闲时主动发送数据导致持续冲突。错误帧里最容易忽略的是 ACK 错误。CAN 帧在发送完成后发送节点期望至少有一个其他节点回 ACK 显性位。如果总线上只有发送节点一个节点或者接收节点的控制器没有配置成监听模式发送就会一直报 ACK 错误。这在单节点台架测试时非常常见但不代表协议栈有问题属于测试环境配置问题。7.2 诊断超时与无响应的常用排查路径UDS 请求发出去ECU 一直没响应。我排查时第一步不是看协议栈而是先看 CAN 层诊断仪能不能看到自己发的报文ECU 有没有收到如果 CAN 层有收发再确认响应 ID 是否和 DBC 里一致。很多新开发的项目ECU 的物理寻址请求 ID 和响应 ID 配置错了比如请求 ID 是 0x7E0响应用了 0x7E8结果诊断仪在 0x7E9 上等必然无响应。如果 CAN 层正常再看会话状态。ECU 可能还停留在默认会话而请求的服务只能在扩展会话下执行此时 ECU 会回 NRC 0x22 或直接无响应。此时手动先发一个 0x10 03 进入扩展会话再重新发原请求往往问题就解决了。最后再看安全访问状态写操作被拒绝时先执行 0x27 流程再重试。7.3 UDS 协议栈状态机常见状态卡死问题说一个我在自己项目里修过的问题。ECU 在执行 0x31 例程控制时如果例程内部因为硬件等待卡住UDS 状态机一直停留在“正在处理例程”状态之后诊断仪发任何服务都不响应。后来我在例程执行逻辑里加了一个看门狗计数器超过 3 秒强制返回 NRC 0x10 并恢复空闲状态这个问题才算解决。协议栈状态机设计的时候一定要考虑所有可能卡死的路径。比如正在等待 CAN TP 发送完成时如果 CAN 控制器因为总线错误关闭了状态机会永远等不到发送完成标志。这时候需要一个全局超时超时后强制复位传输层状态。我在代码里专门加了一个诊断模块的 tick 函数每 10 ms 调用一次负责检查各种超时条件。这个设计虽然简单但在项目里救了很多次场。7.4 常见问题速查表问题现象可能原因快速排查方法总线上收不到任何报文MCU 引脚复用不对检查 GPIO 配置和 CAN TX/RX 波形偶发丢帧总线错误率高采样点配置不匹配抓波形计算位宽调整位时间参数UDS 请求无响应请求 ID / 响应 ID 不一致核对 DBC 和诊断配置服务报 NRC 0x31请求参数超出范围检查 DID、例程编号、数据长度清除 DTC 失败清除条件不满足检查整车状态条件比如车速、挡位刷写中途失败0x36 块大小或序列号错误核对最大传输字节和块序号安全访问一直被拒种子密钥算法不匹配从种子生成模块排查确认 key 长度和字节序8. 后续扩展与接口趋势分析8.1 从 CAN 诊断到 DoIP 的架构演进现在很多新车型开始引入 DoIP主要是因为软件刷写的包越来越大传统 CAN 刷写一个控制器可能要几十分钟而以太网只需几分钟。DoIP 的协议栈跟 CAN 时代的思路完全不同它基于 TCP/IP诊断报文使用 ISO 13400 封装逻辑上更接近网络开发。如果之前只做过 CAN 上的 UDS转向 DoIP 时要重点理解三个概念DoIP 实体通过 UDP 广播做车辆发现Vehicle Announcement 车辆公告TCP 建立可靠的诊断连接每个诊断请求响应通过 payload type 区分不同的消息类型。物理和逻辑上DoIP 的诊断仪要先拿到车的 IP 地址再发起 TCP 连接连接建立后才能发送 UDS 数据。8.2 CAN FD 和以太网对上层诊断协议的支撑变化CAN FD 对 UDS 最大的帮助就是单帧能带的字节数变多了。经典 CAN 一次只能传 8 字节很多 UDS 响应要拆好几帧CAN FD 一帧能到 64 字节0x22 读取大块 DID 的响应很多时候一帧就搞定了。但要注意UDS over CAN FD 的地址格式和 N_PDU 结构与经典 CAN 并不完全一致传输层在数据长度对齐上有区别实现时不能直接把经典 CAN TP 代码拿来改个名字就用。以太网场景下UDS 可以走 TCP/IP使用 DoIP 封装诊断请求和响应的体量不再受 CAN 帧长限制但仍然要遵循 UDS 的请求响应模型。对于刷写类服务0x34/0x36/0x37DoIP 为大量数据的快速传输提供便利同时配合 TLS 做加密通道这对车载信息安全也是加分项。8.3 信息安全需求对 UDS 开发的影响现在主机厂对诊断安全的要求越来越严。以前 0x27 安全访问可能只是简单的种子固定算法密钥现在很多项目要求用 AES 对称加密做密钥校验还有的在传输层就要求加密通信。UDS 开发者也必须了解安全启动Secure Boot和刷写校验的机制比如在 0x34 请求下载阶段就要把固件的校验信息哈希值或签名一并传给 ECUECU 在写入和启动时做校验防止未授权程序被刷进去。信息安全这块很容易被只做功能开发的工程师忽略但说实话直接决定一个 ECU 能不能过车厂的安全审核。我自己在这方面的经验是尽早让安全团队介入协议设计不要等代码写完了再补另外种子和密钥算法的实现一定不要硬编码在 UDS 层代码里做成一个独立的安全模块方便审计和替换。9. 项目总结与经验沉淀9.1 关键技术难点回顾整个项目做下来我觉得最考验人的不是单个协议的理解而是多层协议栈的联调和异常处理。CAN 层要考虑物理层信号质量、时钟误差、总线仲裁CAN TP 层要处理分包重组和流控UDS 层要管理会话、安全、服务分发和 NRC 映射。任何一层出现问题最终表现可能都是“诊断没响应”但根因可能在完全不同的层面。这也是为什么我特别强调逐层排查的方法论。9.2 踩坑之后总结的几条开发原则第一先做最小可行链路。在写任何复杂诊断业务之前先让 CAN 驱动能自发自收一个单帧 UDS 请求比如 0x10 01 回 0x50 01。这条路通了再往上加功能会省去大量联调时间。第二协议栈代码必须可观测。我建议在每个模块的入口和出口都加可配置的日志打印开发阶段打开量产阶段关闭。没有日志车载现场出现问题时只能靠猜。第三所有超时都做成可配置的。不同的总线负载、不同的 ECU 性能诊断超时时间差异很大。把超时时间定义成宏或者配置项而不是直接在代码里写死后续做跨项目移植时会非常省心。9.3 从项目本身延伸出去的进一步学习路径如果你看完文章想继续深入我建议按这个路径走先自己写一个基于 STM32 的最小 CAN 驱动把收发调通然后用 PCAN 加 PCAN-View 看报文再写一个简单的 CAN TP 模块用自己的上位机测试 UDS 0x10 和 0x22 服务最后再考虑集成安全访问和刷写功能。每一步都有明确的验证手段不会觉得迷茫。另外强烈建议多读几遍 ISO 14229 和 ISO 15765-2 的原文。虽然看标准文档很枯燥但所有工具的底层逻辑都源自这几份文档。等你在实际项目里碰到协议栈的边界问题再回头看标准里的描述会突然有种豁然开朗的感觉。最后就留一句我自己的体会车载开发这一行技术栈再深终究是给整车质量兜底的工程活。把每一个报文、每一个状态位、每一个超时时间都抠清楚比追求新潮框架更有意义。希望这篇文章能帮你少踩几个坑。