2、诊断会话控制:诊断会话的概念、默认会话、扩展会话、编程会话的切换与实现

各位同学,咱们今天聊聊诊断会话控制。说实话,这是UDS协议里最基础、也最容易踩坑的一个环节。我刚开始做诊断栈的时候,就因为在会话切换上没处理好,导致ECU在产线上死活刷不进程序。嗯,那滋味可不好受。

2.1 什么是诊断会话?

先说说概念。诊断会话,说白了就是ECU当前所处的“工作模式”。ECU不是什么时候都允许你干所有事情的。比如你开车在路上,ECU正在控制喷油点火,这时候你突然发个指令说要刷写程序,那不是要命吗?

所以UDS定义了不同的会话状态。每个会话下,允许执行的服务是不一样的。我个人习惯把会话理解成“权限等级”。等级越高,能做的事情越多,但风险也越大。

2.2 三种核心会话

UDS里定义了三种最常用的会话,我列个表给大家看:

会话类型 SID (0x10子功能) 典型用途 安全等级
默认会话 (Default Session) 0x01 正常通信、读取故障码
扩展会话 (Extended Session) 0x03 读写参数、执行例程
编程会话 (Programming Session) 0x02 刷写Bootloader/应用程序

你想想看,默认会话就像ECU的“待客模式”。你读个故障码、看看版本号,这些都没问题。但如果你想改个标定参数,ECU就会告诉你:“不好意思,权限不够。”

扩展会话呢,就是“工程师模式”。我在项目里经常用它来做产线终检。比如调整某个传感器的偏移量,或者执行某个自检例程。这个模式下,大部分诊断服务都能用了。

编程会话是最特殊的。它直接关系到ECU的固件安全。我记得有一次,客户要求我们在编程会话下必须关闭所有中断,防止刷写过程中被其他任务打断。这个后面会细说。

2.3 会话切换的规则

会话切换不是你想切就能切的。UDS协议规定了几条硬性规则:

  • 从默认会话可以切换到任何会话:这是最自由的。ECU上电后默认就在默认会话,你可以直接发指令切到扩展或编程会话。
  • 从扩展会话可以切回默认会话:但不能直接切到编程会话。我遇到过有人想从扩展直接切编程,结果ECU回了NRC 0x12(子功能不支持)。
  • 从编程会话只能切回默认会话:编程会话是个“单向门”。进去之后,要么刷写成功切回默认,要么超时自动切回。

核心要点:会话切换必须经过默认会话。你可以理解为:默认会话是“中转站”。

2.4 超时机制与S3定时器

这里有个重要的概念——S3定时器。ECU不会永远待在高级别会话里。如果一段时间没有收到诊断请求,ECU会自动切回默认会话。

为什么这样设计?说白了是为了安全。你想想看,如果诊断仪拔掉了,ECU还留在编程会话里,万一哪个任务误操作了怎么办?

S3定时器的典型值是5秒。我个人的习惯是设成3秒,更安全一些。但要注意,这个值不能太短,否则诊断仪还没来得及发下一个请求,会话就切回去了。

小技巧:在编程会话下,建议把S3定时器设长一点,比如10秒。因为刷写过程中,擦除Flash可能需要好几秒,这时候发不了诊断请求。如果定时器太短,刷到一半会话切回去了,那就麻烦了。

2.5 代码实现示例

下面我给大家看一段简化的会话切换实现。这是MCU底层代码,用C写的:

// 会话状态枚举
typedef enum {
    SESSION_DEFAULT = 0x01,
    SESSION_PROGRAMMING = 0x02,
    SESSION_EXTENDED = 0x03
} SessionType_t;

// 当前会话状态
static SessionType_t currentSession = SESSION_DEFAULT;
static uint32_t s3Timer = 0;

// 诊断会话控制服务处理
uint8_t DiagSessionControl_Handler(uint8_t subFunction) {
    uint8_t response = 0x50; // 肯定响应SID
    
    // 检查当前会话是否允许切换到目标会话
    switch(currentSession) {
        case SESSION_DEFAULT:
            // 默认会话可以切换到任何会话
            if(subFunction == SESSION_PROGRAMMING || 
               subFunction == SESSION_EXTENDED ||
               subFunction == SESSION_DEFAULT) {
                currentSession = subFunction;
                // 重置S3定时器
                s3Timer = 0;
            } else {
                return 0x7F + 0x12; // NRC: 子功能不支持
            }
            break;
            
        case SESSION_EXTENDED:
            // 扩展会话只能切回默认
            if(subFunction == SESSION_DEFAULT) {
                currentSession = subFunction;
                s3Timer = 0;
            } else {
                return 0x7F + 0x12;
            }
            break;
            
        case SESSION_PROGRAMMING:
            // 编程会话只能切回默认
            if(subFunction == SESSION_DEFAULT) {
                currentSession = subFunction;
                s3Timer = 0;
            } else {
                return 0x7F + 0x12;
            }
            break;
            
        default:
            return 0x7F + 0x12;
    }
    
    return response;
}

// 1ms定时器中断中调用,用于S3超时检测
void S3Timer_Handler(void) {
    if(currentSession != SESSION_DEFAULT) {
        s3Timer++;
        // 假设S3超时时间为5000ms
        if(s3Timer >= 5000) {
            // 超时,切回默认会话
            currentSession = SESSION_DEFAULT;
            // 这里可以加一些清理工作
            // 比如关闭扩展会话下开启的特殊功能
        }
    }
}

这段代码看起来简单,但实际项目中要考虑的东西很多。比如在编程会话下,你切回默认会话时,要确保Flash刷写操作已经完成或者被安全中止。我曾经遇到过一个问题:刷写到一半,S3超时了,代码直接切回默认会话,结果Flash里的数据是半残的,ECU直接变砖了。

避坑指南:在编程会话下,建议在切回默认会话前,先检查是否有正在进行的刷写操作。如果有,要么等待完成,要么返回NRC 0x78(请求正确接收,但响应待定)。千万不要强行切换!

2.6 实际项目中的经验

最后分享几个我在项目中踩过的坑:

  • 会话切换和诊断仪握手:有些诊断仪在发送0x10请求后,会立刻发下一个服务。如果你的ECU处理速度慢,可能还没来得及切会话,下一个请求就到了。这时候ECU会认为当前会话不支持该服务,返回NRC。我建议在会话切换后,加一个短暂的状态同步延时。
  • 多会话并发问题:有些ECU有多个诊断通道(比如CAN和DoIP同时在线)。每个通道的会话状态是独立的。我曾经见过一个bug:CAN通道切到了编程会话,DoIP通道还在默认会话,结果两个通道互相干扰。解决方案是:所有通道共享同一个会话状态,或者严格隔离。
  • Bootloader中的会话处理:在Bootloader里,通常只支持默认会话和编程会话。扩展会话一般不需要。因为Bootloader的功能很单一,就是刷写。如果你在Bootloader里实现了扩展会话,反而增加了代码量和风险。

好了,关于诊断会话控制,今天就讲这么多。下一节我们会聊0x22服务——读取数据标识符。到时候我会讲讲怎么用这个服务读取ECU的版本号、VIN码这些信息。大家有什么问题,可以在课后交流。