UDS 0x3D:按地址写入的报文与实现边界
台架上有一条写请求:起始地址落在允许区域,记录长度也不大,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 及网络参数,不从本服务表推断所有传输行为。



