做车载诊断测试这几年,0x2E 服务(WriteDataByIdentifier,按标识符写数据)是用得最多、也是最容易踩坑的诊断服务之一。很多测试同学拿到需求文档后,第一反应是“这不就是往 DID 里写个值吗”,结果一写用例就发现漏了安全解锁、条件约束、数据长度校验、NRC 优先级这些关键点。
这篇文章从协议基础开始,结合需求文档拆解 0x2E 服务的用例设计方法,给出完整的测试用例模板、自动化脚本思路和常见问题排查清单。不管是刚入行的诊断测试工程师,还是负责 UDS 协议栈开发的同学,都可以直接参考。
1. 0x2E 服务是什么,为什么用例难设计
1.1 从“写数据”这个动作说起
0x2E 服务是 UDS(Unified Diagnostic Services,统一诊断服务)协议中用于写入数据的服务。它允许诊断仪向 ECU(电子控制单元)的某个 DID(Data Identifier,数据标识符)写入一段数据。
通俗点说:
- 0x22 服务是“读数据”,从 ECU 里把数据读出来。
- 0x2E 服务是“写数据”,把数据写进 ECU 里。
- 0x2F 服务是“按地址写数据”,通过内存地址直接写入。
0x2E 和 0x2F 看起来都和数据写入相关,但使用场景完全不同。0x2E 面向的是逻辑数据标识符,比如“车辆识别码 VIN”“软件版本号”“配置字”;0x2F 面向的是物理内存地址,通常用于 Bootloader 或底层标定。
在诊断测试中,0x2E 服务常用于:
- 写入 VIN、生产日期等车辆身份信息。
- 写入配置参数,如车型代码、变速箱类型。
- 写入标定值或学习值,如转向角传感器零点。
- 写入测试模式开关,如生产线末端的 EOL(End of Line)配置。
1.2 为什么 0x2E 的用例容易漏
0x2E 不是简单的“下发数据 - 收到响应”就结束了。它背后涉及一整套安全机制和条件约束:
- 安全访问:大多数可写 DID 都受安全等级保护,必须先通过 0x27 服务解锁。
- 会话切换:部分写入操作只能在扩展会话或编程会话下执行。
- 条件校验:车辆状态、电源模式、车速等条件不满足时,ECU 会拒绝写入。
- 数据校验:DID 数据长度、数据范围、校验和、格式必须符合定义。
- 写入策略:有些 DID 支持“立即写”,有些需要先写入 RAM 再执行“存储”操作。
- 总线时序:写入过程中总线掉线、会话超时、重复请求都会影响结果。
所以,0x2E 的用例设计难点不在于“怎么写正例”,而在于把需求文档里隐含的约束条件一条条挖出来,转成可执行的测试用例。
2. 0x2E 服务协议细节拆解
2.1 请求与响应帧格式
0x2E 服务的请求格式如下:
| 字节 | 名称 | 值 | 说明 |
|---|---|---|---|
| Byte 0 | SID | 0x2E | 服务标识符 |
| Byte 1-2 | DID | 0xXXXX | 数据标识符,如 0xF190 表示 VIN |
| Byte 3...N | dataRecord | 变长 | 要写入的数据,长度由 DID 定义 |
正响应格式:
| 字节 | 名称 | 值 | 说明 |
|---|---|---|---|
| Byte 0 | SID+0x40 | 0x6E | 正响应服务标识符 |
| Byte 1-2 | DID | 0xXXXX | 与请求中的 DID 一致 |
负响应格式:
| 字节 | 名称 | 值 | 说明 |
|---|---|---|---|
| Byte 0 | 0x7F | 0x7F | 负响应服务标识符 |
| Byte 1 | SID | 0x2E | 原服务标识符 |
| Byte 2 | NRC | 0xXX | 负响应码 |
一个最简单的 CAN 报文示例:
发送(CAN ID 0x7E0): 02 2E F1 90 31 32 33 34 35 36 37 38 39 30 31 32 33 34 35 36 37 接收(CAN ID 0x7E8): 04 6E F1 90 00 00 00 00这里0x02是 CAN 帧里第一个字节,表示后续还有 2 个字节(因为采用 ISO-TP 单帧传输时,第一个字节的高四位表示帧类型,低四位表示后续数据长度)。2E是 SID,F1 90是 VIN 的 DID,后面的 ASCII 字符就是 VIN 内容。
如果 ECU 拒绝写入,负响应示例:
发送(CAN ID 0x7E0): 02 2E F1 90 31 32 33 34 35 36 37 38 39 30 31 32 33 34 35 36 37 接收(CAN ID 0x7E8): 03 7F 2E 33这里0x33表示安全访问被拒绝(SecurityAccess Denied),说明没有先执行解锁操作。
2.2 常见 NRC 及含义
| NRC | 名称 | 含义 | 是否常见 |
|---|---|---|---|
| 0x13 | IncorrectMessageLengthOrInvalidFormat | 报文长度错误或格式无效 | 高频 |
| 0x14 | ResponseTooLong | 响应太长 | 极少 |
| 0x22 | ConditionsNotCorrect | 条件不满足 | 高频 |
| 0x24 | RequestSequenceError | 请求序列错误 | 中频 |
| 0x31 | RequestOutOfRange | 请求超出范围(DID 不存在或数据值非法) | 高频 |
| 0x33 | SecurityAccessDenied | 安全访问被拒绝 | 高频 |
| 0x36 | ExceededNumberOfAttempts | 超过尝试次数 | 低频(与 0x27 配合) |
| 0x72 | GeneralProgrammingFailure | 一般编程失败 | 中频 |
| 0x73 | WrongBlockSequenceCounter | 块序列计数器错误 | 极少(与 0x36 配合) |
不同 ECU 对 NRC 的优先级处理可能不同,比如“未解锁 + DID 不存在”时,有的 ECU 返回 0x31,有的返回 0x33。这种差异正是用例设计时要重点验证的点。
2.3 与 0x22、0x2F 的边界区分
用例设计之前,要明确 0x2E 与相邻服务的边界,避免把“读”的用例和“写”的用例混在一起:
- 0x22:读取 DID 数据,不影响 ECU 存储内容。
- 0x2E:写入 DID 数据,可能立即生效,也可能需要重启或执行存储操作后生效。
- 0x2F:通过内存地址写入,不关注 DID 概念,通常用于底层开发。
实际测试过程中,0x2E 写入后,通常会用 0x22 回读来验证写入结果。这个“写入后回读”的习惯,是诊断测试的基本操作,也是用例设计中不可缺少的一环。
3. 根据需求设计 0x2E 用例的准备工作
很多测试同学拿到 0x2E 相关的需求文档,第一反应是“直接照着 DID 列表写正反例”,结果写完发现漏了安全条件、漏了时序测试。正确步骤是先做需求拆分,再提取测试要点,最后设计用例。
3.1 拆解需求文档中的 DID 属性
一份合格的诊断需求文档,针对每个可写 DID 至少会给出这些信息:
| 属性 | 说明 | 示例 |
|---|---|---|
| DID | 16 位十六进制标识 | 0xF190 |
| 数据名称 | 功能含义 | 车辆识别码 VIN |
| 数据长度 | 固定长度或变长 | 17 字节 |
| 数据类型 | ASCII / 数值 / 位映射 | ASCII |
| 取值范围 | 合法数据范围 | 0x00 - 0x0F |
| 读写属性 | 只读 / 只写 / 读写 | 读写 |
| 安全等级 | 是否需要解锁 | 需要安全等级 1 |
| 会话要求 | 默认会话 / 扩展会话 / 编程会话 | 扩展会话 |
| 条件约束 | 车速、挡位、电源状态等 | 车速为 0,电源 ON |
| 写入生效方式 | 立即生效 / 重启生效 / 需存储指令 | 立即生效 |
| 写入次数限制 | 是否有次数限制 | 最多 5 次 |
| 错误码定义 | 边界值对应 NRC | 超范围返回 0x31 |
把需求文档里的 DID 属性整理成上面的表格,是设计用例前的第一步。不要跳过这一步,因为很多需求文档的信息是分散的,不整理容易漏。
3.2 提取隐含条件与约束
除了 DID 本身的信息,还需要从整车级需求中提取隐含条件:
- 安全解锁流程:解锁后是否有时间窗口?在窗口内才能写。
- 会话状态切换:写入后是否需要切回默认会话?
- 写入时序:是否要求连续写多个 DID?中间是否有间隔要求?
- 条件状态机:比如 EOL 模式下可写,下线后不可写。
- 重复写入策略:重复写同一 DID 是否允许?是否需要先擦除?
- 写失败恢复:写失败后数据是保持原值还是变成无效值?
举个实际例子:某 ECU 的“转向角传感器零点标定”DID,要求车速为 0、方向盘在中间位置、电源电压在 9V 到 16V 之间。如果你只看 DID 属性表,可能会漏掉“方向盘在中间位置”这个条件。而恰恰是这个条件不满足时,ECU 会返回 0x22。
3.3 确定用例设计方法
0x2E 服务用例推荐组合以下方法:
| 方法 | 适用场景 |
|---|---|
| 等价类划分 | 合法数据范围、非法数据范围 |
| 边界值分析 | 数据最小/最大长度、数值上下界 |
| 状态转换法 | 默认会话/扩展会话/编程会话切换 |
| 场景法 | 生产线 EOL 配置、售后维修写入 |
| 错误推断法 | 未解锁写入、重复写入、总线中断 |
4. 0x2E 服务用例设计完整实战
下面以“VIN 写入”和“配置字写入”两个典型 DID 为例,演示完整用例设计过程。示例需求如下:
| 属性 | VIN(0xF190) | 配置字(0xF1A0) |
|---|---|---|
| 长度 | 17 字节 ASCII | 2 字节十六进制 |
| 取值范围 | 合法 VIN 字符 | 0x0000 - 0xFFFF |
| 安全等级 | 需要解锁 | 需要解锁 |
| 会话 | 扩展会话 | 默认会话 |
| 写入次数 | 最多 10 次 | 无限制 |
| 生效方式 | 重启后生效 | 立即生效 |
| 条件约束 | 车速为 0 | 电源 ON |
4.1 功能正例设计
功能正例的核心目标是验证“在合法条件下,写入合法数据,ECU 返回正响应”。
| 用例编号 | 测试步骤 | 预期结果 |
|---|---|---|
| TC_0x2E_001 | 进入扩展会话,完成安全解锁,发送 17 字节合法 VIN | 返回 0x6E F1 90 |
| TC_0x2E_002 | 用 0x22 回读 VIN | 读到的 VIN 与写入一致 |
| TC_0x2E_003 | 重启 ECU,再次回读 VIN | VIN 保持不变,说明已持久化 |
| TC_0x2E_004 | 写入配置字 0x1234 | 返回 0x6E F1 A0 |
| TC_0x2E_005 | 回读配置字 | 读回 0x1234 |
正例用例要注意一点:写入后必须回读验证,不能只看响应帧。有的 ECU 正响应正常,但数据没有真正写入 Flash,重启后数据丢失。回读和重启验证是 0x2E 正例的必备步骤。
4.2 功能反例设计
反例用于验证异常输入时的 NRC 响应是否符合需求。
| 用例编号 | 测试步骤 | 预期结果 |
|---|---|---|
| TC_0x2E_101 | 未解锁直接写 VIN | 返回 0x7F 2E 33 |
| TC_0x2E_102 | 写不存在的 DID(如 0xF1FF) | 返回 0x7F 2E 31 |
| TC_0x2E_103 | VIN 数据长度只有 10 字节 | 返回 0x7F 2E 13 |
| TC_0x2E_104 | VIN 数据长度 18 字节 | 返回 0x7F 2E 13 |
| TC_0x2E_105 | 写入非法 VIN 字符(如中文、特殊符号) | 返回 0x7F 2E 31 |
| TC_0x2E_106 | 配置字写入 0x1FFFF(超出范围) | 返回 0x7F 2E 31 |
| TC_0x2E_107 | 默认会话下写 VIN | 返回 0x7F 2E 22 |
| TC_0x2E_108 | 车速不为 0 时写 VIN | 返回 0x7F 2E 22 |
反例设计有一个容易忽略的点:NRC 的优先级。比如“未解锁 + DID 不存在”时,不同 ECU 返回的 NRC 可能不同。需求文档如果没写清楚,测试用例就要把这种组合场景列出来,实际测试时记录 ECU 的真实行为,再和开发确认是否符合规范。
4.3 边界值用例设计
边界值用例是 0x2E 测试的重头戏,因为很多隐藏 bug 都出在长度边界和数值边界上。
| 用例编号 | 测试步骤 | 预期结果 |
|---|---|---|
| TC_0x2E_201 | VIN 写入 17 字节全 0x20(空格) | 返回正响应,回读为 17 个空格 |
| TC_0x2E_202 | VIN 写入 17 字节全 0x7A(z) | 返回正响应 |
| TC_0x2E_203 | 配置字写入 0x0000 | 返回正响应 |
| TC_0x2E_204 | 配置字写入 0xFFFF | 返回正响应 |
| TC_0x2E_205 | 配置字写入 0x00FF | 返回正响应 |
| TC_0x2E_206 | VIN 数据最后少 1 字节(16 字节) | 返回 0x7F 2E 13 |
| TC_0x2E_207 | VIN 数据多 1 字节(18 字节) | 返回 0x7F 2E 13 |
| TC_0x2E_208 | 配置字写入 0xFF 00(高字节合法,低字节越界) | 返回 0x7F 2E 31 |
边界值用例这里要特别说明一下 0x13 和 0x31 的区别:长度不对返回 0x13,长度正确但值不在范围内返回 0x31。有的同学会把“长度为 10 字节的 VIN”写成预期 0x31,这是不对的,长度错误按协议应返回 0x13。
4.4 时序与状态转换用例
诊断测试最容易出错的地方就是时序。0x2E 的时序用例要覆盖这些场景:
| 用例编号 | 测试步骤 | 预期结果 |
|---|---|---|
| TC_0x2E_301 | 完成解锁后等待超过超时时间再写 VIN | 返回 0x7F 2E 33 |
| TC_0x2E_302 | 解锁后在超时时间内写 VIN | 返回正响应 |
| TC_0x2E_303 | 扩展会话下写 VIN,然后切回默认会话 | 写入成功,数据保持 |
| TC_0x2E_304 | 重复写 VIN 超过 10 次 | 第 11 次返回 0x7F 2E 72 |
| TC_0x2E_305 | 写入过程中 ECU 复位 | ECU 重启后,VIN 保持原值 |
| TC_0x2E_306 | 先写配置字再写 VIN,然后重启 | 两个数据都生效 |
| TC_0x2E_307 | 合法写入后立即发送下一条写入请求 | 连续两次写入均返回正响应 |
时序用例中,最常见的失败场景是超时窗口。有的 ECU 解锁后允许写入的时间窗口只有 5 秒,测试脚本如果解锁后没有及时发送 0x2E 请求,就会出现偶发性的 0x33。这种问题在手工测试时很难复现,但在自动化测试中很容易暴露。
4.5 安全与并发用例
安全用例重点关注写入操作与安全机制的交互:
| 用例编号 | 测试步骤 | 预期结果 |
|---|---|---|
| TC_0x2E_401 | 安全等级 1 解锁后写安全等级 2 的 DID | 返回 0x7F 2E 33 |
| TC_0x2E_402 | 安全等级 2 解锁后写安全等级 1 的 DID | 返回正响应 |
| TC_0x2E_403 | 错误解锁 3 次后写 VIN | 返回 0x7F 2E 36 |
| TC_0x2E_404 | 解锁后切换到默认会话再写 VIN | 返回 0x7F 2E 33 |
| TC_0x2E_405 | 学习值写入与 0x22 回读并发执行 | 回读结果稳定,无总线错误 |
安全用例这里有一个概念需要区分:0x27 服务返回正响应表示解锁成功,但这个成功可能只是“当前诊断会话内有效”。一旦切换会话,安全状态会复位。所以“解锁后切会话再写”这个用例,预期结果是 0x33,而不是正响应。
5. 0x2E 服务自动化测试脚本示例
实际项目里,0x2E 服务用例通常通过 CAPL、Python(配合 CAN 盒)或 vTestStudio 来执行。这里给出两个最常用的脚本示例。
5.1 CAPL 脚本示例(CANoe 环境)
CAPL 是 CANoe 的脚本语言,常用于诊断测试。下面是一个简单的 0x2E 写入 VIN 的示例脚本:
/* CAPL 脚本:0x2E 写入 VIN 示例 */ variables { // 诊断报文的 TX/RX CAN ID const int DIAG_REQ_ID = 0x7E0; const int DIAG_RES_ID = 0x7E8; // 请求数组 byte reqData[24]; byte resData[24]; int resLen; } void WriteVIN() { int i; // 构造 0x2E 请求:SID + DID(F1 90) + 17 字节 VIN reqData[0] = 0x2E; reqData[1] = 0xF1; reqData[2] = 0x90; // VIN 内容:测试用 VIN "LSVAM4187C2188888" reqData[3] = 'L'; reqData[4] = 'S'; reqData[5] = 'V'; reqData[6] = 'A'; reqData[7] = 'M'; reqData[8] = '4'; reqData[9] = '1'; reqData[10] = '8'; reqData[11] = '7'; reqData[12] = 'C'; reqData[13] = '2'; reqData[14] = '1'; reqData[15] = '8'; reqData[16] = '8'; reqData[17] = '8'; reqData[18] = '8'; reqData[19] = '8'; // 发送诊断请求 DiagRequestRaw(DIAG_REQ_ID, reqData, 20); resLen = DiagGetResponseRaw(DIAG_RES_ID, resData, 24); // 验证响应 if (resLen >= 3 && resData[0] == 0x6E) { write("0x2E Write VIN: Positive Response"); } else { write("0x2E Write VIN: Negative Response NRC=0x%02X", resData[2]); } }这个脚本是一个核心片段,实际使用时需要根据你的 CANoe 工程里的诊断层 API 调整。关键点是把 VIN 的 ASCII 码逐字节填入请求数组,然后通过诊断请求函数发送。
5.2 Python 脚本示例(CAN 盒调用)
如果用 Python 配合 CAN 盒(如 PCAN、CANoe 的 Python 接口),核心代码如下:
# 0x2E 写入 VIN 示例 import can def write_did(bus, vid, did, data): """ 发送 0x2E 服务请求 :param bus: CAN 总线实例 :param vid: 诊断请求 CAN ID :param did: 数据标识符,如 0xF190 :param data: 要写入的数据,bytes 类型 """ # 使用 python-can 的 send 方法发送原始 CAN 帧 # 这里假设 ISO-TP 层已经由上层处理 req = bytes([0x2E, (did >> 8) & 0xFF, did & 0xFF]) + data msg = can.Message(arbitration_id=vid, data=req, is_extended_id=False) bus.send(msg) # 使用示例 if __name__ == "__main__": bus = can.interface.Bus(channel='PCAN_USBBUS1', bustype='pcan') vin = b"LSVAM4187C2188888" write_did(bus, 0x7E0, 0xF190, vin)这个示例简化了 ISO-TP 层,实际工程中需要用isotp库来分包和组包。Python 的好处是方便和测试报告、Excel 用例集集成,适合批量跑回归用例。
5.3 自动化用例集的组织建议
实际项目中,0x2E 的自动化用例应该直接对应手工用例编号,一个用例一个脚本函数。推荐结构如下:
test_0x2e/ ├── common/ │ ├── security.py # 安全解锁封装 │ ├── session.py # 会话切换封装 │ └── bus.py # CAN 总线封装 ├── test_vin_write.py # VIN 写入用例集 ├── test_config_write.py # 配置字写入用例集 └── test_0x2e_negative.py # 反例用例集用例函数内部通过assert或测试框架的断言机制来校验响应 NRC,用例执行完毕后生成 HTML/Excel 测试报告。这样可以做到手工用例和自动化用例一一对应,可追溯性好。
6. 常见问题与排查思路
6.1 写入返回 0x33
| 可能原因 | 排查步骤 | 解决思路 |
|---|---|---|
| 未进行安全解锁 | 检查是否发送 0x27 服务并收到正响应 | 按安全等级解锁后再发送 0x2E |
| 解锁超时 | 检查解锁到写入的时间间隔 | 缩短时序,在超时窗口内发送 |
| 切换了会话 | 检查是否从扩展会话切到了默认会话 | 保持解锁会话不变 |
| 安全等级不够 | 检查 DID 要求的安全等级 | 确认所用密钥对应的等级 |
0x33 是 0x2E 测试中最常见的 NRC。遇到这个问题,先别急着查 0x2E 代码,优先查测试脚本里 0x27 的解锁时序,以及解锁后是否发生了会话切换。
6.2 写入返回 0x22
| 可能原因 | 排查步骤 | 解决思路 |
|---|---|---|
| 条件不满足 | 检查车速、挡位、电源状态 | 调整车辆状态到需求条件 |
| 会话错误 | 确认当前会话是否为需求要求的会话 | 切换正确会话 |
| DID 写入使能未开启 | 确认 ECU 是否允许当前状态下写入 | 检查 EOL 或生产模式标志 |
0x22 表示“条件不满足”,但具体是哪个条件不满足,协议没有定义。排查时需要结合需求文档逐项核对车辆状态。
6.3 写入返回 0x13
| 可能原因 | 排查步骤 | 解决思路 |
|---|---|---|
| 数据长度错误 | 核对 DID 定义长度 | 调整数据长度 |
| ISO-TP 分包错误 | 检查连续帧发送是否正确 | 用 CANoe 的诊断层发送 |
| 请求格式错误 | 检查 SID 或 DID 字节顺序 | 确认 DID 高位在前 |
0x13 在 0x2E 里通常意味着请求长度和 DID 定义不符。要特别注意某些 ECU 对 0x2E 请求要求“数据长度必须精确匹配”,多一个字节都不行。
6.4 写入返回正响应但数据未生效
| 可能原因 | 排查步骤 | 解决思路 |
|---|---|---|
| 数据写入 RAM 未持久化 | 重启 ECU 后回读 | 检查是否还需要执行存储指令 |
| 写入后需要复位 | 查看需求文档的生效方式 | 执行 ECU 复位 |
| 写入校验失败但未报错 | 对比回读数据 | 检查 ECU 内部校验逻辑 |
这个问题最隐蔽。ECU 返回 0x6E 不代表数据已经持久化保存。有些 ECU 的写流程分两步:先写 RAM,再执行“存储到 Flash”的指令(可能是另一条 0x2E 或 0x31 服务)。用例设计时一定要包含“重启后回读”的步骤。
7. 0x2E 服务用例设计的最佳实践
7.1 用例组织规范
0x2E 服务用例建议按 DID 分组,每组内部按正例、反例、边界、时序、安全、并发分层组织。用例编号采用模块化规则,比如TC_0x2E_F190_001表示 DID 0xF190 的第 1 条用例。这样的编号在自动化测试和缺陷管理系统中都很容易追溯。
7.2 条件前置处理
0x2E 用例的“前置条件”要写清楚,不能只写“车辆正常状态”。建议明确写出:
- 当前诊断会话。
- 安全解锁状态。
- 车辆条件(车速、挡位、电源)。
- 相关 DID 的初始值。
- ECU 是否处于生产模式。
前置条件写不清楚,用例执行时就会出现“你以为解锁了,其实没解锁”的情况,浪费排查时间。
7.3 数据管理与持久化验证
测试过程中要维护一张“DID 写入记录表”,记录每个测试用例写入的数据、回读的数据、重启后的数据。不要只在用例步骤里写“写入 0x1234”,要把回读结果也记录下来。这样一旦出现数据丢失问题,可以快速定位是哪个环节出了问题。
7.4 关注写失败后的恢复策略
写失败后的数据恢复也是用例设计的重要维度。比如写入 VIN 到第 10 字节时总线掉线,ECU 内部是保持原 VIN,还是变成全 FF?如果需求文档没有定义,这个用例应该作为“待确认项”提给系统工程师。
8. 总结与后续学习方向
0x2E 服务用例设计的核心,是把需求文档里的“能写”扩展成“什么条件下能写、什么数据能写、写入后怎么验证、写失败了怎么恢复”的完整测试矩阵。
本文梳理了 0x2E 的协议帧格式、NRC 定义、需求拆分方法、完整用例设计示例、自动化脚本和常见问题排查思路。其中优先级最高的是三件事:一是正例必须包含回读和重启验证;二是反例必须关注 NRC 优先级;三是时序用例必须覆盖安全解锁超时场景。
下一步可以继续深入学习 0x27 安全访问的用例设计、0x31 例程控制服务的测试方法,以及如何在 CANoe 中使用诊断控制台和 CAPL 搭建完整的 UDS 自动化测试工程。如果本文对你有帮助,可以收藏备用,后续会有更多网络诊断系列文章更新。