3、UDS服务基础(上):诊断会话控制(0x10)、ECU复位(0x11)、安全访问(0x27)的协议细节与实现

好,咱们正式开始啃UDS协议栈的硬骨头了。今天要聊的三个服务——0x10、0x11、0x27,可以说是诊断通信的"敲门砖"。你想想看,ECU上电后第一件事是什么?不是跑应用,而是先确定自己处在哪个会话模式下。这就是0x10要做的事。

我个人习惯把这三个服务称为"诊断三件套"。搞不定它们,后面的读写数据、刷写程序想都别想。咱们一个一个来拆解。

3.1 诊断会话控制(0x10)——ECU的"工作模式"开关

0x10服务说白了就是告诉ECU:"嘿,我要开始跟你聊天了,你准备好没?"ECU收到这个请求后,会切换到对应的会话模式。

UDS标准里定义了三种默认会话模式:

会话模式 Sub-function值 典型用途
默认会话(Default Session) 0x01 ECU上电后的初始模式,只允许少量诊断服务
编程会话(Programming Session) 0x02 用于ECU刷写/升级,会禁用部分通信
扩展诊断会话(Extended Diagnostic Session) 0x03 允许执行读写、例程控制等高级诊断功能

嗯,这里要注意:不同OEM可能会自定义更多的会话模式,比如0x40~0x5F是留给OEM自由发挥的。我在做某个国产车项目时,就遇到过客户自定义了0x50作为"产线终检模式",专门用来做下线前的快速检测。

关键点:ECU在任何时刻只能处于一种会话模式。当你发送0x10请求切换到新模式时,ECU会退出当前模式并进入新模式。如果当前模式不允许某些服务,ECU会直接回复否定响应。

来看一个典型的请求/响应交互:

// 客户端请求:切换到扩展诊断会话
请求:02 10 03
// 02:数据长度(2字节)
// 10:服务ID
// 03:Sub-function(扩展诊断会话)

// 正常响应
响应:06 50 03 00 32 00 00
// 06:响应长度
// 50:服务ID + 0x40(肯定响应标识)
// 03:Sub-function回显
// 00 32:P2_server_max(500ms)
// 00 00:P2*_server_max(0ms,表示不扩展)

响应里的P2_server_max和P2*_server_max是什么意思?这是ECU告诉客户端:"我处理一个请求最多需要500ms,如果超过这个时间还没回复,你就得考虑超时重发了。"这个参数在实际项目中特别重要,我见过有人把超时设得太短,导致正常通信频繁超时,排查了半天才发现是P2参数没对齐。

实战技巧:在ETAS工具链中,你可以在ISOLAR或CANape里配置每个会话模式的P2定时器。我个人习惯把默认会话的P2设短一些(比如50ms),扩展会话设长一些(比如500ms),因为扩展会话里经常要执行耗时操作。

3.2 ECU复位(0x11)——让ECU"重启一下"

0x11服务,顾名思义就是让ECU执行复位操作。你可能会问:"为什么诊断协议要管复位?直接断电不就行了?"

原因很简单:诊断复位是受控的、可追溯的。ECU在复位前可以保存关键数据、关闭外设、通知其他节点。而直接断电可能会丢失数据,甚至损坏Flash。

0x11服务支持的Sub-function包括:

  • 0x01:硬复位(Hard Reset)——模拟ECU断电再上电,所有RAM数据丢失
  • 0x02:钥匙下电复位(Key Off/On Reset)——模拟点火开关切换,部分数据保持
  • 0x03:软复位(Soft Reset)——仅复位CPU核心,外设状态保持
  • 0x04:使能快速路由(Enable Rapid Power Shutdown)——用于快速断电场景
  • 0x05:禁用快速路由(Disable Rapid Power Shutdown)——恢复正常断电流程

警告:千万不要在生产环境下随意发送0x11 0x01!我曾经在调试时手滑发了一次硬复位,结果ECU正在写EEPROM,数据直接损坏,导致ECU变砖了。后来我们不得不用Bootloader重新刷写。从那以后,我写测试脚本时都会加一个"确认弹窗"。

复位服务的请求格式很简单:

// 请求硬复位
请求:02 11 01

// 正常响应(ECU会在复位前回复)
响应:01 51
// 51:服务ID + 0x40
// 注意:响应后ECU会立即执行复位,客户端需要等待至少100ms再发送新请求

这里有个容易踩的坑:ECU回复肯定响应后,并不会立刻复位。它会先完成一些内部清理工作,比如关闭CAN控制器、保存诊断日志等。这个延迟时间通常在10~50ms之间,但不同ECU差异很大。我建议你在发送复位请求后,至少等待200ms再尝试重新建立通信。

3.3 安全访问(0x27)——诊断功能的"门禁系统"

0x27服务,是UDS协议里最容易被误解的服务之一。很多人以为它就是"密码验证",其实它的核心是"种子-密钥"(Seed-Key)机制。

为什么要用种子-密钥?直接传密码不行吗?

你想想看,如果直接传密码,攻击者只要抓一次报文就能拿到密码,以后想干嘛就干嘛。而种子-密钥机制每次通信都生成不同的种子,密钥也动态变化,安全性高得多。

安全访问的流程分为两步:

  1. 请求种子(Request Seed):客户端发送0x27 + 子功能(奇数),ECU返回一个随机种子
  2. 发送密钥(Send Key):客户端用种子计算出密钥,发送0x27 + 子功能+1(偶数),ECU验证通过后解锁

来看一个具体的例子:

// 步骤1:请求种子(安全等级1)
请求:02 27 01
// 27:服务ID
// 01:请求种子(安全等级1的奇数子功能)

响应:06 67 01 04 1A 2B 3C 4D
// 67:肯定响应
// 01:子功能回显
// 04:种子长度(4字节)
// 1A 2B 3C 4D:种子值(每次不同)

// 步骤2:发送密钥(安全等级1)
// 假设密钥算法:种子 + 0x12345678
// 计算:1A2B3C4D + 12345678 = 2C5F92C5
请求:06 27 02 2C 5F 92 C5
// 27:服务ID
// 02:发送密钥(安全等级1的偶数子功能)
// 2C 5F 92 C5:计算出的密钥

响应:02 67 02
// 解锁成功!

重要概念:安全等级(Security Level)决定了你能访问哪些服务。通常等级1只能做简单的读写,等级3才能刷写程序。不同等级对应不同的种子-密钥算法,算法越复杂,安全等级越高。

在ETAS工具链中实现安全访问,我建议你用ISOLAR的"Security Access"模块。它提供了现成的算法接口,你只需要实现一个回调函数:

// ETAS ISOLAR中的安全访问回调示例
uint32_t CalculateKey(uint32_t seed, uint8_t securityLevel)
{
    // 根据安全等级选择算法
    switch(securityLevel)
    {
        case 1:
            // 简单异或算法
            return seed ^ 0xA5A5A5A5;
        case 3:
            // 复杂算法:种子 + 固定偏移 + 循环移位
            seed += 0x12345678;
            return (seed << 8) | (seed >> 24);
        default:
            return 0;
    }
}

避坑指南:我曾经遇到过一个奇葩问题——ECU在解锁后,如果连续30秒没有诊断请求,会自动重新锁定。测试时我每次发完解锁就去喝咖啡,回来发现所有诊断都失败了。后来我养成了习惯:解锁后立即执行需要的操作,不要拖太久。

3.4 三个服务的协同工作

这三个服务在实际项目中是串联使用的。典型的诊断流程是这样的:

  1. ECU上电后处于默认会话(0x10 0x01)
  2. 客户端发送0x10 0x03切换到扩展会话
  3. 客户端发送0x27 0x01请求种子,ECU返回种子
  4. 客户端计算密钥,发送0x27 0x02完成解锁
  5. 解锁后,客户端可以执行读写、例程控制等操作
  6. 操作完成后,发送0x10 0x01回到默认会话,或者直接发0x11复位

嗯,这里有个细节:有些ECU在默认会话下不允许执行安全访问。你必须先切换到扩展会话或编程会话,才能发起0x27请求。这个限制是为了防止未授权访问。

我记得在做一个ADAS域控制器的项目时,客户要求:只有在编程会话下才能执行安全等级3的解锁。这意味着刷写流程变成了:默认会话 → 编程会话 → 安全等级3解锁 → 刷写。多了一步切换,但安全性确实提高了。

性能注意事项:每次会话切换都会触发ECU内部的状态机迁移,这个过程可能涉及内存分配、定时器重设等操作。频繁切换会话会导致ECU负载升高,甚至触发看门狗复位。我建议你在一个会话里完成所有操作,不要来回切换。

好了,这一章的内容就到这里。0x10、0x11、0x27这三个服务是UDS诊断的基石,理解了它们,后面的服务学起来就轻松多了。下一章咱们继续聊剩下的基础服务——读写数据(0x22/0x2E)和例程控制(0x31)。