2. 刷写任务模型:刷写任务的生命周期与调度策略

刷写任务,说白了就是ECU里那套“烧录”动作的完整流程。很多人以为刷写就是发个文件、等进度条走完就完事了。其实没那么简单。一个刷写任务从创建到结束,要经历好几个阶段,每个阶段都有严格的检查和状态管理。

我刚开始做刷写开发时,就吃过亏。有一次任务状态没处理好,刷到一半断电了,结果ECU变砖,只能拆下来用编程器救。嗯,从那以后,我对任务生命周期就特别较真。

2.1 刷写任务的四个核心阶段

一个标准的刷写任务,生命周期分为四个阶段:初始化、执行、完成、回滚。每个阶段都有明确的任务状态和触发条件。

阶段 状态码 触发条件 关键动作
初始化 INIT 刷写请求到达 资源分配、环境检查、版本校验
执行 RUNNING 初始化成功 数据传输、擦除、编程、验证
完成 COMPLETED 所有步骤成功 资源释放、状态上报、日志记录
回滚 ROLLBACK 执行阶段出错 恢复备份、标记失败、通知上层

2.2 初始化阶段:别急着干活,先检查清楚

初始化阶段,说白了就是“准备工作”。我个人习惯,在这个阶段至少做三件事:

  • 资源检查:确认Flash空间够不够、RAM缓冲区有没有分配好、通信通道是否正常。
  • 环境验证:检查ECU当前状态是否允许刷写(比如车速为0、发动机没转、电源电压稳定)。
  • 版本比对:对比待刷文件和当前固件的版本号,防止刷错或降级。
注意:初始化阶段如果发现任何异常,必须立即终止任务,不能硬着头皮往下走。我曾经遇到过初始化时电压波动,没处理就直接执行,结果写到一半ECU掉电,整个Bootloader都坏了。

初始化成功后,任务状态从 INIT 切换到 RUNNING。这时候才开始真正的数据传输。

2.3 执行阶段:核心动作,步步惊心

执行阶段是刷写任务的主体。它包含几个子步骤,每个子步骤都可能失败:

  1. 擦除(Erase):把目标Flash区域清空。注意,有些芯片擦除时间很长,要设置合理的超时。
  2. 编程(Program):把数据写入Flash。通常按块传输,每块都要校验。
  3. 验证(Verify):读回写入的数据,和源文件比对。这一步不能省。

执行阶段最怕什么?怕通信中断。我记得有一次在车上刷写,诊断线接触不良,数据块传了一半就断了。还好我做了断点续传机制,不然又得从头来。

经验之谈:执行阶段建议加一个“心跳监控”。每传输一个数据块,都检查一下通信链路是否正常。如果连续3次心跳超时,自动进入回滚流程。

2.4 完成阶段:收尾工作不能马虎

所有数据写完了,不代表任务就结束了。完成阶段要做几件事:

  • 释放占用的内存和缓冲区
  • 更新ECU的刷写状态标志位
  • 记录刷写日志(时间、版本、结果)
  • 通知上层应用:刷写已完成

完成阶段如果出问题,比如日志写失败,我个人建议不要影响主流程。日志丢了可以补,但ECU功能必须正常。

2.5 回滚阶段:最后的保命手段

回滚,说白了就是“后悔药”。当执行阶段出现不可恢复的错误时,任务必须回滚到刷写前的状态。

回滚策略有两种:

策略 原理 适用场景
备份恢复 刷写前把原固件备份到另一个区域,出错后恢复 Flash空间充足,安全性要求高
标记回滚 不恢复数据,只标记刷写失败,下次启动时自动重试 Flash空间紧张,允许重试
避坑指南:我曾经在回滚时犯过一个低级错误——回滚过程中又断电了。结果备份区和目标区都坏了。后来我加了“双备份”机制:刷写前保留两份备份,回滚时先校验备份的完整性。

2.6 任务优先级与调度策略

一个ECU里可能同时有多个刷写任务在排队。比如同时刷写多个ECU,或者刷写任务和诊断任务冲突。这时候就需要调度策略。

我常用的优先级规则:

  • 紧急刷写(如安全更新):优先级最高,抢占式调度
  • 普通刷写:中等优先级,按FIFO排队
  • 诊断任务:低优先级,刷写任务执行时暂停诊断

调度策略的核心原则:刷写任务不能被其他非关键任务打断。你想想看,刷写到一半,突然来个诊断请求要读故障码,这时候如果切换上下文,Flash写入就可能失败。

核心要点:刷写任务的调度,建议使用“非抢占式+优先级队列”模型。高优先级任务可以插队,但一旦开始执行,就不允许被中断,直到完成或回滚。

2.7 任务状态机总览

下面这张图,是我画的一个刷写任务状态机。它把四个阶段和状态切换关系都串起来了。你仔细看看,每个箭头都代表一个触发条件。

刷写任务状态机 刷写任务状态机 初始化 INIT 执行 RUNNING 完成 COMPLETED 回滚 ROLLBACK 初始化成功 所有步骤成功 执行出错 回滚完成,允许重试 说明: 实线箭头表示正常流程,虚线箭头表示回滚后的重试路径。每个状态切换都有严格的触发条件。

这张图里,我特意画了回滚到初始化的虚线路径。为什么?因为有些场景下,回滚完成后可以自动重试。比如通信临时中断,恢复后重新刷写,不用等人工干预。

2.8 实际项目中的调度配置

最后,我分享一个实际项目中的调度配置模板。这个配置用在某款量产ECU上,经过多轮测试,效果不错。

/* 刷写任务调度配置 */
typedef struct {
    uint8_t taskPriority;       /* 0-紧急,1-普通,2-低优 */
    uint8_t maxRetryCount;      /* 最大重试次数 */
    uint16_t timeoutMs;         /* 单步超时时间 */
    bool enableRollback;        /* 是否启用回滚 */
    bool enableBackup;          /* 是否启用备份恢复 */
} FlashTaskConfig;

/* 默认配置实例 */
const FlashTaskConfig g_defaultFlashConfig = {
    .taskPriority = 1,          /* 普通优先级 */
    .maxRetryCount = 3,         /* 最多重试3次 */
    .timeoutMs = 5000,          /* 5秒超时 */
    .enableRollback = true,     /* 启用回滚 */
    .enableBackup = true        /* 启用备份 */
};
小建议:配置里的 maxRetryCount 不要设太大。重试3次还失败,说明问题不是偶然的,再重试也没意义。不如直接报错,让上层决定下一步。

好了,刷写任务的生命周期和调度策略,就讲到这里。你记住四个阶段、三种调度优先级、以及状态机的切换逻辑,基本就能应对大部分刷写场景了。