5.5 交互式诊断终端——命令行上的 UDS 控制台
场景:当协议变成对话
你面前打开了一个终端窗口。黑色的背景,绿色的光标在闪烁。
你敲下:
uds> session 03
不到 50 毫秒,屏幕上出现了:
uds> session 03
Tx [2]: 10 03
Rx [6]: 50 03 00 32 13 88 rea✓ POSITIVE: origSID=0x10
你又敲下:
uds> read F190
50 毫秒后:
uds> read F190
Tx [3]: 22 F1 90
Rx [3]: 7F 22 33 ⚠ NEGATIVE: SID=0x22 NRC=0x33 (SecurityAccessDenied)
VIN 受安全保护——你需要先解锁才能读取。 这是 UDS 的访问控制机制在 shell 中的直接体现:你无法在默认会话(未解锁)下读取受安全保护的 DID。这比“看打印的十六进制“更直观——shell 直接告诉你“SecurityAccessDenied“。
uds> security
Tx [2]: 27 01
Rx [6]: 67 01 A3 D4 5F 12
✓ POSITIVE: origSID=0x27
Seed: A3 D4 5F 12
Key hint: XOR 0x5A
uds> sendkey F9 8E 05 48
Tx [6]: 27 02 F9 8E 05 48
Rx [2]: 67 02
✓ POSITIVE: origSID=0x27
=> Security Level 1 unlocked
uds> read F190
Tx [3]: 22 F1 90
Rx [20]: 62 F1 90 57 30 4C 30 30 30 30 30 31 32 33 34 35 36 37 38 39
✓ POSITIVE: origSID=0x22
DID 0xF190 = "W0L00000123456789"
这就是交互式 UDS 终端的魔力。 你不再是一个“观察协议的人“,你成了一个“和 ECU 对话的人“。字节消失了,语义留下了。
在上一节,你写了一个完整的工作流——但它像一条流水线,按下按钮,所有步骤自动执行。你无法中途探索、重试、实验。交互式终端改变了这一切:它把 UDS 的每一个服务变成了一条命令,把 ECU 的每一个响应变成了一条回复。
这很像早期的计算机——从卡片机(批处理)到命令行(交互式)的跃迁。诊断工具的发展史也遵循相同的轨迹:从早期的自动测试脚本,到 Vector CANoe 和 Softing DTS 的可视化面板和脚本语言,再到今天你可以直接在任何 Linux 终端里敲 UDS 命令。
核心洞察: 交互式终端的意义不在于“方便“——而在于缩短反馈回路。当你敲下一个命令,50ms 后看到响应,你的大脑在建立“SID → 响应格式“的神经映射。这和阅读协议文档的学习效果完全不同:阅读是“存储知识“,交互是“建立反射“。 你用 uds_shell 调试一个下午,比你读一个月的 ISO 14229-1 更深刻地理解 UDS。
Shell 的整体架构
┌────────────────────────────────────────────────────┐
│ uds_shell │
│ │
│ while(running) { │
│ 1. 打印提示符 "uds>" 并读取一行 │
│ 2. 解析命令行 → 确定 service + parameters │
│ 3. 构建原始 UDS 请求字节序列 │
│ 4. 通过 TCP 发送给 ECU │
│ 5. 等待接收响应(带超时) │
│ 6. 解析响应 → 判断正/负 │
│ 7. 将响应翻译成人类可读格式打印 │
│ } │
└────────────────────────────────────────────────────┘
Shell 不引入任何新的 UDS 逻辑——它复用 uds_msg.h 的编解码和 uds_client.c 的发送-接收管道。它唯一的额外职责是人类 ↔ 字节的双向翻译。
主循环实现
/* uds_shell.c —— 交互式 UDS 终端 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <ctype.h>
#include "uds_common.h
#include "uds_msg.h
static int sock_fd = -1;
static int connect_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) { perror("inet_pton"); close(sock_fd); return -1; }
if (connect(sock_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { perror("connect"); close(sock_fd); return -1; }
struct timeval tv = {UDS_RX_TIMEOUT_SEC, 0};
setsockopt(sock_fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
return 0;
}
static void send_and_recv(const u8 *data_in, u16 len)
{
u8 buf[UDS_MAX_MSG_LEN];
memcpy(buf, data_in, len);
if (sock_fd < 0) { printf(" Not connected\n"); return; }
printf(" Tx [%u]: ", len);
for (u16 i = 0; i < len; i++) printf("%02X ", buf[i]);
printf("\n");
send(sock_fd, buf, len, 0);
ssize_t n = recv(sock_fd, buf, UDS_MAX_MSG_LEN, 0);
if (n <= 0) { printf(" No response (timeout)\n"); return; }
printf(" Rx [%zu]: ", n);
for (ssize_t i = 0; i < n; i++) printf("%02X ", buf[i]);
/* 解析响应类型 */
if (n >= 1 && buf[0] == NEGATIVE_RESPONSE_SID) {
const char *nrc_str = "Unknown";
if (n >= 3) {
switch (buf[2]) {
case NRC_SERVICE_NOT_SUPPORTED: nrc_str = "ServiceNotSupported"; break;
case NRC_SUBFUNCTION_NOT_SUPPORTED: nrc_str = "SubFunctionNotSupported"; break;
case NRC_INCORRECT_MESSAGE_LENGTH: nrc_str = "IncorrectMessageLength"; break;
case NRC_CONDITIONS_NOT_CORRECT: nrc_str = "ConditionsNotCorrect"; break;
case NRC_REQUEST_SEQUENCE_ERROR: nrc_str = "RequestSequenceError"; break;
case NRC_REQUEST_OUT_OF_RANGE: nrc_str = "RequestOutOfRange"; break;
case NRC_SECURITY_ACCESS_DENIED: nrc_str = "SecurityAccessDenied"; break;
case NRC_INVALID_KEY: nrc_str = "InvalidKey"; break;
case NRC_WRONG_BLOCK_SEQ_COUNTER: nrc_str = "WrongBlockSequenceCounter"; break;
case NRC_RESPONSE_PENDING: nrc_str = "ResponsePending"; break;
case NRC_SERVICE_NOT_IN_ACTIVE_SESSION: nrc_str = "ServiceNotSupportedInActiveSession"; break;
}
}
printf(" ⚠ NEGATIVE: SID=0x%02X NRC=0x%02X (%s)", buf[1], buf[2], nrc_str);
} else if (n >= 1 && (buf[0] & SID_POSITIVE_RESPONSE_MASK)) {
printf(" ✓ POSITIVE: origSID=0x%02X", buf[0] & ~SID_POSITIVE_RESPONSE_MASK);
}
printf("\n");
}
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 Interactive Diagnostic Shell ###\n");
connect_ecu(ip, port);
printf(" Connected. Type 'help' for commands.\n\n");
char line[512];
for (;;) {
printf("uds> "); fflush(stdout);
if (!fgets(line, sizeof(line), stdin)) break;
/* 解析命令... */
}
close(sock_fd);
return 0;
}
命令解析引擎
char cmd[32] = {0}, rest[480] = {0};
int n = sscanf(line, "%31s %479[^\n]", cmd, rest);
if (n < 1) continue;
if (strcmp(cmd, "quit") == 0 || strcmp(cmd, "exit") == 0) break;
else if (strcmp(cmd, "help") == 0 || strcmp(cmd, "?") == 0) print_help();
else if (strcmp(cmd, "session") == 0) cmd_session(rest);
else if (strcmp(cmd, "security") == 0) cmd_security();
else if (strcmp(cmd, "sendkey") == 0) cmd_sendkey(rest);
else if (strcmp(cmd, "read") == 0) cmd_read(rest);
else if (strcmp(cmd, "write") == 0) cmd_write(rest);
else if (strcmp(cmd, "dtc") == 0) cmd_dtc(rest);
else if (strcmp(cmd, "clear") == 0) cmd_clear();
else if (strcmp(cmd, "routine") == 0) cmd_routine(rest);
else if (strcmp(cmd, "download") == 0) cmd_download(rest);
else if (strcmp(cmd, "xfer") == 0) cmd_xfer_data(rest);
else if (strcmp(cmd, "xexit") == 0) cmd_xfer_exit();
else if (strcmp(cmd, "tp") == 0) cmd_tp();
else if (strcmp(cmd, "reset") == 0) cmd_reset(rest);
else if (strcmp(cmd, "dtcset") == 0) cmd_dtc_setting(rest);
else printf(" Unknown command: %s (type 'help')\n", cmd);
命令解析采用的是经典的 sscanf 方式——简单但能覆盖所有需求。每个命令函数独立处理其参数验证,这样新的命令可以随时添加而不影响其他命令。
帮助系统——Shell 的自文档
static void print_help(void)
{
printf(
"\n UDS Shell Commands:\n"
" ─────────────────────────────────────────────────────────────\n"
" session <01|02|03> 切换诊断会话 (01=默认 02=编程 03=扩展)\n"
" security 请求安全种子 (再发 sendkey <4 hex>)\n"
" sendkey <hex4> 发送安全密钥 (4字节hex)\n"
" read <did> 读取DID (如 read f190)\n"
" write <did> <hex> 写入DID (如 write ff01 aabb)\n"
" dtc [mask] 读取DTC (可选状态掩码, 默认0x08)\n"
" clear 清除全部DTC\n"
" routine <rid> 启动例行程序 (如 routine 0201)\n"
" download [addr] [sz] 请求下载 (如 download 40000 100)\n"
" xfer [seq] 传输数据块 (默认seq=1)\n"
" xexit 传输终止\n"
" tp TesterPresent 保活\n"
" reset [hard|soft|keyoff] ECU复位\n"
" dtcset [on|off] 暂停/恢复DTC记录\n"
" help 显示此帮助\n"
" quit 退出\n"
" ─────────────────────────────────────────────────────────────\n"
);
}
TCP 通信管道(shell 版)
shell 版的通信管道就是 send_and_recv:发送 + 接收 + 自动解析正/负响应并打印。不再单独维护连接/断开命令,启动时自动连接。
命令实现示例
static void cmd_session(const char *arg)
{
unsigned int s;
if (sscanf(arg, "%x", &s) != 1) { printf(" Usage: session <01|02|03>\n"); return; }
u8 req[] = {SID_DIAGNOSTIC_SESSION_CONTROL, (u8)s};
send_and_recv(req, 2);
}
static void cmd_security(void)
{
u8 req1[] = {SID_SECURITY_ACCESS, SUB_REQUEST_SEED};
printf(" --- RequestSeed ---\n");
send_and_recv(req1, 2);
}
static void cmd_sendkey(const char *arg)
{
u8 key[4];
u16 klen = parse_hex(arg, key, 4);
if (klen < 4) { printf(" Usage: sendkey <4 hex bytes>\n"); return; }
u8 req[6] = {SID_SECURITY_ACCESS, SUB_SEND_KEY, 0};
memcpy(&req[2], key, 4);
printf(" --- SendKey ---\n");
send_and_recv(req, 6);
}
static void cmd_read(const char *arg)
{
u16 did = parse_did(arg);
u8 req[3] = {SID_READ_DATA_BY_IDENTIFIER, (did >> 8) & 0xFF, did & 0xFF};
send_and_recv(req, 3);
}
static void cmd_write(const char *arg)
{
char did_str[8], data_str[256];
sscanf(arg, "%7s %255[^\n]", did_str, data_str);
u16 did = parse_did(did_str);
u8 data[128];
u16 dlen = parse_hex(data_str, data, sizeof(data));
u8 req[3 + 128];
req[0] = SID_WRITE_DATA_BY_IDENTIFIER;
req[1] = (did >> 8) & 0xFF; req[2] = did & 0xFF;
if (dlen > 128) dlen = 128;
memcpy(&req[3], data, dlen);
send_and_recv(req, 3 + dlen);
}
static void cmd_reset(const char *arg)
{
unsigned int t = SUB_HARD_RESET;
if (arg && *arg) {
if (strcmp(arg, "soft") == 0) t = SUB_SOFT_RESET;
else if (strcmp(arg, "keyoff") == 0) t = SUB_KEY_OFF_ON_RESET;
}
u8 req[] = {SID_ECU_RESET, (u8)t};
send_and_recv(req, 2);
}
注意 cmd_reset:ISO 14229 定义 0x01=hardReset、0x02=keyOffOnReset、0x03=softReset。壳命令 reset soft 发送 0x03,reset hard(默认)发送 0x01。
ReadDID——最常用的命令
static void cmd_read(const char *arg)
{
u16 did = parse_did(arg);
u8 req[3] = {SID_READ_DATA_BY_IDENTIFIER, (did >> 8) & 0xFF, did & 0xFF};
send_and_recv(req, 3);
}
Shell 的 send_and_recv 自动打印 Tx/Rx 字节和正/负响应标识,用户不需要自行解析。
安全访问——两步命令
Shell 将安全访问拆为两条命令:security(请求 seed)和 sendkey <hex4>(发送 key)。key 的计算需要在 Shell 外部手动完成(seed[i] ^ 0x5A),因为 Shell 是一个通用 hex 发送器而非安全工作流编排器。这种设计让用户可以自由探索不同的 key 算法。
例程控制和文件下载
static void cmd_routine(const char *arg)
{
u16 rid = parse_did(arg);
u8 req[] = {SID_ROUTINE_CONTROL, SUB_START_ROUTINE, (rid >> 8) & 0xFF, rid & 0xFF};
send_and_recv(req, 4);
}
static void cmd_download(const char *arg)
{
u32 addr = 0, size = 256;
sscanf(arg, "%x %x", &addr, &size);
u8 req[10] = {
SID_REQUEST_DOWNLOAD, 0x44,
(addr >> 24) & 0xFF, (addr >> 16) & 0xFF, (addr >> 8) & 0xFF, addr & 0xFF,
(size >> 24) & 0xFF, (size >> 16) & 0xFF, (size >> 8) & 0xFF, size & 0xFF
};
printf(" --- RequestDownload (addr=0x%08X, size=%u) ---\n", addr, size);
send_and_recv(req, 10);
}
static void cmd_xfer_data(const char *arg)
{
unsigned int seq = 1;
if (arg && *arg) sscanf(arg, "%x", &seq);
u8 buf[UDS_MAX_MSG_LEN];
buf[0] = SID_TRANSFER_DATA; buf[1] = (u8)seq;
u16 blk = 20;
memset(&buf[2], 0xAA, blk);
send_and_recv(buf, 2 + blk);
}
static void cmd_xfer_exit(void)
{
u8 req[] = {SID_REQUEST_TRANSFER_EXIT};
send_and_recv(req, 1);
}
工程实践:Shell 的可用性技巧
- 命令缩写——
tp代替testerpresent,read代替readdid。短命令降低了输入阻力。 - 自动响应解析——
send_and_recv自动识别正/负响应并打印标识(✓ / ⚠)。 - 数字自适应的输入格式——
session 3、session 03、session 0x03都能被解析。 - xfer 调试命令——
xfer [seq]和xexit让用户可以手动走遍下载三次握手。
核心洞察: 交互式终端的本质是把“协议规范“翻译成“人类语言“。
0x22 0xF1 0x90变成了read F190,正响应0x62 0xF1 0x90 0x4C 0x53...变成了DID 0xF190 = "LSVNUDS1234567890"。信息还是那些信息,但呈现方式决定了理解效率。 一个好的诊断工具的价值不在于它能请求多少 SID——而在于它将原始字节翻译成诊断语义的速度和质量。
本篇小结
- 交互式 shell 将 UDS 从“批处理协议“变成了“可对话的命令行“——约 300 行 C 代码
- 核心架构:读命令 → 解析 → 构建请求 →
send_and_recv→ 自动解析响应 - 命令拆分为独立 cmd_* 函数:
cmd_session、cmd_security、cmd_read、cmd_dtc等 - 安全访问拆为两步:
security(请求 seed)+sendkey <hex4>(发送 key) - 下载命令演示完整的 0x34→0x36→0x37 三次握手,需先
session 02进入编程会话 dtcset on|off命令对应 0x85 ControlDTCSetting 服务
【下集预告】:全部代码已经就绪。在最后一节,让
uds_client一键跑完从会话切换到 ECU 复位的完整 8 步诊断流程——每一帧字节的请求/响应结构你都已在第 2 章读过、在 5.3 的代码中写过、在本节的 shell 命令中亲手敲过。现在,让它们自动跑一次。你写的 1000 行 C 代码,能否完成一次完整的诊断闭环?答案马上揭晓。