test/freetest/IAP升级方案.md

745 lines
27 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# BMS 从机 CAN-UDS IAP 升级方案
> 基于 ISO 14229 (UDS) + ISO 15765 (CAN-TP) 标准
> 硬件: STM32F103C8T6 (64KB Flash, 20KB RAM)
> 通信: CAN 500kbps, 标准帧
> 日期: 2026-08-06
---
## 一、Flash 存储分区
### 1.1 地址布局
| 区域 | 起始地址 | 结束地址 | 大小 | 页数 | 说明 |
|------|----------|----------|------|------|------|
| Bootloader | 0x08000000 | 0x08003FFF | 16KB | 16 | 引导程序,不可升级 |
| Application | 0x08004000 | 0x0800EFFF | 44KB | 44 | 应用程序,IAP目标区 |
| 参数区 | 0x0800F000 | 0x0800FFFF | 4KB | 4 | 升级标志 + 用户参数 |
### 1.2 升级标志定义(参数区首字 0x0800F000)
| 标志值 | 含义 |
|-------|------|
| 0xFFFFFFFF | 空闲,无升级任务 |
| 0xA5A55A5A | 有新固件待处理 |
| 0x55AA55AA | 升级进行中(断电续传标志) |
| 0x12345678 | 升级完成,可跳转App |
### 1.3 参数区布局(断电续传)
```
偏移 字段 大小 说明
0x00 flag 4字节 升级标志(见1.2)
0x04 fw_size 4字节 固件总大小
0x08 bitmap_lo 4字节 进度位图(块0-31, 1=未完成, 0=完成)
0x0C bitmap_hi 4字节 进度位图(块32-63)
```
**进度位图原理**:
- Flash 擦除后全 1 (0xFFFFFFFF),表示所有块未完成
- 每完成一个 1KB 块,将对应位清 0 (1→0,符合 Flash 物理特性)
- 上电后扫描位图,第一个为 1 的位即为续传起点
- **优点**: 每次仅写 1 个字(~40μs),无需擦除参数页,不阻塞 CAN 接收
### 1.4 启动流程
```
上电
Bootloader 读取 0x0800F000 标志
├─ IAP_FLAG_COMPLETE (0x12345678) → 检查App合法性 → 跳转App
├─ IAP_FLAG_UPGRADING(0x55AA55AA) → 读取进度位图 → 等待IAP续传
├─ IAP_FLAG_NEW_FW (0xA5A55A5A) → 等待IAP升级
└─ IAP_FLAG_NONE (0xFFFFFFFF) → 检查App栈顶 → 跳转或等待
```
---
## 二、UDS 服务详解 (ISO 14229)
### 2.1 服务总览
| SID | 服务名称 | 阶段 | 说明 |
|-----|---------|------|------|
| 0x10 | DiagnosticSessionControl | 握手 | 切换诊断会话 |
| 0x22 | ReadDataByIdentifier | 握手 | 读取版本/固件信息 |
| 0x27 | SecurityAccess | 握手 | 安全解锁(Seed/Key) |
| 0x31 | RoutineControl | 校验 | 擦除Flash / CRC校验 |
| 0x34 | RequestDownload | 传输 | 请求下载,声明固件大小 |
| 0x36 | TransferData | 传输 | 传输固件数据 |
| 0x37 | RequestTransferExit | 传输 | 结束传输 |
| 0x11 | ECUReset | 完成 | 复位跳转App |
### 2.2 0x10 DiagnosticSessionControl
**功能**: 切换诊断会话模式,升级前必须进入编程会话。
**请求格式**:
```
[A0] [SID] [SubFunction]
0x10 0x01 = Default Session
0x02 = Programming Session
0x03 = Extended Session
```
**正响应**:
```
[A0] [SID|0x40] [SubFunction echo]
0x50 0x02
```
**否定响应**:
```
[A0] [0x7F] [SID] [NRC]
0x7F 0x10 见NRC表
```
**本项目实现**: 收到 0x10 0x02 后进入 `IAP_STATE_SESSION`,允许后续升级服务。
---
### 2.3 0x22 ReadDataByIdentifier
**功能**: 通过DID读取数据,用于握手和版本确认。
**请求格式**:
```
[A0] [SID] [DID_High] [DID_Low]
0x22 0xF1 0x00 (DID=0xF100: 版本信息)
```
**正响应**:
```
[A0] [SID|0x40] [DID_High] [DID_Low] [Data...]
0x62 0xF1 0x00 "IAP01" (5字节ASCII)
```
**DID 定义**:
| DID | 含义 | 数据格式 |
|-----|------|---------|
| 0xF100 | Bootloader版本 | 5字节ASCII "IAP01" |
| 0xF101 | 固件大小 | 4字节大端 |
| 0xF102 | CRC32 | 4字节大端 |
---
### 2.4 0x27 SecurityAccess
**功能**: 安全访问,升级前必须解锁。采用Seed/Key机制。
**请求Seed (子功能=0x01)**:
```
[A0] [SID] [SubFunction]
0x27 0x01
```
**正响应(返回Seed)**:
```
[A0] [SID|0x40] [SubFunction] [Seed 4字节]
0x67 0x01 0x12 0x34 0x56 0x78
```
**发送Key (子功能=0x02)**:
```
[A0] [SID] [SubFunction] [Key 4字节]
0x27 0x02 0xED 0xCB 0xA9 0x87
```
**Key算法**: `Key = ~Seed` (简化版,实际项目应使用更复杂算法)
| Seed | Key |
|------|-----|
| 0x12345678 | 0xEDCBA987 |
| 0xAABBCCDD | 0x55443322 |
**正响应**:
```
[A0] [SID|0x40] [SubFunction]
0x67 0x02
```
**否定响应**: Key错误时返回 NRC=0x35 (InvalidKey) 或 0x36 (ExceededNumberOfAttempts)。
---
### 2.5 0x31 RoutineControl
**功能**: 例程控制,用于擦除Flash和CRC校验。
**请求格式**:
```
[A0] [SID] [SubFunction] [RoutineID_High] [RoutineID_Low]
0x31 0x01 0xFF 0x01 (EraseFlash)
0x01 0xFF 0x02 (VerifyCRC)
```
**RoutineID 定义**:
| RoutineID | 含义 | 触发动作 |
|-----------|------|---------|
| 0xFF01 | EraseFlash | 擦除App区44页 |
| 0xFF02 | VerifyCRC | 读回Flash计算CRC32 |
**正响应**:
```
[A0] [SID|0x40] [SubFunction] [RoutineID_High] [RoutineID_Low]
0x71 0x01 0xFF 0x01
```
---
### 2.6 0x34 RequestDownload
**功能**: 请求下载,声明固件大小。标准ISO 14229格式。
**标准请求格式**:
```
[A0] [SID] [DataFormatId] [AddrSizeFormatId] [MemoryAddr] [MemorySize]
0x34 0x00 0x44 4字节 4字节
```
| 字段 | 值 | 说明 |
|------|-----|------|
| DataFormatId | 0x00 | 无压缩,无加密 |
| AddrSizeFormatId | 0x44 | 地址4字节,大小4字节 |
| MemoryAddr | 0x08004000 | App区起始地址 |
| MemorySize | 固件大小 | 大端4字节 |
**本项目简化格式**(CAN 8字节限制):
```
[A0] [SID] [Size_3] [Size_2] [Size_1] [Size_0]
0x34 0x00 0x00 0x5A 0x00 (size=23040=0x5A00)
```
**正响应(支持断电续传)**:
```
[A0] [SID|0x40] [LengthFormatId] [Offset_3] [Offset_2] [Offset_1] [Offset_0]
0x74 0x08 0x00 0x00 0x00 0x00
```
| 字段 | 说明 |
|------|------|
| LengthFormatId | 0x08, 每帧可接收8字节数据 |
| Offset | 续传偏移量(大端4字节), 0=全新升级, >0=从该偏移续传 |
**断电续传判断逻辑**:
- 板子收到 0x34 后,检查参数区 `flag``fw_size`
-`flag == IAP_FLAG_UPGRADING``fw_size` 匹配 → **续传模式**
- 不擦除 App 区,从进度位图计算 offset
- 上位机从固件 offset 处开始发送数据
- 否则 → **全新升级**
- 擦除 App 区,初始化参数区(flag=UPGRADING, fw_size, bitmap=0xFFFFFFFF)
- offset = 0
---
### 2.7 0x36 TransferData
**功能**: 传输固件数据。
#### 标准格式(带块序号)
**请求格式**:
```
[A0] [SID] [BlockSeqCounter] [Data 0~6字节]
0x36 0x01 [固件数据]
```
**BlockSequenceCounter**: 从0x01开始递增,到0xFF后回绕到0x00。
**正响应**:
```
[A0] [SID|0x40] [BlockSeqCounter echo]
0x76 0x01
```
每帧有效数据: `8 - 2 = 6字节`(SID + 序号占2字节)。
#### 本项目优化格式(纯数据模式 + 乒乓缓冲区)
为提高传输效率,采用**两阶段数据传输 + 乒乓缓冲区**:
**阶段1: 命令帧(0x36 0x01)**
```
[A0] [SID] [BlockSeqCounter]
0x36 0x01 → 正响应 0x76, 进入纯数据模式
```
**阶段2: 纯数据帧(无SID,无序号,乒乓缓冲区)**
```
[A0] [Data 0] [Data 1] [Data 2] [Data 3] [Data 4] [Data 5] [Data 6] [Data 7]
8字节全部是固件数据, 写入乒乓缓冲区, 无响应
```
**乒乓缓冲区机制**:
- 两个1KB缓冲区交替使用(共2KB RAM)
- CAN数据先写入活跃缓冲区(纯内存操作,极快)
- 缓冲区满(128帧×8字节=1KB)后切换到另一个缓冲区
- 上一轮满缓冲区在下一帧到来时批量写入Flash(只Unlock/Lock一次)
- **每完成1KB块写入Flash后,更新进度位图(断电续传)**
- Flash写入期间,CAN中断继续往frame_buf写入,实现半并行
```
帧1~128: 写入缓冲区A (纯内存, ~0μs/帧)
帧129: 缓冲区A满 → 切换到缓冲区B
→ 批量写Flash(缓冲区A, 1KB=256个字, ~10ms)
→ 更新进度位图(块0完成, 位0清0, ~40μs)
→ 帧129写入缓冲区B
帧130~256: 写入缓冲区B (纯内存)
帧257: 缓冲区B满 → 切换到缓冲区A
→ 批量写Flash(缓冲区B, 1KB)
→ 帧257写入缓冲区A
... 循环往复
```
按固件 `size` 计数,收够自动退出数据模式,发送最终正响应:
```
[A0] [SID|0x40]
0x76 (传输完成通知)
```
**效率对比**:
| 模式 | 每帧有效数据 | 44KB所需帧数 | 传输时间@500kbps |
|------|------------|-------------|-----------------|
| 标准模式(带序号) | 6字节 | 7467帧 | 0.97s |
| 纯数据模式(乒乓) | 8字节 | 5600帧 | 0.73s |
| 提升 | +33% | -25% | -25% |
**Flash写入效率对比**:
| 指标 | 每帧写Flash | 乒乓缓冲区(1KB) |
|------|------------|------------------|
| HAL_FLASH_Unlock/Lock | 每8字节2次 | 每1KB 2次 |
| Flash写入粒度 | 2个字/帧 | 256个字/批 |
| Unlock/Lock次数(44KB) | 11264次 | 88次 |
| Flash写入耗时/批 | ~80μs | ~10ms |
| 写入期间CAN缓冲 | frame_buf(200帧) | frame_buf(200帧) |
| RAM占用 | 0 | 2KB (2×1KB) |
---
### 2.8 0x37 RequestTransferExit
**功能**: 结束数据传输。
**请求格式**:
```
[A0] [SID]
0x37
```
**正响应**:
```
[A0] [SID|0x40]
0x77
```
---
### 2.9 0x11 ECUReset
**功能**: ECU复位,跳转到新App。
**请求格式**:
```
[A0] [SID] [SubFunction]
0x11 0x01 = Hard Reset
0x02 = Key Off On Reset
0x03 = Soft Reset
```
**正响应**:
```
[A0] [SID|0x40] [SubFunction echo]
0x51 0x01
```
正响应发送后,延时100ms确保帧发出,然后 `NVIC_SystemReset()`
---
## 三、完整升级时序
### 3.1 标准升级流程
```
上位机 (Tester) 板子 (ECU)
│ │
│═══ 阶段1: 握手 ═══════════════════════│
│── 0x10 0x02 ──────────────────────→ │ 进入编程会话
│←── 0x50 0x02 ────────────────────── │
│ │
│── 0x22 0xF1 0x00 ────────────────→ │ 读版本
│←── 0x62 0xF1 0x00 "IAP01" ──────── │
│ │
│── 0x27 0x01 ──────────────────────→ │ 请求Seed
│←── 0x67 0x01 [4字节Seed] ────────── │
│ │
│── 0x27 0x02 [4字节Key] ───────────→ │ 发送Key
│←── 0x67 0x02 ────────────────────── │ 解锁成功
│ │
│═══ 阶段2: 数据传输 ═══════════════════│
│── 0x31 0x01 0xFF 0x01 ────────────→ │ 擦除App区
│←── 0x71 0x01 0xFF 0x01 ──────────── │ 擦除完成
│ │
│── 0x34 [固件大小4字节] ────────────→ │ 请求下载
│←── 0x74 0x08 ────────────────────── │ 允许,每帧8字节
│ │
│── 0x36 0x01 ──────────────────────→ │ 开始数据传输
│←── 0x76 0x01 ────────────────────── │ 进入纯数据模式
│ │
│── [8字节数据 #1] ─────────────────→ │ 写入Flash (无响应)
│── [8字节数据 #2] ─────────────────→ │ 写入Flash (无响应)
│── [8字节数据 #3] ─────────────────→ │ 写入Flash (无响应)
│ ... │
│── [8字节数据 #N] ─────────────────→ │ 写入最后一块
│←── 0x76 ──────────────────────────── │ 传输完成(自动)
│ │
│── 0x37 ───────────────────────────→ │ 传输退出
│←── 0x77 ──────────────────────────── │
│ │
│═══ 阶段3: 校验烧写 ═══════════════════│
│── 0x31 0x01 0xFF 0x02 ────────────→ │ CRC校验
│←── 0x71 0x01 0xFF 0x02 [CRC 4字节]─ │ 校验通过
│ │
│── 0x11 0x01 ──────────────────────→ │ 硬复位
│←── 0x51 0x01 ────────────────────── │
│ │ 复位,跳转新App
```
### 3.2 超时机制
| 等待事件 | 超时时间 | 超时处理 |
|---------|---------|---------|
| 会话请求 (0x10) | 5s | 回到IDLE |
| 安全访问 (0x27) | 5s | 回到SESSION |
| 数据帧间隔 | 1s | 回到DOWNLOADING,发NRC |
| 传输完成 (0x37) | 10s | 回到SESSION |
| 整体升级 | 60s | 回到IDLE |
### 3.3 断电续传时序
```
上位机 (Tester) 板子 (ECU)
│ │
│═══ 首次升级(传输到50%断电) ═══════════│
│── 0x34 [size] ────────────────────→ │ 全新升级,擦除App区
│←── 0x74 0x08 [offset=0] ──────────── │ flag=UPGRADING, bitmap=0xFF...
│── 0x36 0x01 ──────────────────────→ │ 开始传输
│←── 0x76 0x01 ────────────────────── │
│── [数据帧...传输到50%] ────────────→ │ 已写22KB(22块完成)
│ ✗ 断电 │ bitmap: 0xFFC00000 (块0-21=0)
│ │
│═══ 重新上电,续传 ═════════════════════│
│ │ Bootloader读flag=UPGRADING
│ │ 扫描bitmap, offset=22KB
│── 0x10 0x02 ──────────────────────→ │ 进入会话
│←── 0x50 0x02 ────────────────────── │
│── 0x27 0x01 ──────────────────────→ │ 安全访问
│←── 0x67 0x01 [Seed] ──────────────── │
│── 0x27 0x02 [Key] ─────────────────→ │
│←── 0x67 0x02 ────────────────────── │
│ │
│── 0x34 [size] ────────────────────→ │ 检查: flag=UPGRADING, size匹配
│←── 0x74 0x08 [offset=22528] ──────── │ 续传模式! 不擦除App区
│ │
│── 0x36 0x01 ──────────────────────→ │ 开始传输
│←── 0x76 0x01 ────────────────────── │
│── [从offset=22KB处开始发数据] ─────→ │ 继续写入Flash
│── [数据帧...] ─────────────────────→ │
│←── 0x76 ──────────────────────────── │ 传输完成
│── 0x37 ───────────────────────────→ │
│←── 0x77 ──────────────────────────── │
│── 0x31 0x01 0xFF 0x02 ────────────→ │ CRC校验
│←── 0x71 0x01 0xFF 0x02 [CRC] ─────── │ 校验通过, flag=COMPLETE
│── 0x11 0x01 ──────────────────────→ │ 复位跳转
│←── 0x51 0x01 ────────────────────── │
```
---
## 四、NRC (否定响应码) 定义
### 4.1 标准NRC表 (ISO 14229)
| NRC | 名称 | 触发条件 |
|-----|------|---------|
| 0x10 | GeneralReject | 通用拒绝(Flash擦写失败等) |
| 0x11 | ServiceNotSupported | 不支持的SID |
| 0x22 | ConditionsNotCorrect | 状态不满足(未解锁就请求下载) |
| 0x31 | RequestOutOfRange | 参数越界(固件大小=0或超限) |
| 0x33 | SecurityAccessDenied | 未解锁就访问受保护服务 |
| 0x35 | InvalidKey | Key错误 |
| 0x36 | ExceededNumberOfAttempts | 超过最大尝试次数 |
| 0x72 | GeneralProgrammingFailure | Flash编程失败 |
| 0x73 | WrongBlockSequenceCounter | 块序号错误 |
| 0x78 | ResponsePending | 响应延迟(处理中) |
### 4.2 本项目使用的NRC
```c
NRC_GENERAL_REJECT 0x10 /* Flash擦写失败 */
NRC_SERVICE_NOT_SUPPORTED 0x11 /* 未知SID */
NRC_CONDITIONS_NOT_CORRECT 0x22 /* 状态不满足 */
NRC_REQUEST_OUT_OF_RANGE 0x31 /* 参数越界 */
```
---
## 五、状态机定义
### 5.1 状态转换图
```
┌──────────────────────────────────────────┐
↓ │
┌────────┐ 0x10 0x02 ┌─────────┐ 0x34 ┌──────────────┐ │
│ IDLE │ ──────────→ │ SESSION │ ─────→ │ DOWNLOADING │ │
└────────┘ └─────────┘ └──────────────┘ │
↑ │ │ │
│ │ 0x11 │ 0x36 0x01 │
│ 0x11 ↓ ↓ │
│ ┌─────────┐ ┌──────────────┐ │
│ | RESET │ │ DATA_MODE │ │
│ └─────────┘ └──────────────┘ │
│ │ │
│ 收够size │ │
│ ↓ │
│ ┌──────────────┐ │
└───────────────────────────────── │TRANSFER_DONE │────┘
0x11 └──────────────┘
```
### 5.2 状态说明
| 状态 | 允许的SID | 说明 |
|------|----------|------|
| IDLE | 0x10 | 仅接受会话控制 |
| SESSION | 0x10, 0x22, 0x27, 0x34, 0x11 | 握手+下载请求 |
| DOWNLOADING | 0x36 | 等待数据传输开始 |
| DATA_MODE | (纯数据) | 8字节直接写Flash,不解析SID |
| TRANSFER_DONE | 0x37, 0x31, 0x11 | 传输完成,校验/退出/复位 |
---
## 六、CRC32 校验
### 6.1 算法
- 多项式: 0xEDB88320 (IEEE 802.3, 反射形式)
- 初始值: 0xFFFFFFFF
- 最终异或: 0xFFFFFFFF
- 输入: 固件数据(按字节)
- 输出: 4字节CRC32
### 6.2 校验流程
1. 上位机计算固件BIN文件的CRC32
2. 数据传输完成后,上位机发送 0x31 0x01 0xFF 0x02 请求校验
3. 板子读回Flash中 `fw_size` 字节,计算CRC32
4. 板子返回CRC32结果
5. 上位机比对,一致则发 0x11 复位
### 6.3 CRC32 实现
```c
uint32_t iap_crc32(const uint8_t *data, uint32_t len)
{
uint32_t crc = 0xFFFFFFFFU;
for (uint32_t i = 0; i < len; i++)
{
crc ^= data[i];
for (uint8_t j = 0; j < 8; j++)
{
if (crc & 1)
crc = (crc >> 1) ^ 0xEDB88320U;
else
crc >>= 1;
}
}
return crc ^ 0xFFFFFFFFU;
}
```
---
## 七、安全机制
### 7.1 安全访问(Seed/Key)
- 升级前必须通过 0x27 安全访问
- Seed由板子随机生成(或基于HAL_GetTick)
- Key算法: `Key = ~Seed` (简化版,生产环境应使用更复杂算法)
### 7.2 固件大小校验
- 0x34 请求下载时检查固件大小
- 限制: `0 < fw_size <= 44KB (IAP_APP_SIZE)`
- 超限返回 NRC=0x31
### 7.3 栈顶地址校验
- 跳转App前检查栈顶地址
- 合法范围: `0x20000000 <= SP <= 0x20005000` (20KB RAM)
- 不合法则不跳转,留在Bootloader
### 7.4 Flash写保护
- 参数区(0x0800F000)只有升级标志操作可写
- Bootloader区(0x08000000-0x08003FFF)不可擦写
- App区(0x08004000-0x0800EFFF)仅IAP模式可擦写
---
## 八、与标准UDS的差异说明
### 8.1 本项目简化项
| 标准要求 | 本项目实现 | 原因 |
|---------|-----------|------|
| 0x34 带地址+大小格式标识符 | 仅带大小(4字节) | CAN 8字节帧限制 |
| 0x36 带块序号,每帧响应 | 纯数据模式+乒乓缓冲区,无序号,无响应 | 提升传输效率33%,Flash批量写入 |
| 0x36 块序号校验 | 按固件size计数 + CRC32校验 | 简化协议,乒乓缓冲区1KB/批 |
| 0x78 ResponsePending | 未实现 | Flash擦写<10ms,不需要 |
| ISO 15765 CAN-TP分包 | 未实现 | 固件44KB,纯数据模式够用 |
### 8.2 风险评估
| 风险 | 影响 | 缓解措施 |
|------|------|---------|
| 纯数据模式无序号,丢帧无法检测 | 固件损坏 | CRC32校验 + 上位机重传 |
| 乒乓缓冲区Flash写入~10ms,期间帧积压 | 最多积压78帧 | frame_buf(200帧)足够缓存 |
| 无CAN-TP分包,单帧8字节 | 传输慢 | 44KB@500kbps仅需0.73s,可接受 |
| 安全访问算法简单 | 可被逆向 | 生产环境应换为AES或自定义算法 |
| 乒乓缓冲区占用2KB RAM | RAM减少 | F103有20KB RAM,可接受 |
| 断电续传位图写失败 | 续传位置不准 | Flash写入有HAL返回值检查 |
| 断电发生在块写入中途 | 该块数据不完整 | 续传时该块重传(位图未清0) |
| 断电发生在位图清0前 | 该块需重传 | 最多重传1KB,可接受 |
---
## 九、上位机实现指南
### 9.1 升级步骤(Python伪代码)
```python
def iap_upgrade(can, fw_path):
fw_data = open(fw_path, 'rb').read()
fw_size = len(fw_data)
fw_crc = crc32(fw_data)
# 阶段1: 握手
can.send(0x180, [0x10, 0x02])
assert can.recv(0x181)[0] == 0x50
can.send(0x180, [0x22, 0xF1, 0x00])
resp = can.recv(0x181)
assert resp[0] == 0x62
can.send(0x180, [0x27, 0x01])
resp = can.recv(0x181)
seed = resp[2:6]
key = bytes([~b & 0xFF for b in seed])
can.send(0x180, [0x27, 0x02] + list(key))
assert can.recv(0x181)[0] == 0x67
# 阶段2: 数据传输(支持断电续传)
can.send(0x180, [0x31, 0x01, 0xFF, 0x01]) # 擦除(仅首次,续传时板子自动跳过)
assert can.recv(0x181)[0] == 0x71
can.send(0x180, [0x34] + list(fw_size.to_bytes(4, 'big'))) # 请求下载
resp = can.recv(0x181)
assert resp[0] == 0x74
offset = int.from_bytes(resp[2:6], 'big') # 读取续传偏移量
can.send(0x180, [0x36, 0x01]) # 开始传输
assert can.recv(0x181)[0] == 0x76
# 纯数据模式: 从offset处开始发送, 8字节/帧, 无响应
for i in range(offset, fw_size, 8):
chunk = fw_data[i:i+8]
chunk = chunk.ljust(8, b'\xFF') # 不足8字节补0xFF
can.send(0x180, list(chunk))
# 等待传输完成响应
assert can.recv(0x181, timeout=2000)[0] == 0x76
can.send(0x180, [0x37]) # 传输退出
assert can.recv(0x181)[0] == 0x77
# 阶段3: 校验
can.send(0x180, [0x31, 0x01, 0xFF, 0x02]) # CRC校验
resp = can.recv(0x181)
calc_crc = int.from_bytes(resp[4:8], 'big')
assert calc_crc == fw_crc
can.send(0x180, [0x11, 0x01]) # 复位
assert can.recv(0x181)[0] == 0x51
print("升级成功!")
```
### 9.2 上位机CAN帧参数
| 参数 | |
|------|-----|
| 帧类型 | 标准帧 |
| 请求ID | 0x180 |
| 响应ID | 0x181 |
| DLC | 8 |
| 波特率 | 500kbps |
| 帧间隔 | 1ms (数据模式可连续) |
---
## 十、测试验证
### 10.1 功能测试项
| 测试项 | 预期结果 | 状态 |
|--------|---------|------|
| 握手流程(0x100x220x27) | 全部正响应 | |
| 擦除App区(0x31 0xFF01) | App区全0xFF | |
| 下载1KB固件 | CRC校验通过 | |
| 下载44KB固件(满载) | CRC校验通过,0.73s完成 | |
| 跳转App(0x11) | App正常运行 | |
| 错误Key | NRC=0x35 | |
| 固件大小超限 | NRC=0x31 | |
| 未解锁就下载 | NRC=0x22 | |
| 断电续传: 传输50%断电 | 重新上电后从断点续传 | |
| 断电续传: 传输1KB后断电 | 重新上电后从1KB处续传 | |
| 断电续传: 块写入中途断电 | 该块重传,CRC校验通过 | |
### 10.2 压力测试
| 测试项 | 预期结果 | 状态 |
|--------|---------|------|
| 连续升级10次 | 每次成功 | |
| 断电续传5次 | 最终CRC校验通过 | |
| 数据帧丢1帧 | CRC校验失败,拒绝跳转 | |
| 总线错误(bus-off) | 自动恢复,重试 | |
---
## 十一、文件清单
| 文件 | 作用 |
|------|------|
| [app_iap.h](file:///d:/00_software/00_keil/01_keil_file/001_BMS/BMS_SLAVE/000_update/freertos/freetest/Core/Inc/app_iap.h) | IAP模块头文件,定义和API |
| [app_iap.c](file:///d:/00_software/00_keil/01_keil_file/001_BMS/BMS_SLAVE/000_update/freertos/freetest/Core/Src/app_iap.c) | IAP模块实现 |
| [app_flash.h](file:///d:/00_software/00_keil/01_keil_file/001_BMS/BMS_SLAVE/000_update/freertos/freetest/Core/Inc/app_flash.h) | Flash读写模块头文件 |
| [app_flash.c](file:///d:/00_software/00_keil/01_keil_file/001_BMS/BMS_SLAVE/000_update/freertos/freetest/Core/Src/app_flash.c) | Flash读写模块实现 |
---
## 十二、待确认事项
1. **Bootloader独立工程**: 当前App工程中包含IAP代码,实际应拆分为独立Bootloader工程(16KB) + App工程(44KB)
2. **中断向量表偏移**: App工程需设置 `VECT_TAB_OFFSET=0x4000`,或在system_stm32f1xx.c中修改 `VECT_TAB_OFFSET`
3. **链接脚本**: App工程的Flash起始地址需改为 0x08004000
4. **安全访问算法**: 当前 `Key=~Seed` 过于简单,生产环境是否需要加强?
5. **数据模式丢帧处理**: 纯数据模式无序号,如果CAN丢帧,CRC校验会失败是否需要增加重传机制?