先从结论说起:做车载诊断开发,真正卡人的往往不是上层那些花哨的功能逻辑,而是底层总线的原理没吃透。你连一帧 CAN 报文怎么仲裁、怎么填充位、波特率误差超了会怎样都说不清楚,写出来的 UDS 诊断栈大概率是空中楼阁。这篇文档只聊一个事:把 CAN 通信和 UDS 诊断协议这条链路拆开揉碎,从物理电平一直到刷写流程,每一步都给出能直接落地的经验和参数。
内容覆盖:CAN 2.0/CAN FD 底层机制、DBC 报文解析、ISO 15765-2 传输层分帧重组、UDS 常用服务(10/27/19/22/2E/31/34/36/37/14)的完整交互流程、NRC 排查方法,以及我实际踩过的坑。适合正在入门车载网络、准备诊断开发面试、或者接手 VCU/BMS/MCU 诊断模块但又觉得协议栈像黑盒的人。
1. 为什么诊断开发必须懂底层 CAN:从一帧报文说起
1.1 物理层先把话说清楚:差分信号、显性隐性仲裁
很多人写 UDS 代码,却不知道诊断仪发出的那帧请求在线上是什么样子。CAN 物理层(ISO 11898-2)用两条线 CAN_H 和 CAN_L 的差分电压表示数据,强调一点:CAN 是“线与”逻辑,显性位(Dominant)对应逻辑 0,隐性位(Recessive)对应逻辑 1,多个节点同时发送时,只要有一个节点发显性位,总线上就是显性。这个看似简单的规则,直接决定了后面所有仲裁机制。
双绞线的好处是抗共模干扰,两条线靠得越近,外部噪声在两根线上产生的电位变化越一致,差分接收器看到的差值就越干净。这也是为啥现场装配时要求双绞,走线要远离高压线束的原因。我见过因为 CAN 线和电机三相线绑在一起,导致通信直接瘫痪的案例,示波器抓波形全是毛刺。
波特率是另一道基础关。高速 CAN 典型配置是 500kbps,也就是 1 bit 占 2us(位时间 = 1/500000 = 2us)。低速/容错 CAN 常用 250kbps 或者 125kbps。位时间又拆成同步段、传播段、相位缓冲段 1、相位缓冲段 2,具体分法由 BTR(总线定时寄存器)决定,反正你记住两个关键量:采样点位置和同步跳转宽度。采样点一般取 75%~87.5% 之间,太靠后容易采到相邻 bit,太靠前又可能没等到信号稳定。
1.2 仲裁不是靠先来后到,而是位级 PK
多节点同时上总线,怎么决定谁先发?仲裁机制。CAN 报文开头是帧起始(SOF),随后是仲裁场(标准帧 11 位 ID + RTR 位,扩展帧 29 位 ID)。因为显性位覆盖隐性位,ID 数值小的节点(高位先发 0 的)会在某个位时刻“压过”ID 大的节点,后者检测到发送位和总线位不一致,立刻转入接收状态,等下一轮再发。
实际经验就是:诊断相关的报文 ID 一定要规划好优先级。不要把所有诊断报文的 ID 都设成同样数值段,这样会有大量总线冲突和重发,实测吞吐率下降明显。我的习惯是:实时控制类报文给低 ID(高优先级),比如 0x100~0x300;诊断请求/响应用 0x700~0x7FF 段;网络管理报文夹在中间。这样既保证实时性,又不至于把诊断报文压得发不出去。
1.3 位填充和时钟误差:为什么“能通”不等于“稳”
CAN 没有单独的时钟线,接收方是靠每一位的跳变沿来同步本地时钟的。为了确保总线持续有跳变,协议规定:连续发送 5 个相同位后,必须插入 1 个反相位(位填充)。接收端看到第 5 个相同位后,就会期待第 6 个反向位,一旦这个反向位没出现,就知道出了异常。
位填充也解释了热词里“CAN 时钟误差”问题。两个节点晶振误差不同(比如 16MHz 晶振 ±0.1% vs 8MHz 晶振 ±0.3%)时,每个位的实际长度会漂移。接收方靠重新同步机制(硬同步 + 重同步跳变)不断校正采样点,但如果波特率设置不对,或者总线长度太长、传播延迟太大,重同步就救不回来,表现就是偶发通信错误。这正是我看到很多同事“明明能通,但一跑耐久就掉线”的深层原因。
2. 关键机制拆解:从 CAN 报文到 DBC 描述文件
2.1 标准帧、扩展帧、远程帧,哪个才是诊断要用的
CAN 2.0 定义四类帧:数据帧、远程帧、错误帧、过载帧。诊断开发主要关心数据帧。标准帧(CAN ID 11 位)和扩展帧(CAN ID 29 位)的区别不在数据长度,而在仲裁场长度。整车厂诊断规范里,UDS 请求/响应通常走扩展帧(29 位 ID),比如常见的 0x18DAxxF1(请求)和 0x18DDF1xx(响应),其中 xx 是物理寻址的目标地址。
远程帧(RTR=1)本身不带数据,用来请求某节点发送数据。但在现代 UDS 诊断里基本不用远程帧做数据交互,大多走周期性发送。如果你在报文解析工具里看到某个 ID 数据长度是 0,大概率就是远程帧,别错误地当成空数据帧去解析。
2.2 DBC 就是 CAN 网络的“翻译词典”
没有 DBC 文件,你看到的总线数据就是一堆十六进制数字,完全不知所云。DBC(Database CAN)描述了每个报文 ID 的信号布局:起始位、长度、字节序(Intel/Motorola)、缩放因子(factor)、偏移量(offset)、取值范围、单位。
这里有个高频错误——端序搞反。Intel 格式(小端)和 Motorola 格式(大端)在信号跨字节时处理方式不同,尤其是超过 8 位的信号,比如车速信号 16 位。很多新人车厂 DBC 上写的是 Motorola,结果用 Intel 方式解析,车速翻了好多倍,还以为是传感器坏了。
一条典型 DBC 信号:
SG_ VehSpd : 0|16@1- (0.01,0) [0,655.35] "km/h" Vector__XXX含义:起始位第 0 bit,长度 16 bit,字节序 @1 表示 Motorola,精度 0.01,偏移 0,单位 km/h。原始值 12345 换算成实际车速 = 12345 × 0.01 = 123.45 km/h。做解析程序时,务必先取原始值再乘系数加偏移,别直接把浮点数塞进总线。
2.3 CAN FD:数据场更大、波特率切换的关键差异
热词里大量出现“CAN FD”,说明行业确实在切换。CAN FD(Flexible Data-rate)相比经典 CAN 做了两件大事:数据场从 8 字节扩展到最多 64 字节;控制场之后的数据段可以切换到更高波特率(比如 2Mbps/5Mbps),仲裁段保持 500kbps 以保证兼容和仲裁安全。
注意一个点:CAN FD 报文有一个 FDF 标志位,经典 CAN 接收器看到 FDF=1 时不会尝试解析整个报文,直接报格式错误并引发错误帧。所以混网阶段必须做好报文 ID 规划,不能同一网络上既有经典 CAN 又有 CAN FD 却使用同一 ID 段,否则经典节点会被 FD 帧反复干扰。
数据段变长、波特率变高,直接好处是刷写效率提升。经典 CAN 刷一个 2MB 的应用文件,按 8 字节一帧要走 26 万帧,每帧还要等确认,常常要十几分钟;CAN FD 一帧最多 64 字节,配合更高的数据波特率,同样的文件时间能压缩一半以上。这也是 UDS 刷写方案逐步转向 CAN FD 的原因。
3. UDS 诊断协议:分层定位与核心服务拆解
3.1 诊断协议栈的分工:应用层、传输层、网络层别混为一谈
UDS 全称 Unified Diagnostic Services,规范在 ISO 14229。它本身是应用层协议,规定了服务 ID(SID)和参数结构。ECU 和诊断仪之间传输数据,走的是 ISO 15765-2(通常称为 CAN TP,传输层)。
很多人写代码时把“一帧诊断请求”和“一帧 CAN 报文”完全等同,这在小数据(≤7 字节)场景下没错,因为单帧(SF)直接塞在一帧 CAN 报文里。但一旦遇到 19 服务读 DTC 大量快照、34/36/37 刷写固件这类大块数据,超过 8 字节(经典 CAN)时,就必须由传输层分帧打包:首帧(FF)、连续帧(CF)、流控帧(FC)。
传输层重要的时序参数有三个:STmin(连续帧最小间隔)、BS(块大小)、N_Cr(连续帧超时)。我一般设 STmin=10ms,BS=0(不限制块大小),适用于大多数 500kbps 总线。如果对方 ECU 处理能力弱,STmin 要加到大几十毫秒,不然对方缓冲区溢出,直接给你 NRC 0x72。
3.2 物理寻址和功能寻址:为什么同一个服务有两种 SID 开头
UDS 请求有两种寻址模式。物理寻址(Physical)点对点,请求帧 ID 和响应帧 ID 一一对应,诊断仪用这个方式跟某一个具体 ECU 单独对话。功能寻址(Functional)是一次性广播给所有 ECU,比如 0x18DB33F1,所有支持该服务的节点都会收到并各自回复,常用于 10 02 会话切换或者 11 01 复位这类全员指令。
一个坑:功能寻址请求发出后,同一总线上可能有多个 ECU 同时响应,这会造成总线仲裁冲突。所以量产诊断仪在功能寻址时一般只要求“收到”,不关心每个节点的回复,而 ECU 在收到功能寻址的复位指令后,也不该带着大负载去回复,否则总线会被冲垮。这也是诊断规范里强调“功能寻址响应需延时或简化”的原因。
3.3 核心服务逐个过:10、27、19、22、2E、31、34/36/37、14
- 0x10(DiagnosticSessionControl):切换会话。默认会话(01)只有基础服务,编程会话(02)才能刷写,扩展会话(03)才能做读写标定和例程控制。几乎所有高级别服务都有会话条件,先切会话再操作。
- 0x27(SecurityAccess):安全解锁。流程是请求 Seed(子功能 01/03/05…),ECU 返回随机种子,诊断仪按算法算 Key,再发 02/04/06… 解锁。注意连续失败次数限制和延迟锁定,通常失败 3 次后要等 10 秒以上。实测中发现有些 ECU 的 Seed 有效时间极短,必须在几十毫秒内回 Key,否则悄悄失败。
- 0x19(ReadDTCInformation):17 服务最复杂。子功能 02(按状态掩码读 DTC)最常用,状态掩码通常是 0x09(含当前故障和历史故障)。子功能 04 读快照,06 读扩展数据。这个服务返回数据往往很长,必须通过 CAN TP 多帧交互,单帧处理会截断。
- 0x22(ReadDataByIdentifier)/0x2E(WriteDataByIdentifier):读写 DID 数据。做标定和参数配置常用。要注意 DID 的读写权限矩阵通常写在诊断规范附录里,不是所有 DID 都能读,更不是所有都能写。
- 0x31(RoutineControl):例程控制。典型 SCU 刷写前擦除 Flash(01 start)、检查编程条件、执行 CRC 校验(03 request result)。子功能 01 启动例程、02 停止例程、03 请求例程结果,这个服务边界很清晰,比 34/36/37 简单得多,但要注意例程是耗时操作,响应超时时间得放长。
- 0x34(RequestDownload)/0x36(TransferData)/0x37(RequestTransferExit):刷写三步走。34 告诉 ECU 我要下载,带地址和长度;36 分块传数据,块大小受发送方和接收方缓冲限制;37 告诉 ECU 传完了,ECU 做校验和跳转。刷写流程最大的坑是 36 请求的数据长度必须和 34 声明的长度严格一致,多发一字节或少发一字节,很多 ECU 会直接拒绝。
- 0x14(ClearDiagnosticInformation):清 DTC。通常在修完车后清码,也用于产线 EOL 结束。前提是必须在扩展会话且很多 ECU 要求解锁。
3.4 NRC 是排查故障的黄金线索
UDS 响应分肯定响应(SID + 0x40)和否定响应(0x7F + SID + NRC)。NRC(Negative Response Code)是诊断工程师最好的朋友。常见 NRC 有:
- 0x10:一般拒绝,子功能不支持或条件未满足。
- 0x12:子功能不支持(SID 支持但 SF 不支持)。
- 0x13:请求报文长度/格式错误。
- 0x22:条件不满足(比如没切会话、没解锁就执行了 27)。
- 0x31:请求超出范围(DID 不存在、数据超限、地址不合法)。
- 0x33:安全校验失败(Seed 正确但 Key 算错,或者没解锁就访问受限服务)。
- 0x72:一般编程失败(擦除失败、数据块校验不通过)。
排查顺序我给个死套路:先看会话对不对,再看解锁没有,再看参数符不符合范围,最后查时序有没有超时。九成 NRC 都出在这四个环节里。
4. 实操过程:刷写流程、检查点与常见问题实录
4.1 一次完整刷写到底经历了什么
拿经典 CAN 刷写一个 ECU 应用为例。第一步建立通信:物理寻址请求 10 02 切到编程会话,ECU 返回 50 02。第二步关闭正常通信:很多 ECU 刷写时必须禁止应用报文持续发送,发 28 00(CommunicationControl)关闭应用报文。第三步安全解锁:发 27 01 拿 Seed,得到种子算出 Key 回 27 02,ECU 返回 67 02。第四步检查编程前提条件:比如发 31 01 检查电压、检查点火状态,不是所有车都做,但有这个例程的一定要走,否则后续刷写容易失败。
第五步请求下载:发 34 02(按地址下载)带起始地址和总长度,ECU 返回一个最大传输块长度(比如 0x200 = 512 字节)。第六步循环传输数据:按最大块长度把固件文件切片,每片发 36 01 + 数据,等肯定响应后再发下一块。第七步请求传输退出:发 37,ECU 返回 77,通常此时 ECU 内部还在做校验和/签名验证。第八步检查结果:发 31 01 例程 0203(CRC 校验例程)查询结果,确认完整有效。第九步复位:发 11 01 软复位,ECU 重新启动进入应用模式。
这个流程里最容易翻车的是第六步的流控。我实测过一个 ECU,它的接收缓冲只有 64 字节,但响应里 maxNumberOfBlockLength 写的 0x800。如果你真按 2048 字节去发,连续帧之间必须等待 STmin 足够大,否则缓冲区直接溢出,NRC 0x72。经验之谈:不要完全信 ECU 报告的数字,先小批量试几块,看响应时序再逐步加大。
4.2 检查点流程(Checkpoint)为什么被反复提及
热词里有“uds检查点流程”。刷写时 ECU 内部通常有 checkpoint(可恢复点)机制。每成功烧录一个 block,ECU 会记录进度。一旦中途断电或通信失败,重新刷写时可以“断点续传”:诊断仪查询最后完成的块号,跳过已经烧录的块,只传剩余内容。
实现细节上,检查点信息通常保存在 ECU 内部独立存储区,比如 Bootloader 数据闪存末尾。诊断规范里一般会定义专门的例程服务或 DID 来读取/清除检查点。所以刷写工具编写时,第一步不要着急发 34,先试读检查点:若检查点提示已完成 30%,就和用户确认是否继续,避免重复劳动浪费时间。
4.3 报错瞬间的现场实测:NRC 0x33 的经典战场
我调试过一个网关路由器,每次刷写都在 27 02 返回 0x33。一开始以为是算法算错,反复核对 Seed&Key 计算,结果发现是 27 01 拿 Seed 之前,没切到编程会话。ECU 在默认会话下返回的 Seed 是假种子,专门用来诱导你算错 Key。把 10 02 提前执行后,27 02 一次成功。这个案例说明:诊断服务之间有严格的依赖关系,调试时别只盯着出错的报文,往前翻十帧看前置条件。
另一个现场是 19 02 读 DTC,诊断仪一直收不全响应。抓总线波形发现响应帧之间有很长的间隙,CAN TP 的 FC 帧发得太早,对方 ECU 还在准备数据,触发超时后传输中止。解决方法是把 FC 的 STmin 调大到 50ms,或者让对方 ECU 侧的 P2 时间配置放宽。
4.4 工具链推荐:从免费到商用怎么选
常用工具有 PCAN-View + PCAN-USB(免费软件,抓包够用)、周立功 CANTest(国内用得多,注意 Win11 下老版本驱动不兼容,要用 4.x 以上版本)、Vector CANalyzer/CANoe(专业级,支持 CAPL 脚本快速仿真节点和自动化测试,就是贵)、Wireshark with CAN(如果走 USB-CAN 设备,可以串到 Wireshark 看协议栈解析)。
我的建议是:开发阶段 PCAN-View 很顺手,能实时过滤 ID,保存日志;但做 UDS 服务自动化测试,CANoe 的 Diagnostic Console 和 CAPL 是无可替代的。预算不够时,可以用 Python + python-can 库 + 一个 USB-CAN 适配器写脚本,配合 CanDump 导出 ASC 或 CSV,照样能实现 19/22/2E 服务的批量测试和报告生成。
5. 避坑清单与后续扩展方向
5.1 先列一条条硬结论
- 总线终端电阻必须接:高速 CAN 在总线两端各接 120 欧姆,测静态电阻应在 60 欧姆左右。接少了通信反射严重,波形台阶明显,跑高速率必然出错。
- 波特率误差控制在 ±0.5% 以内:CAN 控制器允许的误差范围随采样点位置变化,实测 500kbps 下晶振误差超过 ±1% 就有偶发错误帧,长期稳定运行最好用带晶振校准的 MCU。
- 诊断服务时序 P2/P2*:正常响应要快(P2 通常 50ms 内),慢响应要提前发 NRC 0x78(ResponsePending)占坑,防止诊断仪提前超时。写 ECU 端代码时,0x78 机制务必实现,否则耗时操作必炸。
- 字节序和位序永远是第一检查项:解析 DBC 显示大数,多数是端序问题;写入 DID 数据反了,多半是 Motorola/Intel 没对齐。
- CAN ID 不要随意复用:同一个网络上 ID 绝不能重叠,新项目上线前最好用工具做一次全网络 ID 冲突扫描。
5.2 新手怎么模拟 CAN 和 UDS 环境
没有真实 ECU 怎么练?两个方案。方案一:两个 USB-CAN 盒子 + 一个电阻盒,一台电脑装 CANoe(支持 10 天试用版)仿真 ECU,另一台跑 PCAN 当诊断仪;也可以用 Python 脚本在一台电脑上同时开两个虚拟通道,一端模拟 ECU 应用层,一端模拟诊断仪客户端。
方案二(低成本):直接用 canopen 模拟器或者开源项目 openxc 刷到开发板上,配合 Python 脚本手动构造 UDS 报文逐字节发。关键点是要把 CAN TP 的分帧逻辑自己实现一遍,这比看十遍协议文档都有用。我自己就是先手写了一个最小 SF/FF/CF/FC 有限状态机,才彻底搞懂 ISO 15765-2 的时序约束。
5.3 诊断开发未来还会加什么
行业方向已经很明显:CAN FD 会逐步替代经典 CAN,做诊断开发时不要只写着 8 字节的函数,数据长度要按最大 64 字节设计;UDS over DoIP(以太网诊断)越来越多,DoIP 支持同时建立多个 TCP 连接,诊断仪和 ECU 的寻址逻辑完全不同;信息安全(SecOC、HSM、安全启动)会贯穿到 UDS 诊断流程里,27 服务会升级成更复杂的安全解锁协议,比如基于非对称加密的认证。这些方向听起来高大上,但底层依然离不开 CAN 那套“位、帧、仲裁、稳态”的基本功。
我个人在实际操作中的体会是:诊断开发调试时,千万别一上来就怀疑协议栈。先抓总线,看 CAN 层有没有错误帧,再看传输层有没有丢帧和超时,最后才轮到应用层逻辑。底盘功夫扎实了,上层一切都是可查的。这也是我坚持把这套技术文档重点放在 CAN 层的原因——把地基打牢,后面盖楼才不慌。