3. Flash驱动与抽象层:Flash读写擦除操作、Flash分区表设计、驱动层封装与接口定义
好,咱们进入第三章。这一章是Bootloader的基石——Flash操作。你想想看,不管你的升级方案设计得多花哨,最终都得落到Flash的读写擦上。这块要是没弄好,后面全是空中楼阁。
我个人习惯把Flash驱动分成两层:底层硬件操作和上层抽象接口。这样做的目的很明确——万一换芯片了,上层代码基本不用动。
3.1 Flash基本操作:读、写、擦除
Flash这玩意儿,说白了有三个核心操作:读、写、擦除。但有个关键点你得记住——写之前必须先擦除。这不是我啰嗦,很多新手就在这上面栽跟头。
我在项目中遇到过一位同事,调试了一整天,发现数据写进去读出来全是0xFF。最后查出来,他忘了先擦除扇区。嗯,从那以后,我每次写Flash前都会加个擦除检查。
3.1.1 读操作
读Flash其实最简单,直接按地址访问就行。大部分MCU都支持字节、半字、字三种读取方式。
// 字节读取示例
uint8_t flash_read_byte(uint32_t addr)
{
return *(volatile uint8_t*)addr;
}
// 字读取示例(32位)
uint32_t flash_read_word(uint32_t addr)
{
return *(volatile uint32_t*)addr;
}
3.1.2 写操作
写Flash就麻烦多了。你得先解锁Flash控制器,然后按页或按字写入。不同MCU的写入粒度不一样,有的支持单字节写入,有的必须按16位或32位写入。
// STM32 Flash写入示例(按字写入)
HAL_StatusTypeDef flash_write_word(uint32_t addr, uint32_t data)
{
HAL_StatusTypeDef status;
// 1. 解锁Flash
HAL_FLASH_Unlock();
// 2. 清除标志位
__HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPERR);
// 3. 写入数据
status = HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr, data);
// 4. 锁定Flash
HAL_FLASH_Lock();
return status;
}
3.1.3 擦除操作
擦除是Flash操作里最耗时的。擦除的最小单位是扇区(Sector),大小从4KB到128KB不等。有些MCU还支持整片擦除(Mass Erase),但Bootloader里我们一般不用——太危险了。
// 扇区擦除示例
HAL_StatusTypeDef flash_erase_sector(uint32_t sector)
{
HAL_StatusTypeDef status;
uint32_t page_error = 0;
// 1. 解锁Flash
HAL_FLASH_Unlock();
// 2. 执行擦除
status = HAL_FLASHEx_Erase(&erase_init_struct, &page_error);
// 3. 锁定Flash
HAL_FLASH_Lock();
return status;
}
3.2 Flash分区表设计
分区表是Bootloader的灵魂。我见过很多项目,分区表设计得乱七八糟,最后升级时各种踩坑。这里我分享一个经过多个项目验证的分区方案。
3.2.1 典型分区结构
| 分区名称 | 起始地址 | 大小 | 说明 |
|---|---|---|---|
| Bootloader | 0x08000000 | 64KB | 存放Bootloader固件 |
| 参数区 | 0x08010000 | 16KB | 存放升级标志、版本号等 |
| App A(主分区) | 0x08014000 | 256KB | 当前运行的应用程序 |
| App B(备份分区) | 0x08054000 | 256KB | 升级时存放新固件 |
| 日志区 | 0x08094000 | 32KB | 记录升级日志和错误信息 |
这个分区方案的好处很明显:Bootloader和App完全隔离,升级时先写App B,校验通过后再切换。万一升级失败,还能回滚到App A。
3.2.2 分区表定义
我建议把分区表定义成一个结构体,放在头文件里统一管理。这样改分区时,只需要改一个地方。
// 分区表定义
typedef struct {
uint32_t start_addr;
uint32_t size;
uint8_t sector_num;
char name[16];
} flash_partition_t;
// 分区表实例
const flash_partition_t g_partition_table[] = {
{0x08000000, 0x10000, 0, "Bootloader"},
{0x08010000, 0x04000, 1, "Parameter"},
{0x08014000, 0x40000, 2, "App_A"},
{0x08054000, 0x40000, 6, "App_B"},
{0x08094000, 0x08000, 10, "Log"},
};
3.3 驱动层封装与接口定义
驱动层封装,说白了就是给上层提供一个干净、统一的接口。我习惯把所有Flash操作抽象成下面这几个函数:
3.3.1 接口定义
// flash_drv.h
#ifndef __FLASH_DRV_H__
#define __FLASH_DRV_H__
#include <stdint.h>
// 初始化Flash驱动
int32_t flash_drv_init(void);
// 读取数据
int32_t flash_drv_read(uint32_t addr, uint8_t *buf, uint32_t len);
// 写入数据(内部包含擦除操作)
int32_t flash_drv_write(uint32_t addr, const uint8_t *buf, uint32_t len);
// 擦除指定扇区
int32_t flash_drv_erase(uint32_t sector_start, uint32_t sector_count);
// 获取分区信息
int32_t flash_drv_get_partition_info(uint8_t part_id, flash_partition_t *info);
// 校验数据完整性(CRC32)
int32_t flash_drv_verify(uint32_t addr, const uint8_t *buf, uint32_t len, uint32_t crc);
#endif
3.3.2 实现要点
写驱动时,有几个坑你得注意:
- 地址检查:每次操作前都要检查地址是否在合法范围内。我曾经见过一个Bug,写入地址越界,结果把Bootloader区给覆盖了——设备直接变砖。
- 写保护:有些MCU有写保护寄存器,操作前要确保目标区域没有被保护。
- 超时处理:擦除操作可能耗时几百毫秒,要加超时机制,防止死等。
- 错误恢复:写入或擦除失败后,要能恢复到安全状态。
3.4 整体架构图
下面这张图展示了Flash驱动层的整体架构。你可以看到,上层应用(Bootloader主逻辑、升级管理器)只跟抽象接口层打交道,不直接操作硬件。
这张图把Flash驱动分成了四层。最上层是业务逻辑,最下层是硬件寄存器。中间的两层——抽象接口层和硬件抽象层——就是我们今天要重点实现的。
3.5 总结
这一章我们聊了Flash驱动的三个核心:读写擦操作、分区表设计、接口封装。记住几个关键点:
- 写之前必须擦除,这是Flash的物理特性决定的
- 分区表要按扇区对齐,预留足够的隔离空间
- 接口封装要抽象,方便移植和测试
- 错误处理不能偷懒,每个操作都要检查返回值