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诊断测试脚本奠定协议基础。

UDS诊断协议栈与核心服务关系图 应用层(ISO 14229-1)— 诊断服务 会话控制 0x10 数据读取 0x22 / 0x23 数据写入 0x2E 例程控制 0x31 故障码读取 0x19 安全访问 0x27 传输层与网络层(ISO 15765-2 / ISO 13400) CAN-TP(ISO-TP)分段传输 DoIP(以太网) LIN / FlexRay 适配 物理层(ISO 11898 / 100BASE-T1) CAN_H / CAN_L 差分信号 100BASE-T1 双绞线 终端电阻 120Ω 诊断会话状态机 默认会话 编程会话 扩展会话 0x10 服务切换,S3定时器超时回默认 安全访问流程(0x27) 请求种子 计算密钥 发送密钥 种子+算法→密钥,失败有延时锁定

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切换到扩展会话。

经验提醒:在编写CAPL诊断脚本时,建议将DID、SID、NRC等常量定义为全局枚举或宏,避免魔法数字。同时,对于0x78(响应待定)的情况,CAPL的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通常需要配合安全访问使用。典型流程如下:

  1. 发送10 03进入扩展会话(若已在扩展会话可跳过)。
  2. 发送27 01请求种子,接收ECU返回的种子值。
  3. 在CAPL中通过算法计算密钥(如AES或自定义算法),发送27 02 [密钥]完成解锁。
  4. 发送2E F190 [数据]写入标定参数,或发送31 01 FF00 [参数]启动例程。
  5. 对于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=当前存在等),在测试报告中需要进一步解码。

风险提示:0x2E写入操作具有风险,若写入非法数据可能导致ECU功能异常。在自动化测试中,建议先读取原始值备份,写入后立即回读验证,并在测试结束后恢复原值。对于0x31例程,某些例程(如Flash擦除)执行期间ECU可能暂停通信,CAPL脚本需设置较长的超时时间(如P2* = 5000ms)。

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("安全解锁失败,无法执行写入");
}
复盘要点:在编写安全访问相关脚本时,务必注意:① 密钥算法通常由OEM定义,可能是简单的异或、CRC或AES,需从规范文档中获取;② 连续多次密钥错误会触发ECU的延时锁定(通常10秒),测试脚本应捕获NRC=0x36(超过尝试次数)并等待延时结束;③ 会话切换后安全状态会重置,每次进入新会话都需要重新解锁。

4.3.4 常见误区与总结

在诊断协议的学习和CAPL实现中,初学者常犯以下错误:

  • 混淆SID与子功能:例如0x10是服务ID,而0x03是子功能(扩展会话),两者缺一不可。
  • 忽略P2定时器:在CAPL中使用DiagSendRequest时,默认超时可能不够长,需根据ECU规范设置P2*值。
  • 安全访问状态未重置:在测试序列中,如果前一个用例解锁了安全访问,后一个用例可能默认已解锁,但ECU可能因S3超时回到默认会话,导致后续请求失败。
  • DID字节序错误:多字节DID(如0xF190)在CAN报文中的字节序为高字节在前(大端),CAPL的SetDID函数会自动处理,但若手动构造报文则需注意。