3. 性能分析基础
做IAP升级,说白了就是跟Flash打交道。你想想看,Bootloader要把新固件写进芯片里,总得有个靠谱的“搬运工”吧?这个搬运工就是Flash驱动。我见过不少项目,应用层代码写得花里胡哨,结果Flash驱动一跑就崩——要么擦除不干净,要么写入到一半被看门狗打断,整片数据全废了。
嗯,这一章我们就来把Flash驱动这块硬骨头啃下来。我会把我在几个量产项目里踩过的坑、总结的经验都抖出来。
3.1 Flash操作的基本认知
先聊点基础的。Flash存储器有几个特性,你必须刻在脑子里:
- 擦除粒度大:通常按扇区(Sector)擦除,一个扇区可能是1KB、2KB、4KB甚至更大。你不能只擦一个字节。
- 写入粒度小:可以按字节、半字(16位)或字(32位)写入,但写入前对应的区域必须是擦除状态(全0xFF)。
- 写入次数有限:Flash的擦写寿命一般在1万到10万次之间。频繁擦写同一个扇区,它会“累死”的。
- 读操作不影响寿命:随便读,读到天荒地老都没事。
核心原则:Flash操作期间,任何意外中断(掉电、复位、看门狗复位)都可能导致数据损坏。所以,驱动必须设计得“皮实”。
3.2 Flash驱动API封装
我个人习惯把Flash驱动封装成三个基础API:擦除、写入、读取。别小看这三个函数,里面的门道多着呢。
3.2.1 Flash擦除API
擦除是Flash操作里最耗时的,一个扇区擦除可能要几十到几百毫秒。这段时间里,CPU基本在干等。
/**
* @brief 擦除指定扇区
* @param sector_addr : 扇区起始地址(必须对齐到扇区边界)
* @retval 0: 成功, -1: 失败
*/
int Flash_EraseSector(uint32_t sector_addr)
{
// 1. 检查地址是否在有效范围内
if (!IS_FLASH_ADDR_VALID(sector_addr)) {
return -1;
}
// 2. 检查地址是否扇区对齐
if (sector_addr % FLASH_SECTOR_SIZE != 0) {
return -1; // 地址不对齐,直接拒绝
}
// 3. 解锁Flash控制寄存器(不同MCU操作不同)
FLASH->KEYR = FLASH_KEY1;
FLASH->KEYR = FLASH_KEY2;
// 4. 等待上一次操作完成
while (FLASH->SR & FLASH_SR_BSY);
// 5. 设置擦除模式并启动
FLASH->CR |= FLASH_CR_SER; // 扇区擦除模式
FLASH->CR |= (sector_num << 3); // 选择扇区号
FLASH->CR |= FLASH_CR_START; // 启动擦除
// 6. 等待擦除完成
while (FLASH->SR & FLASH_SR_BSY);
// 7. 检查错误标志
if (FLASH->SR & FLASH_SR_PGERR) {
FLASH->SR = FLASH_SR_PGERR; // 清除错误
return -1;
}
// 8. 锁定Flash
FLASH->CR &= ~FLASH_CR_SER;
FLASH->CR |= FLASH_CR_LOCK;
return 0;
}
我的经验:擦除完成后,最好加一个读回校验。我曾经遇到一批芯片,擦除后读回来不是全0xFF,而是有几个bit卡在0。从那以后,我写的擦除函数末尾都会加一段校验代码。
3.2.2 Flash写入API
写入相对快一些,但也要注意时序。很多MCU要求写入前必须关全局中断,防止写入过程中被中断服务函数打断。
/**
* @brief 写入数据到Flash
* @param addr : 目标地址(必须按字对齐)
* @param data : 要写入的数据指针
* @param len : 数据长度(字节)
* @retval 0: 成功, -1: 失败
*/
int Flash_Write(uint32_t addr, uint8_t *data, uint32_t len)
{
uint32_t i;
// 1. 参数检查
if (!IS_FLASH_ADDR_VALID(addr) || data == NULL || len == 0) {
return -1;
}
// 2. 地址对齐检查(假设按字写入)
if (addr % 4 != 0) {
return -1;
}
// 3. 解锁Flash
FLASH->KEYR = FLASH_KEY1;
FLASH->KEYR = FLASH_KEY2;
// 4. 关中断(防止写入被打断)
__disable_irq();
// 5. 按字写入
for (i = 0; i < len; i += 4) {
// 等待上次操作完成
while (FLASH->SR & FLASH_SR_BSY);
// 写入一个字
*(volatile uint32_t *)addr = *(uint32_t *)&data[i];
// 等待写入完成
while (FLASH->SR & FLASH_SR_BSY);
// 检查错误
if (FLASH->SR & FLASH_SR_PGERR) {
FLASH->SR = FLASH_SR_PGERR;
__enable_irq();
return -1;
}
addr += 4;
}
// 6. 开中断
__enable_irq();
// 7. 锁定Flash
FLASH->CR |= FLASH_CR_LOCK;
return 0;
}
注意:关中断的时间不能太长!如果关中断超过看门狗喂狗间隔,系统会被复位。我建议在写入循环里分段喂狗,或者每写完一个扇区就开一次中断让系统喘口气。
3.2.3 Flash读取API
读取最简单,直接指针访问就行。但为了统一接口,我还是封装了一下:
/**
* @brief 从Flash读取数据
* @param addr : 源地址
* @param buf : 目标缓冲区
* @param len : 读取长度
* @retval 0: 成功, -1: 失败
*/
int Flash_Read(uint32_t addr, uint8_t *buf, uint32_t len)
{
uint32_t i;
if (!IS_FLASH_ADDR_VALID(addr) || buf == NULL || len == 0) {
return -1;
}
// 直接内存拷贝
for (i = 0; i < len; i++) {
buf[i] = *(volatile uint8_t *)addr;
addr++;
}
return 0;
}
你看,读取函数不需要解锁、不需要关中断,直接读就行。这也是Flash最友好的操作了。
3.3 扇区保护策略
做IAP最怕什么?最怕把Bootloader自己住的扇区给擦掉了!一旦Bootloader被擦,芯片就变砖了。所以,扇区保护是重中之重。
3.3.1 扇区划分方案
我一般把Flash分成三个区域:
| 区域 | 起始地址 | 大小 | 内容 | 保护策略 |
|---|---|---|---|---|
| Bootloader区 | 0x08000000 | 32KB | Bootloader固件 | 写保护,永不擦除 |
| 参数存储区 | 0x08008000 | 8KB | 升级标志、配置参数 | 按需擦写,但保留关键备份 |
| 应用区 | 0x0800A000 | 剩余空间 | 用户应用程序 | 升级时擦除,正常运行时只读 |
3.3.2 硬件写保护
很多MCU支持硬件写保护,通过配置选项字节(Option Bytes)来锁定某些扇区。一旦锁定,任何软件操作都无法擦写这些扇区,除非通过JTAG/SWD重新配置。
// 示例:配置STM32的Flash写保护(伪代码)
void Flash_EnableWriteProtect(void)
{
// 1. 解锁选项字节
FLASH->OPTKEYR = FLASH_OPTKEY1;
FLASH->OPTKEYR = FLASH_OPTKEY2;
// 2. 设置写保护位(保护Bootloader所在扇区)
FLASH->OB->WRP = 0x0001; // 保护扇区0
// 3. 启动选项字节编程
FLASH->CR |= FLASH_CR_OPTPG;
while (FLASH->SR & FLASH_SR_BSY);
// 4. 锁定
FLASH->CR &= ~FLASH_CR_OPTPG;
}
建议:量产时,一定要把Bootloader扇区的写保护打开。这样即使应用层代码跑飞了,也擦不掉Bootloader。我曾经见过一个产品,因为没开写保护,OTA升级时网络异常导致Bootloader被部分覆盖,整批产品返厂——血的教训。
3.3.3 软件边界检查
除了硬件保护,软件层面也要做边界检查。每次擦除或写入前,都要确认目标地址不在保护区域内。
// 定义保护区域边界
#define BOOTLOADER_START 0x08000000
#define BOOTLOADER_END 0x08007FFF
#define PARAM_START 0x08008000
#define PARAM_END 0x08009FFF
int Flash_IsAddrProtected(uint32_t addr)
{
// Bootloader区绝对保护
if (addr >= BOOTLOADER_START && addr <= BOOTLOADER_END) {
return 1; // 受保护
}
// 参数区在非升级模式下也保护
if (addr >= PARAM_START && addr <= PARAM_END) {
if (!g_is_upgrade_mode) {
return 1; // 非升级模式,参数区也保护
}
}
return 0; // 允许操作
}
3.4 升级中断保护机制
升级过程中最怕什么?掉电、看门狗复位、意外中断。我设计了一套“三段式”保护机制:
- 擦除前备份关键参数:把升级标志、版本号等关键信息先备份到RAM或保留扇区。
- 写入时断点续传:每写完一个扇区,记录当前进度。下次启动时检查进度,从中断处继续。
- 完成后校验:全部写完后,对整个应用区做CRC校验,确保数据完整。
我的做法:在参数区里留两个字节作为“升级状态字”。擦除前写0xAA55,擦除完成写0x55AA,写入完成写0x0000。启动时检查这个状态字,就能知道升级进行到哪一步了。
3.5 本章知识结构图
下面这张图把Flash驱动的核心逻辑串起来了:
3.6 避坑指南
最后,把我这些年踩过的坑列出来,你写驱动的时候绕着走:
- 别在擦除/写入过程中喂狗:有些MCU的Flash操作会锁住总线,喂狗函数可能根本执行不到。我建议在操作前一次性喂狗,然后估算最大耗时,确保不会超时。
- 地址对齐不是小事:很多MCU要求写入地址必须按字(4字节)对齐。不对齐的话,轻则写入失败,重则触发硬件错误。
- 别忘了清除错误标志:Flash操作出错后,错误标志会一直挂着。不清除的话,下一次操作会直接失败。
- 读回校验不能省:擦除和写入完成后,一定要读回来对比一下。Flash也有“软错误”,虽然概率低,但碰到了就是100%。
- 参数区要留冗余:我习惯在参数区存两份相同的配置数据。如果一份校验失败,就用另一份恢复。这样即使升级中途掉电,也不会丢配置。
特别提醒:千万不要在Flash操作函数里调用printf打印调试信息!printf本身可能触发中断、占用大量时间,甚至导致Flash操作超时。我见过有人这么干,结果调试时一切正常,量产时批量死机——因为调试器连着的时printf跑得快,量产时串口没接,printf阻塞了。
好了,Flash驱动这块就聊到这儿。你把这些API封装好、保护策略做到位,后面的IAP流程才能跑得稳。记住一句话:驱动写得糙,升级两行泪。