在嵌入式开发或 Linux 驱动开发中,你一定会遇到“Flash 写入”这一重要操作。但很多初学者在面对类似代码时经常一头雾水:
什么是扇区(Sector)?页(Page)?为什么写入前要擦除?为什么数据不能直接覆盖?什么叫“不能从 0 写回 1”?!
别担心,本文将带你全面理解 Flash 的基本结构和写入流程,解决你理解 Flash 写入代码时的所有障碍。
🧠 一、什么是 Flash?它是怎么组织的?
Flash(闪存)是一种非易失性存储器(断电后仍保留数据),广泛应用于 MCU、U 盘、固态硬盘等设备。
Flash 的结构由大到小如下:
┌───────────────┐
│ Flash 总容量 │ ← 如 512KB
├───────────────┤
│ 扇区(Sector)│ ← 通常为 4KB、8KB 或 64KB 等
├───────────────┤
│ 页(Page) │ ← 通常为 256B、512B
└───────────────┘
扇区是最小的“擦除单位”;页是最小的“编程(写入)单位”;字节 Byte是最小的“读取单位”。
Flash(闪存)是一种非易失性存储器(断电后仍保留数据),广泛应用于 MCU、U 盘、固态硬盘等设备。
Flash 的结构由大到小如下:
┌───────────────┐
│ Flash 总容量 │ ← 如 512KB
├───────────────┤
│ 扇区(Sector)│ ← 通常为 4KB、8KB 或 64KB 等
├───────────────┤
│ 页(Page) │ ← 通常为 256B、512B
└───────────────┘
扇区是最小的“擦除单位”;页是最小的“编程(写入)单位”;字节 Byte是最小的“读取单位”。
Flash(闪存)是一种非易失性存储器(断电后仍保留数据),广泛应用于 MCU、U 盘、固态硬盘等设备。
Flash 的结构由大到小如下:
┌───────────────┐
│ Flash 总容量 │ ← 如 512KB
├───────────────┤
│ 扇区(Sector)│ ← 通常为 4KB、8KB 或 64KB 等
├───────────────┤
│ 页(Page) │ ← 通常为 256B、512B
└───────────────┘
扇区是最小的“擦除单位”;页是最小的“编程(写入)单位”;字节 Byte是最小的“读取单位”。
❗ 二、Flash 的三个限制性原则(必须理解)
1️⃣ 写入前必须先擦除
Flash 不能直接覆盖旧数据,你必须先擦除目标区域(清空为 0xFF)再写入新数据。
2️⃣ 只能从 1 ➜ 0,不能从 0 ➜ 1
比如原来是 11111111(0xFF),可以写成 10101010(0xAA);
但如果原来是 10101010,你不能写回 11111111,只能擦除整个扇区。
3️⃣ 擦除粒度较大(按扇区)
哪怕你只想改一个字节,也必须擦除整个扇区,然后重新写入该字节 + 其它原有数据。
三、Flash 写入的完整流程
举例:你要写入 10 字节数据到地址 0x08005000
步骤一:解锁 Flash
很多芯片(如 STM32)默认 Flash 是锁定状态,需先调用 flash_unlock() 解锁。
步骤二:判断地址是否跨扇区
计算起始地址所在扇区是否能容纳全部数据,如果不能,需要分段处理多个扇区。
步骤三:读取扇区到缓存
由于写入前要擦除整个扇区,你需要把当前扇区的数据读出来保存。
步骤四:修改缓存中的对应区域
把你想写入的数据覆盖到缓存中对应的偏移位置。
步骤五:擦除该扇区
调用如 flash_sector_erase(addr) 擦除整片区域。
步骤六:写回整个缓存
使用 flash_program(addr, buf) 将整个缓冲区重新写入 Flash。
步骤七:锁定flash
写入完成后调用 flash_lock(),防止误操作。
💡 四、扇区 vs 页:区别与用途
项目
扇区(Sector)
页(Page)
功能
擦除单位
写入单位
粒度
大(4KB、64KB)
小(256B)
操作
擦除必须整扇区
可写入单页或多页
场景
擦除 Flash 空间
写入数据
注意:不是所有芯片都对页有显式操作,有些芯片只按字或半字写入,不暴露页概念。
📌 五、Flash 写入为什么复杂?
Flash 不是像 RAM 那样可以随意读写。你需要考虑:
地址对齐问题;页边界、扇区边界;写入效率(一次写多字);掉电保护(写一半掉电会导致数据异常);写入寿命(有限的写入次数,典型为 10 万次)。
因此,为了安全可靠,很多驱动写入逻辑都显得“绕”——它是为了兼容多种边界情况、避免数据损坏。
📁 六、实际代码中你会看到的关键词
术语
含义
flash_unlock()
解锁 Flash 写保护
flash_sector_erase()
擦除指定扇区
flash_program() / flash_write_nocheck()
写入数据(默认要求写入地址为 0xFF)
flash_read()
从 Flash 读取数据
SECTOR_SIZE
每个扇区的大小(通常为宏定义)
offset, remain
当前写入的位置与剩余空间计算
📁 七、应用实例!!!
error_status flash_write(uint32_t write_addr, uint8_t *p_buffer, uint16_t num_write)
{
uint32_t offset_addr;//flash偏移量
uint32_t sector_position;//片区偏移个数
uint16_t sector_offset;//片内偏移量
uint16_t sector_remain;//片区剩余量
uint16_t i;
flash_status_type status = FLASH_OPERATE_DONE;
flash_unlock();
offset_addr = write_addr - FLASH_BASE;//flash偏移量
sector_position = offset_addr / SECTOR_SIZE;////片区偏移个数,从 0 开始的索引编号
sector_offset = (offset_addr % SECTOR_SIZE);//片内偏移量
sector_remain = SECTOR_SIZE - sector_offset;////片区剩余量
if(num_write <= sector_remain)//判断片区剩余量是否足够写入需要的数据大小
sector_remain = num_write;//在主循环中表示,实际要写的字节数
while(1)
{
flash_read(sector_position * SECTOR_SIZE + FLASH_BASE, flash_buf, SECTOR_SIZE );//片区剩余量不够时读取出整个片区的数据到buf中
for(i = 0; i < sector_remain; i++)
{
if(flash_buf[sector_offset + i] != 0xFF)//判断片区剩余量后续地址是否有值
break;
}
if(i < sector_remain)//说明后面存在地址有值
{
status = flash_sector_erase(sector_position * SECTOR_SIZE + FLASH_BASE);//擦除整个片区
if((status == FLASH_PROGRAM_ERROR) || (status == FLASH_EPP_ERROR))//出现flash操作错误
flash_flag_clear(FLASH_PRGMERR_FLAG | FLASH_EPPERR_FLAG);//清除flash的相关错误标志位
else if(status == FLASH_OPERATE_TIMEOUT)//超时未完成
return ERROR;
status = flash_operation_wait_for(ERASE_TIMEOUT);//等待片区擦除完
if(status != FLASH_OPERATE_DONE)
return ERROR;
for(i = 0; i < sector_remain; i++)
{
flash_buf[i + sector_offset] = p_buffer[i];//将需要写入的数据写到buf中与偏移量对应的地方
}
if(flash_write_nocheck(sector_position * SECTOR_SIZE + FLASH_BASE, flash_buf, SECTOR_SIZE ) != SUCCESS)//然后将整个buf写到片区
return ERROR;
}
else//后面的地址没有值
{
if(flash_write_nocheck(write_addr, p_buffer, sector_remain) != SUCCESS)//直接写入
return ERROR;
}
if(num_write == sector_remain)
break;
else//片区剩余大小不够写入数据
{
sector_position++;//片区偏移个数加1
sector_offset = 0;//片区偏移量设为0
p_buffer += sector_remain;//传入数据数组偏移已经写入的值
write_addr += (sector_remain);//写入地址偏移
num_write -= sector_remain;//需要写入的数据个数减去已经写入的数据个数
if(num_write > (SECTOR_SIZE))
sector_remain = SECTOR_SIZE ;
else
sector_remain = num_write;
}
}
flash_lock();
return SUCCESS;
}
这段代码是一个典型的Flash写入函数,适用于如STM32或其他支持按扇区擦写的MCU。改函数设计考虑了Flash的以下特性:
Flash 写入前需要擦除;Flash 不能单字节直接覆盖写,写入前对应位置必须为 0xFF;Flash 按 扇区/页(Sector)擦除;写入可能跨多个扇区。
参数说明:
参数
含义
write_addr
要写入的 Flash 地址
p_buffer
指向要写入的数据的指针
num_write
要写入的数据字节数
🔁 函数总体流程概述
解锁 Flash 写保护;计算地址在哪个扇区、扇区内偏移多少;判断该扇区是否可直接写入;如不可直接写入,需读取整个扇区到缓存 → 修改 → 擦除 → 再整体写回;如果可以直接写入,则跳过擦除步骤;支持跨扇区写入;写入完成后锁定 Flash。
🧠 逐段解析代码逻辑
🔓 解锁 Flash
flash_unlock();
为了能写入或擦除 Flash,需要先解锁。
📍 地址定位
offset_addr = write_addr - FLASH_BASE;
sector_position = offset_addr / SECTOR_SIZE;
sector_offset = (offset_addr % SECTOR_SIZE);
sector_remain = SECTOR_SIZE - sector_offset;
offset_addr:写入地址距离 Flash 起始的偏移量;sector_position:当前地址位于第几个扇区;sector_offset:在扇区内部的偏移位置;sector_remain:当前扇区剩余可写空间(不一定足够写全部数据);
🧪 写入主循环
先读取整个扇区到缓存:
flash_read(sector_position * SECTOR_SIZE + FLASH_BASE, flash_buf, SECTOR_SIZE );
判断要写入的位置是否已被写过(非 0xFF):
for(i = 0; i < sector_remain; i++) {
if(flash_buf[sector_offset + i] != 0xFF)
break;
}
如果存在已写过区域,必须先擦除扇区
status = flash_operation_wait_for(ERASE_TIMEOUT);
// 错误处理
status = flash_sector_erase(...);
// 写入修改后的完整 buf
flash_buf[i + sector_offset] = p_buffer[i];
flash_write_nocheck(...);
如果可以直接写入:
flash_write_nocheck(write_addr, p_buffer, sector_remain);
🔁 判断是否写完
if(num_write == sector_remain)
break;
否则说明本扇区装不下所有数据,更新参数进入下一扇区:
sector_position++;
sector_offset = 0;
p_buffer += sector_remain;
write_addr += sector_remain;
num_write -= sector_remain;
🔒 最后加锁 Flash
flash_lock();
return SUCCESS;
总结:本函数的特点
特性
描述
安全性高
避免直接写入非 0xFF 区域,防止数据错写
支持跨扇区
自动处理跨越多个 Flash 扇区的数据写入
缓存保护
使用 flash_buf 读取整个扇区再写,防止非目标地址被擦除
可扩展性强
可用于 OTA 升级、BootLoader 写 Flash 等场景
✅ 总结
问题
结论
Flash 能像 RAM 一样随便写吗?
❌ 不能,必须先擦除
一个字节能单独擦除吗?
❌ 不行,只能按“扇区”擦除
擦除后 Flash 初始值是什么?
0xFF
可以从 0 写回 1 吗?
❌ 不行,只能 1 ➜ 0
为什么驱动中有这么多“判断剩余空间”的逻辑?
为了适配跨扇区、偏移写入等边界情况