电量计 -- BMS升级:从上位机到MCU的固件下载全流程

BMS升级解密:从上位机到MCU的固件下载全流程

写在前面

想象一下:你手中的电动工具、智能手表、甚至是电动汽车,它们内部都有一颗”电池大脑”——电量计芯片。这颗芯片不仅要实时计算电池剩余电量,还要在漫长的使用过程中不断更新自己的”知识库”——固件。那么问题来了:固件是如何从电脑传入这颗微小芯片的?

本文将深入解析一款基于USB/无线通信的电量计固件升级方案,从架构设计到代码实现。

一、问题背景:为什么要设计固件升级?

1.1 固件升级的必要性

电量计芯片(Fuel Gauge IC)虽然不像手机SOC那样需要频繁更新,但固件升级仍然不可或缺:

  • Bug修复:修正算法中的计算误差,提升电量精度
  • 功能迭代:支持新的电池化学体系或通讯协议
  • 参数优化:调整充放电曲线、告警阈值等配置
  • 安全补丁:修复潜在的安全漏洞

1.2 升级方案的挑战

电量计的固件升级面临独特的挑战:

1
2
3
4
5
6
7
8
9
10
11
12
13
┌─────────────────────────────────────────────────────────┐
│ 升级方案的核心矛盾 │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────┐ ┌───────────────────┐ │
│ │ 上位机(PC) │ │ 电量计芯片(Embedded)│ │
│ │ - 强大算力 │ ──▶ │ - 资源受限 │ │
│ │ - 丰富接口 │ USB │ - 独立运行 │ │
│ │ - 大容量存储 │ │ - 可靠性要求高 │ │
│ └───────────────────┘ └───────────────────┘ │
│ │
│ 核心问题:如何在资源受限的嵌入式芯片中实现可靠的固件烧录? │
└─────────────────────────────────────────────────────────┘

本文要介绍的方案,正是为解决这一矛盾而设计的。

二、系统架构:四级架构的通信模型

2.1 整体架构图

先上一张全景图,了解整个升级系统的组成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
┌─────────────────────────────────────────────────────────────────────┐
│ 上位机 (PC Tool) │
│ 发送命令序列: CONFIG_INFO → BOOT_DOWNLOAD(N次) → BOOT_DOWNLOAD_COMPLETE│
└───────────────────────────────┬─────────────────────────────────────┘
│ USB/无线

┌─────────────────────────────────────────────────────────────────────┐
│ wpb1025 MCU (主控) │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ dev_cps8610.c - app_fw_update() │ │
│ │ └─ 根据command路由到BST7001升级模块 │ │
│ └─────────────────────────────────────────────────────────────┘ │
└───────────────────────────────┬─────────────────────────────────────┘
│ 命令路由

┌─────────────────────────────────────────────────────────────────────┐
│ dev_bst7001_upgrade.c (升级驱动) │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ bst7001_upgrade_process_command() - 命令处理入口 │ │
│ │ ├─ WL_CMD_GET_ID → 返回芯片ID │ │
│ │ ├─ WL_CMD_CONFIG_INFO → bst7001_download_start() │ │
│ │ ├─ WL_CMD_BOOT_DOWNLOAD → bst7001_write_chunk() │ │
│ │ ├─ WL_CMD_BOOT_DOWNLOAD_COMPLETE → bst7001_download_finish()│ │
│ │ └─ WL_CMD_PING_STATUS → 返回当前状态 │ │
│ └─────────────────────────────────────────────────────────────┘ │
└───────────────────────────────┬─────────────────────────────────────┘
│ I2C/SMBus (0x18)

┌─────────────────────────────────────────────────────────────────────┐
│ BST7001 电量计芯片 │
│ (通过I2C进行Flash编程) │
└─────────────────────────────────────────────────────────────────────┘

2.2 每一层的职责

层级 模块 核心职责
L4 上位机 固件文件解析、分包、CRC校验、进度显示
L3 MCU应用层 USB数据包解析、命令路由、状态管理
L2 升级驱动 I2C通信、Flash操作、地址映射
L1 电量计芯片 内部Flash编程、参数存储

三、核心数据结构:命令与状态的载体

3.1 USB命令协议

上位机与MCU之间的”语言”是精心设计的USB命令协议:

1
2
3
4
5
6
7
8
/* USB命令定义 */
typedef enum {
WL_CMD_GET_ID = 0x20, // 获取芯片ID
WL_CMD_CONFIG_INFO = 0x21, // 配置下载信息(固件长度、CRC)
WL_CMD_BOOT_DOWNLOAD = 0x22, // 数据包下载
WL_CMD_BOOT_DOWNLOAD_COMPLETE = 0x23, // 下载完成
WL_CMD_PING_STATUS = 0x40, // 查询状态/进度
} wl_command_t;

这些命令构成了升级流程的”动词”,每一条都有明确的语义。

3.2 配置信息结构

1
2
3
4
5
6
7
/* 配置信息结构 - 告诉MCU要下载多大的固件 */
typedef struct {
uint32_t boot_len; // 固件总长度
uint32_t boot_crc; // 固件CRC校验值
uint32_t firmware_len; // (保留)
uint32_t firmware_crc; // (保留)
} wl_config_info_t;

3.3 升级上下文 - 状态机的”胃”

每个升级会话都对应一个上下文结构,它是状态机的”记忆”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef struct {
/* I2C通信 - 与芯片对话的"电话" */
int32_t fd_dev_ops; // I2C设备句柄
uint8_t i2c_addr; // I2C地址 (0x18)

/* 状态机 - 升级流程的"进度条" */
bst7001_upg_state_t state; // 当前状态
bst7001_upg_error_t last_error; // 最后错误

/* 下载进度 - 断点续传的"接力棒" */
uint32_t bytes_written; // 已写入字节数
uint32_t expected_offset; // 期望的下一个偏移

/* 备份数据 - 防止意外的"救生衣" */
uint8_t info_backup[512]; // Info区备份
} bst7001_upg_ctx_t;

四、USB数据包格式:128字节的艺术

4.1 BOOT_DOWNLOAD数据包结构

这是最核心的数据传输格式,设计非常精巧:

1
2
3
4
5
6
7
8
9
10
┌────────┬────────┬────────┬─────────────┬─────────────┬──────────────┬─────────┐
│ Header │ Length │ Command│ Address │ Data Size │ Data Payload │ CRC16 │
│ 1 byte │ 1 byte │ 1 byte │ 4 bytes │ 4 bytes │ 128 bytes │ 2 bytes │
├────────┼────────┼────────┼─────────────┼─────────────┼──────────────┼─────────┤
│ 0x55 │ 0x89 │ 0x22 │ Little-End │ Little-End │ FW Data │ CRC16 │
└────────┴────────┴────────┴─────────────┴─────────────┴──────────────┴─────────┘

实际示例:
55 89 22 00 00 00 00 80 00 00 00 [128字节固件数据] 15 3B
└─Address=0x0─┘ └─Size=128─┘ └CRC16┘

4.2 为什么要这样设计?

  • Header (0x55):数据包起始标记,用于帧同步
  • Length:整个数据包长度,方便接收方分配缓冲区
  • Command:命令类型,明确本次操作意图
  • Address:Flash目标地址,支持随机写入
  • Data Size:本次传输的数据长度(最大128字节)
  • Data Payload:实际的固件数据
  • CRC16:数据完整性校验

五、下载流程:五步走战略

5.1 完整时序图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
上位机                                  MCU (wpb1025)                    BST7001
│ │ │
│─── WL_CMD_GET_ID ──────────────────────►│ │
│◄── Response (chipid=0x00007001) ────────│ │
│ │ │
│─── WL_CMD_CONFIG_INFO ─────────────────►│ │
│ (boot_len, boot_crc) │── CPU Hold ──────────────────►│
│ │── Backup Info ───────────────►│
│ │── Remap Eflash ──────────────►│
│ │── Page Erase ────────────────►│
│◄── (通过PING_STATUS获取结果) ───────────│ │
│ │ │
│─── WL_CMD_BOOT_DOWNLOAD ───────────────►│ │
│ (offset=0x0000, data[128], CRC16) │── Write Block (128B) ────────►│
│◄── (通过PING_STATUS获取结果) ───────────│ │
│ │ │
│─── WL_CMD_BOOT_DOWNLOAD ───────────────►│ │
│ (offset=0x0080, data[128], CRC16) │── Write Block (128B) ────────►│
│ ...重复直到全部数据发送完成... │ │
│ │ │
│─── WL_CMD_BOOT_DOWNLOAD_COMPLETE ──────►│ │
│ │── Restore Info ──────────────►│
│ │── Remap Eflash ──────────────►│
│ │── Close CADC Int ────────────►│
│ │── Soft Reset ────────────────►│
│ │── Clear CADC Flag ───────────►│
│ │── CPU Run ───────────────────►│
│◄── Success ─────────────────────────────│ │

5.2 关键步骤详解

第一步:CONFIG_INFO - 准备工作

1
2
3
4
5
6
bst7001_download_start() {
// 1. CPU Hold - 暂停CPU,允许Flash编程
// 2. Backup Info - 备份Info区数据 (512字节),防止参数丢失
// 3. Remap Eflash - 重映射Eflash到可编程区域
// 4. Page Erase - 擦除目标页 (跳过Log区)
}

为什么要备份Info区?
BST7001的Flash中有一个特殊的Info区域,存储了校准参数、序列号等关键数据。擦除整个Flash会导致这些数据丢失,所以在擦除前必须备份,完成后恢复。

第二步:BOOT_DOWNLOAD - 数据传输

1
2
3
4
5
6
bst7001_write_chunk() {
// 1. 验证数据包CRC16 - 确保数据未被破坏
// 2. 检查offset顺序 - 确保无数据包丢失
// 3. 128字节数据写入Flash (分4次32字节写入RAM,再触发编程)
// 4. 处理代码区/表区地址跳跃 (0x8400 → 0xD400)
}

地址跳跃是什么意思?
Flash地址不是连续的!代码区从0x0000开始,到0x8400结束。然后是Log区(不擦除),接着是表区(从0xD400开始)。写入时需要处理这种”跳跃”。

第三步:COMPLETE - 收尾工作

1
2
3
4
5
6
7
8
9
bst7001_download_finish() {
// 1. Restore Info - 恢复修改后的Info区数据
// 2. Remap Eflash - 重映射Eflash
// 3. Close CADC Int - 关闭CADC中断
// 4. Soft Reset - 软复位,让新固件生效
// 5. Delay 100ms - 等待复位完成
// 6. Clear CADC Flag - 清除CADC中断标志
// 7. CPU Run - 恢复CPU运行
}

六、状态机:升级流程的”指挥官”

固件升级是一个典型的状态机场景,定义清晰的状态和转换关系至关重要:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
        ┌──────────────────┐
│ IDLE (空闲) │◄───────────────────────────┐
└────────┬─────────┘ │
│ CONFIG_INFO │
▼ │
┌──────────────────┐ │
│ PREPARING (准备) │ CPU Hold, Backup, Erase │
└────────┬─────────┘ │
│ 成功 │
▼ │
┌──────────────────┐ │
┌──────►│ DOWNLOADING │ 循环接收BOOT_DOWNLOAD │
│ │ (下载中) │ │
│ └────────┬─────────┘ │
│ │ BOOT_DOWNLOAD_COMPLETE │
│ ▼ │
│ ┌──────────────────┐ │
│ │ FINISHING (完成) │ Restore, Reset, Run │
│ └────────┬─────────┘ │
│ │ 成功 │
│ ▼ │
│ ┌──────────────────┐ │
│ │ COMPLETE (完成) │────────────────────────────┘
│ └──────────────────┘

│ 失败 ┌──────────────────┐
└───────│ ERROR (错误) │
└──────────────────┘

状态说明

状态 含义 可执行命令
IDLE 初始状态 CONFIG_INFO
PREPARING 准备中(擦除、备份) PING_STATUS
DOWNLOADING 下载中 BOOT_DOWNLOAD, PING_STATUS
FINISHING 完成中(恢复、复位) PING_STATUS
COMPLETE 成功完成 -
ERROR 发生错误 -

七、Flash地址映射:理解物理布局

BST7001的Flash布局如下:

1
2
3
4
5
6
7
8
9
10
BST7001 Flash 布局:
┌───────────────────┬───────────────────┐
│ 0x0000-0x8400 │ 代码区 (Code) │ ← 需要下载
├───────────────────┼───────────────────┤
│ 0x8400-0xD400 │ Log区 (跳过) │ ← 保留,不擦除
├───────────────────┼───────────────────┤
│ 0xD400-0xF400 │ 表区 (Table) │ ← 需要下载
├───────────────────┼───────────────────┤
│ 0x5000 (512B) │ Info区 │ ← 备份后恢复
└───────────────────┴───────────────────┘

这种分区设计的考量:

  • 代码区:存储主程序固件
  • Log区:存储运行时日志,擦除会丢失重要信息
  • 表区:存储查表数据(如电池曲线)
  • Info区:存储校准参数,每个芯片唯一

八、错误处理:容错与恢复

8.1 错误码定义

1
2
3
4
5
6
7
8
9
10
11
12
typedef enum {
BST7001_UPG_OK = 0, // 成功
BST7001_UPG_ERR_I2C = -1, // I2C通信错误
BST7001_UPG_ERR_PEC = -2, // PEC校验错误
BST7001_UPG_ERR_UNLOCK = -3, // 解锁失败
BST7001_UPG_ERR_ERASE = -4, // 擦除失败
BST7001_UPG_ERR_WRITE = -5, // 写入失败
BST7001_UPG_ERR_CHECKSUM = -6, // 校验和错误
BST7001_UPG_ERR_TIMEOUT = -7, // 超时
BST7001_UPG_ERR_PARAM = -8, // 参数错误
BST7001_UPG_ERR_STATE = -9, // 状态错误
} bst7001_upg_error_t;

8.2 关键设计:看门狗保护

在长时间操作中,芯片的看门狗(Watchdog)可能会复位系统。因此需要定期”喂狗”:

1
2
3
// 在每个I2C操作之间调用
delay_ms(BST7001_DL_DELAY_MS);
wwdt_feeddog(); // 喂狗,防止复位

九、架构设计亮点

9.1 分层设计

  • 命令层:定义清晰的USB协议
  • 路由层:根据配置选择不同芯片的驱动
  • 驱动层:实现具体的I2C通信和Flash操作

9.2 可配置性

通过宏开关支持不同芯片:

1
#define CFG_ENABLE_BST7001_UPGRADE    1   // 启用BST7001升级功能

9.3 状态机模式

同步状态机简化了异步复杂度,每个命令都有明确的前置状态检查。

十、总结

本文详细解析了电量计固件升级的全流程,从上位机的USB协议到MCU的状态机管理,再到I2C通信和Flash操作。这个设计方案体现了嵌入式系统的典型设计模式:

  1. 协议驱动:用清晰的命令集定义升级流程
  2. 状态管理:用有限状态机控制流程
  3. 分层抽象:用模块化降低复杂度
  4. 容错设计:用错误码和喂狗机制保证可靠性

理解了这些设计思想,你也能应对类似的固件升级需求。下一篇文章,我们将介绍新一代芯片SD7001的升级方案,看看它是如何在BST7001的基础上进行演进和优化的。


相关阅读