![]()
1. 引言
UDS (Unified Diagnostic Services,統(tǒng)一診斷服務(wù)) 是汽車電子領(lǐng)域最主流的診斷協(xié)議,通常運行于 CAN 總線之上,形成 UDSonCAN。然而 UDS 單條報文最大可達 4095 字節(jié),而標準 CAN 數(shù)據(jù)場僅 8 字節(jié)(CAN FD 可達 64 字節(jié),本文以經(jīng)典 CAN 為例)。為了在有限帶寬上可靠傳輸大塊數(shù)據(jù),ISO 15765-2(即傳輸層,TP)定義了分段與重組機制,通過單幀、首幀、連續(xù)幀和流控幀這四類協(xié)議數(shù)據(jù)單元(PDU),實現(xiàn)了從單幀到多幀的無縫傳輸。理解這一機制,是開發(fā) UDS 診斷棧的基礎(chǔ)。
本文將深入淺出地剖析 UDSonCAN 的傳輸層核心機制,并用 C++ 代碼示例展示如何實現(xiàn)發(fā)送方與接收方的關(guān)鍵邏輯。
2. UDSonCAN 協(xié)議棧架構(gòu)
UDSonCAN 遵循 OSI 模型分層:
應(yīng)用層:UDS 服務(wù)(如 0x10 診斷會話控制、0x22 讀數(shù)據(jù)等)
傳輸層:ISO 15765-2,負責(zé)分段與重組
數(shù)據(jù)鏈路層:CAN 幀 (ID, DLC, 8 字節(jié)數(shù)據(jù))
傳輸層 PDU 通過 CAN 數(shù)據(jù)場中的第一個字節(jié)(或前兩個字節(jié))的N_PCI(協(xié)議控制信息)來區(qū)分幀類型。
幀類型
N_PCI 值 (高半字節(jié))
縮寫
單幀
0x0
SF
完整消息 ≤ 7 字節(jié)
首幀
0x1
FF
多幀消息的第一幀,攜帶總長度
連續(xù)幀
0x2
CF
后續(xù)數(shù)據(jù)塊,帶序列號
流控幀
0x3
FC
接收方控制發(fā)送方節(jié)奏(塊大小、間隔時間)
3. 單幀傳輸(Single Frame)
當(dāng) UDS 請求或響應(yīng)總長度≤ 7 字節(jié)時,使用單幀。格式如下:
字節(jié)0: SF 標識 (0x0) + 有效數(shù)據(jù)長度 (低4位)
字節(jié)1~7: 應(yīng)用層數(shù)據(jù) (最多7字節(jié))
示例:請求22 F1 86(讀 DID 0xF186),長度 3 字節(jié) → 單幀 PCI =0x03(高4位0,低4位3),CAN 數(shù)據(jù)為03 22 F1 86 00 00 00 00。
C++ 發(fā)送單幀示例:
4. 多幀傳輸機制(Multi-frame)#include
#include
// 假設(shè)底層發(fā)送CAN幀的函數(shù)
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);
}
當(dāng) UDS 消息長度超過 7 字節(jié)時,發(fā)送方將其拆分為:
一個首幀(FF):告知總長度
若干個連續(xù)幀(CF):攜帶后續(xù)數(shù)據(jù)
接收方通過流控幀(FC)管理發(fā)送速率
字節(jié)0: FF標識 (0x1) + 總長度高4位
字節(jié)1: 總長度低8位
字節(jié)2~7: 前6字節(jié)數(shù)據(jù)
總長度是一個 12 位值(最大 4095),拆分到字節(jié)0低4位和字節(jié)1整個8位。
4.2 連續(xù)幀(Consecutive Frame)
字節(jié)0: CF標識 (0x2) + 序列號 SN (低4位, 0~15)
字節(jié)1~7: 后續(xù)7字節(jié)數(shù)據(jù)
序列號從 1 開始,每發(fā)一幀加 1,循環(huán) 0~15。用于檢測丟幀。
4.3 流控幀(Flow Control)
接收方在收到 FF 后,若緩沖區(qū)允許,需發(fā)送 FC 告知發(fā)送方:
塊大小(BS):允許連續(xù)發(fā)送的 CF 幀數(shù)(0 表示無限)
最小間隔時間(STmin):兩幀 CF 之間的最小間隔(ms 或 100us 單位)
字節(jié)0: FC標識 (0x3) + 流控狀態(tài) (通常0=繼續(xù)發(fā)送, 1=等待, 2=溢出)
字節(jié)1: 塊大小 BS
字節(jié)2: 最小間隔 STmin (編碼格式見標準)
字節(jié)3~7: 未使用,填0
5. 多幀傳輸時序與狀態(tài)機典型的多幀發(fā)送(請求讀取大量數(shù)據(jù),如 0x22 讀長 VIN 碼):
診斷儀(發(fā)送方) ECU(接收方)
| |
|---- FF (總長度=50, 前6字節(jié)) ---->|
| | 解析FF,分配緩沖區(qū)
|<---- FC (BS=5, STmin=10ms) ------|
| |
|---- CF (SN=1, 7字節(jié)) ----------->|
| (等待10ms) |
|---- CF (SN=2, 7字節(jié)) ----------->|
| (等待10ms) |
|---- CF (SN=3, 7字節(jié)) ----------->|
| (等待10ms) |
|---- CF (SN=4, 7字節(jié)) ----------->|
| (等待10ms) |
|---- CF (SN=5, 7字節(jié)) ----------->| 收到5幀后
| | 發(fā)送新的FC
|<---- FC (BS=5, STmin=10ms) ------|
|---- CF (SN=6, 7字節(jié)) ----------->|
... ...
接收方狀態(tài)機簡化如下:
WAIT_FF:等待首幀,超時則中止
WAIT_CF:等待連續(xù)幀,并計數(shù),每 BS 幀后等待新 FC
WAIT_FC(發(fā)送方視角):發(fā)送完 FF 后等待 FC,超時重試或中止
以下代碼演示了發(fā)送方如何將任意長度的 UDS 消息分段發(fā)送,以及接收方如何重組為完整消息。為簡潔起見,省略了超時、錯誤重傳等細節(jié),僅突出核心機制。
6.1 發(fā)送方類UdsTpSender
6.2 接收方類#include
#include
#include
#include
#includeclass UdsTpSender {
public:
using CanTxFunc = std::function& data)>;
UdsTpSender(CanTxFunc txFunc) : txFunc_(txFunc) {}
// 發(fā)送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. 發(fā)送首幀 (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字節(jié)數(shù)據(jù)到 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. 等待接收方流控幀 (實際應(yīng)使用異步接收回調(diào),這里簡化為同步獲取)
// 實際項目中需要配合接收隊列和超時機制。此處僅展示收到FC后的行為。
// 假設(shè)我們通過回調(diào)得到FC參數(shù): bs, stmin
// 本示例模擬一個默認FC: BS=0(無限), STmin=0(無間隔)
uint8_t blockSize = 0; // 0表示無限
uint8_t stMin = 0; // 0ms
// 偽代碼:實際應(yīng)等待接收FC幀,解析其字節(jié)1和字節(jié)2
// WaitForFlowControl(bs, stmin);
// 3. 發(fā)送連續(xù)幀
size_t offset = firstChunk;
uint8_t seqNum = 1;
while (offset < udsData.size()) {
// 如果 blockSize > 0,需要每發(fā)送 blockSize 幀后等待新的FC
// 本示例簡化:連續(xù)發(fā)完所有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字節(jié)數(shù)據(jù)位置 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;
// 發(fā)送流控幀 (BS=0無限, STmin=0)
SendFlowControl(canId, 0, 0);
break;
}
case 0x2: { // 連續(xù)幀
if (state_ != WAIT_CF) return;
uint8_t seq = lenLow;
if (seq != expectedSeq_) {
// 序列號錯誤,可發(fā)送溢出流控或忽略
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: // 流控幀(接收方不應(yīng)收到主動發(fā)送的FC,除非作為發(fā)送方)
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(繼續(xù)發(fā)送)
fc[1] = blockSize;
fc[2] = stMin;
// 實際需要調(diào)用底層發(fā)送,此處略
// CanSendFrame(canId, fc);
}
State state_ = WAIT_FF;
std::vector reassembled_;
uint8_t expectedSeq_;
size_t remaining_;
std::function&)> onComplete_;
};
6.3 集成示例7. 關(guān)鍵注意事項#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);
// 接收側(cè)模擬
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;
});
// 模擬收到首幀和連續(xù)幀...
// 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 等。實現(xiàn)時需解析并精確延時。
多會話并發(fā):不同 CAN ID 或不同診斷會話可能同時傳輸多幀,接收方需按
(canId, sourceAddr)維護獨立的重組緩沖區(qū)。超時處理:發(fā)送 FF 后等待 FC 超時(通常 1000ms),接收方等待 CF 超時(通常 100ms),需有定時器機制。
錯誤恢復(fù):序列號錯誤、緩沖區(qū)溢出時應(yīng)發(fā)送 FC 狀態(tài) = 溢出(0x2)或中止多幀傳輸。
UDSonCAN 的單幀與多幀傳輸機制,通過精巧的四類 PCI 類型和流控握手,實現(xiàn)了有限帶寬下的可靠大塊數(shù)據(jù)傳輸。理解 SF、FF、CF、FC 的含義及狀態(tài)轉(zhuǎn)換,是開發(fā)車載診斷工具、ECU 固件或仿真器的基礎(chǔ)。本文提供的 C++ 核心代碼展示了發(fā)送與重組的基本骨架,實際產(chǎn)品中還需加入超時管理、多會話并發(fā)、錯誤恢復(fù)等模塊,但萬變不離其宗——ISO 15765-2 定義的這一套簡單而強大的協(xié)議,正是汽車診斷可靠性的基石。
掌握從單幀到多幀的“傳輸奧秘”,你將能輕松應(yīng)對各種 UDS 診斷開發(fā)場景。
特別聲明:以上內(nèi)容(如有圖片或視頻亦包括在內(nèi))為自媒體平臺“網(wǎng)易號”用戶上傳并發(fā)布,本平臺僅提供信息存儲服務(wù)。
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.