2. Flash分区规划与读写驱动
任务说明
本案例面向嵌入式OTA场景,目标是在Flash上建立可管理的分区表,并实现底层读写驱动,支持按分区进行擦除、写入和读取。案例基于STM32F4系列内部Flash(主存储区,1MB)设计,驱动层直接操作Flash寄存器,不依赖HAL库,便于移植到其他Cortex-M平台。
分区规划
将1MB Flash划分为5个区域,分区表结构如下:
| 分区名 | 起始地址 | 大小 | 用途 |
|---|---|---|---|
| BOOT | 0x08000000 | 64KB | Bootloader |
| APP_A | 0x08010000 | 384KB | 应用A(当前运行) |
| APP_B | 0x08070000 | 384KB | 应用B(OTA升级目标) |
| PARAM | 0x080D0000 | 64KB | 参数存储 |
| OTA_META | 0x080E0000 | 128KB | OTA元数据/升级标志 |
分区表以数组形式固化在驱动代码中,提供按名称或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,实际仅需少量字节存储升级标志,剩余空间可扩展存放固件校验值等元数据。