UDS 诊断协议:数据传输机制与 C++ 实现 结论
UDS(统一诊断服务)运行在 CAN 总线之上时,单条诊断消息最大可达 4095 字节,而经典 CAN 数据场仅 8 字节。ISO 15765-2(ISO-TP)通过单帧(SF) 与多帧(FF/CF/FC) 两套机制,在有限带宽上实现了诊断数据的分段与可靠重组。单帧适用于长度 ≤ 7 字节的消息,多帧则通过“首帧宣告长度 → 流控帧协商参数 → 连续帧逐块传输”的三方握手完成长消息传输。以下从协议机制到 C++ 工程实现展开说明。
一、协议栈位置与核心概念
UDS 遵循 OSI 分层模型,其中与 CAN 传输直接相关的两层是:
OSI 层
协议
职责
应用层
ISO 14229 (UDS)
诊断服务(如 0x22 读数据、0x2E 写数据)
传输层
ISO 15765-2 (ISO-TP)
分段、重组、流控
数据链路层
CAN 2.0A/B
8 字节数据场,11/29 位标识符
UDS 本质上是一系列诊断服务,每种服务有独立的服务标识符(SID),如 0x10 诊断会话控制、0x22 读数据、0x2E 写数据。肯定响应的 SID 为请求 SID + 0x40(例如 0x22 → 0x62),否定响应固定为 0x7F。
传输层协议数据单元(PDU)通过 CAN 数据场的第一个字节的高半字节(N_PCI) 区分帧类型:0x0 = 单帧,0x1 = 首帧,0x2 = 连续帧,0x3 = 流控帧。
二、单帧传输(Single Frame)
当 UDS 请求或响应的有效载荷总长度不超过 7 字节时,使用单帧一次性完成传输。
格式:
字节 0:SF 标识(高 4 位 = 0x0)+ 有效数据长度(低 4 位,1~7)
字节 1~7:应用层数据
示例: 请求读取 DID 0xF186(当前诊断会话数据标识符),UDS 报文为 22 F1 86(共 3 字节),构造出的 CAN 数据场为:
03 22 F1 86 00 00 00 00
其中 03 表示单帧携带 3 字节有效数据,22 F1 86 为 UDS 服务请求。
三、多帧传输机制
当有效载荷超过 7 字节时,进入多帧传输流程,使用三种帧类型协同完成。
3.1 首帧(First Frame, FF)
发送方先发出首帧,宣告总数据长度(12 位值,最大 4095),并携带前 6 字节数据。
格式:
字节 0:FF 标识(高 4 位 = 0x1)+ 总长度高 4 位
字节 1:总长度低 8 位
字节 2~7:前 6 字节应用层数据
3.2 流控帧(Flow Control, FC)接收方收到 FF 后,若缓冲区允许,回复流控帧告知发送方如何继续。
格式:
字节 0:FC 标识(高 4 位 = 0x3)+ 流状态(FS,低 4 位)
字节 1:块大小(BS)
字节 2:最小间隔时间(STmin)
FS :0 = 继续发送(CTS),1 = 等待(WT),2 = 溢出/中止(OVFLW)
BS :发送方在等待下一个 FC 之前可连续发送的 CF 数量。0 表示不再需要 FC,一次发完
STmin :连续帧之间的最小时间间隔,控制发送节奏
发送方收到 CTS 流控帧后,开始发送连续帧,每帧携带 7 字节数据。
格式:
字节 0:CF 标识(高 4 位 = 0x2)+ 序列号 SN(低 4 位,1~15 循环)
字节 1~7:后续数据
序列号从 1 开始递增,每发一帧加 1,超过 15 后回绕到 0。接收方通过 SN 检测丢帧。每发送完一个 BS 大小的块后,发送方暂停等待接收方发来下一个 FC 帧。
四、C++ 代码实现
以下实现基于经典 CAN(8 字节数据场),提供发送方的分段逻辑与接收方的重组逻辑。
4.1 发送方:ISO-TP 分段器
4.2 接收方:ISO-TP 重组器#include
#include
#include
#include
class IsoTpSender {
public:
using CanTxCallback = std::function& data)>;
void SetTxCallback(CanTxCallback cb) { txCb_ = std::move(cb); }
// 发送完整的 UDS 报文(自动选择 SF 或 MF)
void SendUdsMessage(uint32_t canId, const std::vector& udsPayload) {
if (udsPayload.empty()) return;
if (udsPayload.size() <= 7) {
SendSingleFrame(canId, udsPayload);
} else {
SendFirstFrame(canId, udsPayload);
}
}
private:
CanTxCallback txCb_;
void SendSingleFrame(uint32_t canId, const std::vector& payload) {
std::vector frame(8, 0x00);
frame[0] = static_cast(payload.size() & 0x0F); // PCI: 0x0N
std::copy(payload.begin(), payload.end(), frame.begin() + 1);
txCb_(canId, frame);
}
void SendFirstFrame(uint32_t canId, const std::vector& payload) {
uint16_t totalLen = static_cast(payload.size());
std::vector ff(8, 0x00);
// PCI 高4位=0x1, 低4位为长度高4位
ff[0] = 0x10 | static_cast((totalLen >> 8) & 0x0F);
ff[1] = static_cast(totalLen & 0xFF);
// 首帧携带前 6 字节数据
size_t copyLen = std::min(6, payload.size());
std::copy(payload.begin(), payload.begin() + copyLen, ff.begin() + 2);
txCb_(canId, ff);
// 实际工程中,此处应等待流控帧(FC)后再继续发送 CF。
// 以下为简化演示:假设接收方已回复 CTS(BS=0, STmin=0)
SendConsecutiveFrames(canId, payload, copyLen, 1);
}void SendConsecutiveFrames(uint32_t canId, const std::vector& payload,
size_t offset, uint8_t startSn) {
uint8_t sn = startSn;
while (offset < payload.size()) {
std::vector cf(8, 0x00);
cf[0] = 0x20 | (sn & 0x0F); // PCI: 0x2N
size_t chunk = std::min(7, payload.size() - offset);
std::copy(payload.begin() + offset, payload.begin() + offset + chunk, cf.begin() + 1);
txCb_(canId, cf);
offset += chunk;
sn = (sn + 1) & 0x0F; // 1~15 循环,跳过 0 亦可,此处简化为回绕到 0
}
}
};
#include
#include
#include
#include
class IsoTpReceiver {
public:
using UdsIndicationCallback = std::function& udsPayload)>;
using CanTxCallback = std::function& data)>;
void SetIndicationCallback(UdsIndicationCallback cb) { indCb_ = std::move(cb); }
void SetTxCallback(CanTxCallback cb) { txCb_ = std::move(cb); }
// 处理从 CAN 总线收到的原始数据帧
void OnCanFrameReceived(uint32_t canId, const std::vector& canData) {
if (canData.size() < 1) return;
uint8_t pciType = (canData[0] >> 4) & 0x0F;
switch (pciType) {
case 0x0: HandleSingleFrame(canId, canData); break;
case 0x1: HandleFirstFrame(canId, canData); break;
case 0x2: HandleConsecutiveFrame(canId, canData); break;
case 0x3: HandleFlowControl(canId, canData); break; // 发送方角色时使用
default: break;
}
}
private:
UdsIndicationCallback indCb_;
CanTxCallback txCb_;
// 多帧接收状态
std::vector rxBuffer_;
uint16_t expectedTotalLen_ = 0;
uint8_t lastSn_ = 0;
bool receivingMultiFrame_ = false;
void HandleSingleFrame(uint32_t canId, const std::vector& canData) {
uint8_t dataLen = canData[0] & 0x0F;
if (dataLen == 0 || dataLen > 7) return;
std::vector payload(canData.begin() + 1, canData.begin() + 1 + dataLen);
if (indCb_) indCb_(payload);
}
void HandleFirstFrame(uint32_t canId, const std::vector& canData) {
expectedTotalLen_ = ((canData[0] & 0x0F) << 8) | canData[1];
rxBuffer_.clear();
rxBuffer_.reserve(expectedTotalLen_);
// FF 携带前 6 字节数据(有效载荷部分,需考虑实际长度)
size_t copyLen = std::min(6, expectedTotalLen_);
rxBuffer_.insert(rxBuffer_.end(), canData.begin() + 2, canData.begin() + 2 + copyLen);
lastSn_ = 0;
receivingMultiFrame_ = true;
// 回复流控帧:CTS, BS=0(一次发完), STmin=0
SendFlowControl(canId, 0x00, 0x00, 0x00);
}
void HandleConsecutiveFrame(uint32_t canId, const std::vector& canData) {
if (!receivingMultiFrame_) return;
uint8_t sn = canData[0] & 0x0F;
// 检查序列号连续性(简化:期望 sn == lastSn_ + 1,回绕到 0 后继续)
uint8_t expectedSn = (lastSn_ + 1) & 0x0F;
if (sn != expectedSn && !(lastSn_ == 15 && sn == 0)) {
// 序列号异常,重置接收
receivingMultiFrame_ = false;
return;
}
size_t remaining = expectedTotalLen_ - rxBuffer_.size();
size_t copyLen = std::min(7, remaining);
rxBuffer_.insert(rxBuffer_.end(), canData.begin() + 1, canData.begin() + 1 + copyLen);
lastSn_ = sn;
if (rxBuffer_.size() >= expectedTotalLen_) {
receivingMultiFrame_ = false;
if (indCb_) indCb_(rxBuffer_);
}
}void SendFlowControl(uint32_t canId, uint8_t fs, uint8_t bs, uint8_t stmin) {
std::vector fc(8, 0x00);
fc[0] = 0x30 | (fs & 0x0F); // PCI: 0x3N
fc[1] = bs;
fc[2] = stmin;
if (txCb_) txCb_(canId, fc);
}
};
使用示例:
五、关键注意事项int main() {
IsoTpSender sender;
IsoTpReceiver receiver;
// 模拟 CAN 收发(实际应绑定 SocketCAN 或 CAN 驱动)
receiver.SetTxCallback([](uint32_t id, const std::vector& d) {
// 接收方发出的 FC 帧通过此回调发出
});
sender.SetTxCallback([](uint32_t id, const std::vector& d) {
// 实际工程中,这里调用 CAN 驱动发送 d 到总线
// 在联调时,可将此帧直接喂给 receiver.OnCanFrameReceived
});
receiver.SetIndicationCallback([](const std::vector& uds) {
// 处理完整的 UDS 报文
// 例如 0x62 F1 86 ... 表示对 0x22 F1 86 的肯定响应
});
// 发送一个超过 7 字节的 UDS 请求示例:0x2E F1 90 + 长数据
std::vector longRequest = {0x2E, 0xF1, 0x90, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08};
sender.SendUdsMessage(0x7DF, longRequest);return 0;
}
流控帧的时序约束:发送方在发出首帧后,必须等待接收方的流控帧才能继续发送连续帧。接收方未回复 FC 或回复等待(WT)帧时,发送方不得发送 CF。实际工程中需实现超时重传机制。
序列号检测:连续帧的 SN 从 1 开始,每帧递增,超过 15 后回绕到 0。接收方通过 SN 是否连续来判断是否有丢帧。若 SN 不连续,应终止当前重组并重置状态。
CAN FD 的差异:若使用 CAN FD(64 字节数据场),单帧可携带的载荷从 7 字节提升到 62 字节,多帧的帧结构需相应调整(首帧携带的数据量、连续帧的 chunk 大小均变化)。上述代码针对经典 CAN 8 字节场景,移植到 CAN FD 时需修改 copyLen 等常量。
SocketCAN 的 ISO-TP 支持:Linux 内核已内置 ISO-TP 协议栈,用户空间可通过 socket(PF_CAN, SOCK_DGRAM, CAN_ISOTP) 创建套接字,内核自动处理分段与重组,应用层只需读写完整数据场。但在嵌入式 ECU 或需精细控制时序的场景中,手动实现 ISO-TP 分段仍是常见做法。
热门跟贴