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码这些信息。大家有什么问题,可以在课后交流。