2. Flash分区规划与读写驱动

任务说明

本案例面向嵌入式OTA场景,目标是在Flash上建立可管理的分区表,并实现底层读写驱动,支持按分区进行擦除、写入和读取。案例基于STM32F4系列内部Flash(主存储区,1MB)设计,驱动层直接操作Flash寄存器,不依赖HAL库,便于移植到其他Cortex-M平台。

分区规划

将1MB Flash划分为5个区域,分区表结构如下:

分区名起始地址大小用途
BOOT0x0800000064KBBootloader
APP_A0x08010000384KB应用A(当前运行)
APP_B0x08070000384KB应用B(OTA升级目标)
PARAM0x080D000064KB参数存储
OTA_META0x080E0000128KBOTA元数据/升级标志

分区表以数组形式固化在驱动代码中,提供按名称或ID查询分区信息的接口。Flash最小擦除单位为扇区(STM32F4主存储区扇区大小不等,本驱动按实际扇区边界对齐处理)。

驱动实现

驱动核心包含三个操作:扇区擦除、半字(16位)写入、读取。STM32F4 Flash编程要求:写入前必须擦除,擦除以扇区为单位;写入最小单位为半字(16位);操作前需解锁Flash,操作完成后重新上锁。以下代码实现完整的分区查询与读写驱动:

// flash_partition.h
#ifndef FLASH_PARTITION_H
#define FLASH_PARTITION_H

#include <stdint.h>

#define FLASH_BASE_ADDR     0x08000000UL
#define FLASH_SIZE          0x100000UL   // 1MB

typedef enum {
    PART_BOOT = 0,
    PART_APP_A,
    PART_APP_B,
    PART_PARAM,
    PART_OTA_META,
    PART_MAX
} partition_id_t;

typedef struct {
    partition_id_t id;
    const char* name;
    uint32_t start_addr;
    uint32_t size;
} partition_t;

// 查询分区信息,成功返回0,失败返回-1
int flash_part_get(partition_id_t id, partition_t* out);
// 擦除整个分区(自动按扇区对齐)
int flash_part_erase(partition_id_t id);
// 向分区内偏移offset处写入len字节数据(len须为偶数)
int flash_part_write(partition_id_t id, uint32_t offset, const uint8_t* data, uint32_t len);
// 从分区内偏移offset处读取len字节数据
int flash_part_read(partition_id_t id, uint32_t offset, uint8_t* buf, uint32_t len);

#endif // FLASH_PARTITION_H
// flash_partition.c
#include "flash_partition.h"
#include <string.h>

// STM32F4 Flash寄存器
#define FLASH_KEYR          (*(volatile uint32_t*)0x40023C04UL)
#define FLASH_SR            (*(volatile uint32_t*)0x40023C0CUL)
#define FLASH_CR            (*(volatile uint32_t*)0x40023C10UL)
#define FLASH_KEY1          0x45670123UL
#define FLASH_KEY2          0xCDEF89ABUL

// 扇区定义:起始地址、大小(STM32F407 1MB)
static const struct { uint32_t addr; uint32_t size; } sector_map[] = {
    {0x08000000, 16*1024}, {0x08004000, 16*1024}, {0x08008000, 16*1024},
    {0x0800C000, 16*1024}, {0x08010000, 64*1024}, {0x08020000, 128*1024},
    {0x08040000, 128*1024}, {0x08060000, 128*1024}, {0x08080000, 128*1024},
    {0x080A0000, 128*1024}, {0x080C0000, 128*1024}, {0x080E0000, 128*1024},
};
#define SECTOR_COUNT (sizeof(sector_map)/sizeof(sector_map[0]))

static const partition_t partition_table[PART_MAX] = {
    {PART_BOOT,    "BOOT",     0x08000000, 64*1024},
    {PART_APP_A,   "APP_A",    0x08010000, 384*1024},
    {PART_APP_B,   "APP_B",    0x08070000, 384*1024},
    {PART_PARAM,   "PARAM",    0x080D0000, 64*1024},
    {PART_OTA_META,"OTA_META", 0x080E0000, 128*1024},
};

static void flash_unlock(void) {
    FLASH_KEYR = FLASH_KEY1;
    FLASH_KEYR = FLASH_KEY2;
}
static void flash_lock(void) {
    FLASH_CR |= (1UL << 31);  // LOCK bit
}
static int flash_wait_busy(void) {
    while (FLASH_SR & (1UL << 16));  // BSY bit
    return (FLASH_SR & 0x1F) ? -1 : 0; // 检查错误标志
}

int flash_part_get(partition_id_t id, partition_t* out) {
    if (id >= PART_MAX || !out) return -1;
    *out = partition_table[id];
    return 0;
}

int flash_part_erase(partition_id_t id) {
    partition_t part;
    if (flash_part_get(id, &part) != 0) return -1;
    if (part.start_addr < FLASH_BASE_ADDR ||
        part.start_addr + part.size > FLASH_BASE_ADDR + FLASH_SIZE) return -1;

    flash_unlock();
    // 遍历所有扇区,擦除与分区重叠的部分
    for (int i = 0; i < SECTOR_COUNT; i++) {
        uint32_t sec_end = sector_map[i].addr + sector_map[i].size;
        uint32_t part_end = part.start_addr + part.size;
        if (sector_map[i].addr < part_end && sec_end > part.start_addr) {
            // 等待空闲
            while (FLASH_SR & (1UL << 16));
            FLASH_CR = (1UL << 1) | (i << 3);  // SER=1, SNB=扇区号
            FLASH_CR |= (1UL << 6);             // STRT=1
            if (flash_wait_busy() != 0) {
                flash_lock();
                return -1;
            }
        }
    }
    FLASH_CR = 0;
    flash_lock();
    return 0;
}

int flash_part_write(partition_id_t id, uint32_t offset, const uint8_t* data, uint32_t len) {
    partition_t part;
    if (flash_part_get(id, &part) != 0) return -1;
    if (offset + len > part.size || (len % 2) != 0) return -1;
    if ((part.start_addr + offset) % 2 != 0) return -1;

    uint32_t addr = part.start_addr + offset;
    flash_unlock();
    FLASH_CR |= (1UL << 0);  // PG=1

    for (uint32_t i = 0; i < len; i += 2) {
        uint16_t halfword = data[i] | (data[i+1] << 8);
        while (FLASH_SR & (1UL << 16));  // 等待BSY=0
        *(volatile uint16_t*)addr = halfword;
        if (flash_wait_busy() != 0) {
            FLASH_CR = 0;
            flash_lock();
            return -1;
        }
        addr += 2;
    }
    FLASH_CR = 0;
    flash_lock();
    return 0;
}

int flash_part_read(partition_id_t id, uint32_t offset, uint8_t* buf, uint32_t len) {
    partition_t part;
    if (flash_part_get(id, &part) != 0) return -1;
    if (offset + len > part.size) return -1;
    memcpy(buf, (const void*)(part.start_addr + offset), len);
    return 0;
}

验证方法

在STM32F407开发板上,通过主函数调用以下测试序列验证驱动正确性:

// main.c 测试片段
#include "flash_partition.h"
#include <stdio.h>

int main(void) {
    partition_t p;
    uint8_t wbuf[64], rbuf[64];
    // 填充测试数据
    for (int i = 0; i < 64; i++) wbuf[i] = (uint8_t)i;

    // 1. 查询分区
    if (flash_part_get(PART_PARAM, &p) != 0) return 1;
    printf("PARAM: addr=0x%08X size=%u\n", p.start_addr, p.size);

    // 2. 擦除PARAM分区
    if (flash_part_erase(PART_PARAM) != 0) return 2;

    // 3. 写入64字节
    if (flash_part_write(PART_PARAM, 0, wbuf, 64) != 0) return 3;

    // 4. 读取并比对
    memset(rbuf, 0, sizeof(rbuf));
    if (flash_part_read(PART_PARAM, 0, rbuf, 64) != 0) return 4;
    if (memcmp(wbuf, rbuf, 64) == 0) {
        printf("PASS: write/read match\n");
    } else {
        printf("FAIL: data mismatch\n");
        return 5;
    }
    return 0;
}

通过条件:编译烧录后,串口输出PARAM: addr=0x080D0000 size=65536和PASS: write/read match。若返回非0错误码,说明对应步骤失败。注意:重复运行测试前,PARAM分区已被擦除并写入,再次运行会先擦除再写入,结果一致。

关键说明

  • STM32F4 Flash写入前必须擦除,且擦除最小单位为扇区。本驱动擦除分区时自动计算覆盖的扇区范围,因此分区大小无需与扇区对齐,但起始地址建议按扇区边界规划(本案例已对齐)。
  • 写入长度必须为偶数(半字对齐),若需写入奇数长度数据,调用方需自行处理最后一个字节。
  • 驱动使用寄存器操作,不依赖HAL库。若使用其他Cortex-M芯片,需替换寄存器地址和扇区映射表。
  • OTA_META分区预留128KB,实际仅需少量字节存储升级标志,剩余空间可扩展存放固件校验值等元数据。