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.3 ECU 端——会话管理、SID 分发与 DID 存储

场景:你成了一个会听话的机器

想象你是一台 ECU。对你而言,诊断通信不是“发送消息“——而是等待

你在安静的嵌入式世界里运转:传感器采样、执行器驱动、CAN 报文收发。突然,TCP 端口 13400 上出现一个连接。你不需要主动做任何事——你只需要响应

TCP 连接建立后,字节流从网络涌入你的接收缓冲区。你不能假设字节会一次性到达(TCP 是流协议),你也不能假设每个请求之间有时间间隔。你唯一能做的,是 recv → 解析 → 决策 → 执行 → 响应 → recv ——一个永不停歇的回环。

这就是 ECU 端的全部工作。你不是对话的发起者,你是对话的响应者。 这和医生的体检过程一模一样:患者不需要知道心电图机怎么工作,心电图机也不需要知道患者今天为什么来——它只需要在医生按下按钮时,忠实地执行每一个指令。

现在,你要用 C 代码实现这个“听话的机器“。

核心洞察: ECU 诊断服务器的核心架构是一个事件驱动的状态机。TCP recv 是事件源,SID 是事件类型,当前会话/安全等级是状态,DID/DTC 是状态内的数据。你不需要线程池,不需要异步 I/O,一个阻塞的 recv() 就足够了——因为在嵌入式诊断的物理世界里,同一时刻只有一台诊断仪在说话。

服务器主循环:从 socket 到响应的完整路径

/* uds_server.c — main函数 */
int main(int argc, char **argv)
{
    int port = (argc > 1) ? atoi(argv[1]) : UDS_SERVER_PORT;

    init_did_table();
    init_dtc_table();

    int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd < 0) { perror("socket"); return 1; }

    int opt = 1;
    setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = INADDR_ANY;
    addr.sin_port = htons(port);

    if (bind(listen_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
        perror("bind"); return 1;
    }
    if (listen(listen_fd, 5) < 0) { perror("listen"); return 1; }

    printf("### UDS Server (ECU) listening on port %d ###\n", port);
    printf("    Session: Default | Security: Locked | DTCs: %d\n", g_dtc_count);
    printf("    Supported DID count: %d\n", DID_COUNT);

    while (1) {
        struct sockaddr_in client_addr;
        socklen_t client_len = sizeof(client_addr);
        int client_fd = accept(listen_fd, (struct sockaddr *)&client_addr, &client_len);
        if (client_fd < 0) { perror("accept"); continue; }

        char client_ip[INET_ADDRSTRLEN];
        inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof(client_ip));
        printf("[ECU] Tester connected from %s:%d\n", client_ip, ntohs(client_addr.sin_port));

        u8 req_buf[UDS_MAX_MSG_LEN], resp_buf[UDS_MAX_MSG_LEN];

        while (1) {
            ssize_t n = recv(client_fd, req_buf, sizeof(req_buf), 0);
            if (n <= 0) {
                if (n < 0) printf("[ECU] Connection timeout or error\n");
                else printf("[ECU] Tester disconnected\n");
                g_session = SESSION_DEFAULT;
                g_security = SECURITY_LOCKED;
                g_seed_pending = FALSE;
                g_xfer.active = FALSE;
                break;
            }

            u16 resp_len = dispatch(req_buf, (u16)n, resp_buf);
            if (resp_len > 0) {
                send(client_fd, resp_buf, resp_len, 0);
            }
        }
        close(client_fd);
    }

    close(listen_fd);
    return 0;
}

这个主循环的结构揭示了几个关键设计决策:

  1. 一次只服务一个客户端——uds-lite 使用阻塞 accept() 的教学简化。生产环境中 DoIP 可支持多客户端并发接入,真实 ECU 需管理多个诊断连接及会话隔离。
  2. 客户端断开后状态重置——会话和安全等级回到默认值。每个诊断会话是独立的。
  3. S3 超时通过客户端断开检测间接实现——实际代码中使用阻塞 recv(),教学上未实现独立的 S3 定时器(生产代码需用 select() 或硬件定时器实现非阻塞超时检测)。
  4. 主循环本身没有超时——recv() 是阻塞的,这是教学简化。生产代码会用 select() 或轮询实现非阻塞 + S3 定时器。

SID 分发——switch 驱动的调度器

uds-lite 采用 switch 语句实现 SID 分发,而非查表。这是教学简化——代码更短、更直观,生产级 AUTOSAR DCM 使用查表(见第 4.4 节)。

static u16 dispatch(const u8 *req, u16 req_len, u8 *resp)
{
    if (req_len < 1) return 0;
    u8 sid = req[0];
    u16 resp_len = 0;

    switch (sid) {
        case SID_DIAGNOSTIC_SESSION_CONTROL:
            handle_session_control(req, req_len, resp, &resp_len); break;
        case SID_TESTER_PRESENT:
            handle_tester_present(req, req_len, resp, &resp_len); break;
        case SID_ECU_RESET:
            handle_ecu_reset(req, req_len, resp, &resp_len); break;
        case SID_SECURITY_ACCESS:
            handle_security_access(req, req_len, resp, &resp_len); break;
        case SID_READ_DATA_BY_IDENTIFIER:
            handle_read_did(req, req_len, resp, &resp_len); break;
        case SID_WRITE_DATA_BY_IDENTIFIER:
            handle_write_did(req, req_len, resp, &resp_len); break;
        case SID_READ_DTC_INFORMATION:
            handle_read_dtc(req, req_len, resp, &resp_len); break;
        case SID_CLEAR_DIAGNOSTIC_INFORMATION:
            handle_clear_dtc(req, req_len, resp, &resp_len); break;
        case SID_ROUTINE_CONTROL:
            handle_routine_control(req, req_len, resp, &resp_len); break;
        case SID_REQUEST_DOWNLOAD:
            handle_request_download(req, req_len, resp, &resp_len); break;
        case SID_TRANSFER_DATA:
            handle_transfer_data(req, req_len, resp, &resp_len); break;
        case SID_REQUEST_TRANSFER_EXIT:
            handle_transfer_exit(req, req_len, resp, &resp_len); break;
        case SID_CONTROL_DTC_SETTING:
            handle_control_dtc_setting(req, req_len, resp, &resp_len); break;
        default:
            resp_len = uds_build_negative_response(resp, sid, NRC_SERVICE_NOT_SUPPORTED);
            break;
    }
    return resp_len;
}

每个 handler 接收裸字节(req/resp)而非 UdsMsg 结构体,因为此处的通信单位还是原始的 TCP/DoIP 字节流。每个 handler 负责验证权限、执行操作、构建响应。

处理函数实现

每个 handler 处理 bare 字节(const u8 *req / u8 *resp),在内部完成权限检查、执行操作、构建响应字节。

会话控制(0x10)

static void handle_session_control(const u8 *req, u16 req_len,
                                    u8 *resp, u16 *resp_len)
{
    if (req_len < 2) {
        *resp_len = uds_build_negative_response(resp,
            SID_DIAGNOSTIC_SESSION_CONTROL, NRC_INCORRECT_MESSAGE_LENGTH);
        return;
    }
    u8 sub = req[1] & 0x7F;
    u8 suppress = (req[1] & SUPPRESS_POS_RSP_MASK) ? 1 : 0;

    if (sub != SESSION_DEFAULT && sub != SESSION_PROGRAMMING
        && sub != SESSION_EXTENDED) {
        *resp_len = uds_build_negative_response(resp,
            SID_DIAGNOSTIC_SESSION_CONTROL, NRC_SUBFUNCTION_NOT_SUPPORTED);
        return;
    }

    g_session = sub;
    g_security = SECURITY_LOCKED;
    g_seed_pending = FALSE;

    if (suppress) { *resp_len = 0; return; }

    u8 extra[] = {sub, (P2_SERVER_DEFAULT >> 8), P2_SERVER_DEFAULT & 0xFF,
                        (P2_STAR_SERVER_DEFAULT >> 8) & 0xFF,
                        P2_STAR_SERVER_DEFAULT & 0xFF};
    *resp_len = uds_build_positive_response(resp,
        SID_DIAGNOSTIC_SESSION_CONTROL, extra, 5);
}

几个关键点:

  • 切换会话时重置安全等级——这是安全要求的,你不能从编程会话退回到默认会话时还保持解锁状态
  • 每个会话返回不同的 P2/P2* 时间——客户端据此设置自己的等待超时
  • 子功能码的 bit7(suppressPosRsp)已经在解码时处理了,这里直接用 bit6-0

TesterPresent(0x3E)

static void handle_tester_present(const u8 *req, u16 req_len,
                                   u8 *resp, u16 *resp_len)
{
    u8 suppress = (req[1] & SUPPRESS_POS_RSP_MASK) ? 1 : 0;
    if (suppress) { *resp_len = 0; return; }
    resp[0] = SID_TESTER_PRESENT | SID_POSITIVE_RESPONSE_MASK;
    resp[1] = SUB_ZERO;
    *resp_len = 2;
}

安全访问(0x27)

uds-lite 使用硬编码 seed(0xA3D45F12),密钥算法为按字节 XOR 0x5A

static void compute_seed(u8 seed[4]) {
    seed[0] = 0xA3; seed[1] = 0xD4; seed[2] = 0x5F; seed[3] = 0x12;
}

static u8 verify_key(const u8 *key, u8 key_len) {
    if (key_len < 4) return FALSE;
    u8 expected[4];
    for (int i = 0; i < 4; i++) expected[i] = g_seed[i] ^ 0x5A;
    return (memcmp(key, expected, 4) == 0);
}

static void handle_security_access(const u8 *req, u16 req_len,
                                    u8 *resp, u16 *resp_len)
{
    u8 sub = req[1] & 0x7F;
    if (sub == SUB_REQUEST_SEED) {
        compute_seed(g_seed);
        g_seed_pending = TRUE;
        u8 extra[5] = {SUB_REQUEST_SEED};
        memcpy(&extra[1], g_seed, 4);
        *resp_len = uds_build_positive_response(resp, SID_SECURITY_ACCESS, extra, 5);
    } else if (sub == SUB_SEND_KEY && g_seed_pending) {
        u8 key[4];
        memcpy(key, &req[2], 4);
        if (verify_key(key, 4)) {
            g_security = SECURITY_LEVEL_1;
            g_seed_pending = FALSE;
            u8 extra[] = {SUB_SEND_KEY};
            *resp_len = uds_build_positive_response(resp, SID_SECURITY_ACCESS, extra, 1);
        } else {
            *resp_len = uds_build_negative_response(resp, SID_SECURITY_ACCESS, NRC_INVALID_KEY);
        }
    }
}

DID 存储与读写(0x22 / 0x2E)

static DidRecord g_did_table[] = {
    {0x010C, {0x0B,0xB8}, 2, FALSE, SESSION_DEFAULT, SECURITY_LOCKED},   /* 转速 3000rpm */
    {0x0105, {0x84}, 1, FALSE, SESSION_DEFAULT, SECURITY_LOCKED},         /* 冷液 92°C */
    {0xF190, "W0L00000123456789", 17, TRUE, SESSION_EXTENDED, SECURITY_LEVEL_1}, /* VIN */
    {0x010D, {87}, 1, FALSE, SESSION_DEFAULT, SECURITY_LOCKED},           /* 车速 */
    {0x0110, {0x00,0xAA}, 2, FALSE, SESSION_DEFAULT, SECURITY_LOCKED},    /* MAF */
    {0xF180, "SW-V1.0.0", 9, FALSE, SESSION_DEFAULT, SECURITY_LOCKED},    /* 版本 */
    {0xFF01, {0x00,0x00}, 2, TRUE, SESSION_EXTENDED, SECURITY_LEVEL_1},   /* 诊断标定 */
    {0x0150, {0x83}, 1, FALSE, SESSION_DEFAULT, SECURITY_LOCKED},         /* 燃油修正 */
};

static void handle_read_did(const u8 *req, u16 req_len, u8 *resp, u16 *resp_len) {
    /* 解析DID → 查表 → 构建响应 {0x62, DID_H, DID_L, data...} */
    u8 out[UDS_MAX_MSG_LEN]; u16 pos = 0;
    out[pos++] = SID_READ_DATA_BY_IDENTIFIER | SID_POSITIVE_RESPONSE_MASK;
    u16 idx = 1;
    while (idx + 1 < req_len) {
        u16 did = uds_read_u16_be(&req[idx]);
        DidRecord *d = find_did(did);
        out[pos++] = (did >> 8) & 0xFF; out[pos++] = did & 0xFF;
        memcpy(&out[pos], d->data, d->data_len); pos += d->data_len;
        idx += 2;
    }
    memcpy(resp, out, pos); *resp_len = pos;
}

static void handle_write_did(const u8 *req, u16 req_len, u8 *resp, u16 *resp_len) {
    u16 did = uds_read_u16_be(&req[1]);
    DidRecord *d = find_did(did);
    u16 data_len = req_len - 3;
    memcpy(d->data, &req[3], data_len);
    u8 extra[] = {(did >> 8) & 0xFF, did & 0xFF};
    *resp_len = uds_build_positive_response(resp, SID_WRITE_DATA_BY_IDENTIFIER, extra, 2);
}

DTC 读取与清除(0x19 / 0x14)

static DtcRecord g_dtc_table[] = {
    {{0x00,0x01,0x02}, DTC_STATUS_TEST_FAILED|DTC_STATUS_CONFIRMED_DTC, ...}, /* P0102 */
    {{0x40,0x02,0x01}, DTC_STATUS_TEST_FAILED|DTC_STATUS_PENDING_DTC, ...},   /* C0421 */
};

static void handle_read_dtc(const u8 *req, u16 req_len, u8 *resp, u16 *resp_len) {
    u8 sub = req[1] & 0x7F;
    u8 status_mask = req[2];
    if (sub == SUB_REPORT_NUMBER_OF_DTC_BY_STATUS_MASK) {
        u8 count = 0;
        for (int i = 0; i < g_dtc_count; i++)
            if (g_dtc_table[i].status & status_mask) count++;
        u8 extra[] = {SUB_REPORT_NUMBER_OF_DTC_BY_STATUS_MASK,
                      status_mask, 0x01, 0, count};
        *resp_len = uds_build_positive_response(resp,
            SID_READ_DTC_INFORMATION, extra, sizeof(extra));
    } else if (sub == SUB_REPORT_DTC_BY_STATUS_MASK) {
        /* 遍历DTC表匹配 status mask, 每条4字节: DTC(3) + status(1) */
        u8 extra[UDS_MAX_MSG_LEN]; u16 pos = 0;
        extra[pos++] = SUB_REPORT_DTC_BY_STATUS_MASK;
        extra[pos++] = status_mask;
        for (int i = 0; i < g_dtc_count; i++) {
            if (g_dtc_table[i].status & status_mask) {
                memcpy(&extra[pos], g_dtc_table[i].code, 3); pos += 3;
                extra[pos++] = g_dtc_table[i].status;
            }
        }
        *resp_len = uds_build_positive_response(resp,
            SID_READ_DTC_INFORMATION, extra, pos);
    }
}

例行控制(0x31)

static void handle_routine_control(const u8 *req, u16 req_len,
                                    u8 *resp, u16 *resp_len)
{
    u16 rid = uds_read_u16_be(&req[2]);
    g_routine.rid = rid;
    g_routine.activated = TRUE;
    u8 extra[4] = {SUB_START_ROUTINE, (rid >> 8) & 0xFF, rid & 0xFF, 0x01};
    *resp_len = uds_build_positive_response(resp, SID_ROUTINE_CONTROL, extra, 4);
}

文件下载(0x34/36/37)

static void handle_request_download(const u8 *req, u16 req_len,
                                     u8 *resp, u16 *resp_len)
{
    g_xfer.active = TRUE;
    g_xfer.max_block_size = 200;
    g_xfer.expected_seq_counter = 1;
    u8 extra[] = {0x20, (g_xfer.max_block_size >> 8) & 0xFF,
                        g_xfer.max_block_size & 0xFF};
    *resp_len = uds_build_positive_response(resp, SID_REQUEST_DOWNLOAD, extra, 3);
}

static void handle_transfer_data(const u8 *req, u16 req_len,
                                  u8 *resp, u16 *resp_len)
{
    u8 seq = req[1];
    u16 block_len = req_len - 2;
    g_xfer.expected_seq_counter++;
    u8 extra[] = {seq};
    *resp_len = uds_build_positive_response(resp, SID_TRANSFER_DATA, extra, 1);
}

static void handle_transfer_exit(const u8 *req, u16 req_len,
                                  u8 *resp, u16 *resp_len)
{
    g_xfer.active = FALSE;
    *resp_len = uds_build_positive_response(resp, SID_REQUEST_TRANSFER_EXIT, NULL, 0);
}

ECU 复位(0x11)

static void handle_ecu_reset(const u8 *req, u16 req_len, u8 *resp, u16 *resp_len)
{
    u8 sub = req[1] & 0x7F;
    /* 正响应在复位之前发送 */
    u8 extra[] = {sub};
    *resp_len = uds_build_positive_response(resp, SID_ECU_RESET, extra, 1);
}

ECUReset 的正响应必须在执行复位之前发送——这是一个黄金规则。如果先复位再发响应,客户端永远收不到这个响应。

三次握手的 flow 是:

  1. RequestDownload:客户端说“我要下载一些数据“,ECU 准备缓冲区和长度
  2. TransferData × N:客户端分块发送数据,每块带有递增的 blockSequenceCounter
  3. RequestTransferExit:客户端说“发送完毕“,ECU 验证完整性并“烧录

blockSequenceCounter 从 1 开始,每次 TransferData 递增 1。如果收到的序号不匹配,返回 NRC 0x73。这是防止网络乱序或丢包的简单校验。

本篇小结

  1. ECU 服务器是一个单客户端、阻塞式循环:accept → recv → dispatch → send,共约 490 行 C
  2. SID 分发采用 switch 语句(教学简化),每个 handler 接收裸字节、内部做权限验证
  3. 会话切换必须重置安全等级,TesterPresent 防止 S3 超时,ECUReset 必须先发响应再执行复位
  4. DID 表 8 条(0x010C 转速、0x0105 冷液温度、0xF190 VIN、0x010D 车速等)
  5. DTC 表 2 条(P0102 MAF、C0421 ABS),按 status mask 过滤
  6. 安全访问使用 XOR 0x5A 算法(教学简化),seed 固定为 0xA3D45F12
  7. 文件下载三次握手:RequestDownload→TransferData×N→RequestTransferExit

【下集预告】:ECU 端已经就绪——它安静地监听着 TCP 13400,等待着第一个诊断请求。下一节我们换一个视角:诊断仪端。你要写出那个主动发起连接、构建请求、解析响应、并在遇到 NRC 时自动纠错的客户端。 如果说 ECU 是“听话的机器“,那诊断仪就是“聪明的机器“——它不仅要会问,还要会听、会想、会改策略