news 2026/5/1 10:41:12

解析UDS诊断请求并生成正响应的驱动逻辑:手把手

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
解析UDS诊断请求并生成正响应的驱动逻辑:手把手

从CAN报文到正响应:手把手实现UDS诊断请求的驱动级处理

你有没有遇到过这样的场景?
在调试一辆新能源车的BMS系统时,诊断仪发出一条22 F1 90读取VIN的命令,却迟迟收不到回应。你翻遍了代码,发现不是硬件问题,也不是CAN通信异常——而是你的ECU“听懂了请求”,却不知道该怎么“开口回答”。

这正是我们今天要解决的核心问题:如何让一个嵌入式ECU,在收到UDS诊断请求后,正确解析、精准执行,并返回符合ISO 14229标准的正响应

这不是简单的字符串拼接,而是一套严谨的协议驱动逻辑。我们将从最底层的CAN中断开始,一步步构建出完整的请求处理链路,最终实现一个可复用、高可靠、易扩展的UDS响应引擎。


UDS诊断的本质:客户端-服务器模型下的“问答系统”

统一诊断服务(Unified Diagnostic Services,UDS)本质上是一个运行在ECU上的“问答系统”。诊断仪是提问者(客户端),ECU是应答者(服务器)。每一次交互都遵循严格的格式规范。

比如:
- 问:“请读取VIN码” → 报文为22 F1 90
- 答:“好的,这是VIN” → 正响应为62 F1 90 LVSZ123456789XYZ

其中,22是服务ID(SID),表示“按标识符读数据”;而响应中的62 = 22 + 0x40,这是UDS协议强制规定的正响应偏移规则

关键点:所有成功的UDS服务响应,其首字节必须是原始SID加上0x40。

这套机制看似简单,但在实际嵌入式开发中,稍有不慎就会导致诊断工具显示“无响应”或“NRC错误”。为什么?因为整个流程涉及多个层次的协同工作:CAN收发、帧解析、服务调度、内存访问、多帧传输……

接下来,我们就一层一层剥开这个黑盒。


第一步:从CAN总线捕获原始请求

在大多数车载ECU中,UDS运行于CAN总线上。物理层和数据链路层的任务由CAN控制器完成,我们的目标是从中断中拿到原始数据帧。

典型的诊断通信使用两个CAN ID:
-Rx ID(Tester → ECU):如0x7E0
-Tx ID(ECU → Tester):如0x7E8

当CAN控制器接收到一帧有效数据时,会触发硬件中断。此时,驱动程序应尽快读取数据并提交给上层处理,避免阻塞其他通信任务。

void CAN_RX_IRQHandler(void) { CanFrame frame; if (CAN_GetReceivedFrame(&hcan, &frame)) { if (frame.id == 0x7E0) { // 收到诊断请求 uds_handle_request(frame.data, frame.dlc); } } }

⚠️注意:中断服务函数要尽可能轻量。这里不做任何复杂解析,只做一件事——把数据交给协议栈处理函数uds_handle_request()


第二步:解析SID,分发服务请求

进入协议层后,第一步就是提取服务ID(SID),它是决定后续行为的关键钥匙。

常见的UDS服务包括:

SID服务名称
0x10诊断会话控制
0x14清除DTC
0x19读取DTC信息
0x22按标识符读数据(Read Data By Identifier)
0x2E按标识符写数据

我们可以用一个简洁的switch-case实现初步分发:

#define SID_READ_DATA_BY_IDENTIFIER 0x22 #define SID_WRITE_DATA_BY_IDENTIFIER 0x2E #define SID_DIAGNOSTIC_SESSION 0x10 uint8_t response_buffer[64]; // 响应缓冲区 uint8_t resp_len; void uds_handle_request(uint8_t *req_data, uint8_t req_len) { if (req_len < 1) return; uint8_t sid = req_data[0]; switch (sid) { case SID_READ_DATA_BY_IDENTIFIER: handle_read_by_identifier(req_data, req_len); break; case SID_WRITE_DATA_BY_IDENTIFIER: handle_write_by_identifier(req_data, req_len); break; case SID_DIAGNOSTIC_SESSION: handle_diagnostic_session(req_data, req_len); break; default: send_negative_response(sid, 0x11); // Service not supported break; } }

如果SID不支持,立即返回负响应NRC0x11—— “服务未支持”,帮助诊断工具快速定位问题。


第三步:处理核心服务 —— 以 $22 为例详解DID解析与响应生成

我们以最常用的$22服务为例,深入剖析其处理逻辑。

请求结构分析

用户发送:22 F1 90
含义:读取DID为F190的数据(通常是VIN码)

  • 字节0:SID = 0x22
  • 字节1~2:DID高位和低位 → 构成16位数据标识符

因此,我们必须确保请求长度至少为3字节,否则就是非法报文。

void handle_read_by_identifier(uint8_t *req, uint8_t len) { if (len < 3) { send_negative_response(0x22, 0x13); // Incorrect message length return; } uint16_t did = (req[1] << 8) | req[2];

DID查表机制:解耦业务与协议

硬编码判断DID虽然直观,但不利于维护。更优的做法是建立一张DID映射表

typedef struct { uint16_t did; uint8_t size; uint8_t* (*getter)(void); // 数据获取函数指针 } DidDescriptor; // 示例DID表 const DidDescriptor did_table[] = { { .did = 0xF190, .size = 17, .getter = get_vin_string }, { .did = 0xF18C, .size = 4, .getter = get_engine_runtime }, { .did = 0xF189, .size = 2, .getter = get_battery_voltage } }; #define DID_TABLE_SIZE (sizeof(did_table)/sizeof(DidDescriptor))

这样,新增变量只需注册函数,无需修改主逻辑。

构建正响应帧

一旦找到对应DID,就可以构造响应:

for (int i = 0; i < DID_TABLE_SIZE; i++) { if (did_table[i].did == did) { uint8_t* data_ptr = did_table[i].getter(); uint8_t data_size = did_table[i].size; // 构建正响应:62 + DID_H + DID_L + [data...] response_buffer[0] = 0x62; // 0x22 + 0x40 response_buffer[1] = req[1]; // DID High response_buffer[2] = req[2]; // DID Low memcpy(&response_buffer[3], data_ptr, data_size); resp_len = 3 + data_size; uds_send_response(response_buffer, resp_len); return; } } // DID不存在 send_negative_response(0x22, 0x31); // Request out of range

响应格式严格遵循:[62][DID_H][DID_L][Data...],这就是ISO 14229的要求。


第四步:智能发送响应 —— 单帧 vs 多帧自动切换

CAN单帧最多携带8字节数据。如果我们要返回的VIN是17字节,显然超出了容量限制。

这时候就需要启用ISO-TP(ISO 15765-2)传输协议来进行分段传输。

但在驱动层,我们希望对外接口保持统一。于是设计一个智能发送函数:

void uds_send_response(uint8_t *data, uint8_t len) { // 如果数据 ≤ 8字节,直接发单帧 if (len <= 8) { CanFrame tx_frame; tx_frame.id = 0x7E8; tx_frame.dlc = len; memcpy(tx_frame.data, data, len); CAN_Transmit(&hcan, &tx_frame); } // 否则启动ISO-TP多帧发送 else { isotp_start_transmission(data, len); } }

📌提示:ISO-TP模块需实现首帧(FF)、连续帧(CF)、流控帧(FC)的完整状态机,本文暂不展开,但它是长数据通信的必备组件。


负响应机制:让错误变得“可读”

除了正响应,负响应(Negative Response Code, NRC)同样重要。它能让开发者快速定位问题根源。

负响应格式固定为三字节:7F + 原始SID + NRC

常见NRC码示例:

NRC含义
0x11服务不支持
0x12子功能不支持
0x13消息长度错误
0x22条件不满足(如未进入扩展会话)
0x31请求超出范围(DID不存在)

封装一个通用发送函数:

void send_negative_response(uint8_t original_sid, uint8_t nrc) { response_buffer[0] = 0x7F; response_buffer[1] = original_sid; response_buffer[2] = nrc; uds_send_response(response_buffer, 3); }

当你在诊断仪上看到7F 22 31,就知道是“试图读取了一个无效的DID”。


系统架构全景:各层职责分明

在一个成熟的ECU诊断系统中,各模块分工明确:

[诊断仪] ↓ / ↑ CAN Bus ↓ / ↑ [CAN Driver] ← 中断处理、帧收发 ↓ / ↑ [ISO-TP Layer] ← 多帧重组与分段 ↓ / ↑ [UDS Protocol Stack] ← SID解析、服务调度、响应生成 ↓ / ↑ [Application] ← 提供真实数据源(EEPROM、RAM、传感器等)

每一层只关心自己的职责,彼此通过清晰接口通信。这种分层设计极大提升了系统的可测试性、可移植性和可维护性


工程实践中的坑点与秘籍

1. 响应延迟太高?别在中断里干重活!

曾经有个项目,工程师在CAN中断里直接调用Flash读取函数,结果导致总线死锁。记住:中断函数必须快进快出,复杂操作移到主循环或任务中处理。

2. 新增DID太麻烦?用查表法+编译期注册

可以结合宏定义和链接段技术,实现DID的自动注册:

#define REGISTER_DID(did_val, size_val, get_func) \ const DidDescriptor __SECTION(".did_table") did_##get_func = { \ .did = did_val, .size = size_val, .getter = get_func \ }; REGISTER_DID(0xF190, 17, get_vin_string) REGISTER_DID(0xF18C, 4, get_engine_runtime)

利用链接脚本将这些描述符收集到同一段,运行时遍历即可。

3. 如何防止非法写入?引入安全访问机制($27服务)

对于$2E写操作,不能无条件允许。应配合$27安全访问服务,要求诊断仪先“解锁”才能执行写入。

if (current_security_level < LEVEL_WRITING_ALLOWED) { send_negative_response(0x2E, 0x24); // Security access denied return; }

4. 内存吃紧?复用响应缓冲区

response_buffer可被所有服务共用。只要保证同一时间只有一个响应在构建,就能节省宝贵RAM资源。


实战案例:读取VIN码全过程演示

假设诊断仪发送:22 F1 90

  1. CAN接收中断触发,识别ID为0x7E0
  2. 提取数据[0x22, 0xF1, 0x90]
  3. 进入handle_read_by_identifier
  4. 解析DID = 0xF190
  5. 查表命中,调用get_vin_string()返回"LVSZ123456789XYZ\0"
  6. 构造响应:62 F1 90 LVSZ123456789XYZ
  7. 总长度19字节 > 8,启动ISO-TP分帧发送
  8. 诊断仪完整接收并解析,成功显示VIN

整个过程毫秒级完成,符合P2Tx时序要求。


结语:掌握底层逻辑,才能驾驭复杂系统

今天我们从零构建了一套完整的UDS请求处理驱动逻辑,涵盖了:

  • CAN中断捕获
  • SID分发机制
  • DID查表与动态响应生成
  • 正/负响应构造
  • 单帧与多帧自适应发送
  • 工程优化技巧

你会发现,无论未来是迁移到DoIP(基于TCP/IP的诊断),还是集成SOME/IP,其核心思想不变:解析请求 → 执行动作 → 返回标准化响应

而这一切的基础,正是你现在掌握的这套“看得见”的协议处理能力。

如果你正在开发一款新的域控制器、电池管理系统或智能网关,不妨立刻动手,把你第一个DID跑通。当诊断仪屏幕上跳出那个绿色的“62”开头的响应时,你会感受到一种独特的成就感——那是机器之间的“语言”被真正理解的瞬间。

💬互动时间:你在实现UDS时踩过哪些坑?欢迎在评论区分享你的故事。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/1 5:47:37

小白必看:通义千问3-Embedding-4B一键部署教程

小白必看&#xff1a;通义千问3-Embedding-4B一键部署教程 1. 引言 在当前大模型驱动的AI应用浪潮中&#xff0c;文本向量化&#xff08;Text Embedding&#xff09;作为构建知识库、语义检索和RAG&#xff08;检索增强生成&#xff09;系统的核心技术&#xff0c;正变得愈发…

作者头像 李华
网站建设 2026/5/1 4:46:45

cv_unet_image-matting文件命名规则说明:输出路径管理实战技巧

cv_unet_image-matting文件命名规则说明&#xff1a;输出路径管理实战技巧 1. 背景与应用场景 在基于 U-Net 的图像抠图项目 cv_unet_image-matting 中&#xff0c;WebUI 界面由开发者“科哥”构建&#xff0c;支持单图与批量处理模式。随着用户对自动化、可追溯性要求的提升…

作者头像 李华
网站建设 2026/4/30 9:15:15

告别手动复制粘贴|PDF-Extract-Kit实现表格公式自动解析

告别手动复制粘贴&#xff5c;PDF-Extract-Kit实现表格公式自动解析 1. 引言&#xff1a;从繁琐操作到智能提取 在科研、工程和日常办公中&#xff0c;PDF文档承载了大量关键信息&#xff0c;尤其是包含复杂数学公式与结构化数据的学术论文和技术报告。传统方式下&#xff0c…

作者头像 李华
网站建设 2026/5/1 4:45:34

BERT中文MLM模型部署痛点解决:低算力环境高效运行案例

BERT中文MLM模型部署痛点解决&#xff1a;低算力环境高效运行案例 1. 引言&#xff1a;BERT 智能语义填空服务的工程挑战 随着自然语言处理技术的发展&#xff0c;基于Transformer架构的预训练语言模型在中文语义理解任务中展现出强大能力。其中&#xff0c;BERT&#xff08;…

作者头像 李华
网站建设 2026/5/1 5:21:34

如何生成古典音乐?NotaGen大模型镜像实践指南

如何生成古典音乐&#xff1f;NotaGen大模型镜像实践指南 1. 引言&#xff1a;AI赋能古典音乐创作的新范式 在人工智能技术快速发展的今天&#xff0c;音乐创作正迎来一场深刻的变革。传统上依赖作曲家灵感与技巧的古典音乐创作&#xff0c;如今可以通过深度学习模型实现高质…

作者头像 李华
网站建设 2026/5/1 4:44:44

5分钟上手GLM-4.6V-Flash-WEB,AI视觉应用不再难

5分钟上手GLM-4.6V-Flash-WEB&#xff0c;AI视觉应用不再难 在人工智能加速落地的今天&#xff0c;多模态大模型正逐步从“实验室技术”走向“真实场景服务”。然而&#xff0c;部署复杂、算力要求高、推理延迟大等问题&#xff0c;长期制约着视觉语言模型在中小规模项目中的普…

作者头像 李华