1. 引言
在当代汽车电子与嵌入式系统中,UDS(Unified Diagnostic Services,统一诊断服务)已成为 ECU(电子控制单元)诊断、软件刷写、生产线终端测试及售后维护的核心协议。它基于 ISO 14229 标准,定义了一套与传输层无关的、面向应用的诊断消息格式与服务流程。无论是传统燃油车还是新能源车,UDS 都是 OBD(车载诊断)之上的“高阶语言”,也是嵌入式软件工程师面试中的高频考点。
本文将从协议分层、服务类型、帧结构、关键流程(安全访问、DTC、会话管理)到底层传输层 CAN-TP(ISO 15765-2),进行系统性的专业讲解,并辅以可落地的 C++ 代码示例,帮助读者将抽象规范转化为实际工程能力。
2. UDS 协议的层次结构
UDS 遵循 OSI 七层模型,但其核心定义在应用层(第 7 层),而具体物理/数据链路层则依赖底层总线(如 CAN、LIN、FlexRay、Ethernet)。常见组合如下:
OSI 层
协议/标准
作用
ISO 14229-1 (UDS)
定义诊断服务、子功能、数据格式
表示/会话层
通常合并到应用层或由传输层处理
传输层
ISO 15765-2 (CAN-TP)
分段/重组、流控(针对 CAN)
网络层
无(CAN 直接寻址)
数据链路层
ISO 11898-1/2 (CAN)
CAN 帧结构、仲裁、错误检测
物理层
ISO 11898-2/3
电气特性、速率
关键点:UDS 服务(如 0x10 诊断会话控制)作为 SID(Service Identifier)位于应用层报文首字节,而底层 CAN-TP 通过 PCI(Protocol Control Information)负责长报文的分包与流量控制,两者协同完成端到端诊断通信。3. UDS 常用服务(SID)概览
UDS 定义了超过 30 种服务,按功能分组。面试中最常涉及的服务及其 SID:
SID(十六进制)
服务名称
简述
0x10
DiagnosticSessionControl
切换诊断会话(默认/编程/扩展),影响服务可用性
0x11
ECUReset
执行 ECU 软复位/硬复位
0x14
ClearDiagnosticInformation
清除 DTC(故障码)
0x19
ReadDTCInformation
读取 DTC 状态、快照、扩展数据等(子功能丰富)
0x22
ReadDataByIdentifier
按 DID(数据标识符)读取数据(如 VIN、软件版本)
0x27
SecurityAccess
安全访问(种子-密钥认证),保护敏感操作
0x2E
WriteDataByIdentifier
按 DID 写入数据(标定、配置)
0x34/36/37
RequestDownload / TransferData / RequestTransferExit
固件下载(Bootloader 刷写)
0x3E
TesterPresent
维持诊断会话活跃,防止超时回退默认会话
0x85
ControlDTCSetting
控制 DTC 记录开启/关闭(用于刷写期间避免误报)
4. UDS 帧类型与报文结构
UDS 使用请求/响应模型,每一帧都遵循固定的应用层格式。
4.1 请求帧(Request)
无子功能:
[SID] + [参数...]有子功能:
[SID] + [SubFunction] + [参数...]子功能最高位(bit 7)常称为 **
suppressPositiveResponse**,若为 1,则 ECU 不返回肯定响应(仅用于周期性查询以节省总线负载)。
无子功能:
[(SID + 0x40)] + [响应数据]有子功能:
[(SID + 0x40)] + [SubFunction] + [响应数据]
例如:0x10 0x02请求进入编程会话 → 肯定响应0x50 0x02。
4.3 否定响应(Negative Response)
固定格式:[0x7F] + [SID] + [NRC]
NRC(Negative Response Code)单字节,表明失败原因。
示例:0x7F 0x22 0x12表示读取 DID 服务因子功能不支持而失败。
5. 重要服务详解 5.1 安全访问(0x27)——种子密钥机制
安全访问用于保护刷写、标定改写等高危操作。流程如下:
请求种子:客户端发送
0x27 0x01(奇数子功能,级别 1 请求种子)。ECU 返回0x67 0x01 [Seed_Bytes](通常 4~8 字节随机数),并启动超时定时器。计算密钥:客户端使用预先约定的算法(如 AES、自定义异或+移位)对种子计算密钥。
发送密钥:客户端发送
0x27 0x02 [Key_Bytes](偶数子功能,级别 1 发送密钥)。验证:ECU 用相同算法计算期望密钥,比较。
0x35(Invalid Key)0x36(Exceeded Number of Attempts)——尝试次数超限,进入锁定态0x37(Required Time Delay Not Expired)——需等待冷却时间
成功:返回
0x67 0x02,解锁对应级别,后续允许访问受保护服务。失败:返回否定响应,常见 NRC:
防暴力破解设计:连续错误超限后,ECU 需冷启动或等待超时才能再次尝试。
5.2 读取 DTC 信息(0x19)
0x19是诊断中最复杂的服务,拥有 28 个子功能。常用子功能:
子功能
名称
用途
0x01
reportNumberOfDTCByStatusMask
按状态掩码统计 DTC 数量
0x02
reportDTCByStatusMask
列出所有满足状态掩码的 DTC 及其状态字节
0x04
reportDTCSnapshotIdentification
读取某个 DTC 的快照记录标识符
0x06
reportDTCSnapshotDataByIdentifier
读取指定 DTC 的快照数据(冻结帧)
0x0A
reportSupportedDTC
列出 ECU 支持的所有 DTC(不关心状态)
DTC 状态字节(Status Byte)是 8 位掩码,每一位代表一种老化/状态维度:
Bit
名称
含义
0
testFailed
当前诊断测试失败(瞬态)
1
testFailedThisOperationCycle
本驾驶循环内曾失败
2
pendingDTC
本循环内发生但尚未确认(等待老化)
3
confirmedDTC
已确认(故障灯 MIL 亮起的依据)
4
testNotCompletedSinceLastClear
自上次清除后测试未完成
5
testFailedSinceLastClear
自上次清除后曾失败
6
testNotCompletedThisOperationCycle
本循环内测试未完成
7
warningIndicatorRequested
请求点亮警告灯(如 MIL)
5.3 会话控制(0x10)与 TesterPresent(0x3E)
诊断会话:ECU 在不同会话下暴露不同的服务集。
默认会话 (0x01):基础诊断,如读取 DTC、读数据。
编程会话 (0x02):允许刷写,通常会禁止 DTC 记录并重置 ECU。
扩展会话 (0x03):允许 I/O 控制、例程执行等高级测试。
会话超时:若在
TsessionTimeout(典型 5s)内无任何诊断请求,ECU 自动回退至默认会话,以保证安全性。
因此,客户端需周期性发送0x3E 0x00(TesterPresent,子功能 0x00 表示无额外参数)来“保活”。ECU 肯定响应为0x7E 0x00。
物理寻址:CAN ID 唯一对应一个 ECU(如请求 0x7A1,响应 0x7A9),用于点对点操作(刷写、读取具体 ECU 信息)。
功能寻址:CAN ID 广播给一组或全部 ECU(如 0x7DF),多个 ECU 可同时响应(通常要求抑制部分响应以防冲突)。常用于全局查询(如所有 ECU 的 DTC 状态)。
当 UDS 报文长度超过单帧 CAN 的有效载荷(CAN 数据场最多 8 字节,但 UDS 应用层占用 PCI 后通常最多 7 字节数据)时,必须借助CAN-TP进行分包和重组。
CAN-TP 定义了四种协议控制信息(PCI)帧类型,位于每个 CAN 帧的首字节(或前两字节):
PCI 类型
高四位 (Nibble)
描述
单帧 SF
0
短报文(总长 ≤ 7),低四位表示数据长度。例:03 22 F1 90
首帧 FF
1
多帧传输的第一帧,低四位 + 后续字节共 12 位表示总长度(≤4095)。例:10 1A 62 F1 90 ...表示总长 26 字节
连续帧 CF
2
后续数据帧,低四位为序列号(从 1 开始,模 16)。例:21 ...,22 ...
流控帧 FC
3
接收方发送,用于控制发送速率。包含 FlowStatus(0=继续,1=等待,2=溢出)、BlockSize、STmin
多帧通信流程:
发送方发送FF,携带总长度。
接收方收到 FF 后,回复FC,告知:
FlowStatus= 0(允许发送)
BlockSize(BS):发送方在收到下一个 FC 前可连续发送的 CF 数量(0 表示不限)
SeparationTime(STmin):两个 CF 间的最小间隔(单位 ms)
发送方根据 FC 参数发送CF,直到发送完总长度。
若 BS > 0,发送方每发送完一个 Block 的 CF 后,必须等待新的 FC 才能继续。
实现要点:接收方需维护重组缓冲区,按 SN 顺序拼接数据;发送方需具备定时器和状态机以处理 FC 和超时。7. C++ 实战:设计与实现 UDS 协议栈核心模块
下面我们使用 C++ 设计一个轻量级 UDS 协议栈,包含:
UDS 消息解析与构造
CAN-TP 分包/组包器
会话与安全访问状态机框架
7.2 解析 UDS 请求(含子功能识别)#include
#include
#include
#include
// UDS 消息结构(应用层)
struct UdsMessage {
uint8_t sid; // 服务标识符
uint8_t subFunc; // 子功能(若存在)
std::vector data; // 参数或响应数据
bool hasSubFunction; // 是否包含子功能
};
// 否定响应码枚举
enum class Nrc : uint8_t {
Positive = 0x00,
GeneralReject = 0x10,
ServiceNotSupported = 0x11,
SubFunctionNotSupported = 0x12,
InvalidMessageLength = 0x13,
ConditionsNotCorrect = 0x22,
RequestSequenceError = 0x24,
InvalidKey = 0x35,
ExceededNumberOfAttempts = 0x36,
RequiredTimeDelayNotExpired = 0x37,
// ... 其他 NRC
};
// 构造肯定响应(无子功能)
std::vector BuildPositiveResponse(uint8_t sid, const std::vector& respData) {
std::vector msg;
msg.push_back(sid + 0x40);
msg.insert(msg.end(), respData.begin(), respData.end());
return msg;
}
// 构造肯定响应(有子功能)
std::vector BuildPositiveResponseWithSub(uint8_t sid, uint8_t subFunc, const std::vector& respData) {
std::vector msg;
msg.push_back(sid + 0x40);
msg.push_back(subFunc);
msg.insert(msg.end(), respData.begin(), respData.end());
return msg;
}// 构造否定响应
std::vector BuildNegativeResponse(uint8_t sid, Nrc nrc) {
return {0x7F, sid, static_cast(nrc)};
}
7.3 安全访问(0x27)简易实现// 解析请求,返回 UdsMessage 对象,并检查合法性
bool ParseUdsRequest(const std::vector& raw, UdsMessage& outMsg) {
if (raw.empty()) return false;
outMsg.sid = raw[0];
outMsg.hasSubFunction = false;
outMsg.subFunc = 0;
outMsg.data.clear();// 判断是否有子功能:通常根据 SID 定义,但简化判断:若长度 >=2 且第二字节 bit7 可能为子功能标志
// 真实实现需要查表,这里简单假设所有服务都可能有子功能(除 0x22 等少数)
if (raw.size() >= 2) {
// 有些服务固定无子功能,此处以实际定义为准(例如 0x22 无子功能)
if (outMsg.sid == 0x22 || outMsg.sid == 0x2E) {
// 无子功能,数据从第1字节开始
outMsg.data.assign(raw.begin() + 1, raw.end());
} else {
outMsg.hasSubFunction = true;
outMsg.subFunc = raw[1];
outMsg.data.assign(raw.begin() + 2, raw.end());
}
} else {
// 单字节请求(如 0x11 无子功能)
outMsg.data.clear();
}
return true;
}
7.4 CAN-TP 分包器(发送方)class SecurityAccess {
public:
enum Level { Level1 = 1, Level2 = 2 };
SecurityAccess() : locked_(true), attemptCounter_(0), unlockLevel_(0) {}
// 处理请求种子 (subFunc 奇数)
std::vector HandleSeedRequest(uint8_t subFunc) {
if (locked_ && attemptCounter_ >= 3) {
// 锁定状态,返回 NRC ExceededNumberOfAttempts
return BuildNegativeResponse(0x27, Nrc::ExceededNumberOfAttempts);
}
// 生成种子(伪随机)
uint8_t seed[4] = {0xA1, 0xB2, 0xC3, 0xD4}; // 实际应从 RNG 获取
currentSeed_ = {seed[0], seed[1], seed[2], seed[3]};
// 返回肯定响应:67 + subFunc + seed
std::vector resp = {0x67, subFunc};
resp.insert(resp.end(), currentSeed_.begin(), currentSeed_.end());
return resp;
}
// 处理发送密钥 (subFunc 偶数)
std::vector HandleKeySend(uint8_t subFunc, const std::vector& key) {
if (locked_) {
return BuildNegativeResponse(0x27, Nrc::ExceededNumberOfAttempts);
}
// 计算期望密钥(采用简单异或示例)
std::vector expectedKey;
for (auto b : currentSeed_) expectedKey.push_back(b ^ 0x55);
// 比较
if (key == expectedKey) {
attemptCounter_ = 0;
unlockLevel_ = (subFunc >> 1); // 简单映射
locked_ = false;
return {0x67, subFunc}; // 肯定响应
} else {
attemptCounter_++;
if (attemptCounter_ >= 3) locked_ = true;
return BuildNegativeResponse(0x27, Nrc::InvalidKey);
}
}
bool IsUnlocked() const { return !locked_; }private:
bool locked_;
int attemptCounter_;
int unlockLevel_;
std::vector currentSeed_;
};
7.5 CAN-TP 组包器(接收方)// 将长 UDS 报文拆分为 SF/FF/CF,并通过回调发送 CAN 帧
class CanTpSplitter {
public:
using CanTxCallback = std::function& frameData)>;
void SetTxCallback(CanTxCallback cb) { txCb_ = cb; }
// 发送 UDS 报文(含完整应用层数据)
void SendUdsMessage(uint32_t targetCanId, const std::vector& udsData) {
if (udsData.size() <= 7) {
// 单帧
std::vector sf;
sf.push_back(static_cast(udsData.size())); // PCI = 0x0N
sf.insert(sf.end(), udsData.begin(), udsData.end());
txCb_(targetCanId, sf);
} else {
// 多帧:首帧
uint16_t totalLen = udsData.size();
std::vector ff;
ff.push_back(0x10 | ((totalLen >> 8) & 0x0F)); // 高4位=1, 低4位为长度高4位
ff.push_back(totalLen & 0xFF);
// 复制前 6 字节数据(FF 已占 2 字节 PCI,剩余 6 字节载荷)
size_t copyLen = std::min(6, udsData.size());
ff.insert(ff.end(), udsData.begin(), udsData.begin() + copyLen);
txCb_(targetCanId, ff);
// 后续连续帧
size_t offset = copyLen;
uint8_t sn = 1;
while (offset < udsData.size()) {
std::vector cf;
cf.push_back(0x20 | (sn & 0x0F));
size_t chunk = std::min(7, udsData.size() - offset);
cf.insert(cf.end(), udsData.begin() + offset, udsData.begin() + offset + chunk);
txCb_(targetCanId, cf);
offset += chunk;
sn = (sn + 1) & 0x0F;
// 实际需考虑 FC 流控,这里简化:若 BS 限制则需等待
// 以下为模拟等待 FC,实际应异步处理
if (offset < udsData.size()) {
// 应当等待接收 FC 后继续,此处演示简单延时
std::this_thread::sleep_for(std::chrono::milliseconds(5));
}
}
}
}private:
CanTxCallback txCb_;
};
8. 综合示例:Bootloader 刷写流程(UDS 0x34/36/37)// 接收 CAN 帧,重组 UDS 报文,并在完整后回调
class CanTpReceiver {
public:
using UdsIndicationCallback = std::function& udsMsg)>;
void SetIndicationCallback(UdsIndicationCallback cb) { indCb_ = cb; }
// 处理收到的 CAN 帧(已剥离 CAN ID)
void OnCanFrameReceived(uint32_t srcCanId, const std::vector& frameData) {
if (frameData.empty()) return;
uint8_t pci = frameData[0];
uint8_t frameType = pci >> 4;
if (frameType == 0) { // 单帧
uint8_t len = pci & 0x0F;
std::vector uds(frameData.begin() + 1, frameData.begin() + 1 + len);
indCb_(srcCanId, uds);
} else if (frameType == 1) { // 首帧
uint16_t totalLen = ((pci & 0x0F) << 8) | frameData[1];
currentUds_.clear();
currentUds_.reserve(totalLen);
// 复制载荷(6 字节)
size_t copyLen = std::min(6, frameData.size() - 2);
currentUds_.insert(currentUds_.end(), frameData.begin() + 2, frameData.begin() + 2 + copyLen);
expectedSn_ = 1;
currentSrcId_ = srcCanId;
// 回复流控帧(FC)
SendFlowControl(srcCanId, 0, 0); // BS=0, STmin=0 表示无限制
} else if (frameType == 2) { // 连续帧
uint8_t sn = pci & 0x0F;
if (sn != expectedSn_) {
// 序列号错误,可发送溢出流控或丢弃
return;
}
currentUds_.insert(currentUds_.end(), frameData.begin() + 1, frameData.end());
expectedSn_ = (expectedSn_ + 1) & 0x0F;
// 若接收完成(实际需根据总长度判断)
if (currentUds_.size() >= currentUds_.capacity()) {
indCb_(currentSrcId_, currentUds_);
currentUds_.clear();
}
}
// 流控帧 (type=3) 为发送方处理,接收方无需处理
}
private:
void SendFlowControl(uint32_t targetId, uint8_t blockSize, uint8_t stMin) {
// 构造 FC 帧:0x30 + BS + STmin
std::vector fc = {0x30, blockSize, stMin};
// 假设可通过接口发送(模拟)
// canTx(targetId, fc);
}uint32_t currentSrcId_;
std::vector currentUds_;
uint8_t expectedSn_;
UdsIndicationCallback indCb_;
};
固件升级是 UDS 最典型的应用场景,顺序如下:
切换会话:
10 02→ 编程会话安全访问:
27 01/27 02→ 解锁请求下载:
34→ 告知下载地址和大小传输数据:循环
36→ 分块传输(每块通常 4096 字节,通过 CAN-TP 分包)请求结束传输:
37→ 完成下载校验程序:
31 01 FF00(例程控制)→ 执行 CRC 校验软复位:
11 01→ ECU 复位进入新程序
每一步都需要处理否定响应(如 NRC 0x33 安全访问未解锁),并设置超时重试机制。9. 总结
UDS 诊断协议是一个庞大但高度结构化的体系,掌握它需要理解:
应用层服务的语义(SID/子功能/NRC)
传输层多帧通信机制(CAN-TP 的 SF/FF/CF/FC 状态机)
安全与状态管理(会话切换、安全访问、DTC 老化)
在 C++ 实现中,应注重模块化:分离应用层解析器、传输层组包/拆包器、会话状态机,并借助回调/异步设计适应真实嵌入式环境(RTOS 或事件驱动)。
本文提供的代码框架可直接扩展为生产级诊断栈,但实际工程还需考虑:
超时与重传(应用层和传输层定时器)
错误恢复与日志
跨平台兼容性(端序、对齐)
希望本文能为读者的面试准备或项目开发提供系统性的知识图谱与实用代码参考。
参考资料:
ISO 14229-1:2020 Road vehicles — Unified diagnostic services (UDS)
ISO 15765-2:2016 Road vehicles — Diagnostic communication over Controller Area Network (DoCAN)
AUTOSAR Specification of Diagnostic Communication (SWS_DCM)
热门跟贴