2.7 读取数据(0x22)——开化验单
诊断最常用的SID
在你挂完号、解锁安全之后,你面临一个选择——你有几百个可能的数据点能读。发动机转速、冷却液温度、节气门角度、氧传感器电压、VIN码每个字符、软件版本号、最新一次的ECU启动时间——这些东西分散在ECU的RAM、ROM、EEPROM的各个角落。你怎么用一条标准命令读到任何一个?
答案是DID——DataIdentifier(数据标识符)。你不需要记住物理地址——你只需要说出DID的编码,ECU就知道你去哪取这个数据。
0x22服务执行的就是这个“DID→数据记录“的映射——UDS里被调用最频繁的SID,没有之一。
为什么是DID而不是物理地址
在你已经读完了第1章和第2.3节之后,我希望你对这个问题有一些直觉。但这里再补一刀——为什么UDS不让你直接说“读地址0x40024000到0x40024007“而是要求你说“读DID 0xF40C“?
两个根本原因:
1. 硬件无关性。 一个ECU可能今天用NXP MPC5744P(发动机转速存储在0xFC050008),明天换成Infineon TC297(发动机转速存储在0x70001200)。你不想因为改了一个传感器供应商就必须改全线的诊断仪软件。DID让诊断仪和硬件解耦——同一辆车不同ECU的不同物理地址都映射到同一个DID 0xF40C。
2. 语义化。 你看到的这个数据不是冷冰冰的字节——它是“发动机转速“——值范围(0~16383.75) rpm。DID携带元信息——值的分辨率、值的物理范围、值的单位——这些写在ECU的诊断配置里,诊断仪根据ISO 22900 (MVCI)或ODX数据库可以自动解析。你不需要理解原始的hex再换算。
核心洞察:DID把寻址从物理层抽离到了语义层。这和OBD-II的PID思想同源。DID是UDS对OBD-II最伟大的继承——它让诊断语言成了真正的“医生和病人之间的语言“,而不是“医生和内存管理单元之间的语言“。
DID的范围共识
ISO 14229-1:2013为不同参与者分配了DID的地址空间:
| DID范围 | 所有者 | 用途 |
|---|---|---|
| 0x0000~0x00FF | 法规保留 | SAE J1979-OBD专用(注:0x22仍可读取此范围内的DID,但0x00~0xFF的编码定义由法规指定) |
| 0x0100~0xDFFF | 车厂自定义 | OBD之外的所有诊断数据 |
| 0xF000~0xF0FF | 数据链路层 | CAN ID、网络配置、响应时间参数 |
| 0xF100~0xF17F | 应用层 | 通用ECU信息——版本号、启动类型、诊断协议信息 |
| 0xF180~0xF18F | 系统供应商 | ECU硬件编码、ECU制造日期等 |
| 0xF190 | 全球唯一 | VIN码(17字符ASCII) |
| 0xF1A0~0xF1EF | 法规 | 法规保留的ECU标识 |
| 0xF200~0xFFFF | 系统供应商自定义 | 供应商自己的诊断数据集 |
请求和正响应格式
请求(多DID):
Byte 0: 0x22 (SID)
Byte 1-2: dataIdentifier #1 (DID1, 高字节在前)
Byte 3-4: dataIdentifier #2 (DID2, 可选, 可重复)
...
一个请求可以包含任意数量的DID——但最大响应长度受传输层协议MTU限制。如果ECU计算出所有请求DID的响应数据总和超过了CAN传输层能承载的单条响应最大值(典型是4095字节),它有两种选择——(a) 返回部分DID并附加NRC 0x14(responseTooLong)(不推荐,因为诊断仪无法确定哪些DID被省略了),或(b) 直接NRC 0x14,诊断仪需要分多次请求。
正响应:
Byte 0: 0x62 (SID+0x40)
Byte 1-2: dataIdentifier #1 (回声)
Byte 3..N: dataRecord #1 (该DID的实际数据)
Byte N+1..N+2: dataIdentifier #2 (如果有第二个DID)
Byte N+3..M: dataRecord #2
... 重复上述格式 ...
核心洞察:正响应的格式是一个配对列表——每个DID后面紧跟它的数据。这个设计让ECU不需要固定的帧结构——DID长度固定(2字节)但数据长度可变——因此总长度可变。诊断仪需要读过DID的定义来知道每一个DID的数据长度——而不是凭帧格式推断。
一次请求多个DID——降低总线负载
诊断仪的一个核心优化:一次性询问多个DID。在CAN总线上,这能节省大量的周转时间(减少帧往返次数):
错误做法(低效):
诊断仪 → ECU: 0x22 0x01 0x0C (读转速)
ECU → 诊断仪: 0x62 0x01 0x0C 0x0B 0xB8
诊断仪 → ECU: 0x22 0x01 0x10 (读MAF)
ECU → 诊断仪: 0x62 0x01 0x10 0x00 0xAA
诊断仪 → ECU: 0x22 0x01 0x11 (读节气门位置)
ECU → 诊断仪: 0x62 0x01 0x11 0x1A
────────────────────────────────────
总发送: 3请求 + 3响应 = 6条CAN帧 (如果每个数据都能在单帧SF内完成)
正确做法(高效):
诊断仪 → ECU: 0x22 0x01 0x0C 0x01 0x10 0x01 0x11
ECU → 诊断仪: 0x62 0x01 0x0C 0x0B 0xB8
0x01 0x10 0x00 0xAA
0x01 0x11 0x1A
总发送: 1请求 + 1响应 (如果数据总长不超MTU)
最小DID请求检查——在ECU里的实现
uint8_t pdu[] = {0x22, 0x01, 0x0C}; // 读发动机转速
uint8_t req_len = 3;
if (pdu[0] != 0x22) send_nrc(NRC_SERVICE_NOT_SUPPORTED);
if ((req_len - 1) % 2 != 0)
send_nrc(0x13); // incorrectMessageLengthOrInvalidFormat
uint16_t did = (pdu[1] << 8) | pdu[2];
DIDConfig *did_cfg = lookup_did(did);
if (did_cfg == NULL)
send_nrc(NRC_REQUEST_OUT_OF_RANGE);
if (did_cfg->session_required > current_session)
send_nrc(NRC_SERVICE_NOT_SUPPORTED_IN_ACTIVE_SESSION);
if (did_cfg->security_required > security_level)
send_nrc(NRC_SECURITY_ACCESS_DENIED);
// 从端口读数据
uint8_t data[MAX_DID_DATA];
uint16_t data_len = read_did_data(did_cfg, data);
uint8_t response[2 + 2 + data_len];
response[0] = 0x62;
response[1] = pdu[1]; // DID高字节
response[2] = pdu[2]; // DID低字节
memcpy(&response[3], data, data_len);
send_positive_response(response, 3 + data_len);
本篇小结
- 0x22 ReadDataByIdentifier 是UDS被调用频率最高的SID——它用DID完成“语义化寻址“,将诊断语言从物理地址抽离到“发动机转速“的语义层面。
- DID范围被预先分配——0x00~0xFF=法规OBD,0x0100~0xDFFF=OEM,0xF000~0xFFFF=系统/供应商。
- 一个0x22请求可以包含多个DID——诊断仪应利用这个能力减少帧往返次数和总线负载。
- 诊断仪必须预先知道每个DID的数据长度——因为ECU的正响应格式不包含长度字段告知。
【下集预告】:光读不能写,只读不治病——下一个服务让你改成ECU的参数。相同DID,不同方向——0x2E按标识符写数据。这是诊断仪向ECU说’改这个参数为这个值’的命令。为什么有些DID写一次就永久生效、有些在下一次ECU重启时消失?我们讲’写’与’擦’的区别。