![]()
1. 引言
UDS (Unified Diagnostic Services,統一診斷服務) 是汽車電子領域最主流的診斷協議,通常運行于 CAN 總線之上,形成 UDSonCAN。然而 UDS 單條報文最大可達 4095 字節,而標準 CAN 數據場僅 8 字節(CAN FD 可達 64 字節,本文以經典 CAN 為例)。為了在有限帶寬上可靠傳輸大塊數據,ISO 15765-2(即傳輸層,TP)定義了分段與重組機制,通過單幀、首幀、連續幀和流控幀這四類協議數據單元(PDU),實現了從單幀到多幀的無縫傳輸。理解這一機制,是開發 UDS 診斷棧的基礎。
本文將深入淺出地剖析 UDSonCAN 的傳輸層核心機制,并用 C++ 代碼示例展示如何實現發送方與接收方的關鍵邏輯。
2. UDSonCAN 協議棧架構
UDSonCAN 遵循 OSI 模型分層:
應用層:UDS 服務(如 0x10 診斷會話控制、0x22 讀數據等)
傳輸層:ISO 15765-2,負責分段與重組
數據鏈路層:CAN 幀 (ID, DLC, 8 字節數據)
傳輸層 PDU 通過 CAN 數據場中的第一個字節(或前兩個字節)的N_PCI(協議控制信息)來區分幀類型。
幀類型
N_PCI 值 (高半字節)
縮寫
單幀
0x0
SF
完整消息 ≤ 7 字節
首幀
0x1
FF
多幀消息的第一幀,攜帶總長度
連續幀
0x2
CF
后續數據塊,帶序列號
流控幀
0x3
FC
接收方控制發送方節奏(塊大小、間隔時間)
3. 單幀傳輸(Single Frame)
當 UDS 請求或響應總長度≤ 7 字節時,使用單幀。格式如下:
字節0: SF 標識 (0x0) + 有效數據長度 (低4位)
字節1~7: 應用層數據 (最多7字節)
示例:請求22 F1 86(讀 DID 0xF186),長度 3 字節 → 單幀 PCI =0x03(高4位0,低4位3),CAN 數據為03 22 F1 86 00 00 00 00。
C++ 發送單幀示例:
4. 多幀傳輸機制(Multi-frame)#include
#include
// 假設底層發送CAN幀的函數
extern bool CanSendFrame(uint32_t canId, const std::vector& data);bool SendSingleFrame(uint32_t canId, const std::vector& udsData) {
if (udsData.size() > 7) return false; // 超長需多幀
std::vector canData(8, 0);
canData[0] = static_cast(udsData.size()); // 高4位0,低4位長度
std::copy(udsData.begin(), udsData.end(), canData.begin() + 1);
return CanSendFrame(canId, canData);
}
當 UDS 消息長度超過 7 字節時,發送方將其拆分為:
一個首幀(FF):告知總長度
若干個連續幀(CF):攜帶后續數據
接收方通過流控幀(FC)管理發送速率
字節0: FF標識 (0x1) + 總長度高4位
字節1: 總長度低8位
字節2~7: 前6字節數據
總長度是一個 12 位值(最大 4095),拆分到字節0低4位和字節1整個8位。
4.2 連續幀(Consecutive Frame)
字節0: CF標識 (0x2) + 序列號 SN (低4位, 0~15)
字節1~7: 后續7字節數據
序列號從 1 開始,每發一幀加 1,循環 0~15。用于檢測丟幀。
4.3 流控幀(Flow Control)
接收方在收到 FF 后,若緩沖區允許,需發送 FC 告知發送方:
塊大小(BS):允許連續發送的 CF 幀數(0 表示無限)
最小間隔時間(STmin):兩幀 CF 之間的最小間隔(ms 或 100us 單位)
字節0: FC標識 (0x3) + 流控狀態 (通常0=繼續發送, 1=等待, 2=溢出)
字節1: 塊大小 BS
字節2: 最小間隔 STmin (編碼格式見標準)
字節3~7: 未使用,填0
5. 多幀傳輸時序與狀態機典型的多幀發送(請求讀取大量數據,如 0x22 讀長 VIN 碼):
診斷儀(發送方) ECU(接收方)
| |
|---- FF (總長度=50, 前6字節) ---->|
| | 解析FF,分配緩沖區
|<---- FC (BS=5, STmin=10ms) ------|
| |
|---- CF (SN=1, 7字節) ----------->|
| (等待10ms) |
|---- CF (SN=2, 7字節) ----------->|
| (等待10ms) |
|---- CF (SN=3, 7字節) ----------->|
| (等待10ms) |
|---- CF (SN=4, 7字節) ----------->|
| (等待10ms) |
|---- CF (SN=5, 7字節) ----------->| 收到5幀后
| | 發送新的FC
|<---- FC (BS=5, STmin=10ms) ------|
|---- CF (SN=6, 7字節) ----------->|
... ...
接收方狀態機簡化如下:
WAIT_FF:等待首幀,超時則中止
WAIT_CF:等待連續幀,并計數,每 BS 幀后等待新 FC
WAIT_FC(發送方視角):發送完 FF 后等待 FC,超時重試或中止
以下代碼演示了發送方如何將任意長度的 UDS 消息分段發送,以及接收方如何重組為完整消息。為簡潔起見,省略了超時、錯誤重傳等細節,僅突出核心機制。
6.1 發送方類UdsTpSender
6.2 接收方類#include
#include
#include
#include
#includeclass UdsTpSender {
public:
using CanTxFunc = std::function& data)>;
UdsTpSender(CanTxFunc txFunc) : txFunc_(txFunc) {}
// 發送UDS消息(可能拆分為多幀)
bool Send(uint32_t canId, const std::vector& udsData) {
if (udsData.size() <= 7) {
return SendSingleFrame(canId, udsData);
} else {
return SendMultiFrame(canId, udsData);
}
}
private:
bool SendSingleFrame(uint32_t canId, const std::vector& data) {
std::vector canData(8, 0);
canData[0] = static_cast(data.size()); // SF PCI
std::copy(data.begin(), data.end(), canData.begin() + 1);
return txFunc_(canId, canData);
}
bool SendMultiFrame(uint32_t canId, const std::vector& udsData) {
// 1. 發送首幀 (FF)
uint16_t totalLen = static_cast(udsData.size());
std::vector ffData(8, 0);
ffData[0] = 0x10 | ((totalLen >> 8) & 0x0F); // 高4位=1, 低4位=長度高4位
ffData[1] = totalLen & 0xFF;
// 拷貝前6字節數據到 ffData[2..7]
size_t firstChunk = std::min(6, udsData.size());
std::copy(udsData.begin(), udsData.begin() + firstChunk, ffData.begin() + 2);
if (!txFunc_(canId, ffData)) return false;
// 2. 等待接收方流控幀 (實際應使用異步接收回調,這里簡化為同步獲取)
// 實際項目中需要配合接收隊列和超時機制。此處僅展示收到FC后的行為。
// 假設我們通過回調得到FC參數: bs, stmin
// 本示例模擬一個默認FC: BS=0(無限), STmin=0(無間隔)
uint8_t blockSize = 0; // 0表示無限
uint8_t stMin = 0; // 0ms
// 偽代碼:實際應等待接收FC幀,解析其字節1和字節2
// WaitForFlowControl(bs, stmin);
// 3. 發送連續幀
size_t offset = firstChunk;
uint8_t seqNum = 1;
while (offset < udsData.size()) {
// 如果 blockSize > 0,需要每發送 blockSize 幀后等待新的FC
// 本示例簡化:連續發完所有CF
std::vector cfData(8, 0);
cfData[0] = 0x20 | (seqNum & 0x0F); // CF PCI + 序列號
size_t copySize = std::min(7, udsData.size() - offset);
std::copy(udsData.begin() + offset, udsData.begin() + offset + copySize,
cfData.begin() + 1);
if (!txFunc_(canId, cfData)) return false;
offset += copySize;
seqNum = (seqNum + 1) & 0x0F;
// 遵守STmin延時
if (stMin > 0) {
std::this_thread::sleep_for(std::chrono::milliseconds(stMin));
}
}
return true;
}
CanTxFunc txFunc_;
};
UdsTpReceiver接收方需要維護多個會話(不同 CAN ID 可能同時多幀傳輸),為簡明,僅處理單會話。
class UdsTpReceiver {
public:
enum State { WAIT_FF, WAIT_CF };
void OnCanFrame(uint32_t canId, const std::vector& canData) {
if (canData.empty()) return;
uint8_t pci = canData[0] >> 4;
uint8_t lenLow = canData[0] & 0x0F;
switch (pci) {
case 0x0: { // 單幀
size_t dataLen = lenLow;
std::vector udsMsg(canData.begin() + 1, canData.begin() + 1 + dataLen);
OnCompleteMessage(canId, udsMsg);
break;
}
case 0x1: { // 首幀
uint16_t totalLen = (lenLow << 8) | canData[1];
// 前6字節數據位置 canData[2..7]
reassembled_.clear();
reassembled_.reserve(totalLen);
size_t firstChunk = std::min(6, totalLen);
reassembled_.insert(reassembled_.end(), canData.begin() + 2, canData.begin() + 2 + firstChunk);
expectedSeq_ = 1;
remaining_ = totalLen - firstChunk;
state_ = WAIT_CF;
// 發送流控幀 (BS=0無限, STmin=0)
SendFlowControl(canId, 0, 0);
break;
}
case 0x2: { // 連續幀
if (state_ != WAIT_CF) return;
uint8_t seq = lenLow;
if (seq != expectedSeq_) {
// 序列號錯誤,可發送溢出流控或忽略
return;
}
size_t copySize = std::min(7, remaining_);
reassembled_.insert(reassembled_.end(), canData.begin() + 1, canData.begin() + 1 + copySize);
remaining_ -= copySize;
expectedSeq_ = (expectedSeq_ + 1) & 0x0F;
if (remaining_ == 0) {
OnCompleteMessage(canId, reassembled_);
state_ = WAIT_FF;
}
break;
}
case 0x3: // 流控幀(接收方不應收到主動發送的FC,除非作為發送方)
default:
break;
}
}
void SetMessageCallback(std::function&)> cb) {
onComplete_ = cb;
}
private:
void SendFlowControl(uint32_t canId, uint8_t blockSize, uint8_t stMin) {
std::vector fc(8, 0);
fc[0] = 0x30; // FC PCI + flowStatus=0(繼續發送)
fc[1] = blockSize;
fc[2] = stMin;
// 實際需要調用底層發送,此處略
// CanSendFrame(canId, fc);
}
State state_ = WAIT_FF;
std::vector reassembled_;
uint8_t expectedSeq_;
size_t remaining_;
std::function&)> onComplete_;
};
6.3 集成示例7. 關鍵注意事項#includeint main() {
auto txFunc = [](uint32_t id, const std::vector& data) {
std::cout << "Tx CAN ID 0x" << std::hex << id << ": ";
for (auto b : data) printf("%02X ", b);
std::cout << std::endl;
return true;
};
UdsTpSender sender(txFunc);
std::vector longUds = {0x22, 0xF1, 0x86, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07};
sender.Send(0x7DF, longUds);
// 接收側模擬
UdsTpReceiver receiver;
receiver.SetMessageCallback([](uint32_t id, const std::vector& msg) {
std::cout << "Rcvd complete UDS (" << msg.size() << " bytes): ";
for (auto b : msg) printf("%02X ", b);
std::cout << std::endl;
});
// 模擬收到首幀和連續幀...
// receiver.OnCanFrame(0x7DF, {0x10, 0x0A, 0x22, 0xF1, 0x86, 0x01, 0x02, 0x03});
// receiver.OnCanFrame(0x7DF, {0x21, 0x04, 0x05, 0x06, 0x07, 0x00, 0x00, 0x00});
return 0;
}
流控幀的 STmin 編碼:ISO 15765-2 定義了多種間隔值,如 0x00~0x7F 表示 0~127 ms,0xF1 表示 100 μs 等。實現時需解析并精確延時。
多會話并發:不同 CAN ID 或不同診斷會話可能同時傳輸多幀,接收方需按
(canId, sourceAddr)維護獨立的重組緩沖區。超時處理:發送 FF 后等待 FC 超時(通常 1000ms),接收方等待 CF 超時(通常 100ms),需有定時器機制。
錯誤恢復:序列號錯誤、緩沖區溢出時應發送 FC 狀態 = 溢出(0x2)或中止多幀傳輸。
UDSonCAN 的單幀與多幀傳輸機制,通過精巧的四類 PCI 類型和流控握手,實現了有限帶寬下的可靠大塊數據傳輸。理解 SF、FF、CF、FC 的含義及狀態轉換,是開發車載診斷工具、ECU 固件或仿真器的基礎。本文提供的 C++ 核心代碼展示了發送與重組的基本骨架,實際產品中還需加入超時管理、多會話并發、錯誤恢復等模塊,但萬變不離其宗——ISO 15765-2 定義的這一套簡單而強大的協議,正是汽車診斷可靠性的基石。
掌握從單幀到多幀的“傳輸奧秘”,你將能輕松應對各種 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.