4、刷写主状态机设计:IDLE状态、PREPROGRAMMING状态、PROGRAMMING状态、POSTPROGRAMMING状态
刷写状态机,说白了就是整个UDS刷写流程的骨架。你想想看,如果没有一个清晰的状态机,ECU刷到一半突然断电,或者收到一个乱序的请求,系统该怎么处理?嗯,这就是状态机存在的意义——它让整个刷写过程变得可控、可预测。
我个人习惯把刷写主状态机分成四个核心状态:IDLE(空闲)、PREPROGRAMMING(预编程)、PROGRAMMING(编程)和POSTPROGRAMMING(后编程)。每个状态都有明确的入口条件、活动内容和退出条件。我在项目中遇到过不少因为状态机设计不严谨导致的刷写失败案例,所以今天咱们就把这块彻底讲透。
4.1 IDLE状态——系统的默认栖息地
IDLE状态是ECU上电后的默认状态。说白了,就是啥也不干,等着诊断仪来敲门。
核心特征:
- ECU处于正常运行模式,应用代码在跑
- 只响应基础的诊断请求(如TesterPresent、ReadDataByIdentifier)
- 不接收任何刷写相关的服务请求
- 如果收到非预期的请求,直接返回NRC 0x78(RequestCorrectlyReceived-ResponsePending)或0x22(条件不满足)
我曾经踩过一个坑:有个项目在IDLE状态下允许了RoutineControl请求,结果测试人员误操作触发了擦除Flash的例程,导致ECU直接变砖。所以我的建议是——IDLE状态下,除了基础诊断服务,其他一律拒掉。安全第一。
小技巧:IDLE状态可以设计一个超时定时器。如果长时间没有收到任何诊断请求,ECU可以自动进入低功耗模式。我在做商用车项目时,这个设计帮我们省了不少电。
4.2 PREPROGRAMMING状态——刷写前的安全检查
从IDLE进入PREPROGRAMMING,通常是通过10 02(DiagnosticSessionControl,编程会话)请求触发的。这个状态的核心任务就一个:确认刷写条件是否满足。
我个人习惯把这个状态再细分成几个子步骤:
- 会话切换:从默认会话切换到编程会话
- 安全访问:通过27服务进行种子密钥验证
- 通信控制:通过28服务关闭非诊断通信(比如CAN网络管理报文)
- 检查刷写前提条件:比如电压是否正常、点火信号是否有效、硬件版本是否匹配
注意:PREPROGRAMMING状态下,如果任何一步失败,状态机应该回到IDLE状态,而不是卡在半路上。我曾经见过一个设计,安全访问失败后状态机还留在PREPROGRAMMING,结果诊断仪发了个擦除请求,ECU居然执行了——这明显是状态机设计缺陷。
这里有个细节:通信控制服务(28 03)。为什么要关闭非诊断通信?你想想看,刷写过程中如果网络管理报文还在发,Flash操作可能会被中断,导致刷写失败。所以,进入PREPROGRAMMING后,我建议先发28 03把非诊断报文都禁掉,等刷完了再恢复。
4.3 PROGRAMMING状态——真正的刷写战场
PROGRAMMING状态是刷写状态机的核心。从PREPROGRAMMING进入PROGRAMMING,通常是通过10 02再次请求(有些实现用34服务直接触发)。
这个状态下的服务调用顺序,我建议严格按照以下流程:
| 步骤 | 服务 | 说明 |
|---|---|---|
| 1 | 34(RequestDownload) | 请求下载,告诉ECU我要写多少数据、写到哪里 |
| 2 | 36(TransferData) | 传输数据,分块发送二进制数据 |
| 3 | 37(RequestTransferExit) | 请求退出传输,ECU完成数据校验和写入 |
| 4 | 31(RoutineControl) | 执行刷写后处理,比如校验完整性、复位应用 |
这里我要强调一个关键点:PROGRAMMING状态下,ECU必须保证操作的原子性。什么意思?就是要么全部刷成功,要么全部回滚。我在做Bootloader时,设计了一个双备份机制:先刷到备份区,校验通过后再交换到主运行区。这样即使刷写过程中断电,ECU也能从备份区启动。
避坑指南:我曾经在36服务的数据块大小设计上吃过亏。如果单包数据太大,CAN驱动层可能溢出;如果太小,刷写效率又太低。我建议根据CAN帧的有效载荷(经典CAN是8字节,CAN FD是64字节)来设计,一般单包64字节比较稳妥。
4.4 POSTPROGRAMMING状态——刷写后的收尾工作
刷写完成后,状态机进入POSTPROGRAMMING。这个状态的主要任务是:恢复ECU的正常运行环境。
具体包括:
- 通过28 03恢复非诊断通信
- 通过11 01执行ECU复位(如果刷的是应用代码)
- 或者通过10 01回到默认会话(如果只是刷写配置参数)
- 记录刷写日志,比如刷写时间、刷写结果、刷写次数
我个人习惯在POSTPROGRAMMING状态加一个延迟退出机制。什么意思?就是刷写完成后,不立即退出,而是等待诊断仪发送一个明确的退出请求(比如10 01或11 01)。这样做的好处是,如果诊断仪还想继续刷其他块,可以直接从PROGRAMMING状态继续,不用重新走一遍PREPROGRAMMING。
经验之谈:POSTPROGRAMMING状态下,我建议保留一个超时定时器。如果诊断仪长时间不发退出请求,ECU应该自动复位或回到IDLE状态。我记得有个项目,测试人员刷完后忘了发退出指令,ECU一直卡在POSTPROGRAMMING,导致应用层功能异常。后来加了超时机制,问题就解决了。
4.5 状态机转换图(伪代码)
最后,我给出一个简单的状态机实现伪代码,方便你理解:
typedef enum {
STATE_IDLE,
STATE_PREPROGRAMMING,
STATE_PROGRAMMING,
STATE_POSTPROGRAMMING
} FlashState;
FlashState currentState = STATE_IDLE;
void StateMachine_Process(DiagnosticRequest *req) {
switch (currentState) {
case STATE_IDLE:
if (req->service == 0x10 && req->subFunc == 0x02) {
// 进入预编程状态
currentState = STATE_PREPROGRAMMING;
// 执行安全访问、通信控制等
} else {
// 拒绝其他请求
SendNegativeResponse(0x22);
}
break;
case STATE_PREPROGRAMMING:
if (/* 所有前提条件满足 */) {
currentState = STATE_PROGRAMMING;
} else if (/* 超时或失败 */) {
currentState = STATE_IDLE;
}
break;
case STATE_PROGRAMMING:
// 处理34/36/37/31服务
if (/* 刷写完成 */) {
currentState = STATE_POSTPROGRAMMING;
}
break;
case STATE_POSTPROGRAMMING:
if (req->service == 0x11 && req->subFunc == 0x01) {
// 执行复位
ExecuteReset();
} else if (req->service == 0x10 && req->subFunc == 0x01) {
currentState = STATE_IDLE;
}
break;
}
}
嗯,到这里,刷写主状态机的四个核心状态就讲完了。你可能会问:为什么没有ERROR状态?其实我习惯把错误处理分散到各个状态中,而不是单独搞一个全局错误状态。因为不同状态下的错误处理逻辑完全不同——IDLE状态下出错直接忽略,PROGRAMMING状态下出错需要回滚。这个设计思路,咱们后面讲错误处理时再细聊。