![]()
1. 引言
现代汽车电子系统由数十个甚至上百个电子控制单元(ECU)构成,它们通过 CAN、LIN、FlexRay 等总线协同工作。当车辆出现故障时,诊断工具(如诊断仪)需要与 ECU 通信,读取故障码、实时数据,执行动作测试等。这一套通信规则就是 汽车诊断协议。
在基于 CAN 总线的诊断应用中,最核心的两个标准是:
ISO 14229 :定义了 统一诊断服务(UDS,Unified Diagnostic Services) ,是 应用层 协议,规定了诊断请求和响应的格式、服务含义。
ISO 15765 :定义了 基于 CAN 总线的诊断传输(DoCAN,Diagnostics over CAN) ,是 传输层/网络层 协议,负责将应用层 UDS 消息拆分成适合 CAN 总线传输的小帧,并在接收端重组。
很多初学者容易混淆这两者。一句话总结:UDS 告诉你“问什么问题”,ISO 15765 教你“怎样把问题装进 CAN 帧里送过去”。而 ISO 14229 正是 UDS 的标准编号。
本文将从概念到代码,深入浅出地比较这两个协议,并给出 C++ 实现示例。
2. 协议栈概览(OSI 模型视角)
OSI 层
汽车诊断协议栈
标准
作用
应用层
UDS
ISO 14229
诊断服务定义:读数据、写数据、例程控制等
传输层
DoCAN (ISO 15765-2)
ISO 15765-2
分段、重组、流控制
数据链路层
CAN 2.0B
ISO 11898
8 字节 CAN 帧、仲裁、错误检测
物理层
CAN 物理层
ISO 11898
电平、拓扑
UDS (ISO 14229) 与 ISO 15765 不是对立关系,而是上下层协作关系。
ISO 15765 同时也定义了与 UDS 无关的 时间参数 (如 P2、P2* 等)和 通信控制 。
UDS 是一套面向汽车场景设计的诊断服务集合,每个服务用 服务标识符(SID, Service Identifier) 表示(1 字节)。典型服务包括:
SID (hex)
服务名称
功能简述
0x10
Diagnostic Session Control
切换诊断会话(默认、编程、扩展等)
0x11
ECU Reset
重置 ECU
0x22
Read Data By Identifier
通过 DID 读取数据(如 VIN、软件版本)
0x2E
Write Data By Identifier
通过 DID 写入数据
0x19
Read DTC Information
读取故障码
0x14
Clear Diagnostic Information
清除故障码
0x31
Routine Control
启动/停止例程(如动作测试)
3.1 UDS 请求/响应格式
请求格式(诊断仪 → ECU):
[SID] [子功能(可选)] [参数...]
例如:读取 DID 0xF190 的数据(假设 DID 占 2 字节)
22 F1 90
0x22 = ReadDataByIdentifier SID
0xF190 = 数据标识符(如 VIN)
肯定响应格式(ECU 成功处理):
[SID+0x40] [子功能(若请求有)] [数据...]
例如对上述请求的肯定响应:
62 F1 90 41 32 ... (实际数据)
0x62 = 0x22+0x40
否定响应格式(ECU 无法处理):
0x7F [SID] [NRC]
例如:请求的 DID 不支持,返回 7F 22 31(0x31 = RequestOutOfRange)
3.2 子功能与抑制肯定响应位
许多服务(如 SessionControl、ECUReset)的第一个字节称为子功能,其最高位(bit7)为 抑制肯定响应位(SPR, Suppress Pos Resp):
SPR = 1:ECU 不发送肯定响应,只发送否定响应(用于减少总线负载)。
SPR = 0:ECU 发送肯定响应。
例如:切换到扩展会话(子功能 0x03),且不需要肯定响应:
10 83 // 0x80 | 0x03 = 0x83
3.3 UDS 示例(无分段)一个完整诊断请求通过 CAN 发送(假设不超 8 字节):
请求:
10 03(切换到扩展会话,需要肯定响应)响应:
50 03 00 32 01 F4(肯定响应,附带一些参数)
CAN 数据链路层一帧最多承载 8 字节数据,而一个 UDS 消息可能长达几十甚至几百字节(如读取 VIN 的 17 个字符,加上 DID 等)。ISO 15765-2(常称为 ISO-TP)定义了分段传输协议,将长消息切分成多个 CAN 帧,并管理发送/接收状态。
4.1 四种帧类型(N_PDU 类型)
帧类型
简称
用途
协议控制信息(PCI)字节
单帧
SF
消息总长 ≤ 7 字节(经典 CAN)
第一个字节高 4 位 = 0x0,低 4 位表示数据长度
首帧
FF
长消息的第一帧,告知总长度
第一个字节高 4 位 = 0x1,低 4 位 + 第二字节组成 12 位总长
连续帧
CF
首帧之后的后续数据
第一个字节高 4 位 = 0x2,低 4 位表示序列号(SN,0~15 循环)
流控制帧
FC
接收端控制发送端速率
第一个字节高 4 位 = 0x3,低 4 位表示流控制状态;后续字节包含块大小(BS)和最小间隔时间(STmin)
4.2 帧格式详表(经典 CAN,11 位 ID,单字节 PCI)
单帧 (SF)
| Byte0 | Byte1 ~ Byte7 (最多7字节数据) |
| 0x0 | Len | UDS 数据 (Len 字节) |
Len = 4 位,范围 0~7(实际长度 0~7,若消息总长 >7 必须用多帧)
注意:有些实现使用 首个字节为 PCI,剩余 7 字节 ,Len 不包含 PCI 自身。
首帧 (FF)
| Byte0 | Byte1 | Byte2~Byte7 |
| 0x1 | Len_H | Len_L | 前 6 字节 UDS 数据 |
总长度 = (Len_H << 8) | Len_L,范围 8~4095 字节
连续帧 (CF)
| Byte0 | Byte1~Byte7 |
| 0x2 | SN | 后续 7 字节 UDS 数据 |
SN 从 1 开始,每发一个 CF 加 1,到 15 后回绕到 0。
流控制帧 (FC)
| Byte0 | Byte1 | Byte2 | Byte3~Byte7(保留)|
| 0x3 | FS | BS (块大小) | STmin (最小间隔) | 0x00... |
FS (Flow Status):0=继续发送(CTS),1=等待(WAIT),2=溢出(OVFLW)
BS (Block Size):发送端每发 BS 个 CF 后,需等待下一个 FC 才可继续(0 表示无限制)
STmin:两个连续帧之间的最小间隔时间(0~127 或 0xF1~0xF9 编码)
发送方(如诊断仪)想发送一个 20 字节的 UDS 消息:
发送 首帧 :PCI=0x10,长度=20,并携带前 6 字节数据。
接收方收到 FF 后,计算所需 CF 数量,准备接收缓冲区,并发送 流控制帧 (BS, STmin)。
发送方根据 FC 发送 连续帧 ,每帧 7 字节数据,直到全部发完。
比较维度
UDS (ISO 14229)
ISO 15765-2 (DoCAN)
OSI 层
应用层
传输层 / 网络层
关注点
诊断服务语义:做什么操作、数据含义、错误码
消息分段、流控制、CAN 帧打包与重组
数据单元
UDS 请求/响应(无长度上限)
N_PDU(单帧/首帧/连续帧/流控制帧)
关键参数
SID、子功能、DID、DTC、NRC
块大小(BS)、最小间隔(STmin)、序列号(SN)
独立性
可运行于其他传输层(如 DoIP、UART)
专为 CAN 设计,可传输任何上层数据(不仅是 UDS,也可以是 OBD、KWP2000)
标准文档
ISO 14229-1
ISO 15765-2
核心关系:UDS 消息被封装成 ISO 15765 的 N_PDU 载荷。没有 ISO 15765,UDS 无法穿越 CAN 总线的 8 字节限制;没有 UDS,ISO 15765 只是在传输无意义的字节流。
6. C++ 代码举例:实现 ISO 15765 打包器与 UDS 消息组装
以下代码演示:
UDSRequest类:构造一个简单的 UDS 请求(如 ReadDataByIdentifier)。ISO15765Packer类:将 UDS 请求字节流转换为多个 CAN 帧(vector)。ISO15765Unpacker类:将收到的 CAN 帧还原成 UDS 消息。
为了简洁,假设 CAN 帧结构为 {id, data[8], dlc},且使用标准 CAN ID(0x7DF 诊断请求,0x7E8 响应)。仅实现发送端打包逻辑。
6.1 基础数据结构
6.2 UDS 请求构造(应用层)#include
#include
#include
#include
struct CanFrame {
uint32_t id; // 标准 11 位 ID
uint8_t dlc; // 数据长度 (0~8)
uint8_t data[8];
};// 辅助打印
void printCanFrame(const CanFrame& frame) {
std::cout << "CAN ID: 0x" << std::hex << frame.id << " DLC:" << std::dec << (int)frame.dlc << " Data:";
for (int i = 0; i < frame.dlc; ++i) {
std::cout << " " << std::hex << (int)frame.data[i];
}
std::cout << std::dec << std::endl;
}
6.3 ISO 15765 打包器(将 UDS 消息转成 CAN 帧序列)class UDSRequest {
public:
UDSRequest(uint8_t sid, conststd::vector& data = {})
: m_sid(sid), m_data(data) {}
std::vector serialize() const {
std::vector msg;
msg.push_back(m_sid);
msg.insert(msg.end(), m_data.begin(), m_data.end());
return msg;
}
// 示例:构造读取 DID 的请求 (0x22)
static UDSRequest makeReadDataByIdentifier(uint16_t did) {
return UDSRequest(0x22, {static_cast(did >> 8), static_cast(did & 0xFF)});
}private:
uint8_t m_sid;
std::vector m_data;
};
6.4 使用示例class ISO15765Packer {
public:
// 输入:UDS 原始字节流(无 PCI)
// 输出:CAN 帧列表(请求 ID 固定为 0x7DF 示例)
static std::vector pack(const std::vector& udsMsg, uint32_t canId = 0x7DF) {
std::vector frames;
size_t len = udsMsg.size();
if (len <= 7) {
// 单帧模式
CanFrame sf;
sf.id = canId;
sf.dlc = static_cast(1 + len); // PCI + 数据长度
sf.data[0] = static_cast(0x00 | len); // 单帧 PCI
std::copy(udsMsg.begin(), udsMsg.end(), &sf.data[1]);
frames.push_back(sf);
} else {
// 多帧模式:首帧 + 连续帧
// 首帧
CanFrame ff;
ff.id = canId;
ff.dlc = 8; // 首帧总是 8 字节
uint16_t totalLen = static_cast(len);
ff.data[0] = 0x10 | ((totalLen >> 8) & 0x0F); // 高 4 位为 1,低 4 位为长度高 4 位
ff.data[1] = totalLen & 0xFF; // 长度低 8 位
size_t firstPayload = std::min((size_t)6, len); // 首帧携带 6 字节数据
std::copy(udsMsg.begin(), udsMsg.begin() + firstPayload, &ff.data[2]);
frames.push_back(ff);// 后续数据按 7 字节分块,生成连续帧
size_t offset = firstPayload;
uint8_t sn = 1; // 连续帧序列号从 1 开始
while (offset < len) {
CanFrame cf;
cf.id = canId;
cf.dlc = 8;
cf.data[0] = 0x20 | (sn & 0x0F); // 连续帧 PCI
size_t chunkSize = std::min((size_t)7, len - offset);
std::copy(udsMsg.begin() + offset, udsMsg.begin() + offset + chunkSize, &cf.data[1]);
// 如果不足 7 字节,剩余字节填 0x00(可选,但通常 DLC=8 会包含无用数据)
if (chunkSize < 7) {
std::fill(&cf.data[1 + chunkSize], &cf.data[8], 0x00);
}
frames.push_back(cf);
offset += chunkSize;
sn = (sn + 1) & 0x0F;
}
}
return frames;
}
};
int main() {
// 1. 构造 UDS 请求:读取 DID = 0xF190
auto udsReq = UDSRequest::makeReadDataByIdentifier(0xF190);
std::vector udsMsg = udsReq.serialize(); // 输出: 0x22 0xF1 0x90 (3 字节)
// 2. 打包成 CAN 帧(单帧)
auto frames = ISO15765Packer::pack(udsMsg, 0x7DF);
for (constauto& frame : frames) {
printCanFrame(frame);
}
// 3. 模拟一个较长的 UDS 消息(例如 20 字节的写数据请求)
std::vector longMsg(20, 0xAA); // 模拟 20 字节数据
longMsg[0] = 0x2E; // WriteDataByIdentifier SID
longMsg[1] = 0xF1;
longMsg[2] = 0x90;
// 剩余 17 字节为写入值
auto framesLong = ISO15765Packer::pack(longMsg, 0x7DF);
std::cout << "\nLong message (20 bytes) packed into " << framesLong.size() << " frames:\n";
for (constauto& frame : framesLong) {
printCanFrame(frame);
}return0;
}
预期输出:
CAN ID: 0x7df DLC:4 Data: 0 22 f1 90
Long message (20 bytes) packed into 4 frames:
CAN ID: 0x7df DLC:8 Data: 10 14 2e f1 90 aa aa aa // 首帧,总长度20
CAN ID: 0x7df DLC:8 Data: 21 aa aa aa aa aa aa aa // CF SN=1
CAN ID: 0x7df DLC:8 Data: 22 aa aa aa aa aa aa aa // CF SN=2
CAN ID: 0x7df DLC:8 Data: 23 aa aa aa aa aa aa aa // CF SN=3 (最后7字节,但剩余6字节,尾部补0)
6.5 ISO 15765 解包器(片段)实际 ECU 需要接收 CAN 帧并重组。以下展示核心逻辑:
class ISO15765Unpacker {
public:
// 输入:一个 CAN 帧,输出:是否完成一个完整的 UDS 消息
bool feed(const CanFrame& frame, std::vector& outUdsMsg) {
if (frame.dlc == 0) returnfalse;
uint8_t pciType = (frame.data[0] >> 4) & 0x0F;
switch (pciType) {
case0x0: { // 单帧
uint8_t len = frame.data[0] & 0x0F;
outUdsMsg.assign(&frame.data[1], &frame.data[1] + len);
returntrue;
}
case0x1: { // 首帧
uint16_t totalLen = ((frame.data[0] & 0x0F) << 8) | frame.data[1];
m_buffer.clear();
m_buffer.reserve(totalLen);
// 首帧携带 6 字节数据
size_t payloadLen = std::min((size_t)6, frame.dlc - 2);
m_buffer.insert(m_buffer.end(), &frame.data[2], &frame.data[2] + payloadLen);
m_expectedLen = totalLen;
m_nextSn = 1;
m_waitingForFC = false; // 本示例简化流控制,实际需要等待 FC
returnfalse;
}
case0x2: { // 连续帧
uint8_t sn = frame.data[0] & 0x0F;
if (sn != m_nextSn) {
// 序列号错误,可做错误处理
returnfalse;
}
size_t payloadLen = std::min((size_t)7, frame.dlc - 1);
m_buffer.insert(m_buffer.end(), &frame.data[1], &frame.data[1] + payloadLen);
m_nextSn = (m_nextSn + 1) & 0x0F;
if (m_buffer.size() >= m_expectedLen) {
outUdsMsg.swap(m_buffer);
returntrue;
}
returnfalse;
}
case0x3: // 流控制帧(发送端处理,接收端不处理)
default:
returnfalse;
}
}
private:
std::vector m_buffer;
uint16_t m_expectedLen = 0;
uint8_t m_nextSn = 0;
bool m_waitingForFC = false;
};
7. 实际应用中的重要参数与注意事项CAN ID 分配:诊断通信通常使用 11 位 ID,如物理寻址(特定 ECU)和功能寻址(广播给所有 ECU)。UDS 不规定 ID,ISO 15765 也不规定,通常由整车厂定义(例如 0x7DF 用于功能请求,0x7E8 为第一个 ECU 的响应)。
流控制参数:
**BS (Block Size)**:接收端可以限制连续帧个数,防止发送端淹没接收端缓冲区。
**STmin (Separation Time)**:两个连续帧之间的最小时间间隔,用于低速 ECU。
时间参数 P2 / P2*:UDS 应用层定义了 ECU 处理请求的最大响应时间(P2 = 50ms 典型值),ISO 15765 不涉及这个,但诊断仪必须实现超时管理。
多协议并存:UDS 也可以运行在 DoIP(以太网)、LIN、FlexRay 上,而 ISO 15765 专用于 CAN。因此 ISO 15765 可视为 UDS over CAN 的“运输层”。
UDS (ISO 14229) 是汽车诊断的“语言”,定义了丰富而统一的服务,让诊断工具与不同 ECU 能够互通。
ISO 15765 是这套语言在 CAN 总线上的“信使”,解决了 CAN 帧过小的限制,提供了可靠的分段、重组与流控制机制。
二者相辅相成:没有 UDS,ISO 15765 传输的只是无意义的字节;没有 ISO 15765,UDS 将无法穿越 CAN 总线的长度壁垒。
理解这两者的分工是开发任何基于 CAN 的诊断工具或 ECU 固件的基础。通过本文的 C++ 代码示例,读者可以亲手打包一个 UDS 请求,观察它如何被拆分成 CAN 帧,从而更直观地掌握汽车诊断协议栈的层次协作。在实际工程中,建议使用成熟的 ISO-TP 库(如 isotp-c)和 UDS 库,但掌握原理始终是调试复杂问题的关键。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.