Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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=hardReset0x02=keyOffOnReset0x03=softReset。壳命令 reset soft 发送 0x03reset 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 的可用性技巧

  1. 命令缩写——tp 代替 testerpresentread 代替 readdid。短命令降低了输入阻力。
  2. 自动响应解析——send_and_recv 自动识别正/负响应并打印标识(✓ / ⚠)。
  3. 数字自适应的输入格式——session 3session 03session 0x03 都能被解析。
  4. xfer 调试命令——xfer [seq]xexit 让用户可以手动走遍下载三次握手。

核心洞察: 交互式终端的本质是把“协议规范“翻译成“人类语言“。0x22 0xF1 0x90 变成了 read F190,正响应 0x62 0xF1 0x90 0x4C 0x53... 变成了 DID 0xF190 = "LSVNUDS1234567890"信息还是那些信息,但呈现方式决定了理解效率。 一个好的诊断工具的价值不在于它能请求多少 SID——而在于它将原始字节翻译成诊断语义的速度和质量。

本篇小结

  1. 交互式 shell 将 UDS 从“批处理协议“变成了“可对话的命令行“——约 300 行 C 代码
  2. 核心架构:读命令 → 解析 → 构建请求 → send_and_recv → 自动解析响应
  3. 命令拆分为独立 cmd_* 函数:cmd_sessioncmd_securitycmd_readcmd_dtc
  4. 安全访问拆为两步:security(请求 seed)+ sendkey <hex4>(发送 key)
  5. 下载命令演示完整的 0x34→0x36→0x37 三次握手,需先 session 02 进入编程会话
  6. dtcset on|off 命令对应 0x85 ControlDTCSetting 服务

【下集预告】:全部代码已经就绪。在最后一节,让 uds_client 一键跑完从会话切换到 ECU 复位的完整 8 步诊断流程——每一帧字节的请求/响应结构你都已在第 2 章读过、在 5.3 的代码中写过、在本节的 shell 命令中亲手敲过。现在,让它们自动跑一次。你写的 1000 行 C 代码,能否完成一次完整的诊断闭环?答案马上揭晓。