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

UDS 节点识别(Node Identification)详解与 C++ 实现 一、什么是 UDS 节点识别

UDS(Unified Diagnostic Services,统一诊断服务)是 ISO 14229 标准定义的汽车诊断协议。在整车网络中,通常存在多个 ECU(电子控制单元),诊断仪(Tester)需要知道"我到底在跟哪个 ECU 说话",以及"网络上还有哪些节点"。

节点识别(Node Identification)就是让诊断设备能够发现、区分、确认网络上各个 ECU 身份的一组服务。它在多 ECU 环境中的价值体现在:

  • 拓扑发现:识别总线上存在哪些 ECU

  • 寻址确认:确认当前通信的目标节点

  • 版本管理:读取各节点的软硬件版本、序列号

  • 诊断路由:在多节点环境下正确分发诊断请求

二、节点识别相关的核心 UDS 服务

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,被正确识别为离线节点排除。

四、真实项目中的注意事项

  1. 寻址方式:物理寻址(点对点,如 0x7A0)用于识别具体 ECU;功能寻址(0x7DF)可广播,但响应会冲突,通常不用于读识别数据。

  2. 超时与重试:request需实现 P2/P2* 超时(默认 50ms / 5000ms),离线节点靠超时判定,注意别阻塞太久——多节点扫描可用异步并发。

  3. 会话与安全:某些 OEM 把序列号放在扩展会话或需0x27解锁后才能读,识别前可能要先0x10 0x03。

  4. 响应长度:VIN 固定 17 字节,但序列号长度因厂商而异,解析时不要硬编码。

  5. 多帧传输:VIN 等较长数据超过 7 字节,需要 ISO-TP(ISO 15765-2)分帧,示例里由传输层屏蔽了。

  6. DID 支持表:不同 ECU 支持的 DID 不同,遇到 NRC0x31属正常,应继续尝试其它 DID。

五、小结

节点识别是 UDS 诊断的基础能力:以0x22 ReadDataByIdentifier为核心,结合0x10、0x27、0x3E等服务,诊断仪就能在多 ECU 网络中发现节点、确认身份、读取 VIN/序列号/版本信息。上面的 C++ 示例通过传输层抽象 + 客户端封装 + 发现器三层结构,把这个流程清晰地实现了出来,把MockTransport换成真实 SocketCAN / DoIP 即可用于实际工程。