电量计 -- BMS升级的移植演进:从SPI直控到I2C桥接

芯片架构演进:SD7001固件升级从SPI直控到I2C桥接

写在前面

在上一篇文章中,我们详细介绍了BST7001电量计芯片的固件升级方案,那是一种直接访问Flash寄存器的模式——MCU通过I2C直接读写芯片内部的Flash寄存器,就像直接打开芯片的”后门”一样简单。

但技术不会止步不前。新一代SD7001芯片采用了完全不同的架构:内置SPI Flash控制器,因为其采用更大的256KB Flash,固件升级需要通过SPI控制器这个”中介”来完成。

本文将深入解析SD7001的固件升级方案,带你理解芯片架构演进的逻辑,以及如何在新的约束下实现可靠的固件烧录。

一、架构变革:从”直接访问”到”间接访问”

1.1 BST7001 vs SD7001:架构对比

先上图,直观感受两种架构的差异:

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
33
34
35
36
37
┌─────────────────────────────────────────────────────────────────────────────┐
│ BST7001 架构 (旧方案) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ MCU(I2C 0x18) ──────────────────────────────────────────▶ BST7001 │
│ │ │
│ │ I2C/SMBus │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ Flash寄存器 │ ◀── 直接读写 │
│ │ 0x7000 - 0x700A │ 固件就暴露在总线上了 │
│ └─────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────────┐
│ SD7001 架构 (新方案) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ MCU(I2C 0x30) ──────────────────────────────────────────▶ SD7001 │
│ │ │
│ │ I2C/SMBus (带PEC校验) │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ SPI控制器 │ ◀── 需要通过"中介" │
│ │ 0x400070xx │ 发命令操作 │
│ └─────────┬──────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ 内部SPI Flash │ ◀── 真正的数据存储 │
│ │ (64KB) │ 隐藏起来了 │
│ └─────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘

1.2 为什么要”多此一举”?

这个问题值得深入思考。增加SPI控制器层看似增加了复杂度,但实际上:

  1. 安全性提升:Flash不再直接暴露在I2C总线上,需要通过控制器验证和转换
  2. 标准化接口:SPI Flash是通用存储单元,方案成熟、易于替换
  3. 读写优化:控制器可以做缓存、ECC校验等高级功能
  4. 功耗管理:可以独立控制Flash电源域

二、SD7001升级方案:关键差异点

2.1 核心差异一览

特性 BST7001 (旧) SD7001 (新)
I2C地址 0x18 0x30
Flash访问方式 直接寄存器 SPI控制器
页大小 128字节 256字节
擦除单位 Page Sector (4KB)
数据传输 128B直写 128B→256B聚合
PEC校验 可选 必须

2.2 最大的挑战:256 vs 128

这是SD7001升级方案中最棘手的问题:

1
2
3
4
上位机发送: 128字节/包  ◀── USB传输效率最优
SPI Flash要求: 256字节/页 ◀── Flash物理特性

矛盾:如何用128字节的"水桶"装满256字节的"水池"?

解决方案:数据聚合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 伪代码示意
uint8_t page_buf[256]; // 256字节页缓冲区
uint16_t page_filled = 0; // 已填充字节数

void write_chunk(uint8_t *data, uint16_t len) {
// 1. 把新数据拷贝到缓冲区
memcpy(page_buf + page_filled, data, len);
page_filled += len;

// 2. 缓冲区满了?写入Flash
if (page_filled >= 256) {
spi_page_program(page_addr, page_buf, 256);
page_filled = 0;
}
}

2.3 固件地址映射:非线性的挑战

SD7001的Flash布局与BST7001不同,固件bin文件的偏移地址需要转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
USB固件格式:
┌────────────────┬────────────────┐
│ 0x00000 │ Code Area │ (约48KB)
│ 0xC3FF │ │
├────────────────┼────────────────┤
│ 0xC400 │ Data Area │ (约8KB)
│ 0xF3FF │ │
└────────────────┴────────────────┘

SPI Flash物理地址:
┌────────────────┬────────────────┐
│ 0x1000 │ Code Area │ ← 偏移0x0 映射到 0x1000
│ 0xC3FF │ │
├────────────────┼────────────────┤
│ 0xD400 │ Data Area │ ← 偏移0xC400 映射到 0xD400
│ 0xF3FF │ │
└────────────────┴────────────────┘

地址转换公式:
if (offset < 0xC400) {
spi_addr = 0x1000 + offset; // 代码区
} else {
spi_addr = 0xD400 + (offset - 0xC400); // 数据区
}

三、通信协议:I2C + PEC校验

3.1 为什么要PEC?

PEC(Packet Error Checking)是SMBus协议的一种CRC-8校验机制。在SD7001方案中,每个I2C写操作都必须带PEC,否则芯片会拒绝执行:

1
2
3
4
5
6
7
8
9
10
┌─────────────────────────────────────────────┐
│ I2C写操作格式 (带PEC) │
├─────────────────────────────────────────────┤
│ │
│ [DeviceAddr][Reg] [Data_Hi] [Data_Lo] [PEC]│
│ 1 byte 1 byte 2 bytes 1 │
│ │
│ PEC = CRC8(DeviceAddr<<1, Reg, Data...) │
│ │
└─────────────────────────────────────────────┘

这增加了通信的可靠性,但也让驱动开发变得更加复杂。

3.2 寄存器映射

SD7001通过I2C寄存器控制SPI控制器:

寄存器 用途
0x11 解锁密码1 (0x6318)
0x12 解锁密码2 (0x6303=enable)
0x20 AHB模式控制
0x21 Remap控制
0x22 CRC控制 (0x0111=禁用)
0x29 SPI外部访问开关
0x01 地址低16位
0x02 地址高16位
0x0F 数据访问命令

3.3 32位寄存器访问

SPI控制器是32位的,需要分步操作:

1
2
3
4
5
6
7
8
9
10
11
// 写4字节到SPI控制器寄存器 (例如 0x40007018)
void block_4bytes_write(uint32_t addr, uint32_t val) {
// 1. 设置低16位地址
write_word(0x01, addr & 0xFFFF);

// 2. 设置高16位地址
write_word(0x02, (addr >> 16) & 0xFFFF);

// 3. 写入数据 (通过0x0F,带PEC)
block_write(0x0F, &val, 4);
}

四、升级流程:五步走

4.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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
┌─────────────────────────────────────────────────────────────┐
│ Step 1: I2C解锁 │
├─────────────────────────────────────────────────────────────┤
│ 1.1 WriteWord(0x11, 0x6318) ← 密码1 │
│ 1.2 WriteWord(0x12, 0x6303) ← 启用AHB │
│ 1.3 WriteWord(0x20, 0x8001) ×2 ← 进入编程模式 │
└─────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│ Step 2: 禁用系统CRC │
├─────────────────────────────────────────────────────────────┤
│ 2.1 WriteWord(0x22, 0x0111) ← 禁用CRC,否则会阻止编程 │
└─────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│ Step 3: SPI Remap │
├─────────────────────────────────────────────────────────────┤
│ 3.1 WriteWord(0x21, 0x1000) ← Remap配置 │
│ 3.2 WriteWord(0x21, 0) ← 执行Remap │
│ 3.3 WriteWord(0x29, 0x02) ← 启用SPI外部访问 │
│ 3.4 Block4BytesWrite(0x40007030, 0xABCD) │
│ 3.5 WriteWord(0x29, 0) ← 禁用SPI外部访问 │
└─────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│ Step 4: SPI Flash初始化 │
├─────────────────────────────────────────────────────────────┤
│ 4.1 PowerOn: 配置时钟,等待就绪 │
│ 4.2 Enable Buffer: 启用写入缓冲(加速) │
│ 4.3 ModeSwitch: 切换到SPI模式 │
│ 4.4 Wakeup: 唤醒Flash │
│ 4.5 Reset: 复位Flash │
│ 4.6 ProgramUnlock: 解锁编程 │
└─────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│ Step 5: 擦除 → 编程 → 完成 │
├─────────────────────────────────────────────────────────────┤
│ 5.1 擦除: 按4KB扇区擦除Code Area和Data Area │
│ 5.2 编程: 128B聚合→256B页写入 │
│ 5.3 完成: 恢复寄存器 + Remap跳转 │
└─────────────────────────────────────────────────────────────┘

4.2 关键函数实现

SPI等待就绪

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 等待SPI Flash操作完成 (轮询WIP位)
sd7001_upg_error_t spi_wait_ready(uint32_t timeout_ms) {
uint32_t start = ticker_read();

do {
delay_ms(10);
wwdt_feeddog(); // 别忘了喂狗!

uint32_t status;
block_4bytes_read(0x40007018, &status);

// bit0 = WIP (Write In Progress)
if ((status & 0x01) == 0) {
return SD7001_UPG_OK;
}

if ((ticker_read() - start) > timeout_ms) {
return SD7001_UPG_ERR_TIMEOUT;
}
} while (1);
}

扇区擦除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 擦除4KB扇区
sd7001_upg_error_t spi_sector_erase(uint32_t sector_addr) {
// 1. 等待Flash就绪
spi_wait_ready(100);

// 2. 发送写使能
block_4bytes_write(0x40007020, 0); // 长度=0
block_4bytes_write(0x40007004, 0x06000000); // Write Enable命令
block_4bytes_write(0x40007020, 2);
spi_wait_ready(50);

// 3. 发送扇区擦除命令
block_4bytes_write(0x40007008, sector_addr); // 目标地址
block_4bytes_write(0x40007020, 0);
block_4bytes_write(0x40007004, 0x20080000); // Sector Erase命令
block_4bytes_write(0x40007020, 2);

return spi_wait_ready(400); // 扇区擦除较慢
}

五、软件架构:分层设计

5.1 模块结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
dev_cps8610.c          # USB回调入口,命令路由

├── CFG_ENABLE_FG_UPGRADE = 1 → BST7001 (旧)

└── CFG_ENABLE_FG_UPGRADE = 2 → SD7001 (新)


dev_sd7001_upgrade.c # SD7001升级实现

├── I2C层 (sd7001_write_word, sd7001_block_4bytes_read/write)

├── SPI操作层 (sd7001_spi_wait_ready, sd7001_spi_erase, sd7001_spi_program)

├── 下载序列 (sd7001_download_start/write_chunk/finish)

└── 公共API (sd7001_upgrade_process_command)

5.2 上下文结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typedef struct {
/* I2C通信 */
int32_t fd_dev_ops; // I2C设备句柄
uint8_t i2c_addr; // 0x30

/* 状态机 */
sd7001_upg_state_t state;
sd7001_upg_error_t last_error;

/* 配置与进度 */
sd7001_upg_config_t config;
uint32_t bytes_written;
uint32_t expected_offset;

/* 页缓冲区 - 128B→256B聚合的关键! */
uint32_t page_base; // 当前页地址
uint16_t page_filled; // 已填充字节
uint8_t page_buf[256]; // 256字节页缓冲
} sd7001_upg_ctx_t;

六、地址转换:理解映射逻辑

这是SD7001方案中最容易出错的地方。固件bin文件的偏移地址是连续的,但SPI Flash的物理地址有”空洞”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* USB固件偏移 → SPI Flash物理地址转换
*
* 固件bin: [0x0000-C3FF] [C400-...]
* ↓ ↓
* SPI Flash: [0x1000-...] [0xD400-...]
*/
static uint32_t convert_offset_to_addr(uint32_t offset) {
if (offset < 0xC400) {
// 代码区: 偏移0x0 → 地址0x1000
return 0x1000 + offset;
} else {
// 数据区: 偏移0xC400 → 地址0xD400
return 0xD400 + (offset - 0xC400);
}
}

七、调试经验:踩坑记录

7.1 PEC计算错误

症状: 芯片不响应,写操作无效

排查: 检查CRC-8计算是否正确, PEC = CRC8(设备地址<<1, 寄存器, 数据…)

7.2 页对齐问题

症状: 写入后数据校验失败

排查: 确保256字节页写入地址是256的倍数,使用缓冲区聚合解决

7.3 地址映射错误

症状: 代码区写到了数据区,或 vice versa

排查: 添加日志打印 offset → addr 转换结果,与预期对比

7.4 看门狗超时

症状: 升级过程中芯片意外复位

排查: 在长时间操作(特别是擦除)中定期调用 wwdt_feeddog()

八、架构演进的启示

回顾BST7001到SD7001的演进,我们可以得到以下启示:

  1. 架构选择没有绝对好坏:直接访问简单但不够安全,间接访问复杂但更灵活

  2. 兼容性设计:虽然底层变了,但上层USB命令协议保持一致,降低了上位机的修改成本

  3. 数据聚合的智慧:面对”消费级”USB和”工业级”Flash的差异,用缓冲区巧妙化解

  4. 状态机的重要性:无论架构如何变,清晰的状态管理始终是可靠性的保障

九、总结

SD7001的固件升级方案展示了芯片架构演进的一种典型路径:通过引入SPI控制器层,在保持I2C接口不变的同时,实现了更高的安全性和灵活性。虽然驱动开发增加了复杂度(特别是PEC校验和256字节页对齐),但这些代价换来了更好的系统设计。

理解这些底层差异,对于固件工程师来说是宝贵的经验。当你在实际项目中遇到类似的需求时,希望本文的分析能给你提供思路。


相关阅读