1. 为什么需要内部Flash模拟EEPROM
在嵌入式开发中,我们经常需要存储一些配置参数或运行数据。传统做法是外接EEPROM芯片,但STM32H7系列微控制器内置了大容量Flash,完全可以利用它来模拟EEPROM功能。这样做有几个明显优势:
首先,省去了外接EEPROM芯片的成本和PCB空间。一颗常见的24C02 EEPROM虽然只要几块钱,但对于大批量生产的产品来说,这笔开销也不小。其次,简化了硬件设计,不需要再考虑I2C或SPI总线的走线问题。最重要的是,STM32H7的Flash容量足够大,比如H743系列有2MB Flash,划出128KB来做数据存储完全不是问题。
不过这种方案也有局限性。Flash的擦写次数通常在10万次左右,而专用EEPROM可以达到100万次。如果你的应用需要频繁写入数据,可能需要考虑磨损均衡算法。另外,Flash的擦除操作是按扇区进行的,STM32H7的扇区大小是128KB,这意味着即使你只想修改1个字节,也需要擦除整个扇区。
2. STM32H7 Flash特性与关键限制
STM32H7的Flash架构有几个关键特性需要特别注意。首先是双Bank设计,Bank1和Bank2可以独立操作,这带来了很大灵活性。比如你可以在一个Bank执行程序的同时,擦写另一个Bank的Flash。
但有几个硬性限制必须遵守:
- 32字节对齐要求:编程操作时,地址必须是32字节对齐的(地址对32求余为0),写入的数据长度也必须是32字节的整数倍。如果数据不足32字节,需要补0。
- 擦除粒度:最小擦除单位是扇区,H7的扇区大小是128KB。擦除后所有bit变为1,编程只能将1改为0。
- 执行中断:当擦写与应用程序在同一Bank时,该Bank的所有操作(包括中断)都会暂停,直到擦写完成。
这里有个实际踩过的坑:我曾经遇到过在擦写Flash时,由于没有关闭中断,导致定时器中断丢失,系统时间不准的问题。解决方法是在擦写操作前关闭中断,完成后立即恢复。
3. 驱动设计与实现细节
3.1 Flash擦除实现
擦除一个扇区的标准流程如下:
uint8_t bsp_EraseCpuFlash(uint32_t addr) { FLASH_EraseInitTypeDef erase; uint32_t sectorError; uint8_t ret; // 获取扇区号 uint32_t sector = bsp_GetSector(addr); HAL_FLASH_Unlock(); // 必须先解锁 erase.TypeErase = FLASH_TYPEERASE_SECTORS; erase.Banks = (addr >= 0x08100000) ? FLASH_BANK_2 : FLASH_BANK_1; erase.Sector = sector; erase.NbSectors = 1; erase.VoltageRange = FLASH_VOLTAGE_RANGE_3; ret = HAL_FLASHEx_Erase(&erase, §orError); HAL_FLASH_Lock(); // 操作完成后重新上锁 return ret; }关键点说明:
- 擦除前必须调用
HAL_FLASH_Unlock()解锁 - 通过
bsp_GetSector函数确定地址所在的扇区 - 擦除完成后要立即上锁,防止误操作
3.2 Flash编程实现
编程操作的实现要复杂一些,因为要处理对齐和长度问题:
uint8_t bsp_WriteCpuFlash(uint32_t addr, uint8_t *data, uint32_t len) { // 检查地址和长度是否有效 if(addr + len > FLASH_BASE + FLASH_SIZE) return 1; // 检查数据是否已存在 if(bsp_CmpCpuFlash(addr, data, len) == FLASH_IS_EQU) return 0; __disable_irq(); // 关闭中断 HAL_FLASH_Unlock(); // 处理32字节整数倍数据 for(int i=0; i<len/32; i++) { uint64_t chunk[4]; // 32字节缓冲区 memcpy(chunk, data, 32); if(HAL_FLASH_Program(FLASH_TYPEPROGRAM_FLASHWORD, addr, (uint64_t)chunk) != HAL_OK) goto error; addr += 32; data += 32; } // 处理剩余不足32字节的数据 if(len % 32) { uint64_t chunk[4] = {0}; memcpy(chunk, data, len % 32); HAL_FLASH_Program(FLASH_TYPEPROGRAM_FLASHWORD, addr, (uint64_t)chunk); } HAL_FLASH_Lock(); __enable_irq(); // 恢复中断 return 0; error: HAL_FLASH_Lock(); __enable_irq(); return 2; }这个实现有几个技术细节:
- 使用
FLASH_TYPEPROGRAM_FLASHWORD编程模式,一次写入32字节 - 不足32字节的数据补零处理
- 编程期间关闭中断,避免Bank冲突
- 编程前检查数据是否已存在,避免不必要的写入
3.3 数据读取实现
读取操作相对简单,因为Flash可以像普通内存一样访问:
uint8_t bsp_ReadCpuFlash(uint32_t addr, uint8_t *buf, uint32_t len) { if(addr + len > FLASH_BASE + FLASH_SIZE) return 1; for(uint32_t i=0; i<len; i++) { buf[i] = *(uint8_t*)(addr + i); } return 0; }4. 编译器配置关键点
这是很多开发者容易忽略的重要环节。你必须明确告诉编译器不要使用你准备用于模拟EEPROM的Flash区域,否则链接器可能会把代码或常量分配到这个区域,导致数据被意外覆盖。
对于Keil MDK,在代码中这样声明:
const uint8_t eeprom_region[128*1024] __attribute__((at(0x08100000)));对于IAR EWARM,使用以下语法:
#pragma location=0x08100000 const uint8_t eeprom_region[128*1024];选择地址时有几个建议:
- 不要使用第一个扇区(通常存放中断向量表)
- 如果应用程序不大,不要使用最后一个扇区,否则会导致整个Flash被占用,下载时间变长
- 最好选择与应用程序不同Bank的扇区,减少擦写时对程序运行的影响
5. 工程实践与优化建议
在实际项目中,我有几个经过验证的优化建议:
磨损均衡:由于Flash擦写次数有限,可以实现简单的磨损均衡算法。比如准备两个扇区交替使用,当一个扇区达到擦写上限后切换到另一个。
数据校验:建议为存储的数据添加CRC校验或校验和,防止数据损坏。可以这样实现:
typedef struct { uint32_t crc; uint32_t data_len; uint8_t data[120]; // 实际数据 } FlashData;批量写入:尽量减少擦写次数,可以积累一定量的数据后一次性写入。比如每10分钟或数据变化达到一定阈值时才执行写入操作。
掉电保护:突然断电可能导致写入失败。可以设计双缓冲机制,先写入新数据到另一个区域,验证无误后再更新指针。
一个实用的工程模板通常包含以下文件:
flash_eeprom.h:接口定义flash_eeprom.c:核心实现flash_eeprom_cfg.h:配置项(地址、大小等)
移植时只需要修改配置头文件,然后调用初始化函数即可。使用时注意:
- 先擦除后写入
- 检查返回值
- 避免频繁写入
- 考虑多任务环境下的互斥访问
通过合理设计和优化,内部Flash模拟EEPROM的方案完全可以满足大多数应用场景的需求,既节省成本又提高可靠性。