5.4 诊断仪端——逐服务发送与响应解析
场景:你成了那个“问问题“的人
上一节你是 ECU——你等待、你响应、你执行。这一节,角色反转:你是诊断仪。
你不再是被动接收指令的机器。你必须主动出击:连接 ECU、选择会话、请求 seed、计算 key、读取数据、查看故障码、启动测试例程——每一步都在你的掌控之中,每一步都可能收到负响应,每一步你都要决定是重试还是放弃。
这更接近真实诊断场景。当你在 4S 店看到维修技师把诊断仪插到 OBD-II 接口上,技师不是在看屏幕发呆——他是在和车对话。“先切到扩展会话……读个 VIN 码……安全解锁……好,现在可以读 DTC 了……有三个故障码,都是历史码……清掉……再读一遍,确认清了……”
诊断仪的代码必须比 ECU 更“聪明“。ECU 只需要忠实地“你问我答“,但诊断仪需要理解 NRC 的含义,并在工作流中自动纠错。
核心洞察: 如果说 ECU 是无状态的“条件反射机器“(stimulus → response),诊断仪就是一个带有目标导向的状态机。它的每一步都服务于“完整诊断“这个总目标,NRC 不是错误——NRC 是导航信号,告诉你接下来该往哪走。
客户端基础框架:TCP 通信管道
/* uds_client.c —— 诊断仪端核心 */
static int sock_fd = -1;
static u8 recv_buf[UDS_MAX_MSG_LEN];
static int connect_to_ecu(const char *ip, int port)
{
sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if (sock_fd < 0) { perror("socket"); return -1; }
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
if (inet_pton(AF_INET, ip, &addr.sin_addr) <= 0) return -1;
if (connect(sock_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) return -1;
struct timeval tv = {UDS_RX_TIMEOUT_SEC, 0};
setsockopt(sock_fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
return 0;
}
static ssize_t send_request(const u8 *data, u16 len) {
return send(sock_fd, data, len, 0);
}
static ssize_t recv_response(u8 *buf, u16 max_len) {
return recv(sock_fd, buf, max_len, 0);
}
客户端最核心的功能只有三个:连接、发送、接收。没有复杂的封装层——教学代码的取舍是:把每一步的裸字节序列清晰地展示出来,让读者看到 UDS 协议在 TCP 线路上真正的样子。
响应解析与显示
static const char *nrc_name(u8 nrc)
{
switch (nrc) {
case NRC_SERVICE_NOT_SUPPORTED: return "ServiceNotSupported";
case NRC_SUBFUNCTION_NOT_SUPPORTED: return "SubFunctionNotSupported";
case NRC_INCORRECT_MESSAGE_LENGTH: return "IncorrectMessageLength";
case NRC_CONDITIONS_NOT_CORRECT: return "ConditionsNotCorrect";
case NRC_REQUEST_SEQUENCE_ERROR: return "RequestSequenceError";
case NRC_REQUEST_OUT_OF_RANGE: return "RequestOutOfRange";
case NRC_SECURITY_ACCESS_DENIED: return "SecurityAccessDenied";
case NRC_INVALID_KEY: return "InvalidKey";
case NRC_WRONG_BLOCK_SEQ_COUNTER: return "WrongBlockSequenceCounter";
case NRC_RESPONSE_PENDING: return "ResponsePending";
case NRC_SERVICE_NOT_IN_ACTIVE_SESSION: return "ServiceNotSupportedInActiveSession";
default: return "Unknown";
}
}
static u8 check_negative_response(const u8 *raw, u16 raw_len) {
if (raw_len >= 3 && raw[0] == NEGATIVE_RESPONSE_SID) {
printf(" !! NEGATIVE RESPONSE: SID=0x%02X NRC=0x%02X (%s)\n",
raw[1], raw[2], nrc_name(raw[2]));
return TRUE;
}
return FALSE;
}
static void dump_hex(const char *label, const u8 *data, u16 len) {
printf(" %s [%u bytes]: ", label, len);
for (u16 i = 0; i < len; i++) printf("%02X ", data[i]);
printf("\n");
}
响应接收设置了 SO_RCVTIMEO 超时——默认 P2=50ms(UDS_RX_TIMEOUT_SEC 在 uds_common.h 中定义)。如果 ECU 超过该时间未回复,recv 返回 -1。这是诊断通信的“心跳等待时长“——ISO 14229 规定默认 P2=50ms。
诊断工作流——每个 step 直接发裸字节
uds-lite 不提供构建请求的辅助函数——每个步骤直接构造裸字节数组并发送:
static void do_session_control(u8 session) {
u8 req[] = {SID_DIAGNOSTIC_SESSION_CONTROL, session};
send_request(req, 2);
ssize_t n = recv_response(recv_buf, sizeof(recv_buf));
if (n <= 0) return;
dump_hex("Rx", recv_buf, n);
}
这种做法在教学上有一个特别的价值:你看到的请求就是 ECU 即将收到的字节——没有中间层隐藏任何细节。0x10 0x03 进入扩展会话,0x22 0xF1 0x90 读 VIN,这就是 UDS 在传输线上真实的样子。
main 函数——完整的 8 步诊断流程
int main(int argc, char **argv)
{
const char *ip = (argc > 1) ? argv[1] : "127.0.0.1";
int port = (argc > 2) ? atoi(argv[2]) : UDS_SERVER_PORT;
printf("### UDS Client (Tester) connecting to %s:%d ###\n\n", ip, port);
if (connect_to_ecu(ip, port) < 0) return 1;
printf("\n=== Step 1: DiagnosticSessionControl (enter session 0x03) ===\n");
do_session_control(SESSION_EXTENDED);
printf("\n=== Step 2: TesterPresent (keep-alive) ===\n");
do_tester_present();
printf("\n=== Step 3: SecurityAccess (seed/key to unlock level 1) ===\n");
do_security_access();
/* 读多个DID */
do_read_did(0x010C, "Engine RPM");
do_read_did(0x0105, "Coolant Temp");
do_read_did(0xF190, "VIN");
do_read_did(0xF180, "SW Version");
printf("\n=== Step 4: ReadDTC ===\n");
do_read_dtc();
printf("\n=== Step 5: RoutineControl ===\n");
do_routine_control(0x0201);
printf("\n=== Step 6: Programming session + Download ===\n");
do_session_control(SESSION_PROGRAMMING);
do_download();
printf("\n=== Step 7: Clear DTC ===\n");
do_session_control(SESSION_EXTENDED);
do_security_access();
do_clear_dtc();
printf("\n=== Step 8: ECU Reset ===\n");
u8 req[] = {SID_ECU_RESET, SUB_SOFT_RESET};
send_request(req, 2);
recv_response(recv_buf, sizeof(recv_buf));
printf("\n### Diagnostic workflow complete ###\n");
close(sock_fd);
return 0;
}
8 个步骤走完一次完整的诊断闭环:
- 扩展会话 — 进入扩展诊断会话,获得P2/P2*定时参数
- TesterPresent — 保活,保持会话不超时
- 安全解锁 — Seed/Key挑战-应答,解锁Level 1
- 读DTC — 按状态掩码读取故障码数量+列表
- 例行控制 — 启动Routine 0x0201
- 编程会话+下载 — 切到编程会话,执行RequestDownload→TransferData→TransferExit
- 清DTC — 切回扩展,重新解锁,清除所有DTC
- ECU复位 — softReset,结束诊断会话
本篇小结
- uds-lite 客户端约 300 行 C 代码,8 步工作流覆盖完整诊断场景
- 安全访问使用 XOR 0x5A 算法(与服务器端一致)
- 每个 step function 直接发送裸字节,不经过中间层封装
- 下载流程:RequestDownload(含 dataFormatIdentifier)→TransferData→TransferExit
- 客户端跟踪可见:seed 解析、key 计算、DTC 计数解析、reset 响应等
【下集预告】:自动化工作流很强大,但它像一条流水线——你按下按钮,它走完。你无法中途问“这个 DID 值是多少?“或者“再试一次但换个参数”。下一节,你将把这个工作流拆成一块块积木,让用户可以用命令行自由组合它们。你敲
read F190,ECU 回答 VIN 码。你敲security unlock 1,ECU 返回 seed,你输入 key。UDS 从协议变成了对话。