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);
    // ...
}
警告:全片擦除会擦除包括Bootloader在内的所有内容。除非你有外部编程器,否则设备将无法启动。我曾经见过一个同事在生产测试时误触发了全片擦除,结果整批20块板子全部变砖,只能返工重烧。

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();
}
注意:修改写保护配置后,通常需要复位才能生效。所以这个操作要谨慎使用。我建议在Bootloader初始化时,一次性配置好保护策略,不要在升级过程中动态修改。

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通信协议设计”,看看怎么把升级数据可靠地传输到目标设备上。