![]()
UDS 节点识别(Node Identification)详解与 C++ 实现 一、什么是 UDS 节点识别
UDS(Unified Diagnostic Services,统一诊断服务)是 ISO 14229 标准定义的汽车诊断协议。在整车网络中,通常存在多个 ECU(电子控制单元),诊断仪(Tester)需要知道"我到底在跟哪个 ECU 说话",以及"网络上还有哪些节点"。
节点识别(Node Identification)就是让诊断设备能够发现、区分、确认网络上各个 ECU 身份的一组服务。它在多 ECU 环境中的价值体现在:
拓扑发现:识别总线上存在哪些 ECU
寻址确认:确认当前通信的目标节点
版本管理:读取各节点的软硬件版本、序列号
诊断路由:在多节点环境下正确分发诊断请求
SID
服务名
用途
0x22
ReadDataByIdentifier
通过 DID 读取 VIN、序列号、软件版本等
0x2E
WriteDataByIdentifier
写入 VIN 等识别信息
0x3E
TesterPresent
保持会话,配合节点在线检测
0x10
DiagnosticSessionControl
切换会话(识别有时需要扩展会话)
0x31
RoutineControl
触发某些节点自检例程
0x27
SecurityAccess
受保护的识别信息需先解锁
常用识别 DID(数据标识符)
DID
含义
0xF190
VIN 车辆识别码
17 位 ASCII
0xF18C
ECU 序列号
唯一标识该 ECU
0xF187
备件号
硬件零件号
0xF189
软件版本号
应用软件版本
0xF191
硬件版本号
硬件版本
0xF195
系统供应商标识
供应商代码
0xF186
当前诊断会话
用于确认在线状态
报文格式(请求/响应)
请求:
22 F1 90
│ └──── DID (2 bytes)
└─────── SID
肯定响应:
62 F1 90
│ │ └── 数据(如 17 字节 VIN)
│ └──────── 回显的 DID
└─────────── SID + 0x40
否定响应:
7F 22
│ │ └── 否定响应码(如 0x31 请求超范围)
│ └───── 原始 SID
└──────── 否定响应标识
三、C++ 实现示例下面用一个简洁但完整的示例,模拟诊断仪对多个 ECU 做节点识别:发送0x22读 VIN/序列号,解析响应,并汇总节点信息。
说明:为聚焦 UDS 逻辑,这里用接口 ITransport 抽象底层(CAN/DoIP),可替换为真实 SocketCAN / ISO-TP 实现。
运行输出#include
#include
#include
#include
#include
#include
#include
// ============================================================
// 1. 基础类型与常量
// ============================================================
using Bytes = std::vector;
namespace Uds {
constexpr uint8_t SID_ReadDataByIdentifier = 0x22;
constexpr uint8_t SID_PositiveOffset = 0x40;
constexpr uint8_t SID_NegativeResponse = 0x7F;
// 常用识别 DID
constexpr uint16_t DID_VIN = 0xF190;
constexpr uint16_t DID_SerialNumber = 0xF18C;
constexpr uint16_t DID_SoftwareVer = 0xF189;
constexpr uint16_t DID_HardwareVer = 0xF191;
}
// 否定响应码
enum class Nrc : uint8_t {
ServiceNotSupported = 0x11,
SubFunctionNotSupported = 0x12,
RequestOutOfRange = 0x31,
SecurityAccessDenied = 0x33,
ConditionsNotCorrect = 0x22,
Unknown = 0xFF
};
// ============================================================
// 2. 传输层抽象(真实项目替换为 CAN / DoIP)
// ============================================================
class ITransport {
public:
virtual ~ITransport() = default;
// 向 target 节点发送请求,返回响应原始字节(不含 ISO-TP 分帧)
virtual std::optional request(uint16_t targetAddr,
const Bytes& payload) = 0;
};
// ============================================================
// 3. UDS 客户端:封装节点识别相关服务
// ============================================================
class UdsClient {
public:
explicit UdsClient(ITransport& transport) : tp_(transport) {}
// 读取指定 DID 的原始数据
struct ReadResult {
bool success = false;
Bytes data; // 成功时的数据段
Nrc nrc = Nrc::Unknown; // 失败时的否定码
};
ReadResult readDataByIdentifier(uint16_t addr, uint16_t did) {
Bytes req = {
Uds::SID_ReadDataByIdentifier,
static_cast(did >> 8),
static_cast(did & 0xFF)
};
ReadResult result;
auto resp = tp_.request(addr, req);
if (!resp || resp->size() < 1) return result;
// 否定响应
if ((*resp)[0] == Uds::SID_NegativeResponse) {
if (resp->size() >= 3) result.nrc = static_cast ((*resp)[ 2]);
return result;
}
// 肯定响应: 0x62 DID_H DID_L
if ((*resp)[0] == (Uds::SID_ReadDataByIdentifier + Uds::SID_PositiveOffset)
&& resp->size() >= 3) {
uint16_t echoDid = (static_cast((*resp)[1]) << 8) | (*resp)[2];
if (echoDid == did) {
result.success = true;
result.data.assign(resp->begin() + 3, resp->end());
}
}
return result;
}
// 读取 VIN(17 字节 ASCII)
std::optional readVin(uint16_t addr) {
auto r = readDataByIdentifier(addr, Uds::DID_VIN);
if (!r.success || r.data.size() != 17) return std::nullopt;
return std::string(r.data.begin(), r.data.end());
}
// 读取序列号(按 ASCII 处理,实际可能是二进制)
std::optional readSerialNumber(uint16_t addr) {
auto r = readDataByIdentifier(addr, Uds::DID_SerialNumber);
if (!r.success) return std::nullopt;
return std::string(r.data.begin(), r.data.end());
}
private:
ITransport& tp_;
};
// ============================================================
// 4. 节点信息 & 节点发现器
// ============================================================
struct NodeInfo {
uint16_t address = 0;
std::string vin;
std::string serialNumber;
std::string softwareVersion;
bool online = false;
};
class NodeIdentifier {
public:
explicit NodeIdentifier(UdsClient& client) : client_(client) {}
// 遍历候选地址,识别在线节点
std::vector discover(const std::vector& candidateAddrs) {
std::vector found;
for (auto addr : candidateAddrs) {
NodeInfo info;
info.address = addr;
if (auto vin = client_.readVin(addr)) {
info.vin = *vin;
info.online = true;
}
if (auto sn = client_.readSerialNumber(addr)) {
info.serialNumber = *sn;
}
// 只有至少读到 VIN 或序列号才认为节点存在
if (info.online || !info.serialNumber.empty()) {
found.push_back(std::move(info));
}
}
return found;
}
private:
UdsClient& client_;
};
// ============================================================
// 5. 模拟传输层:用于演示节点识别流程
// ============================================================
class MockTransport : public ITransport {
public:
// 注册某地址的 DID -> 数据
void registerDid(uint16_t addr, uint16_t did, const Bytes& data) {
db_[addr][did] = data;
}
std::optional request(uint16_t targetAddr,
const Bytes& payload) override {
if (payload.size() < 3 || payload[0] != Uds::SID_ReadDataByIdentifier)
return std::nullopt;
uint16_t did = (static_cast(payload[1]) << 8) | payload[2];
auto nodeIt = db_.find(targetAddr);
if (nodeIt == db_.end()) return std::nullopt; // 无此节点(超时)
auto didIt = nodeIt->second.find(did);
if (didIt == nodeIt->second.end()) {
// 节点存在但不支持该 DID -> 否定响应
return Bytes{Uds::SID_NegativeResponse,
Uds::SID_ReadDataByIdentifier,
static_cast(Nrc::RequestOutOfRange)};
}
// 组装肯定响应: 62 DID_H DID_L
Bytes resp = {static_cast(Uds::SID_ReadDataByIdentifier + 0x40),
static_cast(did >> 8),
static_cast(did & 0xFF)};
resp.insert(resp.end(), didIt->second.begin(), didIt->second.end());
return resp;
}
private:
std::map> db_;
};
// ============================================================
// 6. 使用示例
// ============================================================
int main() {
MockTransport transport;
// ---- 模拟网络上的三个 ECU ----
// ECU A @ 0x7A0: 有完整信息
transport.registerDid(0x7A0, Uds::DID_VIN,
Bytes{'L','S','V','N','M','4','2','B','1','2','3','4','5','6','7','8','9'});
transport.registerDid(0x7A0, Uds::DID_SerialNumber,
Bytes{'S','N','-','E','C','U','A','-','0','0','1'});
// ECU B @ 0x7A2: 只有序列号(没写 VIN)
transport.registerDid(0x7A2, Uds::DID_SerialNumber,
Bytes{'S','N','-','E','C','U','B','-','0','0','2'});
// 0x7A4 不存在(不在 db 中,模拟离线节点)
// ---- 执行节点识别 ----
UdsClient client(transport);
NodeIdentifier identifier(client);
std::vector candidates = {0x7A0, 0x7A2, 0x7A4};
auto nodes = identifier.discover(candidates);std::cout << "===== Node Identification Result =====\n";
for (const auto& n : nodes) {
std::cout << "Addr 0x" << std::hex << n.address << std::dec << "\n"
<< " VIN : " << (n.vin.empty() ? " " : n.vin) << "\n"
<< " Serial : " << (n.serialNumber.empty() ? " " : n.serialNumber) << "\n";
}
std::cout << "Total online nodes: " << nodes.size() << "\n";
return 0;
}
===== Node Identification Result =====
Addr 0x7a0
VIN : LSVNM42B123456789
Serial : SN-ECU-A-001
Addr 0x7a2
VIN :
Serial : SN-ECU-B-002
Total online nodes: 2
0x7A4因为在模拟传输层里没有任何 DID 注册,readVin返回nullopt,被正确识别为离线节点排除。
四、真实项目中的注意事项
寻址方式:物理寻址(点对点,如 0x7A0)用于识别具体 ECU;功能寻址(0x7DF)可广播,但响应会冲突,通常不用于读识别数据。
超时与重试:
request需实现 P2/P2* 超时(默认 50ms / 5000ms),离线节点靠超时判定,注意别阻塞太久——多节点扫描可用异步并发。会话与安全:某些 OEM 把序列号放在扩展会话或需
0x27解锁后才能读,识别前可能要先0x10 0x03。响应长度:VIN 固定 17 字节,但序列号长度因厂商而异,解析时不要硬编码。
多帧传输:VIN 等较长数据超过 7 字节,需要 ISO-TP(ISO 15765-2)分帧,示例里由传输层屏蔽了。
DID 支持表:不同 ECU 支持的 DID 不同,遇到 NRC
0x31属正常,应继续尝试其它 DID。
节点识别是 UDS 诊断的基础能力:以0x22 ReadDataByIdentifier为核心,结合0x10、0x27、0x3E等服务,诊断仪就能在多 ECU 网络中发现节点、确认身份、读取 VIN/序列号/版本信息。上面的 C++ 示例通过传输层抽象 + 客户端封装 + 发现器三层结构,把这个流程清晰地实现了出来,把MockTransport换成真实 SocketCAN / DoIP 即可用于实际工程。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
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.