打开网易新闻 查看精彩图片

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++ 发送单帧示例

#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);
}
4. 多帧传输机制(Multi-frame)

当 UDS 消息长度超过 7 字节时,发送方将其拆分为:

  • 一个首帧(FF):告知总长度

  • 若干个连续帧(CF):携带后续数据

  • 接收方通过流控帧(FC)管理发送速率

4.1 首帧(First Frame)

字节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,超时重试或中止

6. C++ 代码示例:实现发送方和接收方核心逻辑

以下代码演示了发送方如何将任意长度的 UDS 消息分段发送,以及接收方如何重组为完整消息。为简洁起见,省略了超时、错误重传等细节,仅突出核心机制。

6.1 发送方类UdsTpSender

#include  

#include
#include
#include
#include


class 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_;
};
6.2 接收方类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 集成示例

#include  



int 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;
}
7. 关键注意事项
  • 流控帧的 STmin 编码:ISO 15765-2 定义了多种间隔值,如 0x00~0x7F 表示 0~127 ms,0xF1 表示 100 µs 等。实现时需解析并精确延时。

  • 多会话并发:不同 CAN ID 或不同诊断会话可能同时传输多帧,接收方需按(canId, sourceAddr)维护独立的重组缓冲区。

  • 超时处理:发送 FF 后等待 FC 超时(通常 1000ms),接收方等待 CF 超时(通常 100ms),需有定时器机制。

  • 错误恢复:序列号错误、缓冲区溢出时应发送 FC 状态 = 溢出(0x2)或中止多帧传输。

8. 总结

UDSonCAN 的单帧与多帧传输机制,通过精巧的四类 PCI 类型和流控握手,实现了有限带宽下的可靠大块数据传输。理解 SF、FF、CF、FC 的含义及状态转换,是开发车载诊断工具、ECU 固件或仿真器的基础。本文提供的 C++ 核心代码展示了发送与重组的基本骨架,实际产品中还需加入超时管理、多会话并发、错误恢复等模块,但万变不离其宗——ISO 15765-2 定义的这一套简单而强大的协议,正是汽车诊断可靠性的基石。

掌握从单帧到多帧的“传输奥秘”,你将能轻松应对各种 UDS 诊断开发场景。