4、诊断协议基础:UDS(ISO 14229)协议、诊断服务(如10、22、2E、31、19等)、诊断会话与安全访问
UDS(Unified Diagnostic Services,统一诊断服务)是ISO 14229标准定义的车载诊断协议,它建立在ISO 11898(CAN)等传输层之上,为ECU(电子控制单元)提供标准化的诊断通信机制。在CAPL脚本编写与自动化测试中,UDS协议是测试脚本与ECU交互的“共同语言”——无论是读取故障码、写入配置参数,还是执行例程控制,都需要通过UDS服务原语来封装请求和解析响应。本章将系统讲解UDS协议的核心架构、常用诊断服务的报文格式与使用场景、诊断会话切换机制以及安全访问的时序逻辑,为后续编写CAPL诊断测试脚本奠定协议基础。
4.1 UDS协议架构与报文结构
4.1.1 核心原理:分层模型与寻址机制
UDS协议采用OSI参考模型的分层思想,在车载诊断场景中通常运行于CAN总线之上。应用层(ISO 14229-1)定义了诊断服务的语义和格式;网络层(ISO 15765-2,即ISO-TP)负责将超过单帧CAN数据长度(经典CAN为8字节)的诊断报文进行分段传输;数据链路层和物理层则由CAN控制器和收发器实现。在CAPL脚本中,我们通常直接操作应用层的服务原语,但理解ISO-TP的分段机制对处理长报文(如0x22读取VIN码)至关重要。
UDS报文采用“服务ID(SID)+ 子功能/参数”的结构。请求报文首字节为SID(如0x22表示按标识符读取数据),响应报文首字节为SID+0x40(如0x62表示0x22的正响应)。负响应统一使用0x7F作为首字节,后跟请求SID和NRC(负响应码)。在CAPL中,我们通过CANx.sendFrame()或DiagRequest对象来构造和发送这些报文。
4.1.2 判断方法:正响应与负响应的识别
在自动化测试中,判断诊断请求是否成功执行,核心是检查响应报文的第一个字节:
- 正响应:首字节 = 请求SID + 0x40。例如请求0x22,正响应首字节为0x62。
- 负响应:首字节 = 0x7F,第二字节为请求SID,第三字节为NRC。例如
7F 22 31表示0x22请求被拒绝,NRC=0x31(请求超出范围)。 - 无响应:ECU在P2(默认50ms)或P2*(扩展)定时器内未回复,需在CAPL中设置超时处理。
下表总结了常见NRC及其含义,这是CAPL脚本中编写错误处理分支的重要依据:
| NRC(十六进制) | 名称 | 典型触发场景 |
|---|---|---|
| 0x10 | 一般拒绝 | 服务不支持或条件不满足 |
| 0x11 | 服务不支持 | SID无效或当前会话不支持 |
| 0x12 | 子功能不支持 | 子功能参数错误 |
| 0x22 | 条件不满足 | 需要先进入特定会话或完成安全解锁 |
| 0x31 | 请求超出范围 | DID不存在或数据参数越界 |
| 0x33 | 安全访问被拒绝 | 未解锁或密钥错误 |
| 0x78 | 请求正确但响应待定 | ECU需要额外时间处理,会随后发送正响应 |
4.1.3 案例拆解:通过CAPL发送0x22读取ECU软件版本
假设我们需要读取ECU的软件版本号,DID为0xF190。在CAPL中,我们可以使用DiagRequest对象来简化操作:
DiagRequest req;
DiagResponse resp;
char versionStr[64];
// 初始化诊断通道
DiagSetTarget("ECU1");
// 构造0x22请求,DID = 0xF190
req.SetService(0x22);
req.SetDID(0xF190);
// 发送请求并等待响应
if (DiagSendRequest(req, resp) == 0) {
// 检查正响应
if (resp.GetService() == 0x62) {
// 提取数据:从第3字节开始(前两字节为DID)
resp.GetData(2, versionStr, elcount(versionStr));
write("软件版本: %s", versionStr);
} else if (resp.GetService() == 0x7F) {
byte nrc = resp.GetByte(2);
write("负响应 NRC=0x%02X", nrc);
}
} else {
write("请求超时或总线错误");
}
这个案例展示了CAPL中诊断对象的基本用法。实际测试中,我们还需要考虑P2定时器、会话状态等因素。例如,如果ECU当前处于默认会话,某些DID可能不可读,此时需要先发送0x10 03切换到扩展会话。
DiagSendRequest会自动处理等待,但若使用底层CAN帧收发,则需手动实现P2*延长等待逻辑。
4.2 核心诊断服务详解(10、22、2E、31、19)
4.2.1 核心原理:服务功能与报文格式
UDS定义了六大类诊断服务,其中在ECU刷写和功能测试中最常用的是以下五种:
- 0x10 诊断会话控制:切换ECU的工作会话(默认/编程/扩展)。报文格式:
10 [子功能],子功能01=默认会话,02=编程会话,03=扩展会话。正响应:50 [子功能] [P2_hi] [P2_lo] [P2*_hi] [P2*_lo]。 - 0x22 按标识符读取数据:读取DID对应的数据。报文格式:
22 [DID_hi] [DID_lo],可一次请求多个DID。正响应:62 [DID_hi] [DID_lo] [数据...]。 - 0x2E 按标识符写入数据:将数据写入指定的DID。报文格式:
2E [DID_hi] [DID_lo] [数据...]。正响应:6E [DID_hi] [DID_lo]。 - 0x31 例程控制:启动、停止或请求执行结果。报文格式:
31 [子功能] [RID_hi] [RID_lo] [可选参数],子功能01=启动,02=停止,03=请求结果。正响应:71 [子功能] [RID_hi] [RID_lo] [结果...]。 - 0x19 读取故障码信息:按不同子功能读取DTC(诊断故障码)。常用子功能:02=按状态掩码读取DTC,04=读取快照记录,06=读取扩展数据。报文格式:
19 [子功能] [参数...]。
4.2.2 操作流程:0x2E写入与0x31例程的典型时序
在ECU标定或配置写入场景中,0x2E和0x31通常需要配合安全访问使用。典型流程如下:
- 发送
10 03进入扩展会话(若已在扩展会话可跳过)。 - 发送
27 01请求种子,接收ECU返回的种子值。 - 在CAPL中通过算法计算密钥(如AES或自定义算法),发送
27 02 [密钥]完成解锁。 - 发送
2E F190 [数据]写入标定参数,或发送31 01 FF00 [参数]启动例程。 - 对于0x31,可发送
31 03 FF00查询例程执行结果。
下表对比了0x2E和0x31的适用场景差异:
| 维度 | 0x2E 写入数据 | 0x31 例程控制 |
|---|---|---|
| 数据持久性 | 写入DID,通常存储在EEPROM/Flash | 执行临时操作,结果不持久保存 |
| 典型用途 | 写入VIN码、标定参数、配置字 | 执行自检、复位学习值、启动编程 |
| 响应内容 | 仅回显DID | 可返回例程执行结果(如校验和、计数) |
| 安全要求 | 通常需要安全解锁 | 部分例程(如只读例程)无需解锁 |
4.2.3 案例拆解:使用0x19读取DTC并解析状态
在自动化测试中,读取故障码是验证ECU故障诊断功能的关键步骤。以下是一个CAPL脚本片段,演示如何发送0x19 02读取所有DTC及其状态掩码:
DiagRequest req;
DiagResponse resp;
byte statusMask = 0xFF; // 读取所有状态的DTC
req.SetService(0x19);
req.SetSubFunction(0x02);
req.SetParameter(0, statusMask);
if (DiagSendRequest(req, resp) == 0) {
if (resp.GetService() == 0x59) {
int dtcCount = resp.GetByte(1); // 第2字节为DTC数量
write("DTC数量: %d", dtcCount);
for (int i = 0; i < dtcCount; i++) {
word dtc = resp.GetWord(2 + i*3); // 每个DTC占3字节:2字节DTC+1字节状态
byte status = resp.GetByte(4 + i*3);
write("DTC: 0x%04X, 状态: 0x%02X", dtc, status);
}
}
}
该脚本中,我们通过GetWord和GetByte方法解析响应数据。实际DTC状态掩码的每一位代表不同含义(如bit0=测试失败,bit1=当前存在等),在测试报告中需要进一步解码。
4.3 诊断会话与安全访问机制
4.3.1 核心原理:会话状态机与定时器
UDS定义了三种标准诊断会话:默认会话(Default Session)、编程会话(Programming Session)和扩展会话(Extended Session)。ECU上电后默认进入默认会话,该会话下仅允许执行有限的服务(如0x10、0x19、0x22等)。编程会话用于Flash刷写,扩展会话则允许执行标定、写入等操作。会话切换通过0x10服务实现,且每个会话都有对应的P2和P2*定时器(默认会话P2=50ms,P2*=5000ms;扩展会话P2=50ms,P2*=5000ms)。如果ECU在S3定时器(默认5000ms)内未收到任何诊断请求,会自动回退到默认会话。
安全访问(0x27服务)是保护敏感操作(如写入、例程控制)的机制。其流程为:测试仪发送27 01请求种子,ECU返回种子(通常4字节);测试仪根据种子和密钥算法计算密钥,发送27 02 [密钥];ECU验证密钥正确后返回正响应67 02,此后在本次会话内可执行受保护的服务。若密钥错误,ECU返回NRC=0x35(无效密钥),并启动延时计数器(通常10秒内不允许再次尝试)。
4.3.2 判断方法:会话状态与安全状态的检测
在CAPL脚本中,我们需要在发送敏感请求前检查当前会话和安全状态。可以通过以下方式判断:
- 会话状态:发送
10 01请求切换到默认会话,观察正响应中的P2值;或通过DigGetSession()函数(部分CAPL版本支持)查询当前会话。 - 安全状态:尝试发送一个受保护的服务(如0x2E),若收到NRC=0x33,则说明未解锁。更优雅的方式是发送
27 03查询安全状态(部分ECU支持)。 - 定时器监控:在CAPL中使用
timer或msTimer监控S3超时,在超时前发送周期性的10 03保持扩展会话。
4.3.3 案例拆解:完整的会话切换与安全解锁流程
以下是一个CAPL函数,封装了进入扩展会话并完成安全解锁的完整流程,适用于后续所有需要安全访问的测试用例:
int EnterExtendedAndUnlock(word seedKeyAlgo) {
DiagRequest req;
DiagResponse resp;
// 步骤1:切换到扩展会话
req.SetService(0x10);
req.SetSubFunction(0x03);
if (DiagSendRequest(req, resp) != 0) return -1;
if (resp.GetService() != 0x50) return -2;
// 步骤2:请求种子
req.SetService(0x27);
req.SetSubFunction(0x01);
if (DiagSendRequest(req, resp) != 0) return -3;
if (resp.GetService() != 0x67) return -4;
// 步骤3:提取种子(假设4字节)
dword seed = resp.GetDWord(1);
// 步骤4:计算密钥(调用自定义算法函数)
dword key = CalculateKey(seed, seedKeyAlgo);
// 步骤5:发送密钥
req.SetService(0x27);
req.SetSubFunction(0x02);
req.SetParameter(0, key);
if (DiagSendRequest(req, resp) != 0) return -5;
if (resp.GetService() != 0x67) return -6;
return 0; // 成功
}
该函数返回0表示成功,负值表示失败步骤。在测试脚本中,我们可以这样调用:
if (EnterExtendedAndUnlock(ALGO_XOR) == 0) {
// 执行受保护的写入操作
WriteCalibrationData(0xF190, calData);
} else {
write("安全解锁失败,无法执行写入");
}
4.3.4 常见误区与总结
在诊断协议的学习和CAPL实现中,初学者常犯以下错误:
- 混淆SID与子功能:例如0x10是服务ID,而0x03是子功能(扩展会话),两者缺一不可。
- 忽略P2定时器:在CAPL中使用
DiagSendRequest时,默认超时可能不够长,需根据ECU规范设置P2*值。 - 安全访问状态未重置:在测试序列中,如果前一个用例解锁了安全访问,后一个用例可能默认已解锁,但ECU可能因S3超时回到默认会话,导致后续请求失败。
- DID字节序错误:多字节DID(如0xF190)在CAN报文中的字节序为高字节在前(大端),CAPL的
SetDID函数会自动处理,但若手动构造报文则需注意。