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;
}
这个主循环的结构揭示了几个关键设计决策:
- 一次只服务一个客户端——uds-lite 使用阻塞
accept()的教学简化。生产环境中 DoIP 可支持多客户端并发接入,真实 ECU 需管理多个诊断连接及会话隔离。 - 客户端断开后状态重置——会话和安全等级回到默认值。每个诊断会话是独立的。
- S3 超时通过客户端断开检测间接实现——实际代码中使用阻塞
recv(),教学上未实现独立的 S3 定时器(生产代码需用select()或硬件定时器实现非阻塞超时检测)。 - 主循环本身没有超时——
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 是:
- RequestDownload:客户端说“我要下载一些数据“,ECU 准备缓冲区和长度
- TransferData × N:客户端分块发送数据,每块带有递增的 blockSequenceCounter
- RequestTransferExit:客户端说“发送完毕“,ECU 验证完整性并“烧录
blockSequenceCounter 从 1 开始,每次 TransferData 递增 1。如果收到的序号不匹配,返回 NRC 0x73。这是防止网络乱序或丢包的简单校验。
本篇小结
- ECU 服务器是一个单客户端、阻塞式循环:
accept → recv → dispatch → send,共约 490 行 C - SID 分发采用 switch 语句(教学简化),每个 handler 接收裸字节、内部做权限验证
- 会话切换必须重置安全等级,TesterPresent 防止 S3 超时,ECUReset 必须先发响应再执行复位
- DID 表 8 条(0x010C 转速、0x0105 冷液温度、0xF190 VIN、0x010D 车速等)
- DTC 表 2 条(P0102 MAF、C0421 ABS),按 status mask 过滤
- 安全访问使用 XOR 0x5A 算法(教学简化),seed 固定为
0xA3D45F12 - 文件下载三次握手:RequestDownload→TransferData×N→RequestTransferExit
【下集预告】:ECU 端已经就绪——它安静地监听着 TCP 13400,等待着第一个诊断请求。下一节我们换一个视角:诊断仪端。你要写出那个主动发起连接、构建请求、解析响应、并在遇到 NRC 时自动纠错的客户端。 如果说 ECU 是“听话的机器“,那诊断仪就是“聪明的机器“——它不仅要会问,还要会听、会想、会改策略。