UDS 0x3D:按地址写入的报文与实现边界

2026-11-071

台架上有一条写请求:起始地址落在允许区域,记录长度也不大,ECU 却返回了 7F 3D 31。检查最后一个字节才发现,这笔写入跨过了标定区边界。另一个版本只检查起始地址,报文虽然成功,后面的配置却被覆盖了。

按地址写入时,地址、长度和记录必须放在一起检查。真正需要守住的是整段目标区域,以及从接收请求到存储收尾的执行边界。本文从 0x3D 的报文出发,说明这些检查应该落在哪一层,并给出带边界保护的 C 语言实现。

按地址写入,首先要确定地址指向什么

WriteMemoryByAddress 允许诊断仪从指定起始地址开始,向一个或多个连续存储位置写入数据。请求中的 memorySize 表示写入字节数,后面的 dataRecord 才是实际内容。

ISO 14229-1:2020 第 11.8 节提到了非易失性存储清理、标定值修改等用途,但没有规定一个全车通用的可写地址表。地址映射、存储设备、写入条件、记录含义以及是否需要访问保护,仍然要查 ECU/OEM 诊断定义。

因此,抓到 20 00 10 20,只能先把它解释为该请求携带的地址值。它是否映射到 MCU 的 0x20001020、另一个处理器的存储区或驱动内部的逻辑窗口,要由项目说明。不能直接把诊断报文里的整数强制转换成指针,再调用 memcpy。

几个容易混用的服务,入口其实不同:

服务 请求指定的对象 ECU 实现要对齐的边界
0x2E WriteDataByIdentifier 一个 DID 对应的数据记录 DID 布局、可写属性与写回调
0x3D WriteMemoryByAddress 起始地址和连续字节数 地址映射、整段访问权限与存储操作
0x34 / 0x36 / 0x37 下载请求、分块传输、传输退出 下载状态、块序号、传输长度与收尾流程

0x3D 没有下载服务那套分块计数机制,不能因为它也能写存储器,就默认用它替代固件下载流程。较大的写请求还受应用缓冲区、ISO-TP PDU 能力和项目单次写入上限限制。

ALFID 两个半字节不要读反

请求字段顺序固定为:

3D | ALFID | memoryAddress | memorySize | dataRecord

addressAndLengthFormatIdentifier,后文简称 ALFID,分别编码两个字段的长度:高半字节是 memorySize 的字节数,低半字节是 memoryAddress 的字节数。报文里依然先放地址,再放长度。

设地址长度为 a、长度字段的长度为 s,实际写入字节数为 L,则:

项目 长度或偏移,以 UDS PDU 的 0 为起点
SID 偏移 0,1 字节
ALFID 偏移 1,1 字节;a = ALFID & 0x0F,s = ALFID >> 4
memoryAddress 偏移 2,a 字节,高有效字节在前
memorySize 偏移 2 + a,s 字节,高有效字节在前
dataRecord 偏移 2 + a + s,恰好 L 字节
完整请求 2 + a + s + L 字节
正响应 2 + a + s 字节

例如,标准表 289 的请求是 3D 12 20 48 02 00 8C。0x12 表示地址占两字节、长度占一字节;从地址 0x2048 开始写两个字节,内容为 00 8C。12 不是写入长度,02 也不是子功能。

编码宽度与实际写入长度是两件事

ALFID 地址字段 长度字段 一个解析示例
0x12 2 字节 1 字节 20 48 / 02:地址 0x2048,写 2 字节
0x14 4 字节 1 字节 20 00 10 20 / 05:地址 0x20001020,写 5 字节
0x24 4 字节 2 字节 20 00 10 20 / 00 05:同一地址,仍然只写 5 字节

标准允许使用固定 ALFID,并在地址或长度字段的高位补 00。这些 00 属于参数本身,不能在请求解析或响应回显时随意删掉。

memorySize 的数值必须与后续记录字节数一致。长度字段是一字节时,正整数上限是 0xFF = 255;数值 00 在本服务里属于零长度请求,返回 0x31,不能借用其他协议的习惯把它解释成 256。附录 H 是 ALFID 组合示例,应与第 11.8.4 节的零长度限制一起读。

ALFID 的两个半字节为 0,意味着缺少必要的地址或长度字段,不能作为有效写入格式。其他宽度组合是否受 ECU 支持,要以项目支持表为准。标准参数采用半字节编码,不代表每个 ECU 必须接受所有非零组合,也不代表实现只能处理四字节地址。

本服务没有 SubFunction,也没有抑制正响应位。例如 ALFID 的 bit7 是长度编码的一部分,不能把它当作 suppressPosRspMsgIndicationBit 清掉。

memory identifier 需要单独配置

表 285 允许把地址字段的最高有效字节用于 memory identifier,以区分处理器或存储设备。例如某项目约定 ALFID 为 0x15,五字节地址中的首字节 01 选择设备,其余四字节 20 00 10 20 是设备内地址。

这里的 01 没有跨项目通用含义。解析器必须先依据诊断定义拆出设备与地址,再对选中的设备做整段范围检查;不能把五字节地址截断成 uint32_t 后继续写。本文代码不实现 memory identifier,并通过 ALFID 白名单拒绝该格式。

7D 回显的是参数,不是写入内容

前面的两字节地址请求,对应正响应为:

请求:3D 12 20 48 02 00 8C
响应:7D 12 20 48 02

根据表 286~287,正响应 SID 为 0x7D,后面依次原样回显 ALFID、memoryAddress、memorySize;不带 dataRecord。

客户端不能只判断首字节是 7D。应检查响应总长度恰好为 2 + a + s,并将 SID 后面的全部字节与本次请求的参数头逐字节比较。请求使用 0x24 和 00 05,响应也必须保留这组编码,不能为了缩短响应改成 0x14 和 05。

收到 7D,表示服务写操作成功。具体目标是临时 RAM、标定缓冲区还是要求掉电保持的配置,需要由该地址区的接口契约明确。若项目承诺持久化,底层只返回“已排队”就回复成功,会让诊断结果与实际存储状态脱节。

读回可以通过项目允许的 ReadMemoryByAddress(0x23) 等读取路径完成。读回、存储提交和断电后保持是不同证据;其中任何一个都不能自动替代另外两个。

在 CAN 上,把三层报文拆开看

以下抓包采用经典 CAN、11 位 ID、ISO-TP normal addressing。物理请求 ID 为 0x7E0,响应 ID 为 0x7E8,CAN 数据区固定八字节,填充为 00。这些都是本例网络配置。

标准的两字节写入,恰好装进一个单帧

方向 CAN ID CAN 数据区,十六进制 UDS 与填充边界
诊断仪到 ECU 7E0 07 3D 12 20 48 02 00 8C 07 为 SF PCI,后面七字节是完整请求,没有填充
ECU 到诊断仪 7E8 05 7D 12 20 48 02 00 00 正响应五字节,最后两个 00 是填充
ECU 到诊断仪,失败分支 7E8 03 7F 3D 31 00 00 00 00 地址、长度或格式不受支持时的负响应示例

第三行与第二行是不同执行结果,不是先成功再失败。请求中的 00 是待写数据,正响应末尾的 00 却是 CAN 填充,必须按 ISO-TP 长度区分。

四字节地址示例:五个数据字节也可能需要多帧

本文项目示例向逻辑地址 0x20001020 写入五字节 11 22 33 44 55:

3D 14 20 00 10 20 05 11 22 33 44 55

UDS PDU 长度是 2 + 4 + 1 + 5 = 12,即 0x0C。只计算 dataRecord 长度,会误判成能够用单帧发送。

次序 方向、CAN ID CAN 数据区 说明
1 诊断仪到 ECU,7E0 10 0C 3D 14 20 00 10 20 FF 声明 12 字节,本帧携带前六字节 UDS 数据
2 ECU 到诊断仪,7E8 30 00 05 00 00 00 00 00 FC:CTS、BS=0、STmin=5 ms,仅为本例取值
3 诊断仪到 ECU,7E0 21 05 11 22 33 44 55 00 CF 的六个有效数据字节完成请求,末字节为填充
4 ECU 到诊断仪,7E8 03 7F 3D 78 00 00 00 00 若写操作需要等待,按适用定时规则发送 ResponsePending
5 ECU 到诊断仪,7E8 07 7D 14 20 00 10 20 05 最终正响应七字节,恰好放入 SF

30 开头的流控帧属于 ISO-TP;7F 3D 78 是完整的 UDS 挂起响应。两者都不是写入成功确认。收到 FF 时,服务端也不能依据已经出现的地址提前启动写入,必须等完整 PDU 重组及全部检查结束。

如果同一请求改用 ALFID 0x24,正响应会变成 7D 24 20 00 10 20 00 05,共八字节。在上述经典 CAN 与 normal addressing 条件下,连正响应也需要多帧:

方向 CAN ID CAN 数据区 说明
ECU 到诊断仪 7E8 10 08 7D 24 20 00 10 20 FF,正响应总长八字节
诊断仪到 ECU 7E0 30 00 00 00 00 00 00 00 接收方诊断仪发送 FC
ECU 到诊断仪 7E8 21 00 05 00 00 00 00 00 CF 中 00 05 是 memorySize,后五字节是填充

这里的单帧容量结论不能直接套到 CAN FD、extended addressing 或其他传输配置。诊断定义、ISO-TP 版本及网络参数需要一起核对。

范围检查必须覆盖最后一个字节

表 288 要求检查区间 [MA, MA + MS - 1] 中的地址。只检查起始地址是否可写,会漏掉跨区域、跨权限和跨设备边界的请求。

假设项目开放一个 256 字节逻辑窗口 0x20001000~0x200010FF,单次最多写 64 字节。起始地址为 0x200010FC、长度为 8 时,末地址为 0x20001103,已经越过窗口;即使前四字节可写,也应在存储接口调用之前拒绝整笔请求。

直接用 uint32_t end = address + size - 1 做校验还会遇到整数回绕。例如地址 0xFFFFFFFE 加四字节,不能因为运算结果落回低地址就当作有效区间。

本例采用减法检查:先确认地址不小于窗口基址,再求 offset = address - base;只有 offset < span 且 size <= span - offset,整段才落在窗口中。size == 0 必须单独排除。这样不需要先算一个可能溢出的末地址。

检查层 至少要验证什么 不能遗漏的情况
参数解析 ALFID、字段是否完整、记录长度是否精确匹配 截断、额外尾字节、高位补零
逻辑区域 所有待写字节属于允许的设备、会话和区域 跨白名单边界、范围内的禁止区、地址回绕
物理存储 对齐、编程粒度、擦除粒度、写保护和执行限制 只写两个字节却擦除了包含其他对象的整个扇区
应用一致性 当前使用者是否允许更新、缓存及版本如何切换 另一任务读到半条记录、RAM 与恢复副本不一致

多个允许区之间存在空洞时,不能把最低和最高地址连成一个大窗口。Flash 的“目标字节均允许”也不等于“擦除涉及的全部字节均允许”;驱动适配层必须核对实际操作范围。读改写、双槽或日志如何保留邻近对象,属于项目存储设计。

NRC 按图 27 的检查阶段定位

负响应格式为 7F 3D NRC。表 288 跨两页列出了以下服务级响应码:

NRC 名称 触发含义与排查方向
0x13 incorrectMessageLengthOrInvalidFormat 报文长度错误;检查参数头是否完整、记录是否与 memorySize 相等
0x22 conditionsNotCorrect ECU 操作条件不满足;按项目核对电源、车辆状态、应用互锁等条件
0x31 requestOutOfRange ALFID 无效,长度为零或不受支持,区间内存在无效或禁止地址;逐层检查整段映射
0x33 securityAccessDenied 请求区间涉及受安全保护的地址,而对应安全状态尚未解锁
0x34 authenticationRequired 配置了认证限制而当前认证权限不足;不能与 0x27 安全解锁混为一谈
0x72 generalProgrammingFailure ECU 检测到写存储位置时的错误;查写接口及驱动作业结果,而非先改报文长度

这份 2020 文档在 0x34 的说明中沿用了 dataIdentifier 字样,但本服务请求没有 DID 字段。实现应结合图 27 的 Authentication check 和通用访问控制定义理解认证要求,不能据此往请求里额外插入一个 DID。

按图27定位写地址请求的检查阶段

检查点或阶段 处理结果
01 完整PDU ISO-TP完成整条请求重组后,进入地址写入服务;收到首帧时不提前写。
02 最小长度 至少5字节;不足返回NRC 0x13。
03 ALFID 地址及长度字段宽度受支持;无效返回NRC 0x31。
04 总长度 参数头完整,memorySize与dataRecord长度精确一致;不符返回NRC 0x13。
05 地址、长度与区域会话支持 整段有效且在当前会话允许,非零并满足长度限制;不符返回NRC 0x31。
06 认证 配置了认证限制时,核对客户端权限;不足返回NRC 0x34。
07 安全 区间含受安全保护地址时,核对解锁状态;未解锁返回NRC 0x33。
08 操作条件 核对项目运行条件及互锁;不满足返回NRC 0x22。
09 写入与结果 执行存储操作并等待明确完成;成功返回7D及原参数头,检测到写错误返回NRC 0x72。

图 27 的最小长度为五字节:SID、ALFID、至少一字节地址、至少一字节长度和至少一字节记录。随后才检查 ALFID,再根据该格式做总长度检查,最后检查地址与长度的有效性、认证、安全及运行条件。服务在当前会话是否开放,则由通用服务准入层先处理,不能与“服务开放但该区域不可写”混为一谈。

多个错误并存时,检查顺序会影响看到的 NRC。例如支持格式为 0x14,参数头声明写五字节但没有带够记录,先返回 0x13;不能跳过总长度检查,直接按目标区域或权限返回另一个错误。零长度在参数头与总长度成立后走 0x31。

0x78 是通用的挂起机制,不能当作写失败;资源忙也不等于 0x72。是否采用通用的繁忙响应,由诊断连接和资源准入策略决定,本文代码用本地返回值与线上 NRC 分开表达。

一次写操作有三种不同的时间边界

第一种是传输层的 FF、FC、CF 监督。第二种是应用层的 P2/P2* 响应监督。第三种是项目对 Flash、EEPROM 或其他存储作业设置的最长执行时间。三者不能用一个超时变量代替。

在服务端,接收完整请求后,诊断层应在适用的 P2Server 时间内启动最终响应,或在需要继续处理时发送 7F 3D 78,随后按 P2*Server 管理后续响应。数值应来自会话参数、ISO 14229-2 及项目配置,不能从这篇文章抄一个固定毫秒数。

客户端 P2Client 的起算与完整请求发送确认有关;收到响应开始指示后,还需要传输层监督剩余响应。若上层只拿到重组后的 PDU,适配层应提供响应开始时刻或按项目定时契约处理,不能把任务最后一次读取报文的时刻当成总线响应开始。

诊断层与存储层的异步写入时序

检查点或阶段 处理结果
01 传送请求 诊断仪经ISO-TP交付完整3D请求;诊断层处理服务准入与连接。
02 检查请求 地址服务完成格式、整段范围、权限和运行条件检查。
03 提交作业 存储适配层接受稳定记录和目标地址,返回有效票据。
04 条件挂起 适用P2时间内不能完成时,按诊断定时规则发送7F 3D 78,并继续监督P2*。
05 明确完成 地址服务轮询或接收异步完成事件,存储达到项目约定完成点。
06 最终响应 服务交付成功与原参数头;诊断层发送7D ALFID address size。

收到 0x78 后,客户端继续等待最终结果,不重发同一写请求。若最后超时,也不能断言“没有写进去”:存储可能已经提交,只是正响应丢失了。0x3D 没有通用事务编号;重发可能再次擦写、触发外设动作或覆盖其他写入者的新值。需要按目标区域定义,结合读回、存储作业及版本记录决定恢复策略。

C 代码把解析、准入和异步收尾分开

下面是一份可以按顺序拼接并以 C11 编译的 ECU 端服务核心。项目示例只接受 ALFID 0x14、0x24,目标是前述 256 字节逻辑窗口,每次写 1~64 字节;不支持 memory identifier。标准中的 0x12 例子用于解释报文,不在这份项目配置的 ALFID 支持表中。

这段代码不会直接访问 MCU 地址。底层 begin/poll/release 接口负责逻辑地址映射、物理存储约束和实际写入;诊断层负责连接、服务会话准入、ISO-TP、P2/P2* 与 0x78 发送。为了让重点落在地址边界上,这里不展开完整 DCM 或 Flash 驱动。

类型与整段范围判断

窗口基址是示例配置,不表示所有 ECU 都把这段数值映射成 RAM。计算中先比较再减法,避免对未经验证的地址与长度求和。

#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>
 
#define WM_MAX_DATA 64u
#define WM_MAX_ECHO 7u
#define WM_WINDOW_BASE UINT32_C(0x20001000)
#define WM_WINDOW_SIZE UINT32_C(0x100)
 
typedef enum {
    WM_VALID, WM_ACCEPTED, WM_PENDING, WM_POSITIVE, WM_NEGATIVE,
    WM_BUSY, WM_BAD_ARGUMENT, WM_UNKNOWN, WM_DRAINING,
    WM_DRAIN_OK, WM_DRAIN_FAILED, WM_NO_JOB
} WmResult;
 
typedef struct {
    uint32_t address;
    size_t count;
    size_t echo_length;
    uint8_t echo[WM_MAX_ECHO];
    uint8_t data[WM_MAX_DATA];
} WmRequest;
 
static WmResult WmReject(uint8_t *nrc, uint8_t value)
{
    *nrc = value;
    return WM_NEGATIVE;
}
 
static uint32_t WmReadBe(const uint8_t *p, size_t count)
{
    uint32_t value = 0u;
    for (size_t i = 0u; i < count; ++i) {
        value = (value << 8) | p[i];
    }
    return value;
}
 
static bool WmInWindow(uint32_t address, uint32_t count)
{
    if (count == 0u || address < WM_WINDOW_BASE) {
        return false;
    }
    uint32_t offset = address - WM_WINDOW_BASE;
    return offset < WM_WINDOW_SIZE &&
           count <= WM_WINDOW_SIZE - offset;
}

完整检查之后才复制记录

WmDecode 接收 ISO-TP 已重组的完整 PDU。先验证最小长度和受支持格式,确认参数头可读后再解析长度,通过精确总长度及整段范围检查后,才复制数据。失败时不会写入输出请求结构。

这里把记录长度与 length - header 比较,避免先把不可信的长度加到参数头上。两个受支持 ALFID 把地址宽度固定为四字节、长度宽度限制为一或两字节,因此每次移位最多处理四字节,不存在把五字节地址静默截断的问题。

/* Project profile: only ALFID 0x14/0x24, no memory identifier. */
WmResult WmDecode(const uint8_t *pdu, size_t length,
                  WmRequest *out, uint8_t *nrc)
{
    if (pdu == NULL || out == NULL || nrc == NULL) {
        return WM_BAD_ARGUMENT;
    }
    *nrc = 0u;
    if (length < 5u) {
        return WmReject(nrc, 0x13u);
    }
    if (pdu[0] != 0x3Du) {
        return WM_BAD_ARGUMENT; /* Diagnostic dispatch contract. */
    }
    if (pdu[1] != 0x14u && pdu[1] != 0x24u) {
        return WmReject(nrc, 0x31u);
    }
 
    size_t address_bytes = pdu[1] & 0x0Fu;
    size_t size_bytes = pdu[1] >> 4;
    size_t header = 2u + address_bytes + size_bytes;
    if (length < header) {
        return WmReject(nrc, 0x13u);
    }
    uint32_t count = WmReadBe(&pdu[2u + address_bytes], size_bytes);
    if ((size_t)count != length - header) {
        return WmReject(nrc, 0x13u);
    }
    uint32_t address = WmReadBe(&pdu[2], address_bytes);
    if (count > WM_MAX_DATA || !WmInWindow(address, count)) {
        return WmReject(nrc, 0x31u);
    }
 
    WmRequest decoded = {0};
    decoded.address = address;
    decoded.count = (size_t)count;
    decoded.echo_length = header - 1u;
    memcpy(decoded.echo, &pdu[1], decoded.echo_length);
    memcpy(decoded.data, &pdu[header], decoded.count);
    *out = decoded;
    return WM_VALID;
}

保留 echo 的目的是按原始编码回显。例如 0x24 的两字节长度即使高字节为零,也原样存放。客户端随后用这些参数关联本次响应。

存储接口必须说清楚所有权

WmJob 必须是长期有效的对象,初始以 {0} 初始化。所有接口由同一个任务串行调用;中断或其他任务的完成通知,应先投递到该任务。结构体不提供跨任务或跨核互锁。

typedef enum {
    WM_IO_STARTED, WM_IO_BUSY, WM_IO_FAILED, WM_IO_UNCERTAIN
} WmStart;
typedef enum {
    WM_IO_WAIT, WM_IO_DONE, WM_IO_ERROR, WM_IO_UNKNOWN
} WmIoState;
 
typedef struct {
    void *ctx;
    WmStart (*begin)(void *ctx, uint32_t address,
                     const uint8_t *data, size_t length,
                     uint32_t *ticket);
    WmIoState (*poll)(void *ctx, uint32_t ticket);
    void (*release)(void *ctx, uint32_t ticket);
} WmIo;
 
typedef struct {
    bool region_in_session;
    bool authentication_ok;
    bool security_ok;
    bool conditions_ok;
} WmAccess;
 
typedef enum {
    WM_IDLE, WM_ACTIVE, WM_DRAIN, WM_ISOLATED
} WmState;
typedef struct {
    WmState state;
    WmRequest request;
    WmIo io;
    uint32_t ticket;
    uint32_t started_ms;
    uint32_t budget_ms;
} WmJob;

底层接口契约需要逐条落实:

  • WM_IO_STARTED 表示底层接受作业并提供有效票据,可以在结束前只读借用 job->request.data。WM_IO_UNCERTAIN 同样必须提供有效票据,供后台追踪。
  • WM_IO_BUSY 表示没有接受作业;WM_IO_FAILED 表示明确检测到启动写路径错误,并且没有留下仍在执行或引用缓冲区的作业。不能用后者包装队列满或结果不明。
  • WM_IO_DONE 表示达到该地址区约定的完成点;若接口承诺持久化,则不能只报告 RAM 镜像更新。WM_IO_ERROR 表示已得到明确写错误,相关部分写入及恢复处理符合项目契约。
  • release 只在明确终态调用一次。调用结束后,底层不得再访问记录缓冲区。非法启动返回值会进入隔离态,不能拿可疑票据继续轮询,也不能靠清零对象解除隔离。

准入中的认证、安全和运行条件从项目配置取得。不使用某种限制时,适配层将对应条件置为 true;并非所有地址写入都必须经过同一种会话或安全等级。

/* Long-lived job; all entry points run in one serialized task. */
WmResult WmSubmit(WmJob *job, const WmIo *io,
                  const WmAccess *access,
                  const uint8_t *pdu, size_t length,
                  uint32_t now_ms, uint32_t budget_ms,
                  uint8_t *nrc)
{
    if (job == NULL || io == NULL || access == NULL ||
        pdu == NULL || nrc == NULL || io->begin == NULL ||
        io->poll == NULL || io->release == NULL ||
        budget_ms == 0u || budget_ms > (uint32_t)INT32_MAX) {
        return WM_BAD_ARGUMENT;
    }
    *nrc = 0u;
    if (job->state != WM_IDLE) {
        return WM_BUSY;
    }
    WmRequest request;
    WmResult decoded = WmDecode(pdu, length, &request, nrc);
    if (decoded != WM_VALID) {
        return decoded;
    }
    if (!access->region_in_session) {
        return WmReject(nrc, 0x31u);
    }
    if (!access->authentication_ok) {
        return WmReject(nrc, 0x34u);
    }
    if (!access->security_ok) {
        return WmReject(nrc, 0x33u);
    }
    if (!access->conditions_ok) {
        return WmReject(nrc, 0x22u);
    }
 
    job->request = request;
    job->io = *io;
    uint32_t ticket = 0u;
    WmStart start = job->io.begin(job->io.ctx, request.address,
                                 job->request.data, request.count,
                                 &ticket);
    if (start == WM_IO_BUSY) {
        return WM_BUSY;
    }
    if (start == WM_IO_FAILED) {
        return WmReject(nrc, 0x72u);
    }
    job->ticket = ticket;
    if (start != WM_IO_STARTED) {
        job->state = start == WM_IO_UNCERTAIN ?
                     WM_DRAIN : WM_ISOLATED;
        return WM_UNKNOWN;
    }
    job->started_ms = now_ms;
    job->budget_ms = budget_ms;
    job->state = WM_ACTIVE;
    return WM_ACCEPTED;
}

WM_ACCEPTED 只说明存储作业开始,不能直接转换成 7D。WM_BUSY、WM_BAD_ARGUMENT、WM_UNKNOWN 都是本地结果,不是线上 NRC;特别是后者,不能直接拼成一个不存在的 UDS 响应码。

超时后继续收尾,不能释放仍在使用的缓冲区

budget_ms 是项目存储监督期限,范围为 1~INT32_MAX,与 P2/P2* 分开。系统应保证轮询正常调度,无符号差值计算也不能容忍相隔一整个 32 位计时周期才再次调用。

WmResult WmPoll(WmJob *job, uint32_t now_ms,
                uint8_t *positive, size_t capacity,
                size_t *positive_length, uint8_t *nrc)
{
    if (job == NULL || positive == NULL ||
        positive_length == NULL || nrc == NULL) {
        return WM_BAD_ARGUMENT;
    }
    *positive_length = 0u;
    *nrc = 0u;
    if (job->state == WM_IDLE) {
        return WM_NO_JOB;
    }
    if (job->state == WM_ISOLATED) {
        return WM_DRAINING;
    }
    if (job->state == WM_ACTIVE &&
        capacity < 1u + job->request.echo_length) {
        return WM_BAD_ARGUMENT;
    }
    if (job->state == WM_ACTIVE &&
        (uint32_t)(now_ms - job->started_ms) >= job->budget_ms) {
        job->state = WM_DRAIN;
        return WM_UNKNOWN;
    }
 
    WmIoState stored = job->io.poll(job->io.ctx, job->ticket);
    if (stored == WM_IO_WAIT) {
        return job->state == WM_DRAIN ? WM_DRAINING : WM_PENDING;
    }
    if (stored != WM_IO_DONE && stored != WM_IO_ERROR) {
        bool was_active = job->state == WM_ACTIVE;
        job->state = WM_DRAIN;
        return was_active ? WM_UNKNOWN : WM_DRAINING;
    }
 
    bool drained = job->state == WM_DRAIN;
    job->io.release(job->io.ctx, job->ticket);
    job->state = WM_IDLE;
    if (drained) {
        return stored == WM_IO_DONE ? WM_DRAIN_OK : WM_DRAIN_FAILED;
    }
    if (stored == WM_IO_ERROR) {
        return WmReject(nrc, 0x72u);
    }
    positive[0] = 0x7Du;
    memcpy(&positive[1], job->request.echo, job->request.echo_length);
    *positive_length = 1u + job->request.echo_length;
    return WM_POSITIVE;
}
 
/* Detach the diagnostic transaction; keep tracking the memory job. */
void WmDetach(WmJob *job)
{
    if (job != NULL && job->state == WM_ACTIVE) {
        job->state = WM_DRAIN;
    }
}

只有 WM_POSITIVE 可以使用 positive_length 指定的内容发送正响应;只有 WM_NEGATIVE 才使用本次输出 NRC。WM_PENDING 表示还在进行,由诊断层依据真实定时决定是否发送 7F 3D 78,不能每轮轮询都发一次。

达到监督期限时,代码先返回一次 WM_UNKNOWN,把作业留在 WM_DRAIN。诊断适配层必须按项目故障策略结束或隔离本次等待、停止继续挂起,并保留后台追踪;这不是标准新增的负响应。如果存在项目规定的终止响应,必须有实际错误依据,不能拿超时冒充已确认写失败。

随后即使得到 WM_DRAIN_OK,也只记录迟到提交,不再向已经结束的诊断事务补发 7D。底层返回非法启动结果时进入 WM_ISOLATED,需要项目故障恢复确认硬件及作业已静止后再处理。上述异常处置不代替完整诊断栈的符合性设计。

地址写入作业状态与所有权

检查点或阶段 处理结果
IDLE到ACTIVE 完整检查通过,底层接受作业;长期对象持有记录。
ACTIVE保持 底层仍忙;诊断定时与存储监督期限分别处理。
ACTIVE到IDLE 得到明确完成或写错误,释放票据,交付结果。
ACTIVE到DRAIN 存储监督到期、连接取消或结果不明;保留数据及作业。
IDLE到DRAIN 启动结果不明,但底层提供有效票据供追踪。
DRAIN保持 继续后台追踪,拒绝新写入,不能清零复用缓冲区。
DRAIN到IDLE 明确终态后释放一次;只登记收尾,不补发旧事务响应。
IDLE到ISOLATED 底层启动返回值非法;不轮询可疑票据。
ISOLATED保持 由项目故障恢复确认作业和硬件静止后处理。

即使底层写作业已结束,最终响应是否发送完成、连接能否接收下一条服务,仍由 DCM 管理。不能把 WM_IDLE 当成完整诊断连接的空闲状态。

掉电、并发和寻址要写进区域定义

如果采用 AUTOSAR,可以由 DSL 管理连接与定时,DSP 或服务处理路径负责地址解析及访问检查,再通过应用/存储适配层访问 NvM、EEPROM 或 Flash。具体接口以所用版本和供应商实现为准,这种模块划分不是 ISO 14229-1 强制的软件架构。

对 NvM 管理的对象,按地址写入不能绕过它的 RAM 镜像、CRC、块状态及恢复规则。直接改物理存储中的几个字节,可能在下一次 NvM 写回时被旧镜像覆盖,也可能使整个块校验失败。项目若无法定义一致的访问路径,就不应把该区域开放为原始可写窗口。

DEM 也不是每次写地址都必须调用的模块。是否把存储写失败记录为事件或 DTC,应由诊断事件定义决定,不能把 0x72 固定映射成某个通用 DTC。

时间或并发窗口 必须明确的项目行为
已接受作业,尚未写入 数据归谁持有,断开连接后是否继续,取消能否得到确定结果
正在擦写或更新提交信息 如何识别完整版本、保留邻近对象,断电恢复是否允许部分结果
已提交,正响应未到客户端 读回与恢复路径看到哪个版本,如何处理诊断仪重试
正响应已经到达 若承诺持久化,突然掉电后应满足相应保留要求
CAN、DoIP、应用或生产接口同时写 依据实际重叠区域或共享存储对象互锁,避免丢失更新

请求看起来不重叠,也可能落在同一个擦除扇区或 NvM 块里,锁粒度应覆盖真实共享资源。长时间等待 Flash 时不宜一直占用全局临界区,可以保留作业所有权、在短临界区切换状态,并由任务继续处理。

功能寻址是否开放,应遵守网络与 ECU 诊断定义。多个 ECU 可能同时执行写入,部分负响应又受到通用功能寻址抑制规则影响,因此“没有响应”不能证明“没有改动”。本文示例采用物理寻址,也不把物理寻址的多帧交换直接套到功能地址。

测试时既看响应,也核对禁止的写入没有发生

台架日志至少记录连接上下文、原始 ALFID、目标设备与地址、字节数、存储票据、状态迁移及最终结果。对真实配置数据按项目要求脱敏,不把访问凭证写进日志。

编号 输入或前置条件 预期结果与关键观察
T01 0x14 请求向 0x20001020 写五字节 明确完成后返回七字节正响应;内容与原参数头一致
T02 同一地址与记录改用 0x24 长度高位 00 保留;正响应八字节,传输层正确多帧发送
T03 少于五字节、参数头截断 0x13;不读取缺失字段,不调用存储
T04 ALFID 半字节为零,或项目未支持的格式 达到最小长度后按格式检查返回 0x31
T05 记录少一字节、多一字节或带额外结束符 0x13;不能只写已经收到的部分
T06 完整参数头,memorySize=0,记录为空 0x31;零不解释为 256
T07 白名单首地址、末地址、末地址再写两字节 前两种在其他条件满足时允许;跨界请求 0x31,实际写次数为零
T08 地址接近 UINT32_MAX,长度导致末地址回绕 0x31,不能回绕后落入允许区
T09 单次长度 64、65,或区间包含禁止地址 核对项目上限及整段权限;拒绝请求无部分写入
T10 区域会话、认证、安全与运行条件分别不满足 按检查阶段核对 0x31 / 0x34 / 0x33 / 0x22,并覆盖多错误并存
T11 启动资源忙、明确写错误、结果不明 分别走资源策略、0x72、后台追踪,不相互替代
T12 接收缓冲区在提交后被复用 底层借用的稳定副本不变,原始回显参数不变
T13 作业超过 P2,随后在允许时间内成功 0x78 时机满足配置,最终只有一次 7D,客户端不重发
T14 项目监督到期、计时回绕、取消后迟到完成 保留对象并收尾;旧诊断事务不补发响应;票据只释放一次
T15 CF 丢失、序号错误或 FC 超时 ISO-TP 结束失败传输;不完整 PDU 对应的实际写次数为零
T16 目标很短,但驱动需要读改写整个擦除块 邻近对象、CRC 和共享锁保持一致,不扩大未经授权的修改范围
T17 写入中、提交后及响应前后分别断电 对照区域持久化契约核对恢复值;软件复位不能代替全部断电窗口
T18 重叠区域多写者、功能寻址、超时后重试 按项目互锁及恢复策略处理,检查擦写次数与迟到覆盖

事务核心的单元测试还应检查输出容量不足、空指针、底层非法返回值、精确到期边界和隔离期间的新请求。主机测试能验证解析与对象生命周期,不能证明真实 Flash 具有原子提交能力;DCM 定时、ISO-TP、缓存一致性与电源窗口仍需要集成和台架验证。

回到那条 7F 3D 31

先按 ALFID 找到地址和长度,确认完整 PDU 中确实有相等数量的记录字节,再把请求覆盖的每个区域映射到实际设备和访问规则。起始地址允许而末字节越界,就应在驱动看到请求之前结束这笔操作。

如果已经收到 7D 却读不回预期值,继续查逻辑地址映射、生效时间、缓存与存储完成点;如果断电后才丢失,则查提交及恢复路径。两条线索的检查位置不同,不必反复调整 CAN 填充字节。

按地址写入的报文字段不多,但每个字段都直接影响修改范围。评审时把地址支持表、一次完整抓包、底层操作范围和断电恢复记录放在一起,才能确认这条写入链路确实受控。

标准依据与示例边界

  • ISO 14229-1:2020 第 11.8.1~11.8.3 节、表 284~287:连续地址写入、ALFID、memory identifier、无子功能及正响应原参数回显。
  • 第 11.8.4 节、表 288 与图 27:服务级 NRC、五字节最小长度、格式及总长度检查、整段地址和访问条件。
  • 第 11.8.5 节、表 289~294:两、三、四字节地址的报文例子;附录 H 为 ALFID 组合的资料性示例,第 8.7 节规定通用请求与响应行为。
  • 本文的逻辑窗口、64 字节上限、ALFID 白名单、地址映射、存储监督及后台收尾均为项目示例;认证、安全、对齐、擦除、持久化及并发策略必须按实际 ECU 定义。
  • 完整定时和 CAN 传输还应结合项目采用的 ISO 14229-2、ISO 14229-3、ISO 15765-2 及网络参数,不从本服务表推断所有传输行为。

创作许可协议

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

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