1. NG32单片机内部Flash编程实战指南
在嵌入式开发中,内部Flash的读写操作是每个工程师必须掌握的基本功。不同于外部存储芯片,内部Flash直接集成在MCU内部,具有访问速度快、无需额外电路等优势。今天我就以国民技术的NG32系列单片机为例,手把手带你掌握内部Flash的编程技巧。
NG32系列作为国产MCU的优秀代表,其Flash操作逻辑与STM32等主流ARM芯片高度兼容。但在实际项目中,很多开发者对Flash操作仍存在不少误区:有的直接照搬网络代码导致数据丢失,有的不了解擦除机制导致写入失败,更常见的是对地址对齐和写入保护机制认识不足。本文将结合NG32F103的实战经验,详细解析Flash操作的每个技术细节。
2. Flash存储结构解析
2.1 NG32 Flash物理布局
以NG32F103系列为例,其内部Flash主要分为两个区域:
- 主存储区(Main Memory):64KB容量,分为128页,每页512字节
- 信息区(Information Block):包含出厂预置的校准数据等,通常只读
重要提示:用户程序和数据都应存放在主存储区,信息区存储着芯片出厂校准参数,误操作可能导致芯片功能异常。
通过查阅NG32的用户手册(第3章存储器结构),我们可以获取具体的地址映射:
- 主存储区起始地址:0x08000000
- 信息区起始地址:0x1FFFF000
- 每页大小:512字节(0x200)
2.2 Flash特性要点
Flash存储有几个关键特性需要特别注意:
- 擦除特性:必须以页为单位擦除,擦除后所有位变为1
- 写入特性:只能将1改为0,不能将0改为1(需要先擦除)
- 寿命限制:典型擦写次数约1万次,需避免频繁写操作
- 电压要求:工作电压需稳定在2.7V-3.6V范围,否则可能写入失败
3. Flash操作实战
3.1 基础操作流程
标准的Flash操作应遵循以下流程:
- 解锁Flash(解除写保护)
- 擦除目标页(全写为1)
- 按字(32bit)写入数据
- 验证数据一致性
- 重新上锁Flash
3.2 关键代码实现
3.2.1 Flash写入函数
c复制/**
* @brief 写入数据到Flash
* @param _pBuf: 源数据缓冲区地址
* @param _uiWriteAddr: Flash目标地址(必须4字节对齐)
* @param _usWriteSize: 写入数据长度(字节数)
* @retval 0-成功 1-擦除失败 2-校验失败
*/
uint8_t n32_flash_write(const uint8_t* _pBuf, uint32_t _uiWriteAddr, uint16_t _usWriteSize)
{
uint32_t i;
FLASH_Status status;
// 地址必须页对齐
if(_uiWriteAddr % FLASH_PAGE_SIZE != 0)
return 3; // 地址错误
// 解锁Flash
FLASH_Unlock();
// 擦除目标页
status = FLASH_EraseOnePage(_uiWriteAddr);
if(status != FLASH_COMPLETE)
{
FLASH_Lock();
return 1; // 擦除失败
}
// 按字(32bit)写入
for(i=0; i<_usWriteSize; i+=4)
{
if(FLASH_ProgramWord(_uiWriteAddr+i, *(uint32_t*)(_pBuf+i)) != FLASH_COMPLETE)
{
FLASH_Lock();
return 1; // 写入失败
}
// 立即校验
if(*(uint32_t*)(_uiWriteAddr+i) != *(uint32_t*)(_pBuf+i))
{
FLASH_Lock();
return 2; // 校验失败
}
}
FLASH_Lock();
return 0;
}
3.2.2 Flash读取函数
c复制/**
* @brief 从Flash读取数据
* @param _pBuf: 目标缓冲区地址
* @param _uiReadAddr: Flash源地址
* @param _uiSize: 读取数据长度
*/
void n32_flash_read(uint8_t* _pBuf, uint32_t _uiReadAddr, uint16_t _uiSize)
{
uint16_t i;
for(i=0; i<_uiSize; i++)
{
_pBuf[i] = *(__IO uint8_t*)(_uiReadAddr+i);
}
}
3.3 实战注意事项
-
地址对齐问题:
- 擦除地址必须页对齐(NG32为512字节边界)
- 写入地址必须4字节对齐(32位架构要求)
- 读取地址无特殊要求
-
数据缓冲区处理:
c复制// 正确做法:使用__attribute__((aligned(4)))确保缓冲区对齐 uint8_t write_buf[128] __attribute__((aligned(4))); -
中断处理:
Flash操作期间应禁用中断,避免打断关键操作:c复制__disable_irq(); // Flash操作代码 __enable_irq(); -
电源稳定性:
建议在写入前检查电源电压,并添加大容量滤波电容:c复制if(GetVDD() < 2700) // 单位mV return 4; // 电压过低
4. 高级应用技巧
4.1 磨损均衡实现
由于Flash有擦写次数限制,频繁更新的数据应采用磨损均衡算法。一个简单的实现方案:
c复制#define DATA_SLOTS 4 // 使用4个槽位轮换
struct {
uint32_t addr; // 槽位起始地址
uint8_t valid; // 有效标志
} slot_table[DATA_SLOTS];
void wear_leveling_write(uint32_t base_addr, uint8_t* data, uint16_t size)
{
static uint8_t current_slot = 0;
// 查找下一个可用槽位
for(int i=1; i<=DATA_SLOTS; i++)
{
uint8_t try_slot = (current_slot + i) % DATA_SLOTS;
if(slot_table[try_slot].valid == 0)
{
uint32_t target_addr = base_addr + try_slot * size;
if(n32_flash_write(data, target_addr, size) == 0)
{
slot_table[try_slot].valid = 1;
current_slot = try_slot;
return;
}
}
}
// 所有槽位已满,需要整体擦除
FLASH_Unlock();
for(int i=0; i<DATA_SLOTS; i++)
{
FLASH_EraseOnePage(base_addr + i * size);
slot_table[i].valid = 0;
}
FLASH_Lock();
// 重新写入
wear_leveling_write(base_addr, data, size);
}
4.2 掉电保护设计
突然断电可能导致Flash数据损坏,建议采用以下策略:
- 使用状态标志位:写入数据前先设置"正在写入"标志
- 采用双备份机制:同时维护两份数据副本
- 添加CRC校验:每次读取时验证数据完整性
c复制#define MAGIC_NUMBER 0xAA55CC33
typedef struct {
uint32_t magic;
uint32_t crc;
uint8_t data[100];
} safe_flash_data;
void safe_write(uint32_t addr, uint8_t* data, uint16_t size)
{
safe_flash_data sf_data;
sf_data.magic = MAGIC_NUMBER;
memcpy(sf_data.data, data, size);
sf_data.crc = calculate_crc(data, size);
n32_flash_write((uint8_t*)&sf_data, addr, sizeof(safe_flash_data));
}
uint8_t safe_read(uint32_t addr, uint8_t* data, uint16_t size)
{
safe_flash_data sf_data;
n32_flash_read((uint8_t*)&sf_data, addr, sizeof(safe_flash_data));
if(sf_data.magic != MAGIC_NUMBER)
return 1; // 数据无效
if(calculate_crc(sf_data.data, size) != sf_data.crc)
return 2; // 数据损坏
memcpy(data, sf_data.data, size);
return 0;
}
5. 常见问题排查
5.1 写入失败分析
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 擦除失败 | 地址未页对齐 | 检查地址是否为512字节整数倍 |
| 写入失败 | 电压不稳定 | 测量VDD电压,添加滤波电容 |
| 校验失败 | 未先擦除 | 确保每次写入前执行擦除操作 |
| 数据错位 | 缓冲区未对齐 | 使用aligned属性声明缓冲区 |
5.2 性能优化建议
- 批量写入:合并多次小数据写入为单次大块写入
- 缓存机制:在RAM中维护频繁修改的数据,定期同步到Flash
- 后台操作:在系统空闲时执行Flash擦写操作
c复制#define CACHE_SIZE 512
uint8_t flash_cache[CACHE_SIZE] __attribute__((aligned(4)));
uint32_t cache_dirty = 0;
void cache_write(uint32_t offset, uint8_t* data, uint16_t size)
{
memcpy(flash_cache+offset, data, size);
cache_dirty = 1;
}
void cache_flush(uint32_t base_addr)
{
if(cache_dirty)
{
n32_flash_write(flash_cache, base_addr, CACHE_SIZE);
cache_dirty = 0;
}
}
在实际项目中,我发现很多Flash相关的问题都源于对底层机制理解不够深入。比如有一次产品在现场频繁出现数据丢失,最终发现是客户环境电源质量差导致写入时电压跌落。后来我们增加了电压检测和重试机制,问题得到彻底解决。这也提醒我们,Flash操作不能只考虑理想情况,必须做好各种异常处理。