第四章 CAN刷写协议核心:刷写流程总览与状态机设计

刷写协议,说白了就是一套“谁先说话、说什么、怎么确认”的规矩。我见过不少工程师把UDS协议栈背得滚瓜烂熟,但一到实际刷写就出问题——为什么?因为流程没理清楚,状态机设计有漏洞。

这一章,我们就来拆解刷写流程的核心三要素:总览流程、状态机设计、数据包格式。搞懂这三样,你就能自己搭一套刷写协议框架。

4.1 刷写流程总览:从开始到结束

一个完整的CAN刷写流程,通常包含以下几个阶段。我习惯把它分成“三段式”:

  1. 预编程阶段:检查条件、解锁安全访问、进入扩展会话
  2. 主编程阶段:擦除、写入、校验——这是核心
  3. 后编程阶段:复位、验证应用是否正常启动

你想想看,如果ECU正在运行应用,你直接发个擦除指令会怎样?大概率会死机。所以预编程阶段就是做“软着陆”。

核心原则:刷写流程必须保证“可回退”。一旦中间某步失败,ECU要能回到安全状态,而不是变砖。

下面这张图,是我自己项目里常用的刷写流程框架。你看一眼就能明白整体脉络。

CAN刷写三段式流程总览 阶段一:预编程(Pre-Programming) 10 02 扩展会话 27 01/02 安全访问 31 01 例程控制 85 02 禁用DTC → 检查刷写条件(电压、点火、VIN匹配)→ 解锁 → 关闭通信干扰 阶段二:主编程(Main Programming) 31 01 擦除内存 34 36 写入数据 31 01 校验完整性 31 01 编程依赖检查 → 按地址范围擦除 → 分块写入(每包最多8字节CAN数据)→ CRC校验 阶段三:后编程(Post-Programming) 11 01 硬件复位 10 01 默认会话 22 F1 90 验证应用启动 → 复位后检查应用有效性 → 如果应用无效,回滚到备份区或Bootloader

我的经验:预编程阶段最容易忽略的是“关闭DTC记录”。如果不关,刷写过程中产生的临时故障码会填满内存,导致刷写失败。我曾经因为这个坑,在客户现场排查了整整两天。

4.2 刷写状态机设计:状态与迁移

刷写状态机,是Bootloader的“大脑”。它决定了ECU在什么状态下该做什么事。我见过最糟糕的设计,是整个刷写逻辑写在一个大switch-case里,状态迁移全靠全局变量——调试起来简直噩梦。

好的状态机设计,应该满足三个条件:

  • 状态清晰:每个状态只做一件事
  • 迁移可控:非法输入不会导致状态“飞掉”
  • 超时处理:每个状态都有超时保护

下面是我常用的刷写状态机定义。注意看,我把“空闲”和“完成”也作为独立状态——很多人会忽略这两个。

状态编号 状态名称 描述 超时时间
0x00 IDLE 空闲状态,等待刷写请求 无
0x10 EXTENDED_SESSION 已进入扩展会话,等待安全访问 5000 ms
0x20 SECURITY_ACCESS 安全访问已解锁,等待擦除指令 5000 ms
0x30 ERASE_PENDING 正在擦除内存,等待擦除完成 30000 ms
0x40 DOWNLOAD_PENDING 已准备好接收数据,等待34服务 10000 ms
0x50 DATA_TRANSFER 正在传输数据,等待36服务 2000 ms(每包)
0x60 VERIFY_PENDING 数据接收完成,等待校验 10000 ms
0x70 RESET_PENDING 校验通过,等待复位 5000 ms
0x80 COMPLETED 刷写完成,应用已启动 无
0xFF ERROR 错误状态,需要诊断工具干预 无

注意:超时时间不是随便写的。擦除时间取决于Flash芯片型号,有些老款NAND Flash擦除一个扇区就要20秒。我建议你在设计阶段就拿到目标芯片的擦除/写入时序参数,否则超时设短了,刷写永远失败。

状态迁移的逻辑,我习惯用一张图来表示。你看下面这个状态机,每个箭头都代表一个UDS服务请求。

刷写状态机迁移图 IDLE (0x00) EXT_SESSION (0x10) SEC_ACCESS (0x20) ERASE (0x30) DOWNLOAD (0x40) DATA_XFER (0x50) VERIFY (0x60) RESET (0x70) COMPLETED (0x80) ERROR (0xFF) 10 02 27 01/02 31 01 擦除 34 00 36 00 36 完成 31 01 校验 擦除完成 11 01 超时/错误

避坑指南:我曾经在DATA_TRANSFER状态里,没有处理“连续两包数据超时”的情况。结果刷写过程中CAN总线被干扰了一下,ECU就卡死在半传输状态,必须断电重启。后来我加了一个“连续超时计数器”,超过3次直接进ERROR状态。

4.3 刷写数据包格式定义

数据包格式,是刷写协议的“语法”。CAN刷写的数据包,本质上就是UDS服务请求的CAN帧封装。但这里有几个关键点,我单独拎出来说。

4.3.1 单帧与多帧传输

CAN一帧最多传8字节数据。如果刷写数据超过8字节(比如一个256KB的固件),就必须拆成多帧。UDS协议里用到了两种传输机制:

  • 单帧(SF):数据长度 ≤ 7字节(CAN标准帧)
  • 多帧(FF + CF):数据长度 > 7字节,先发首帧(FF),再发连续帧(CF)

我习惯把刷写数据包分成三类,你看下面这个表格:

包类型 CAN ID PCI字节 数据域 说明
请求包 0x7E0 0x02~0x07 SID + 参数 诊断工具→ECU
响应包 0x7E8 0x02~0x07 SID+0x40 + 数据 ECU→诊断工具
数据包 0x7E0 0x10~0x1F (FF)
0x20~0x2F (CF)
固件数据 多帧传输时使用

4.3.2 34服务(请求下载)数据包格式

34服务是刷写的“开闸”指令。它告诉ECU:我要往哪个地址写数据,写多少。格式如下:

请求:34 00 44 AA BB CC DD EE FF
       |  |  |  \_____________/
       |  |  |        |
       |  |  |    数据长度(3字节,大端)
       |  |  |
       |  |  地址长度(0x44 = 4字节地址)
       |
       34服务 + 压缩/加密标志(0x00 = 无压缩无加密)

响应:74 00 44 AA BB CC DD EE FF
       |  |  |  \_____________/
       |  |  |        |
       |  |  |    最大接收长度(ECU告诉工具一次能收多少)
       |
       74服务(34+0x40)

关键点:响应里的“最大接收长度”很重要。我见过有些ECU Flash写入速度慢,一次只能收4字节。如果工具不管这个值,一股脑发8字节,ECU就会丢包。所以,工具端一定要解析这个字段,动态调整每包数据量。

4.3.3 36服务(传输数据)数据包格式

36服务是真正的“搬砖”指令。每包数据都带一个块序列号,ECU用这个序列号来检测丢包和乱序。

请求:36 01 [7字节数据]
       |  |
       |  块序列号(从0x01开始,每包+1,循环到0xFF后归零)
       |
       36服务

响应:76 01 [可选数据]
       |
       76服务(36+0x40),块序列号回显

注意:块序列号循环到0xFF后,下一包必须是0x00,而不是0x100。有些工具实现时没处理好这个循环,导致ECU收到0x00后以为是重传包,直接丢弃。我踩过这个坑,排查了三天才发现是序列号溢出问题。

4.3.4 37服务(退出传输)数据包格式

37服务用来结束数据传输。发送完所有数据后,工具必须发这个指令,ECU才会开始校验。

请求:37
响应:77

就这么简单。但要注意,37服务必须在所有36包发送完成后才能发。如果提前发了,ECU会认为数据不完整,校验必然失败。

4.4 实战中的流程控制要点

说了这么多理论,最后分享几个我在项目中总结的实战要点:

  • 流控制(FC):多帧传输时,ECU通过流控制帧告诉工具“慢点发”或“继续发”。BS(块大小)和STmin(最小间隔时间)这两个参数一定要正确解析。
  • 超时分层:我习惯把超时分为三层——CAN控制器层(硬件超时)、传输层(ISO-TP超时)、应用层(刷写状态超时)。每层独立计时,互不干扰。
  • 错误码映射:UDS的NRC(否定响应码)要映射到状态机的错误处理。比如收到0x78(请求正确接收,但响应待定),状态机不能超时退出,要等待最终响应。

我的习惯:在刷写过程中,我会在每包数据后加一个“心跳”机制——ECU每收到N包数据,就发一个周期性的“仍在工作”信号。这样工具端就知道ECU没死机。这个机制在刷写大固件(比如2MB以上)时特别有用。