![]()
一、UDS协议概述
UDS(Unified Diagnostic Services,统一诊断服务)是汽车电子领域广泛使用的诊断通信协议,由ISO 14229标准定义。它使外部诊断工具(Tester/Client)能够与车辆内部的电子控制单元(ECU/Server)进行标准化的请求-响应式通信,应用于车辆开发、生产测试、售后维修及软件刷写等场景。
UDS本质上是一系列服务的集合,运行在OSI模型的应用层(第7层)和会话层(第5层)。它独立于底层总线,可运行在CAN(DoCAN)、以太网(DoIP)、FlexRay(DoFR)等多种总线上。在CAN总线上实现UDS时,需要ISO 15765-2(CAN-TP)传输层协议的支持。
协议分层结构
┌──────────────────────────────────────┐
│ 应用层:UDS (ISO 14229) │ ← 诊断服务、报文格式
├──────────────────────────────────────┤
│ 传输层:ISO 15765-2 (CAN-TP) │ ← 分帧/重组、流控
├──────────────────────────────────────┤
│ 数据链路层:CAN (ISO 11898) │ ← CAN帧收发
├──────────────────────────────────────┤
│ 物理层:CAN总线硬件 │ ← 差分信号传输
└──────────────────────────────────────┘
ISO 14229定义了诊断服务的内容和交互逻辑,而ISO 15765-2则解决了“如何可靠送达”的问题——由于经典CAN每帧只能传输8字节数据,超过8字节的诊断报文需要被拆分重组。
二、UDS报文格式与寻址 2.1 报文格式
UDS明确定义了三种报文的基本格式:
请求报文 :
SID + 子功能/参数正响应报文 :
(SID + 0x40) + 参数负响应报文 :
0x7F + 请求SID + NRC
以读取数据服务(0x22)为例:
请求: 22 F1 90 → 读取DID 0xF190的数据(如VIN码)
正响应:62 F1 90 57 4F 4C 56 4F... → 62 = 0x22 + 0x40,后跟DID和数据
负响应:7F 22 31 → 0x7F表示负响应,0x22是被拒绝的服务,0x31表示"请求超出范围"
2.2 寻址模式UDS支持两种寻址方式:
物理寻址(1对1) :诊断仪针对单个ECU发起通信,如请求CAN ID为0x7E0,响应CAN ID为0x7E8。
功能寻址(1对N) :诊断仪向多个ECU同时发送请求,标准功能寻址ID为0x7DF,ECU不返回正响应(避免总线拥塞),仅异常时返回负响应。
ISO 14229定义了超过26种服务,常用核心服务如下:
SID
服务名称
功能说明
0x10
DiagnosticSessionControl
诊断会话控制(切换默认/编程/扩展会话)
0x11
ECUReset
ECU复位
0x14
ClearDiagnosticInformation
清除诊断故障码
0x19
ReadDTCInformation
读取故障码信息
0x22
ReadDataByIdentifier
按DID读取数据
0x27
SecurityAccess
安全访问(种子-密钥认证)
0x2E
WriteDataByIdentifier
按DID写入数据
0x31
RoutineControl
例程控制(启动/停止/查询结果)
0x34
RequestDownload
请求下载(Bootloader刷写)
0x36
TransferData
数据传输
0x37
RequestTransferExit
请求退出传输
0x3E
TesterPresent
诊断仪在线(维持会话)
四、ISO 15765-2传输层
当UDS报文长度超过CAN单帧载荷(经典CAN为7字节有效数据)时,需要ISO-TP进行分帧传输。ISO-TP定义了四种帧类型:
单帧(SF) :报文 ≤ 7字节(经典CAN),PCI高4位为0,低4位为数据长度。例如
03 22 F1 90表示3字节有效数据。首帧(FF) :多帧传输的第一帧,PCI高4位为1,低4位与第二字节组成12位总长度。例如
10 14 2E F1 90...表示总长20字节。连续帧(CF) :携带后续数据,PCI高4位为2,低4位为序列号(从1递增,循环到0)。
流控帧(FC) :接收方发送,控制发送方节奏,包含流状态(FS)、块大小(BS)和最小间隔时间(STmin)。
多帧传输流程为:发送方发送首帧(FF)→ 接收方回复流控帧(FC)→ 发送方按BS和STmin发送连续帧(CF)→ 接收方重组完整报文。
五、C++实现示例
以下实现了一个简化的UDS-on-CAN诊断客户端,涵盖了CAN-TP分帧解帧和UDS服务的发送接收。代码采用面向对象设计,实际项目中可将CAN收发替换为SocketCAN或PCAN等具体硬件接口。
5.1 数据结构定义
5.2 CAN-TP 传输层实现// uds_types.h
#pragma once
#include
#include
#include
#include
// CAN帧结构
struct CanFrame {
uint32_t id; // CAN ID (11位或29位)
uint8_t dlc; // 数据长度 (0~8)
uint8_t data[8] = {0}; // 数据字节
};
// ISO-TP 帧类型 (PCI高4位)
enum class IsoTpPci : uint8_t {
SingleFrame = 0x0,
FirstFrame = 0x1,
ConsecutiveFrame = 0x2,
FlowControl = 0x3
};
// 流控帧状态
enum class FlowStatus : uint8_t {
ContinueToSend = 0x0,
Wait = 0x1,
Overflow = 0x2
};
// UDS负响应码 (NRC)
enum class Nrc : uint8_t {
GeneralReject = 0x10,
ServiceNotSupported = 0x11,
SubFunctionNotSupported = 0x12,
BusyRepeatRequest = 0x21,
ConditionsNotCorrect = 0x22,
RequestOutOfRange = 0x31,
SecurityAccessDenied = 0x33,
InvalidKey = 0x35,
ExceedNumberOfAttempts = 0x36,
RequiredTimeDelayNotExpired = 0x37
};// UDS响应结构
struct UdsResponse {
bool isPositive; // 正响应或负响应
uint8_t sid; // 响应SID
uint8_t nrc = 0; // 负响应码(仅负响应时有效)
std::vector payload; // 响应数据(不含SID/NRC)
};
5.3 UDS 诊断客户端// can_tp.h
#pragma once
#include "uds_types.h"
#include
#include
class CanTp {
public:
// CAN发送回调 — 实际项目中绑定到硬件驱动
using CanSendFunc = std::function;
explicit CanTp(CanSendFunc sendFunc)
: sendFunc_(std::move(sendFunc)) {}
// 发送UDS报文(自动决定单帧或多帧)
// txId: 发送CAN ID, rxId: 接收CAN ID
bool sendUdsMessage(const std::vector& payload,
uint32_t txId, uint32_t rxId) {
if (payload.size() <= 7) {
return sendSingleFrame(payload, txId);
}
return sendMultiFrame(payload, txId, rxId);
}
// 接收并重组UDS报文 — 由CAN接收回调驱动
// 返回true表示已重组出完整UDS报文
bool onCanFrameReceived(const CanFrame& frame,
std::vector& outPayload) {
std::lock_guard lock(rxMutex_);
uint8_t pciType = (frame.data[0] >> 4) & 0x0F;
switch (static_cast (pciType)) {
case IsoTpPci::SingleFrame: {
uint8_t len = frame.data[0] & 0x0F;
outPayload.assign(frame.data + 1, frame.data + 1 + len);
return true;
}
case IsoTpPci::FirstFrame: {
uint16_t totalLen = ((frame.data[0] & 0x0F) << 8) | frame.data[1];
rxBuffer_.assign(frame.data + 2, frame.data + 8);
rxTotalLen_ = totalLen;
rxExpectedSeq_ = 1;
rxInProgress_ = true;
// 发送流控帧允许对方继续发送
sendFlowControl(frame.id, FlowStatus::ContinueToSend, 0, 0);
return false;
}
case IsoTpPci::ConsecutiveFrame: {
if (!rxInProgress_) return false;
uint8_t seq = frame.data[0] & 0x0F;
if (seq != rxExpectedSeq_) {
rxInProgress_ = false;
return false; // 序列号错误,丢弃
}
uint8_t remain = rxTotalLen_ - static_cast(rxBuffer_.size());
uint8_t copyLen = (remain < 7) ? remain : 7;
rxBuffer_.insert(rxBuffer_.end(),
frame.data + 1, frame.data + 1 + copyLen);
rxExpectedSeq_ = (rxExpectedSeq_ + 1) & 0x0F;
if (rxBuffer_.size() >= rxTotalLen_) {
outPayload = rxBuffer_;
rxInProgress_ = false;
return true;
}
return false;
}
case IsoTpPci::FlowControl:
default:
return false;
}
}
private:
bool sendSingleFrame(const std::vector& payload, uint32_t txId) {
CanFrame frame;
frame.id = txId;
frame.dlc = static_cast(payload.size() + 1);
frame.data[0] = static_cast(
(static_cast(IsoTpPci::SingleFrame) << 4) |
(payload.size() & 0x0F));
for (size_t i = 0; i < payload.size(); ++i)
frame.data[i + 1] = payload[i];
return sendFunc_(frame);
}
bool sendMultiFrame(const std::vector& payload,
uint32_t txId, uint32_t rxId) {
// 首帧
CanFrame ff;
ff.id = txId;
ff.dlc = 8;
ff.data[0] = static_cast(
(static_cast(IsoTpPci::FirstFrame) << 4) |
((payload.size() >> 8) & 0x0F));
ff.data[1] = static_cast(payload.size() & 0xFF);
for (size_t i = 0; i < 6 && i < payload.size(); ++i)
ff.data[i + 2] = payload[i];
if (!sendFunc_(ff)) return false;
// 此处应等待流控帧再发送CF,简化处理直接连续发送
size_t offset = 6;
uint8_t seq = 1;
while (offset < payload.size()) {
CanFrame cf;
cf.id = txId;
cf.dlc = 8;
cf.data[0] = static_cast(
(static_cast(IsoTpPci::ConsecutiveFrame) << 4) |
(seq & 0x0F));
for (size_t i = 0; i < 7 && offset < payload.size(); ++i)
cf.data[i + 1] = payload[offset++];
if (!sendFunc_(cf)) return false;
seq = (seq + 1) & 0x0F;
}
return true;
}
void sendFlowControl(uint32_t txId, FlowStatus fs,
uint8_t blockSize, uint8_t stMin) {
CanFrame fc;
fc.id = txId;
fc.dlc = 3;
fc.data[0] = static_cast(
(static_cast(IsoTpPci::FlowControl) << 4) |
(static_cast(fs) & 0x0F));
fc.data[1] = blockSize;
fc.data[2] = stMin;
sendFunc_(fc);
}CanSendFunc sendFunc_;
std::mutex rxMutex_;
std::vector rxBuffer_;
uint16_t rxTotalLen_ = 0;
uint8_t rxExpectedSeq_ = 0;
bool rxInProgress_ = false;
};
5.4 使用示例// uds_client.h
#pragma once
#include "can_tp.h"
#include
#include
class UdsClient {
public:
UdsClient(CanTp& canTp, uint32_t txId, uint32_t rxId)
: canTp_(canTp), txId_(txId), rxId_(rxId) {}
// 通用请求发送 — 自动识别正/负响应
UdsResponse request(std::vector req, int timeoutMs = 2000) {
lastResponseReceived_ = false;
lastResponse_ = {};
if (!canTp_.sendUdsMessage(req, txId_, rxId_)) {
return {false, req[0], 0x10, {}};
}
auto deadline = std::chrono::steady_clock::now() +
std::chrono::milliseconds(timeoutMs);
while (!lastResponseReceived_ &&
std::chrono::steady_clock::now() < deadline) {
std::this_thread::sleep_for(std::chrono::milliseconds(5));
}
return lastResponse_;
}
// 由外部CAN接收线程调用
void onCanFrame(const CanFrame& frame) {
std::vector payload;
if (canTp_.onCanFrameReceived(frame, payload) && payload.size() >= 1) {
parseUdsResponse(payload);
}
}
// ---- 常用服务封装 ----
// 0x10 诊断会话控制
bool diagnosticSessionControl(uint8_t sessionType) {
auto resp = request({0x10, sessionType});
return resp.isPositive && resp.sid == 0x50;
}
// 0x27 安全访问 — 请求种子
std::vector securityAccessSeed(uint8_t level) {
auto resp = request({0x27, level});
if (resp.isPositive && resp.sid == 0x67)
return resp.payload; // 返回种子
return {};
}
// 0x27 安全访问 — 发送密钥
bool securityAccessKey(uint8_t level, const std::vector& key) {
std::vector req = {0x27, static_cast(level + 1)};
req.insert(req.end(), key.begin(), key.end());
auto resp = request(req);
return resp.isPositive && resp.sid == 0x67;
}
// 0x22 按DID读取数据
std::vector readDataByIdentifier(uint16_t did) {
auto resp = request({0x22,
static_cast(did >> 8),
static_cast(did & 0xFF)});
if (resp.isPositive && resp.sid == 0x62)
return resp.payload; // 返回DID + 数据
return {};
}
// 0x2E 按DID写入数据
bool writeDataByIdentifier(uint16_t did,
const std::vector& data) {
std::vector req = {0x2E,
static_cast(did >> 8),
static_cast(did & 0xFF)};
req.insert(req.end(), data.begin(), data.end());
auto resp = request(req);
return resp.isPositive && resp.sid == 0x6E;
}
// 0x3E TesterPresent (功能寻址,抑制正响应)
bool testerPresent() {
auto resp = request({0x3E, 0x80}, 500); // 0x80 = suppressPosRsp
// 功能寻址且抑制响应时,无响应即为正常
return true;
}
// 0x11 ECU复位
bool ecuReset(uint8_t resetType = 0x01) {
auto resp = request({0x11, resetType});
return resp.isPositive && resp.sid == 0x51;
}
private:
void parseUdsResponse(const std::vector& data) {
UdsResponse resp;
if (data[0] == 0x7F) {
// 负响应: 7F + SID + NRC
resp.isPositive = false;
resp.sid = (data.size() > 1) ? data[1] : 0;
resp.nrc = (data.size() > 2) ? data[2] : 0;
} else {
resp.isPositive = true;
resp.sid = data[0];
resp.payload.assign(data.begin() + 1, data.end());
}
lastResponse_ = resp;
lastResponseReceived_ = true;
}CanTp& canTp_;
uint32_t txId_; // 物理寻址发送ID,如0x7E0
uint32_t rxId_; // 物理寻址接收ID,如0x7E8
UdsResponse lastResponse_;
bool lastResponseReceived_ = false;
};
六、工程实践要点// main.cpp
#include "uds_client.h"
#include
// 模拟CAN发送 — 实际项目中替换为硬件驱动
bool mockCanSend(const CanFrame& frame) {
printf("[TX] ID=0x%03X DLC=%d Data=", frame.id, frame.dlc);
for (int i = 0; i < frame.dlc; ++i)
printf("%02X ", frame.data[i]);
printf("\n");
return true;
}
int main() {
CanTp canTp(mockCanSend);
UdsClient client(canTp, 0x7E0, 0x7E8); // 物理寻址
// 1. 切换到扩展会话 (0x03)
printf("=== 切换到扩展会话 ===\n");
if (client.diagnosticSessionControl(0x03))
printf("会话切换成功\n");
// 2. 安全访问 — 请求种子
printf("\n=== 安全访问 ===\n");
auto seed = client.securityAccessSeed(0x01);
if (!seed.empty()) {
printf("收到种子 (%zu字节)\n", seed.size());
// 实际项目中根据OEM算法计算密钥
std::vector key(seed.size(), 0xAA); // 示例密钥
if (client.securityAccessKey(0x01, key))
printf("安全访问解锁成功\n");
}
// 3. 读取VIN码 (DID 0xF190)
printf("\n=== 读取VIN ===\n");
auto vinData = client.readDataByIdentifier(0xF190);
if (!vinData.empty()) {
// 跳过前2字节的DID回显,打印VIN字符串
std::string vin(vinData.begin() + 2, vinData.end());
printf("VIN: %s\n", vin.c_str());
}
// 4. 维持会话
client.testerPresent();return 0;
}
S3定时器与TesterPresent:非默认会话模式下,ECU维护S3定时器(通常5秒),若超时未收到任何诊断请求,自动退回默认会话。诊断仪需周期性发送3E 80维持会话。
流控帧处理:上述示例在发送多帧时未等待FC再发送CF,实际项目中需严格遵循ISO-TP的流控机制——收到FC后按BS(块大小)和STmin(最小间隔)发送,每发送BS个CF后等待新的FC。
寻址与CAN ID映射:典型的诊断CAN ID分配为——物理请求0x7E0~0x7E7,物理响应0x7E8~0x7EF,功能寻址0x7DF。ECU的OEM规范中会明确定义每个ECU使用的地址。
安全访问算法:种子-密钥算法由OEM定义,ECU端和诊断工具端需使用相同算法。常见算法包括固定异或、查表、AES等,通常固化在诊断规范中,需向OEM获取。
多帧传输的性能优化:CAN-TP的BS和STmin参数直接影响刷写速度。STmin可设置为0(最小间隔由硬件决定),BS可设较大值以减少FC帧数量,但需考虑ECU接收缓冲区的容量。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
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.