UDS 0x22:DID读取与响应解析

2026-11-1510

台架上最容易被误判的一类问题是:诊断仪发送 22 F1 90,ECU 能返回 62 F1 90,但一换成多个 DID,响应就变成了 7F 22 31,或者长响应在第二个连续帧后被测试工具判成乱码。真正需要先确认的不是“0x22 能不能读”,而是 DID 的数据定义、访问条件、响应长度和 ISO-TP 重组是否一致。

本文按嵌入式诊断工程的排查顺序,说明 ReadDataByIdentifier 的标准边界、DID 请求与响应的逐字节格式、CAN/ISO-TP 抓包、P2/P2* 时序、NRC 处理,以及一份可以作为 ECU 服务骨架的 C 语言实现。示例中的 CAN ID、DID 值、会话权限和记录长度属于演示配置,量产项目应以 ODX、诊断数据库和 ECU 诊断规范为准。

1. 先把三个层次分开

ReadDataByIdentifier 是 ISO 14229 应用层服务。它只定义 0x22、DID 和 dataRecord 的排列方式,不定义 CAN 标识符,也不定义一条 CAN 帧如何装下长响应。本例采用经典 CAN、普通寻址和 8 字节数据区;扩展寻址或 CAN FD 的 PCI/有效载荷空间要按实际传输配置重新计算。

层次: CAN 链路层

本文示例: 0x7E0、0x7E8、8 字节数据区

由谁决定: ECU 网络矩阵、寻址配置

排查时看什么: ID、仲裁、总线错误、填充字节

层次: ISO-TP 传输层

本文示例: 03、10 14、21、22、30 00 00

由谁决定: ISO 15765-2 和传输层配置

排查时看什么: PCI、长度、SN、FC、STmin、超时

层次: UDS 应用层

本文示例: 22 F1 90、62 F1 90 ...

由谁决定: ISO 14229-1 和 DID 诊断数据库

排查时看什么: SID、DID、dataRecord、NRC、访问条件

在抓包工具中,03 是单帧 PCI,不能当成 UDS 字段;10 14 的 0x14 是 UDS PDU 总长度,也不能当成 DID。只有完成 ISO-TP 重组后,服务解析器才应该看到 22 F1 90 这样的应用层数据。

报文到数据:六步分层处理

阶段 输入、处理与输出
01 CAN 接收网络配置指定的 CAN ID 和原始数据区。
02 ISO-TP 识别 PCI、长度和序号,重组完整 UDS PDU。
03 请求解析 22 + 一个或多个两字节 DID,没有子功能。
04 访问检查 筛选会话支持项;检查认证、安全和车辆条件。
05 数据读取 应用或 NvM 接口读取一致性快照,明确长度和编码。
06 响应发送 62 + DID + 数据;长响应交给 ISO-TP 分段。

2. 0x22 在 ISO 14229 中到底定义了什么

2.1 服务职责和 DID 边界

ISO 14229-1:2020 第 11.2 节把 0x22 定义为:客户端通过一个或多个两字节 dataIdentifier 请求服务器读取相应的数据记录。服务端把每个 DID 的标识符回显到正响应中,并在其后放置对应的 dataRecord。

标准并不要求每个 ECU 实现全部 DID。附录 C.1 对 DID 值域及部分标识用途有明确分配;在这些边界内,OEM/供应商定义实际支持项、dataRecord 的长度、编码、缩放、字节序和访问权限。VIN、软件版本、零件号、测量值和内部状态都可以作为记录,但不能随意占用标准保留或已分配的标识。

例如,附录 C.1 将 F190 分配给 VIN、F187 分配给车辆制造商备件号、F189 分配给 ECU 软件版本号。标识用途有标准依据,但 ECU 是否支持、记录如何编码仍要查项目定义;不能把 F187 当成软件版本,也不能推断所有 DID 都是 ASCII 字符串。

2.2 请求长度和重复 DID

0x22 没有 SubFunction 字节。应用层请求由一个 SID 和一个或多个 DID 组成:

22 DID_H DID_L [DID_H DID_L] ...

因此请求的最小 UDS 长度为 3 字节,总长度必须满足 1 + 2 × n,也就是奇数长度。22 F1 共 2 字节,既小于最小长度,也缺少 DID 低字节。同一个 DID 可以在同一请求中出现多次,服务器应把它们当成独立参数,并在响应中按请求次数重复返回。项目通常会限制一次请求的 DID 数量,或者限制响应总长度;限制值应从诊断数据库和 ECU 配置读取。

2.3 用 DID 数据字典先解决“怎么拆响应”

一个工程上可用的 DID 字典至少要记录以下信息:

字段: DID

示例: 0xF190

用途: 识别数据记录

字段: 记录长度

示例: 17 字节

用途: 解析响应边界、计算 ISO-TP 长度

字段: 编码

示例: ASCII

用途: 转换为显示值

字段: 字节序

示例: 大端或按字节

用途: 解析多字节数值

字段: 会话

示例: 默认/扩展

用途: 服务访问门控

字段: 安全等级

示例: 未解锁/等级 1

用途: 安全访问检查

字段: 读取回调

示例: read_vin()

用途: 从应用、NvM 或状态缓存取值

dataRecord 在响应中没有统一的长度前缀。诊断客户端必须知道每个 DID 的长度,或者使用项目约定的解码器;只靠 62 后面“剩余多少字节”无法可靠拆出多个 DID。

3. 先看一组完整的 0x22 抓包

下面的例子使用物理寻址 CAN ID 0x7E0(诊断仪到 ECU)和 0x7E8(ECU 到诊断仪)。这些 ID 只是常见项目配置,不是 0x22 服务规定的值。

3.1 读取 VIN:单帧请求、ISO-TP 长响应

假定项目 DID F190 的 dataRecord 是 17 个 ASCII 字节,本文演示 VIN 为 W0L000043MB541326。

诊断仪发送请求:

CAN ID 0x7E0 03 22 F1 90 00 00 00 00

其中 03 表示后面有 3 个 UDS 字节,真正的应用层请求是 22 F1 90。ECU 的 UDS 正响应包含 62 F1 90 加 17 字节 VIN,共 20 字节,所以需要 ISO-TP 多帧发送:

CAN ID 0x7E8 10 14 62 F1 90 57 30 4C # First Frame,UDS 长度 0x14
CAN ID 0x7E0 30 00 00 00 00 00 00 00 # Flow Control:Continue、BS=0、STmin=0
CAN ID 0x7E8 21 30 30 30 30 34 33 4D # Consecutive Frame,SN=1
CAN ID 0x7E8 22 42 35 34 31 33 32 36 # Consecutive Frame,SN=2

去掉 ISO-TP PCI 后,重组得到的 UDS 响应是:

62 F1 90 57 30 4C 30 30 30 30 34 33 4D 42 35 34 31 33 32 36

62 是 0x22 + 0x40,F1 90 是回显的 DID,后面的 17 字节才是 dataRecord。本例首帧带 6 字节 UDS 数据,两个连续帧各带 7 字节,正好共 20 字节,响应没有额外填充。请求帧末尾的 00 才是填充;其他长度场景的最后一帧若有填充,也必须按 ISO-TP 总长度去掉。

3.2 多个 DID 时,长度必须由字典确定

假设项目定义:0x010A 返回 11 字节测量值,0x0110 返回 1 字节电池状态。请求可以写成:

CAN ID 0x7E0 05 22 01 0A 01 10 00 00

响应的应用层排列应为:

62 01 0A [11 字节 dataRecord] 01 10 [1 字节 dataRecord]

客户端只有在知道 010A 的记录长度为 11 后,才能把紧随其后的 01 10 识别为第二个 DID。若 010A 实际是变长记录,项目必须提供长度字段或专用解码规则;不能把一个变长 DID 和后续 DID 直接拼接后再猜边界。

3.3 重复 DID 不是格式错误

标准允许同一 DID 重复出现。例如:

22 F1 90 F1 90

只要请求没有超过一次允许的 DID 数量、数据访问条件和响应长度限制,服务端应返回两组 F1 90 + dataRecord。内部可以复用同一份稳定快照,线上却不能擅自合并为一组;把重复 DID 当作格式错误,也不符合第 11.2.1 节的规定。

4. 请求和响应逐字节解析

4.1 请求消息

UDS 字节: 1

字段: ReadDataByIdentifier Request SID

长度: 1

说明: 固定 0x22

UDS 字节: 2

字段: DID 第 1 字节

长度: 1

说明: 16 位 DID 的高字节

UDS 字节: 3

字段: DID 第 2 字节

长度: 1

说明: 16 位 DID 的低字节

UDS 字节: 4…

字段: 后续 DID

长度: 2×(n-1)

说明: 每个 DID 都是两个字节

DID 在诊断报文中按高字节在前发送。ECU 内部把 DID 存成 uint16_t 还是拆成两个寄存器,是实现细节;不能因为 CPU 使用小端,就把线上顺序写成低字节在前。

请求检查建议按以下顺序执行:

  1. 检查 req != NULL、req_len >= 3 和首字节是否为 0x22。
  2. 检查 (req_len - 1) 是否为偶数,并计算 DID 数量。
  3. 检查 DID 数量、响应长度上限和当前连接的寻址上下文。
  4. 按项目的访问矩阵检查会话、安全状态和车辆条件。
  5. 按图 20 逐个检查当前会话支持项:不支持的 DID 跳过;至少有一个支持项才继续组包。只有全部不支持时,才因这个原因返回 0x31。

4.2 正响应消息

正响应使用 0x62 作为 SID:

62 DID_H DID_L dataRecord... [DID_H DID_L dataRecord...]
字段 含义
0x62 0x22 + 0x40
DID 与请求中的 DID 顺序一致并逐个回显
dataRecord 该 DID 的编码值,长度和格式由项目定义

服务器应在一个 0x22 正响应中返回本次请求在当前会话下支持的 DID 数据,保留对应顺序和重复次数。例如 22 F1 90 12 34 中只有 F190 受支持,正响应只含 F190 + VIN,不为 1234 插入空记录;全部不支持才返回 0x31。若读取过程需要异步等待,可以先按通用诊断时序发送 7F 22 78,最终仍要发送正响应或最终负响应;不能在发送过 0x78 后悄悄结束本次请求。

4.3 负响应和排查顺序

ISO 14229-1:2020 第 11.2.4 节列出了 0x22 需要支持的服务级 NRC:

NRC: 0x13

名称: incorrectMessageLengthOrInvalidFormat

标准触发条件: 请求长度无效,或请求 DID 数量超过一次允许的最大数量

工程排查方向: DID 是否成对;请求数量上限;是否误带 PCI

NRC: 0x14

名称: responseTooLong

标准触发条件: 总响应长度超出可用响应/底层传输能力

工程排查方向: 计算全部支持记录的总长度;区分传输上限和临时缓存不足

NRC: 0x22

名称: conditionsNotCorrect

标准触发条件: 服务器当前运行条件不满足

工程排查方向: 电源模式、车速、点火状态、应用数据是否准备好

NRC: 0x31

名称: requestOutOfRange

标准触发条件: 全部 DID 在设备/当前会话下不支持,或请求的动态 DID 尚未分配

工程排查方向: DID 字典、会话配置、动态 DID 生命周期

NRC: 0x33

名称: securityAccessDenied

标准触发条件: 至少一个 DID 需要解锁,而服务器仍处于锁定状态

工程排查方向: 0x27 的等级、密钥状态和 DID 安全矩阵

负响应格式固定为:

7F 22 NRC

0x13 和 0x14 必须分开:超过允许的请求 DID 数量返回 0x13;请求格式和数量合法,但支持记录合计响应过长,才进入 0x14 路径。单帧装不下,应首先由 ISO-TP 分段;某个局部工作数组太小,也不能直接冒充底层传输极限。图 20 还包含认证检查失败的 0x34 authenticationRequired 分支,采用认证访问控制的项目需要在诊断框架中实现;它与 0x27 的安全等级检查不是同一件事。

排查时按“长度格式 → DID 是否存在 → 会话/安全 → 车辆条件 → 数据读取回调 → 响应长度”顺序逐层收敛,通常比反复修改 DID 值更快。

5. 会话、安全访问和数据一致性

5.1 0x22 没有统一的“必须先做 0x10/0x27”规则

ReadDataByIdentifier 服务本身不规定所有 DID 必须在扩展会话或安全解锁后才能读取。常见做法是:公开的版本号、VIN 允许在默认会话读取;标定数据、密钥相关状态或受保护的生产数据则要求扩展会话和指定安全等级。

下面的表格是项目访问矩阵示例,不是 ISO 14229 的通用值:

DID: F190

默认会话: 允许

扩展会话: 允许

安全等级: 无

数据来源: NvM 中的 VIN

DID: F189

默认会话: 允许

扩展会话: 允许

安全等级: 无

数据来源: 软件版本常量

DID: 010A

默认会话: 允许

扩展会话: 允许

安全等级: 无

数据来源: DEM/应用实时快照

DID: 1234(演示项目 DID)

默认会话: 拒绝

扩展会话: 允许

安全等级: 等级 1

数据来源: 受保护应用数据

对于当前会话支持的 DID,安全检查失败应按图 20 返回 0x33;不能把受保护 DID 静默过滤后返回其他公开数据。与之不同,设备或当前会话不支持的 DID 会被跳过。测试用例要把“不支持”和“支持但被安全锁定”分开。

5.2 读取实时量时不要长时间持有应用锁

DID 的读取回调可能访问应用任务快照、DEM 状态或 NvM 缓存。推荐先在数据提供任务中建立一致性快照,再由 DCM/DSL/DSP 读取快照;不要在持有 NVM 写锁或应用互斥锁的情况下等待 ISO-TP 连续帧。

对于跨多个任务更新的数据,可以采用版本号或双缓冲:读取开始时记录版本,复制记录后再次比较版本;若版本变化,返回 0x22、重新采样,或按项目定义发送 0x78 后重试。不能把半更新的 16 位数值拆成两个字节直接发出去。

6. P2/P2*、ISO-TP 和服务状态机

6.1 时序边界

服务端 P2Server 从完整请求接收后计时,约束到响应开始发送,不能从第一帧 CAN 到达就简单计时。无法在当前窗口内开始最终响应时,框架需及时安排 7F 22 78,随后以 P2*Server 监督下一响应开始。客户端在请求发送完成后装载 P2Client,收到合法 0x78 后转入 P2*Client;再次收到 0x78 时重新装载扩展等待窗口,期间不重发请求。服务器数值通常来自 0x10 会话参数,客户端还要加入传输与调度裕量,不能把工具默认的 50 ms 或 5 s 写成统一标准值。

正响应的发送还要经过 ISO-TP:长响应先由传输层发送 First Frame,等待客户端 Flow Control,再发送 Consecutive Frame。首帧在应用时限内开始,不要求所有连续帧都在 P2 内收完。N_Bs、N_Cr 等 N 系列计时器监督传输等待,STmin 约束连续帧最小间隔;STmin 本身不是超时计时器。测试脚本应把这些事件分别记录。

6.2 推荐的状态机

服务状态与响应决策

状态 完成条件或响应动作
接收和校验 完整请求长度、DID 配对和数量上限;无效返回 7F 22 13。
逐 DID 门控 跳过当前会话不支持项;支持项需通过认证、安全和条件检查。
组包前检查 全部不支持返回 31;合计响应超过连接能力返回 14。
读取稳定记录 数据未就绪,保留事务并由框架在当前窗口内安排 78。
异步继续 继续监督 P2*,不覆盖原请求;完成后发送最终响应。
发送和释放 62 正响应或最终 NRC,经 ISO-TP 发送;取消/断链也释放上下文。

服务状态机应绑定一个诊断连接和一个请求上下文。新的物理寻址请求到来时,不能覆盖尚未完成的 0x22 上下文;功能寻址下如果项目允许多个 ECU 响应,还要在测试端按 CAN ID 分开维护状态。

7. C 语言实现:先规划响应,再读取数据

在 AUTOSAR 中,CanTp 负责 ISO-TP;DCM 的 DSL 管理连接/时序,DSD 分发服务,DSP 处理 DID 读取。应用或 NvM 提供记录;DEM 只有在该 DID 确实映射到故障数据时才参与。下面是项目接口骨架,接口名不代表 AUTOSAR 标准 API。

代码只演示固定长度静态 DID。诊断框架先完成连接准入与认证门控,每个 DID 再检查会话、安全和条件。DID 数量、会话位图、响应容量都是本例的资源配置。

7.1 数据字典和内部结果

#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>
 
#define RDB_MAX_DIDS 8u
 
typedef enum { READ_OK, READ_BUSY, READ_ERROR } ReadRc;
typedef enum {
    RDB_READY, RDB_PENDING, RDB_INTERNAL, RDB_ARGUMENT
} RdbRc;
 
/* snapshot 属于当前事务,回调必须不阻塞且不越界。
 * 多次调用应读取同一稳定版本;READ_BUSY 时由框架稍后重试。 */
typedef ReadRc (*ReadDid)(void *snapshot, uint8_t *dst,
                          size_t cap, size_t *written);
typedef bool (*CheckDid)(void *snapshot);
typedef struct {
    uint16_t did;
    size_t length;
    uint8_t session_mask;
    bool secured;
    CheckDid condition; /* NULL 表示无额外条件 */
    ReadDid read;
} DidEntry;
typedef struct {
    uint8_t session_bit; /* 例如 1、2;不是线上会话号 */
    bool unlocked;
    void *snapshot;
} ReadEnv;
 
static const DidEntry *FindDid(const DidEntry *table, size_t count,
                                uint16_t did)
{
    for (size_t i = 0u; i < count; ++i)
        if (table[i].did == did) return &table[i];
    return NULL;
}
 
static RdbRc Negative22(uint8_t *p, size_t *n, uint8_t nrc)
{
    p[0] = 0x7Fu; p[1] = 0x22u; p[2] = nrc;
    *n = 3u;
    return RDB_READY;
}

RDB_PENDING 是调度结果,表示这次调用尚未构造最终响应,不能直接当作“已发 0x78”。RDB_INTERNAL 表示字典或底层接口违反契约,由框架记录原因并进入明确的故障处理路径;不随意映射成清除/编程相关的 0x72。

7.2 先筛选支持项并完成访问检查

RdbRc Handle22(const uint8_t *req, size_t req_len,
               uint8_t *rsp, size_t cap, size_t *rsp_len,
               const DidEntry *table, size_t table_count,
               const ReadEnv *env)
{
    const DidEntry *plan[RDB_MAX_DIDS];
    size_t used = 0u, total = 1u;
    if (rsp_len != NULL) *rsp_len = 0u;
    if ((req == NULL) || (rsp == NULL) || (rsp_len == NULL) ||
        (table == NULL) || (env == NULL) || (cap < 3u))
        return RDB_ARGUMENT;
    if ((req_len == 0u) || (req[0] != 0x22u))
        return RDB_ARGUMENT; /* 由服务分发器保证 SID */
    if ((req_len < 3u) || (((req_len - 1u) % 2u) != 0u))
        return Negative22(rsp, rsp_len, 0x13u);
    size_t requested = (req_len - 1u) / 2u;
    if (requested > RDB_MAX_DIDS)
        return Negative22(rsp, rsp_len, 0x13u);
 
    for (size_t i = 0u; i < requested; ++i) {
        size_t at = 1u + 2u * i;
        uint16_t did = ((uint16_t)req[at] << 8u) | req[at + 1u];
        const DidEntry *e = FindDid(table, table_count, did);
        if ((e == NULL) || ((e->session_mask & env->session_bit) == 0u))
            continue; /* 当前会话不支持,跳过这个 DID */
        if (e->secured && !env->unlocked)
            return Negative22(rsp, rsp_len, 0x33u);
        if ((e->condition != NULL) && !e->condition(env->snapshot))
            return Negative22(rsp, rsp_len, 0x22u);
        if ((e->read == NULL) || (e->length == 0u)) return RDB_INTERNAL;
        plan[used++] = e; /* 保留请求次序和重复次数 */
    }
    if (used == 0u) return Negative22(rsp, rsp_len, 0x31u);

7.3 长度预检后才允许写数据

下面的代码接在 Handle22 内。cap 在本例代表该连接可完成发送的配置响应上限,缓冲区至少有这么大。若量产实现使用分页或流式发送,就需要独立计算传输上限,不能把临时页缓冲区大小当成 0x14 的条件。

/* 续接上面的 Handle22 函数 */
    for (size_t i = 0u; i < used; ++i) {
        if (((cap - total) < 2u) ||
            (plan[i]->length > (cap - total - 2u)))
            return Negative22(rsp, rsp_len, 0x14u);
        total += 2u + plan[i]->length;
    }
 
    rsp[0] = 0x62u;
    size_t pos = 1u;
    for (size_t i = 0u; i < used; ++i) {
        const DidEntry *e = plan[i];
        size_t written = 0u;
        ReadRc rc = e->read(env->snapshot, &rsp[pos + 2u],
                             e->length, &written);
        if (rc == READ_BUSY) return RDB_PENDING;
        if ((rc != READ_OK) || (written != e->length)) return RDB_INTERNAL;
        rsp[pos] = (uint8_t)(e->did >> 8u);
        rsp[pos + 1u] = (uint8_t)e->did;
        pos += 2u + written;
    }
    *rsp_len = pos;
    return RDB_READY;
}

只有 RDB_READY 才发送 rsp_len 字节。返回 PENDING/INTERNAL 时长度仍为 0,前面已经写入的临时数据不能被发送。底层回调必须遵守容量契约;返回后检查 written 无法补救已经发生的越界写入。

异步重试必须保留请求、稳定快照和连接归属,避免把前半段旧数据和后半段新数据拼起来。框架在 P2Server/P2*Server 截止前安排挂起响应,并在取消、断链、复位和 ISO-TP 发送失败时释放上下文。同步读取不能在服务函数里自旋等 NvM。

7.4 客户端按预期 DID 顺序和长度解析

以下 expected 是本次请求在当前会话下应返回的支持项列表,可以含重复 DID。需要检查不支持项是否被正确跳过时,测试脚本先按诊断数据库构造这个列表;未知长度的响应不能直接猜着拆。

typedef struct { uint16_t did; size_t length; } ExpectedDid;
typedef struct {
    uint16_t did;
    const uint8_t *data;
    size_t length;
} DidView;
typedef enum {
    RDB_OK, RDB_ARG, RDB_PROTOCOL, RDB_OUTPUT_FULL,
    RDB_TRANSPORT, RDB_P2_TIMEOUT, RDB_P2STAR_TIMEOUT,
    RDB_TOTAL_TIMEOUT, RDB_NEGATIVE
} ClientRc;
 
static ClientRc Parse22(const uint8_t *p, size_t n,
                         const ExpectedDid *expected, size_t count,
                         DidView *views, size_t view_cap, size_t *view_count)
{
    if (view_count != NULL) *view_count = 0u;
    if ((p == NULL) || (expected == NULL) || (views == NULL) ||
        (view_count == NULL) || (count == 0u)) return RDB_ARG;
    if ((n < 4u) || (p[0] != 0x62u)) return RDB_PROTOCOL;
    if (count > view_cap) return RDB_OUTPUT_FULL;
    size_t pos = 1u;
    for (size_t i = 0u; i < count; ++i) {
        if (((n - pos) < 2u) || (expected[i].length == 0u))
            return RDB_PROTOCOL;
        uint16_t did = ((uint16_t)p[pos] << 8u) | p[pos + 1u];
        pos += 2u;
        if ((did != expected[i].did) || (expected[i].length > (n - pos)))
            return RDB_PROTOCOL;
        views[i] = (DidView){did, &p[pos], expected[i].length};
        pos += expected[i].length;
    }
    if (pos != n) return RDB_PROTOCOL;
    *view_count = count;
    return RDB_OK;
}

解析器严格检查顺序、长度、重复次数和尾部剩余字节。失败时 view_count 保持 0,调用者不能消费部分结果;视图引用响应缓冲区,缓冲区失效后也必须停止使用。变长 DID 要换成显式长度解码回调。

7.5 P2/P2* 等待不混进字段解析器

这层针对物理寻址,一条连接同时只允许一个事务。receive 收取完整 UDS PDU,等待参数约束响应开始;多帧开始后由 ISO-TP 的 N 系列计时器监督。适配层必须保证有限时间返回,并同时遵守传入的总事务剩余预算。

typedef enum { RX_PDU, RX_TIMEOUT, RX_TOO_LARGE, RX_ERROR } RxRc;
typedef struct {
    void *ctx;
    uint32_t (*now_ms)(void *);
    RxRc (*receive)(void *, uint8_t *, size_t, size_t *,
                     uint32_t start_wait_ms, uint32_t total_left_ms);
} ReadIo;
typedef struct {
    uint32_t p2_ms, p2star_ms, total_ms;
} ReadTiming;
 
static ClientRc Wait22(const ReadIo *io, const ReadTiming *t,
                        uint8_t *p, size_t cap, size_t *length, uint8_t *nrc)
{
    if (length != NULL) *length = 0u;
    if ((io == NULL) || (t == NULL) || (p == NULL) || (length == NULL) ||
        (nrc == NULL) || (io->now_ms == NULL) || (io->receive == NULL) ||
        (cap < 3u) || (t->p2_ms == 0u) || (t->p2star_ms == 0u) ||
        (t->total_ms == 0u) || (t->p2_ms >= UINT32_C(0x80000000)) ||
        (t->p2star_ms >= UINT32_C(0x80000000)) ||
        (t->total_ms >= UINT32_C(0x80000000))) return RDB_ARG;
    *nrc = 0u;
    uint32_t start = io->now_ms(io->ctx);
    uint32_t stage = start, window = t->p2_ms;
    bool pending = false;
    for (;;) {
        uint32_t now = io->now_ms(io->ctx);
        uint32_t elapsed = now - start, waited = now - stage;
        if (elapsed >= t->total_ms) return RDB_TOTAL_TIMEOUT;
        if (waited >= window)
            return pending ? RDB_P2STAR_TIMEOUT : RDB_P2_TIMEOUT;
        uint32_t left = t->total_ms - elapsed;
        uint32_t wait = window - waited;
        if (wait > left) wait = left;
        size_t n = 0u;
        RxRc rc = io->receive(io->ctx, p, cap, &n, wait, left);
        if ((io->now_ms(io->ctx) - start) >= t->total_ms)
            return RDB_TOTAL_TIMEOUT;
        if (rc == RX_TIMEOUT)
            return pending ? RDB_P2STAR_TIMEOUT : RDB_P2_TIMEOUT;
        if (rc == RX_TOO_LARGE) return RDB_OUTPUT_FULL;
        if (rc != RX_PDU) return RDB_TRANSPORT;
        if ((n == 0u) || (n > cap)) return RDB_PROTOCOL;
        if (p[0] == 0x7Fu) {
            if ((n != 3u) || (p[1] != 0x22u)) return RDB_PROTOCOL;
            if (p[2] != 0x78u) {
                *nrc = p[2];
                return RDB_NEGATIVE;
            }
            pending = true;
            stage = io->now_ms(io->ctx);
            window = t->p2star_ms;
            continue;
        }
        if (p[0] != 0x62u) return RDB_PROTOCOL;
        *length = n;
        return RDB_OK; /* 随后交给 Parse22 做完整字段校验 */
    }
}

调用顺序是:独占连接 → ISO-TP 完成请求发送 → Wait22 → Parse22 → 释放连接并清理残留接收状态。发送失败不要调用等待器;任一路径返回后都要释放资源。total_ms 是项目防止无限挂起的总期限;32 位单调毫秒计数允许一次回绕,本例预算均限制在半个计数周期内。

0x22 没有子功能,因此不能把 DID 高字节 bit 7 当成抑制正响应位。功能寻址时还要执行第 8.7 节的通用负响应抑制规则:例如没有发过 pending 的 0x31 通常不在线上返回;不能因为某个 ECU 静默就判断它的 DID 读到了空值。需要完整长响应时,优先物理寻址逐 ECU 查询,并按网络配置验证 FF/FC/CF 通道。

8. 掉电、并发和非易失性存储

0x22 是读服务,但“读到什么”仍然可能受 NvM 和应用任务时序影响:

  1. VIN、软件版本等静态数据通常从 NvM 启动加载到 RAM,再由 DID 回调读取。
  2. 车辆电压、转速、温度等实时值可能来自周期任务快照,而不是直接访问外设寄存器。
  3. 故障状态或生产配置在 NvM 写入尚未完成时,读取结果可能处于旧版本、过渡版本或“数据未准备好”状态。

RAM 快照可以采用临界区复制、不可变缓冲区或带版本的同步方案;NvM 块再按项目要求使用 CRC 和恢复策略。读取前后比较版本只是一部分:对共享内存还必须满足平台内存序和同步要求,volatile 不能代替互斥或原子操作。版本不一致时重试或按项目条件路径结束,不能发送半更新数据。不要把“读取函数返回成功”理解为数据一定已经落盘。

服务端还要考虑以下并发场景:

  • 同一诊断连接上一个 0x22 尚未结束时,新的请求是否排队、覆盖还是返回忙碌错误。
  • DTC 监控任务更新状态时,0x22 读取的快照是否允许短暂不一致。
  • 多个诊断连接同时读取大块 DID 时,是否共享响应缓冲区,是否会互相覆盖。
  • ISO-TP 发送长响应时,是否释放应用锁,避免连续帧等待阻塞 NvM 或监控任务。

9. 测试用例和验证方法

编号: TC-01

场景: 单 DID 短数据

请求/前置条件: 22 F1 89,默认会话

预期结果: 62 F1 89 ...

验证重点: SID、DID 回显、记录长度

编号: TC-02

场景: VIN 长响应

请求/前置条件: 22 F1 90

预期结果: ISO-TP FF/FC/CF,最终 62 F1 90 + 17 字节

验证重点: 首帧长度、SN、填充字节

编号: TC-03

场景: 多 DID

请求/前置条件: 22 01 0A 01 10

预期结果: 一条正响应按字典顺序返回两组记录

验证重点: DID 边界和总长度

编号: TC-04

场景: 重复 DID

请求/前置条件: 22 F1 90 F1 90,数量/容量合法

预期结果: 返回两组 DID 记录,不能静默去重

验证重点: 顺序、重复次数和稳定快照

编号: TC-05

场景: 缺少 DID 低字节

请求/前置条件: 22 F1

预期结果: 7F 22 13

验证重点: 不把缺失的 DID 低字节当成 0

编号: TC-06

场景: 未知 DID

请求/前置条件: 22 FF FF

预期结果: 7F 22 31

验证重点: 诊断数据库和会话映射

编号: TC-07

场景: 安全未解锁

请求/前置条件: 请求受保护 DID

预期结果: 7F 22 33

验证重点: 0x27 等级、锁定状态

编号: TC-08

场景: 条件不满足

请求/前置条件: 车辆数据快照无效

预期结果: 7F 22 22 或项目规定的等待流程

验证重点: 有效标志和 P2/P2*

编号: TC-09

场景: 响应超长

请求/前置条件: 请求数量合法,合计响应超过连接能力

预期结果: 7F 22 14

验证重点: 完整长度计算,不返回截断成功报文

编号: TC-10

场景: Flow Control 异常

请求/前置条件: 客户端不发 FC、CF 丢失或序号错误

预期结果: 传输层超时,服务上下文释放

验证重点: N_Bs/N_Cr、重入恢复

编号: TC-11

场景: 版本切换并发

请求/前置条件: 应用任务更新快照时读取

预期结果: 稳定快照或明确 0x22

验证重点: 版本号、锁和双缓冲

编号: TC-12

场景: 掉电恢复

请求/前置条件: NvM 写入后立即复位,再读取 DID

预期结果: 结果符合项目 NvM 恢复策略

验证重点: CRC、有效标志和旧版本回退

编号: TC-13

场景: DID 数量超限

请求/前置条件: 一次请求 9 个 DID,本例上限 8

预期结果: 7F 22 13

验证重点: 与响应过长分开验证

编号: TC-14

场景: 部分不支持

请求/前置条件: 支持 F190,不支持 1234;22 F1 90 12 34

预期结果: 只返回 F190 记录

验证重点: 无空记录、无静默安全过滤

编号: TC-15

场景: pending 后成功

请求/前置条件: 多次 7F 22 78 后 62 ...

预期结果: 客户端不重发,最终严格解析

验证重点: P2* 重装载、总期限

编号: TC-16

场景: 异常正响应

请求/前置条件: DID 错序、尾部多字节、记录截断

预期结果: 客户端协议错误

验证重点: 不输出部分结果

抓包验证时,建议同时记录三份日志:原始 CAN 帧、ISO-TP 重组后的 UDS PDU、按 DID 解码后的业务值。这样能快速判断问题位于总线、传输层还是数据字典,而不是只看诊断工具最后显示的一行字符串。

10. 工程边界和常见误区

  • 把 03、10 14 这样的 ISO-TP PCI 当成 0x22 的 DID 或数据。
  • 把 CAN ID 0x7E0/0x7E8 写成 ISO 14229 对 0x22 的固定要求。
  • 认为所有 DID 都是两字节整数,忽略 ASCII、缩放值、枚举、位域和变长记录。
  • 只判断正响应 SID 0x62,不检查 DID 回显、记录长度和字节序。
  • 把 F190、010A 等常见示例值域直接复制到另一个 ECU,而没有核对 ODX 和诊断数据库。
  • 收到一个受保护 DID 后,客户端反复重试请求,却没有先确认会话和 0x27 安全等级。
  • 把 0x14 responseTooLong 当作“单帧装不下”的同义词,忽略 DID 数量、服务器缓存和 ISO-TP 配置。
  • 在读取回调中同步等待 NvM 或应用锁,导致 P2 超时,随后又把 ISO-TP 超时误判为 ECU 不支持 0x22。
  • 用最大记录长度盲目切分响应,导致一个变长 DID 的尾部被错当成下一个 DID。
  • 在 ECU 返回 7F 22 78 后立即结束测试,不等待最终响应,也没有记录 P2* 内的重复 pending 行为。

11. 结语

ReadDataByIdentifier 的服务号只有一个字节,工程难点却集中在 DID 数据字典和分层边界。先用 ISO-TP 重组出完整 UDS PDU,再按 DID 的访问矩阵和记录长度解析;遇到负响应时按长度、权限、条件和响应容量逐层排查。对实时数据建立一致性快照,对长响应分别验证 P2/P2* 和 ISO-TP 超时,才能让 0x22 的抓包、代码和测试结果彼此对应。

12. 标准与验证依据

  • ISO 14229-1:2020,第 11.2 节、表 186~190:0x22 请求、响应与服务级 NRC。
  • ISO 14229-1:2020,图 20:多 DID 的会话支持、认证、安全、条件与响应长度检查顺序。
  • ISO 14229-1:2020,第 8.7 节、附录 C.1:通用请求/响应行为和 DID 定义边界。
  • CAN 承载和传输参数另按项目适用版本的 ISO 14229-3、ISO 15765-2 及网络矩阵配置核对。

创作许可协议

本项目采用 CC BY(署名) 进行许可。

评论(0)
- 没有更多了 -