如何真正读懂UDS诊断请求帧?从一个CAN报文开始讲起
你有没有遇到过这样的场景:手握示波器和CAN分析仪,抓到一串看似杂乱的十六进制数据——02 10 03 00 00 00 00 00,旁边同事说:“这是在切诊断会话。”
可你心里嘀咕:这8个字节里到底藏着什么秘密?为什么第一个是02?10和03又代表什么?
别急。今天我们不堆术语、不甩标准文档,就从这一行最基础的CAN帧出发,带你一步步拆解UDS服务请求帧的真实结构。这不是一份速查手册,而是一次“庖丁解牛”式的实战剖析,让你从此看懂每一个字节背后的逻辑。
一条CAN报文,如何承载整车诊断命令?
我们先放下“UDS”“ISO 14229”这些高大上的词,回到工程现场。
假设你在调试一辆新能源车的BMS(电池管理系统),想确认它当前是否进入扩展会话模式。你用诊断工具发了一条指令,CAN总线上捕获到如下数据:
CAN ID: 0x7E0 Data: 02 10 03 xx xx xx xx xx表面看只是8个字节,但它其实是一个精心封装的“诊断信封”。要打开它,得一层层剥开:
[CAN帧头] → [协议控制信息 PCI] → [服务ID SID] → [子功能/参数]每一层都有明确职责。下面我们逐层深挖。
第一步:透过PCI看清传输机制 —— 单帧是怎么定义的?
开头那个02很关键。它不是随机值,而是N_PDU中的协议控制信息(N_PCI),决定了整个消息的组织方式。
在CAN这种最多传8字节数据的链路中,UDS靠N_PCI来判断:“这条消息是独立完成,还是要分段?”
最常见的就是单帧(Single Frame, SF),适用于短命令,比如切换会话、读取一个DID。
// 示例:02 10 03 N_PCI = 0x02 → 表示这是一个单帧,且后续有效数据为 2 字节这里有个技巧:单帧长度由N_PCI低4位决定。
即:
有效数据长度 = N_PCI & 0x0F所以:
-0x02→ 后面有2字节数据(SID + Subfunction)
-0x03→ 3字节数据(如 SID + DID_H + DID_L)
⚠️ 常见坑点:如果 DLC(数据长度)小于 N_PCI 指示的长度,ECU应返回
NRC 0x12(incorrectMessageLengthOrInvalidFormat)
也就是说,哪怕你只发了02 10,少了第三个字节,ECU也会拒绝响应——因为它期待两个有效字节,结果只收到一个。
第二步:SID才是真正的“命令开关”
紧随其后的是10,这就是服务标识符(Service Identifier, SID),相当于操作系统里的“系统调用号”。
每个SID对应一类操作。记住这几个常用编号,胜过背十页标准:
| SID | 名称 | 功能说明 |
|---|---|---|
0x10 | Diagnostic Session Control | 切换诊断会话(默认/扩展/编程等) |
0x22 | ReadDataByIdentifier | 根据DID读数据 |
0x2E | WriteDataByIdentifier | 写入参数 |
0x27 | Security Access | 安全解锁,刷写必备 |
0x3E | Tester Present | 心跳保活,防止退出诊断模式 |
0x14 | ClearDTC | 清除故障码 |
所以10就是在告诉ECU:“我要切换你的诊断状态了。”
但光有10还不够,还得告诉它是切到哪种会话。这就引出了下一个字段——子功能(Subfunction)。
第三步:子功能决定“怎么执行”,而非“做什么”
继续看上面的例子:02 10 03
02: 单帧,共2字节数据10: 执行“诊断会话控制”03: 子功能 = 切换至扩展会话(Extended Session)
这里的03就是子功能码。不同SID支持的子功能范围不同:
| 主会话类型 | 子功能值 |
|---|---|
| 默认会话 | 0x01 |
| 编程会话 | 0x02 |
| 扩展会话 | 0x03 |
| 系统安全访问会话 | 0x04 |
✅ 实战提示:很多初学者误以为只要发个
10 03就能永久留在扩展会话。错!大多数ECU会在无Tester Present心跳时自动退回到默认会话。这就是为什么自动化测试脚本必须周期性发送3E 00保活。
高阶玩法:读取数据ByID——DID是如何工作的?
现在我们换个需求:读取车辆VIN码。
已知VIN对应的DID是0xF190,我们构造请求:
Request: 03 22 F1 90分解一下:
-03→ 单帧,后面有3字节数据
-22→ ReadDataByIdentifier
-F1 90→ DID = 0xF190(注意高位在前)
为什么DID要用两个字节?因为地址空间要够大。ISO预留了几个典型区间:
| 区间 | 用途说明 |
|---|---|
0xF1xx | 车辆级信息(VIN、ECU名称、软硬件版本等) |
0xF2xx | 运行时数据(温度、电压、里程等) |
0xF4xx | 故障相关统计 |
0x0000~0xEFFF | OEM自定义信号 |
ECU收到这个请求后,会去内部的DID映射表查找0xF190是否可读。如果注册了,就返回:
Response: 62 F1 90 57 31 32 33 ...其中:
-62=22 + 0x40→ 正响应SID(所有正响应都在原SID上加0x40)
- 接着是原样回显DID
- 再往后是ASCII编码的VIN字符串(如W123XYZ…)
🔍 调试经验:如果你收到
7F 22 31,那说明DID无效或超出范围(NRC=0x31 requestOutOfRange)。第一反应应该是查DID拼写有没有错,而不是怀疑通信链路。
多DID批量读取:效率提升的关键技巧
实际项目中,你不会一次只读一个DID。比如做产线检测,可能需要同时获取VIN、生产日期、固件版本、电池序列号……
UDS允许你在一次请求中连续列出多个DID:
Request: 07 22 F1 90 F1 88 F1 8A表示一次性请求:
-0xF190: VIN
-0xF188: ECU软件版本
-0xF18A: ECU硬件版本
ECU将按顺序逐一返回数据,响应帧也会更长,甚至触发多帧传输机制。
这时候你就不能再用单帧了,必须升级为:
-首帧(First Frame, FF)
-连续帧(Consecutive Frame, CF)
例如,当响应超过7字节时,ECU会这样发:
FF: 10 12 34 ... // 10表示FF,1234是总长度(hex) CF: 21 AA BB CC ... // 序号1 CF: 22 DD EE FF ... // 序号2但这属于进阶内容,本文聚焦请求端,暂不展开。只需记住一点:请求能否使用单帧,取决于你要传多少参数。
C语言实战:写一个能识别“读DID”请求的解析函数
下面这段代码可以直接用在嵌入式网关或诊断代理中,用于过滤并解析ReadDataByIdentifier请求。
#include <stdint.h> #include <stdio.h> #define SID_READ_DATA 0x22 #define N_PCI_SF_MASK 0xF0 // 高4位用于区分帧类型 #define N_PCI_SF 0x00 typedef struct { uint32_t can_id; uint8_t data[8]; uint8_t dlc; } CanMessage; void handle_uds_request(const CanMessage* msg) { // 检查基本合法性 if (msg->dlc == 0) return; uint8_t pci = msg->data[0]; // 只处理单帧 if ((pci & N_PCI_SF_MASK) != N_PCI_SF) { return; // 不处理首帧/连续帧 } uint8_t data_len = pci & 0x0F; // 实际应用数据长度 if (data_len < 1 || data_len > 7 || msg->dlc < data_len + 1) { printf("Malformed frame: invalid length\n"); return; } uint8_t sid = msg->data[1]; if (sid != SID_READ_DATA) { return; // 不是我们关心的服务 } // 开始解析DID列表 int offset = 2; int remaining = data_len - 1; // 减去SID后的可用字节数 while (remaining >= 2) { uint16_t did = (msg->data[offset] << 8) | msg->data[offset + 1]; printf("Client requested DID: 0x%04X\n", did); offset += 2; remaining -= 2; } // 若有多余字节,可能是格式错误 if (remaining == 1) { printf("Warning: odd number of DID bytes, last byte ignored.\n"); } }📌重点解读:
- 使用N_PCI_SF_MASK屏蔽高四位,确保只处理单帧。
- 对data_len做双重校验:既不能超限,也不能超过DLC。
- DID采用大端序拼接,符合ISO规定。
- 支持批量DID解析,适合自动化测试平台。
你可以把这个函数集成进CAN接收中断回调中,实现“看到读DID请求就打印日志”的监控功能。
工程实践中常见的“踩坑”场景
❌ 坑1:DID字节顺序搞反了
新手常把F1 90解释成0x90F1,导致查不到数据。记住:高位在前,大端序!
正确的做法:
did = (high_byte << 8) | low_byte;❌ 坑2:忽略会话限制
有些DID只能在扩展会话下访问。如果你还在默认会话就去读0xF201(高压电池温度),ECU会直接回7F 22 7E(generalReject) 或NRC 0x22(conditionsNotCorrect)。
解决方案:先发10 03切会话,再读数据。
❌ 坑3:没处理安全访问锁
对某些敏感DID(如标定参数、里程修改),即使进入了扩展会话也不行,必须先通过27服务解锁。
典型流程:
→ 27 01 // 请求种子 ← 67 01 [4-byte] // 返回随机数 → 27 02 [4-byte] // 回传密钥 ← 67 02 // 解锁成功 → 2E XX YY ... // 现在可以写入了这类机制常见于OTA刷写或4S店专用功能,防止非法篡改。
在真实系统中,这条请求经历了什么?
让我们把视野拉远一点。当你按下诊断仪上的“读取VIN”按钮时,背后其实是这样一个链条:
[PC诊断软件] ↓ (USB/CAN or DoIP) [Vehicle Gateway] ↓ (Routing based on CAN ID & Target Address) [CAN Bus] → [BMS ECU]在这个过程中:
- 网关负责路由:根据目标地址(如0x7E1为BMS响应ID)转发请求
- BMS运行UDS协议栈(可能是AUTOSAR实现),解析SID和DID
- 协议栈调用RTE接口,从BSW层获取VIN变量
- 组装响应帧并通过0x7E1回传
整个过程高度模块化,但起点始终是那一行简单的03 22 F1 90。
结语:掌握请求帧,就掌握了诊断的钥匙
你看,一条短短的CAN报文,背后竟藏着如此严密的设计逻辑。
PCI控制传输形态,SID定义服务类型,Subfunction/DID指定具体行为——三位一体,构成了UDS诊断的基石。
当你下次再看到02 27 01,你应该立刻反应过来:
“这是Tester在向ECU请求安全种子,准备进行刷写前的身份验证。”
这才是真正意义上的“读懂”UDS。
而这,仅仅是旅程的开始。接下来你可以深入:
- 如何构建完整的UDS协议栈?
- 多帧传输中的流控机制(FC帧)如何工作?
- DoIP替代CAN后,UDS又是如何演进的?
但无论如何进阶,都请记得回头看看这条02 10 03的请求帧。它是你进入汽车诊断世界的第一把钥匙。
💬 如果你在项目中遇到过离谱的UDS通信问题,欢迎留言分享。我们一起排雷。