第四章 CAN刷写协议核心:刷写流程总览与状态机设计
刷写协议,说白了就是一套“谁先说话、说什么、怎么确认”的规矩。我见过不少工程师把UDS协议栈背得滚瓜烂熟,但一到实际刷写就出问题——为什么?因为流程没理清楚,状态机设计有漏洞。
这一章,我们就来拆解刷写流程的核心三要素:总览流程、状态机设计、数据包格式。搞懂这三样,你就能自己搭一套刷写协议框架。
4.1 刷写流程总览:从开始到结束
一个完整的CAN刷写流程,通常包含以下几个阶段。我习惯把它分成“三段式”:
- 预编程阶段:检查条件、解锁安全访问、进入扩展会话
- 主编程阶段:擦除、写入、校验——这是核心
- 后编程阶段:复位、验证应用是否正常启动
你想想看,如果ECU正在运行应用,你直接发个擦除指令会怎样?大概率会死机。所以预编程阶段就是做“软着陆”。
核心原则:刷写流程必须保证“可回退”。一旦中间某步失败,ECU要能回到安全状态,而不是变砖。
下面这张图,是我自己项目里常用的刷写流程框架。你看一眼就能明白整体脉络。
我的经验:预编程阶段最容易忽略的是“关闭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服务请求。
避坑指南:我曾经在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以上)时特别有用。