4. Flash驱动设计:Flash擦除、写入、读取API,扇区保护与解锁
好,咱们进入第四章。这一章可以说是Bootloader的“地基”——Flash驱动。
你想想看,Bootloader的核心任务是什么?说白了,就是把新的固件程序写到芯片的Flash里。如果Flash驱动写不好,那后面所有的升级流程都是空中楼阁。我在做第一个Bootloader项目时,就因为在Flash驱动上偷了个懒,结果现场升级时把整个系统搞“砖”了……嗯,从那以后,我对这块就特别较真。
4.1 为什么Flash驱动这么重要?
Flash驱动不仅仅是“写数据”那么简单。它涉及到几个关键点:
- 可靠性:写错了,系统就挂了。尤其是Bootloader所在的区域,一旦被破坏,芯片就变砖。
- 时序要求:Flash的擦写有严格的时序,操作不当会导致数据错误。
- 扇区管理:Flash是按扇区(Sector)来管理的,不能像RAM那样随意读写。
- 保护机制:很多芯片有硬件保护,防止误操作。你得知道怎么解锁。
我个人习惯,在设计Flash驱动时,会先画一张“Flash内存映射图”。把Bootloader区、应用程序区、参数存储区、备份区都标清楚。这样写代码时心里有底。
4.2 Flash擦除操作
擦除是Flash操作中最“重”的一个。为什么?因为擦除是以扇区为单位的,而且耗时较长。一个扇区可能是4KB、8KB甚至更大。
我记得有一次,我在调试时不小心对整个芯片执行了全片擦除……结果你懂的,Bootloader都没了。从那以后,我写擦除函数时,一定会加一个“地址范围检查”。
4.2.1 扇区擦除函数
下面是一个典型的扇区擦除函数。以STM32为例,它的Flash是按扇区管理的。
/**
* @brief 擦除指定的Flash扇区
* @param sector: 扇区编号 (0, 1, 2, ...)
* @retval 0: 成功, -1: 失败
*/
int Flash_EraseSector(uint32_t sector)
{
// 1. 检查扇区号是否合法
if (sector >= FLASH_SECTOR_COUNT) {
return -1; // 非法扇区
}
// 2. 解锁Flash(如果之前锁了)
Flash_Unlock();
// 3. 等待上一次操作完成
while (FLASH->SR & FLASH_SR_BSY);
// 4. 检查是否有编程错误
if (FLASH->SR & FLASH_SR_PGERR) {
FLASH->SR = FLASH_SR_PGERR; // 清除错误标志
return -1;
}
// 5. 配置擦除参数并启动
FLASH->CR |= FLASH_CR_SER; // 选择扇区擦除模式
FLASH->CR |= (sector << 3); // 选择要擦除的扇区
FLASH->CR |= FLASH_CR_STRT; // 启动擦除
// 6. 等待擦除完成
while (FLASH->SR & FLASH_SR_BSY);
// 7. 检查擦除是否成功
if (FLASH->SR & FLASH_SR_EOP) {
FLASH->SR = FLASH_SR_EOP; // 清除完成标志
Flash_Lock(); // 重新锁定
return 0; // 成功
}
Flash_Lock();
return -1; // 失败
}
4.2.2 全片擦除(慎用!)
全片擦除一般只在工厂初始化时使用。在Bootloader中,我建议你永远不要调用全片擦除。除非你想让设备变砖。
// 这个函数我一般只放在测试代码里,不会放在正式产品中
int Flash_MassErase(void)
{
// 危险操作!请确保你清楚自己在做什么
Flash_Unlock();
while (FLASH->SR & FLASH_SR_BSY);
FLASH->CR |= FLASH_CR_MER; // 全片擦除模式
FLASH->CR |= FLASH_CR_STRT;
while (FLASH->SR & FLASH_SR_BSY);
// ...
}
4.3 Flash写入操作
写入操作相对简单,但要注意:Flash写入只能将1变成0,不能将0变成1。所以写入前,目标地址必须是已擦除状态(全0xFF)。
说白了,就是“先擦后写”。
4.3.1 字节/半字/字写入
不同芯片支持的写入粒度不同。有的支持字节写入,有的只支持半字(16位)或字(32位)写入。我建议统一使用“字写入”,效率更高。
/**
* @brief 写入一个字(32位)到Flash
* @param addr: 目标地址(必须是4字节对齐)
* @param data: 要写入的数据
* @retval 0: 成功, -1: 失败
*/
int Flash_WriteWord(uint32_t addr, uint32_t data)
{
// 1. 检查地址对齐
if (addr & 0x03) {
return -1; // 地址未4字节对齐
}
// 2. 检查地址是否在Flash范围内
if (!Flash_IsValidAddress(addr)) {
return -1;
}
// 3. 解锁
Flash_Unlock();
// 4. 等待空闲
while (FLASH->SR & FLASH_SR_BSY);
// 5. 清除错误标志
FLASH->SR = FLASH_SR_PGERR | FLASH_SR_WRPRTERR;
// 6. 配置编程模式并写入
FLASH->CR |= FLASH_CR_PG; // 选择编程模式
*(__IO uint32_t*)addr = data; // 写入数据
// 7. 等待写入完成
while (FLASH->SR & FLASH_SR_BSY);
// 8. 检查结果
if (FLASH->SR & FLASH_SR_EOP) {
FLASH->SR = FLASH_SR_EOP;
Flash_Lock();
return 0;
}
Flash_Lock();
return -1;
}
4.3.2 批量写入函数
在实际升级中,我们通常是一次性写入一包数据(比如256字节或512字节)。所以我会封装一个批量写入函数。
/**
* @brief 批量写入数据到Flash
* @param addr: 起始地址
* @param data: 数据缓冲区
* @param len: 数据长度(字节)
* @retval 0: 成功, -1: 失败
*/
int Flash_WriteBuffer(uint32_t addr, uint8_t *data, uint32_t len)
{
uint32_t i;
uint32_t wordData;
// 确保地址和长度都是4字节对齐
if ((addr & 0x03) || (len & 0x03)) {
return -1;
}
for (i = 0; i < len; i += 4) {
// 将4个字节拼成一个字
wordData = (uint32_t)data[i] |
((uint32_t)data[i+1] << 8) |
((uint32_t)data[i+2] << 16) |
((uint32_t)data[i+3] << 24);
if (Flash_WriteWord(addr + i, wordData) != 0) {
return -1; // 写入失败
}
}
return 0;
}
4.4 Flash读取操作
读取操作就简单多了。Flash的读取和RAM读取一样,直接通过指针访问即可。但要注意:有些芯片在读取Flash时,如果地址未对齐,会触发硬件错误。
/**
* @brief 从Flash读取一个字(32位)
* @param addr: 目标地址
* @retval 读取到的数据
*/
uint32_t Flash_ReadWord(uint32_t addr)
{
// 直接指针读取
return *(__IO uint32_t*)addr;
}
/**
* @brief 从Flash读取一段数据到缓冲区
* @param addr: 起始地址
* @param buffer: 目标缓冲区
* @param len: 读取长度(字节)
*/
void Flash_ReadBuffer(uint32_t addr, uint8_t *buffer, uint32_t len)
{
uint32_t i;
for (i = 0; i < len; i++) {
buffer[i] = *(__IO uint8_t*)(addr + i);
}
}
嗯,这里要注意:读取操作不需要解锁Flash。所以你可以随时读取,不用担心影响其他操作。
4.5 扇区保护与解锁
这是很多新手容易忽略的地方。为了防止误擦写,芯片通常提供了硬件保护机制。比如STM32的Flash有写保护(WRP)和读保护(RDP)。
4.5.1 写保护(Write Protection)
写保护可以防止某个扇区被意外擦除或写入。在Bootloader中,我们通常会把Bootloader所在的扇区设置为写保护。
/**
* @brief 设置扇区写保护
* @param sector: 扇区编号
* @param protect: 1-保护, 0-解除保护
*/
void Flash_ProtectSector(uint32_t sector, uint8_t protect)
{
// 以STM32F4为例,写保护是通过选项字节配置的
Flash_Unlock();
// 等待上一次操作完成
while (FLASH->SR & FLASH_SR_BSY);
// 解锁选项字节
FLASH->OPTKEYR = 0x08192A3B;
FLASH->OPTKEYR = 0x4C5D6E7F;
if (protect) {
// 设置保护
FLASH->CR |= FLASH_CR_OPTPG;
// 写入选项字节...
} else {
// 解除保护
FLASH->CR |= FLASH_CR_OPTER;
FLASH->CR |= FLASH_CR_STRT;
while (FLASH->SR & FLASH_SR_BSY);
}
// 触发系统复位使选项字节生效
NVIC_SystemReset();
}
4.5.2 读保护(Read Protection)
读保护是为了防止别人通过调试接口读取你的固件。在商业产品中,这个功能很重要。
| 保护级别 | 描述 | 应用场景 |
|---|---|---|
| Level 0 | 无保护 | 开发调试阶段 |
| Level 1 | 禁止通过调试接口读取Flash | 量产产品(常用) |
| Level 2 | 永久保护,不可降级 | 高安全性产品 |
我个人习惯,在量产时至少设置Level 1保护。这样即使别人拿到了你的设备,也无法通过JTAG/SWD读取固件。
4.6 驱动设计的避坑指南
最后,分享几个我踩过的坑:
- 不要在主循环中长时间擦写Flash:擦除一个扇区可能需要几十毫秒,这段时间内系统无法响应其他中断。我建议在擦写前关闭所有不必要的中断,或者使用DMA方式。
- 注意电源稳定性:Flash擦写对电压有要求。如果电压过低,写入可能会失败。我在一个项目中遇到过,电池快没电时升级,结果写到一半电压掉下去了,Flash数据全乱套了。
- 写完后一定要回读验证:不要相信“写入成功”的返回值。我见过芯片在极端环境下返回成功但实际上没写进去的情况。
- 预留备份扇区:在升级时,先把新固件写入备份区,确认无误后再覆盖主程序区。这样即使升级过程中断电,也不会变砖。
核心总结:Flash驱动是Bootloader的基石。擦除要谨慎,写入要验证,保护要到位。记住一句话:“先擦后写,写后必验,保护先行”。
好了,这一章就到这里。下一章我们会讲“CAN通信协议设计”,看看怎么把升级数据可靠地传输到目标设备上。