存储设备驱动
本页介绍 SDK 中基于 SPI 的 NOR Flash 存储设备驱动:统一的设备抽象层(device_drive.h)、norflash 驱动的分区管理、软硬件 SPI 收发、读/写/擦除实现、4KB 写缓存机制以及设备操作注册(norflash_dev_ops / norfs_dev_ops)。
Purpose and Scope
本页覆盖以下内容:
- 设备驱动抽象层:
dev_open/dev_read/dev_write/dev_ioctl等统一设备接口的用法与约定; norflash驱动(sdk/app/bsp/common/norflash/):WinBond W25X 系列命令集、分区模型、软硬件 SPI 切换、字节读写与页写入、扇区/块/芯片擦除、4KB 写缓存同步;- 设备操作表
norflash_dev_ops与norfs_dev_ops的语义差异及其注册方式; - S25FL 系列 4 字节地址模式、掉电/唤醒等特殊功能。
以下主题属于其他目录页,本页不展开:
- 上层文件系统(如 norfs、fatfs)的具体实现与挂载逻辑 —— 见文件系统相关页面;
- 分区表与烧录布局(
post_build下的dir_ex_flash、flash_wp_info.csv等)—— 见构建/烧录相关页面; - 写保护(flash WP)功能 —— 见
flash_wp.h相关页面。
Overview
在杰理 AD1NN 系列 MCU SDK 中,几乎所有外设(UART、SPI、SD、NOR Flash 等)都通过一套统一的设备抽象层暴露给上层应用。设备驱动需要实现 struct device_operations 中定义的回调(init、online、open、read、write、ioctl、close 等),然后以设备名(如 __EXT_FLASH_NANE、"S25FL256")注册到设备节点表中,上层通过 dev_open() 拿到 struct device * 句柄后即可使用统一的 dev_byte_read()、dev_byte_write()、dev_ioctl() 等接口访问存储介质。
NOR Flash 驱动位于 sdk/app/bsp/common/norflash/norflash.c,具有以下关键设计决策:
- 双操作表(ops):
norflash_dev_ops提供 512 字节块(bulk)粒度接口,norfs_dev_ops提供 1 字节粒度接口且写前不检查、不擦除(擦除交给上层文件系统)。两种 ops 可以同时使用、服务于不同分区。 - 静态分区模型:最多 4 个分区(
MAX_NORFLASH_PART_NUM),每个分区可注册为独立设备名,分区地址重叠或越界会触发ASSERT()。 - 可裁剪的 SPI 层:通过
TCFG_FLASH_SPI_TYPE_SELECT在硬件 SPI(spi1.h)与软件 SPI(spi_soft.h)之间切换,所有收发操作经由一组宏抽象,便于移植。 - 可选 4KB 写缓存:
FLASH_CACHE_ENABLE开启后,读路径可命中 4KB 缓存,写路径先落缓存、定时或关闭时再"擦除+写回",减少擦写次数、延长 Flash 寿命,同时缩短写操作耗时。 - 裸机互斥:
NORFLASH_NO_SYS模式下用自定义flash_mutex(基于jiffies与看门狗)保证多任务/中断上下文访问串行化,并在等待期间喂狗避免复位。
flowchart TD
subgraph sg_App["应用层"]
FS["文件系统 / 上层应用"]
Test["驱动测试代码<br/>norflash_test / s25_flash_test"]
end
subgraph sg_DevLayer["设备抽象层 (device_drive.h)"]
API["dev_open / dev_byte_read<br/>dev_byte_write / dev_ioctl"]
Ops["struct device_operations"]
end
subgraph sg_Norflash["NOR Flash 驱动 (norflash.c)"]
Ops2["norflash_dev_ops<br/>norfs_dev_ops"]
Part["分区管理<br/>norflash_partition × 4"]
Cache["4KB 写缓存<br/>FLASH_CACHE_ENABLE"]
Mutex["flash_mutex 互斥"]
Cmd["W25X 命令集<br/>读/写/擦除"]
end
subgraph sg_Spi["SPI 层"]
SpiHw["硬件 SPI (spi1.h)"]
SpiSoft["软件 SPI (spi_soft.h)"]
end
subgraph sg_Hw["硬件"]
Flash[("NOR Flash 芯片<br/>W25Q / S25FL 系列")]
Gpio["CS 引脚 GPIO"]
end
FS --> API
Test --> API
API --> Ops
Ops --> Ops2
Ops2 --> Part
Ops2 --> Cache
Ops2 --> Mutex
Ops2 --> Cmd
Cmd --> SpiHw
Cmd --> SpiSoft
SpiHw --> Flash
SpiSoft --> Flash
Cmd --> Gpio
Gpio --> Flash
核心数据结构
设备操作表
norflash.h 声明了两个外部设备操作表,由 norflash.c 实现:
extern const struct device_operations norflash_dev_ops;
extern const struct device_operations norfs_dev_ops;
Source: norflash.h
norfs_dev_ops 的注册内容(norflash.c 末尾):
const struct device_operations norfs_dev_ops = {
.init = norflash_dev_init,
.online = norflash_dev_online,
.open = norflash_dev_open,
.read = norflash_byte_read,//按字节(byte)读取
.write = norflash_byte_write,//按字节写入,写前不进行检查与擦除
.bulk_read = NULL,
.bulk_write = NULL,
.ioctl = norflash_byte_ioctl,
.close = norflash_dev_close,
};
Source: norflash.c
关键语义(来自源码注释):
- 外部调用时地址与长度以 1 字节 为单位;驱动
write不自己处理擦除,使用本 ops 时擦除由上层文件系统负责; - 若希望驱动内部处理擦除,需将
FLASH_CACHE_ENABLE置 1,并且norfs_dev_read()/norfs_dev_write()调用_norflash_read()时cache实参填 1; norfs_dev_ops可被多个设备名注册,每个设备名被视为不同分区,需填不同起始地址与大小;分区重叠或最大结束地址大于 Flash 容量会触发ASSERT();IOCTL_GET_CAPACITY:多分区时返回分区大小;仅 1 个分区且起始地址为 0 时返回 Flash 容量;- 本文件内的各个 ops 可以同时使用。
平台数据
struct norflash_dev_platform_data {
s8 spi_hw_num; //只支持SPI1或SPI2
u8 spi_cs_port; //cs的引脚
u8 spi_read_width; //flash读数据的线宽
const struct spi_platform_data *spi_pdata;
u32 start_addr; //分区起始地址
u32 size; //分区大小,若只有1个分区,则这个参数可以忽略
};
#define NORFLASH_DEV_PLATFORM_DATA_BEGIN(data) \
const struct norflash_dev_platform_data data = {
#define NORFLASH_DEV_PLATFORM_DATA_END() \
};
Source: norflash.h
spi_hw_num 只支持 SPI1 或 SPI2;spi_read_width 控制读数据线宽(1/2/4 BIT);start_addr/size 描述该设备名对应的分区范围。NORFLASH_DEV_PLATFORM_DATA_BEGIN/END 宏用于在 board 配置文件中声明平台数据实例。
分区管理与内部状态
驱动使用一个全局单例 _norflash 保存所有运行期状态:
struct norflash_info {//last
u32 flash_id;
u32 flash_capacity;
s8 spi_num;
u8 spi_cs_io;
u8 spi_r_width;
u8 part_num;
u8 open_cnt;
flash_mutex mutex;
struct norflash_partition *const part_list;
u32 max_end_addr;
};
static struct norflash_info _norflash = {
.spi_num = (s8) - 1,
.part_list = nor_part,
};
Source: norflash.c
分区表是静态数组,最多 4 个分区:
#define MAX_NORFLASH_PART_NUM 4
struct norflash_partition {
const char *name;
u32 start_addr;
u32 size;
struct device device;
};
static struct norflash_partition nor_part[MAX_NORFLASH_PART_NUM];
Source: norflash.c
分区的生命周期由三个静态函数管理:
norflash_find_part(name):线性扫描分区表,按名字匹配返回分区指针;norflash_new_part(name, addr, size):在空槽位创建分区,并维护max_end_addr(所有分区结束地址的最大值,用于 open 时与 Flash 容量比对);norflash_verify_part(p):校验地址重叠 —— 若某分区起始地址落在其他分区范围内且名字不同,返回-1拒绝注册。
设计意图:分区信息在编译期/初始化期静态登记,运行期不做动态分配,避免了内存碎片与分配失败路径;同时通过 verify_part 与 open 时的 ASSERT(max_end_addr <= flash_capacity) 双重防护,从根上杜绝分区越界访问。
SPI 抽象层
驱动通过一组宏把底层 SPI 访问完全抽象化,TCFG_FLASH_SPI_TYPE_SELECT 决定使用硬件 SPI 还是软件 SPI:
#if(TCFG_FLASH_SPI_TYPE_SELECT)
#define spi_read_byte() spi_recv_byte(_norflash.spi_num, NULL)
#define spi_write_byte(x) spi_send_byte(_norflash.spi_num, x)
#define spi_dma_read(x, y) spi_dma_recv(_norflash.spi_num, x, y)
#define spi_dma_write(x, y) spi_dma_send(_norflash.spi_num, x, y)
#define spi_set_width(x) spi_set_bit_mode(_norflash.spi_num, x)
#define spi_init() spi_open(_norflash.spi_num)
#define spi_closed() spi_close(_norflash.spi_num)
#define spi_suspend() hw_spi_suspend(_norflash.spi_num)
#define spi_resume() hw_spi_resume(_norflash.spi_num)
#else
#define spi_read_byte() soft_spi_recv_byte(_norflash.spi_num, NULL)
#define spi_write_byte(x) soft_spi_send_byte(_norflash.spi_num, x)
#define spi_dma_read(x, y) soft_spi_dma_recv(_norflash.spi_num, x, y)
#define spi_dma_write(x, y) soft_spi_dma_send(_norflash.spi_num, x, y)
#define spi_set_width(x) soft_spi_set_bit_mode(_norflash.spi_num, x)
#define spi_init() soft_spi_open(_norflash.spi_num)
#define spi_closed() soft_spi_close(_norflash.spi_num)
#define spi_suspend() soft_spi_suspend(_norflash.spi_num)
#define spi_resume() soft_spi_resume(_norflash.spi_num)
#endif
Source: norflash.c
CS 引脚通过 GPIO 位带操作直接控制(不经过 SPI 控制器),并配套初始化/去初始化宏:
#define spi_cs_init() \
do { \
gpio_set_die(_norflash.spi_cs_io, 1); \
gpio_set_direction(_norflash.spi_cs_io, 0); \
gpio_write(_norflash.spi_cs_io, 1); \
} while (0)
#define spi_cs_uninit() \
do { \
gpio_set_die(_norflash.spi_cs_io, 0); \
gpio_set_direction(_norflash.spi_cs_io, 1); \
gpio_set_pull_up(_norflash.spi_cs_io, 0); \
gpio_set_pull_down(_norflash.spi_cs_io, 0); \
} while (0)
#define spi_cs_h() gpio_write(_norflash.spi_cs_io, 1)
#define spi_cs_l() gpio_write(_norflash.spi_cs_io, 0)
Source: norflash.c
设计意图:软硬件 SPI 通过编译期宏切换而非运行期函数指针,保证零额外开销;CS 独立于 SPI 控制器便于复用 SD 卡的 SPI IO(见 SPI_SD_IO_REUSE)。
读 / 写 / 擦除实现
等待空闲(_norflash_wait_ok)
每次命令后轮询状态寄存器 1 的 BUSY 位(BIT(0)),超时约 8 万次轮询(8 * 1000 * 1000 / 100),期间每次 delay(100):
static int _norflash_wait_ok()
{
u32 timeout = 8 * 1000 * 1000 / 100;
u8 reg_1 = 0;
while (timeout--) {
spi_cs_l();
spi_write_byte(WINBOND_READ_SR1);
reg_1 = spi_read_byte();
spi_cs_h();
if (!(reg_1 & BIT(0))) {
break;
}
delay(100);
}
if (timeout == 0) {
log_error("norflash_wait_ok timeout!\r\n");
return 1;
}
...
}
Source: norflash.c
读取(_norflash_read)
读取路径优先尝试命中 4KB 缓存(FLASH_CACHE_ENABLE 且 cache 参数非 0),未命中则走 SPI:先发 WINBOND_FAST_READ_DATA(0x0B),2BIT 线宽时改用 WINBOND_FAST_READ_DUAL_OUTPUT(0x3B)并切换 SPI_MODE_UNIDIR_2BIT 后 DMA 收数;4BIT 在 SPI1 上不支持(打印错误日志):
spi_cs_l();
#if(TCFG_FLASH_SPI_TYPE_SELECT)//soft spi only have 1BIT
if (_norflash.spi_r_width == SPI_MODE_UNIDIR_2BIT) {
spi_write_byte(WINBOND_FAST_READ_DUAL_OUTPUT);
_norflash_send_addr(addr);
spi_write_byte(0);
spi_set_width(SPI_MODE_UNIDIR_2BIT);
spi_dma_read(buf, len);
spi_set_width(SPI_MODE_BIDIR_1BIT);
} else if (_norflash.spi_r_width == SPI_MODE_UNIDIR_4BIT) {
log_error("flash write error!SPI1 don't support 4BIT!");
} else
#endif
{
spi_write_byte(WINBOND_FAST_READ_DATA);
_norflash_send_addr(addr);
spi_write_byte(0);
spi_dma_read(buf, len);
}
spi_cs_h();
Source: norflash.c
页写入(_norflash_write_pages)
NOR Flash 页编程命令(WINBOND_PAGE_PROGRAM 0x02)一次最多写 256 字节。实现先算首页剩余长度(256 - addr % 256),然后逐页发送 写使能(0x06)→ 拉低 CS → 发命令与地址 → DMA 写数据 → 拉高 CS → 等待 BUSY 的完整序列:
static int _norflash_write_pages(u32 addr, u8 *buf, u32 len)
{
int reg;
u32 first_page_len = 256 - (addr % 256);
first_page_len = len > first_page_len ? first_page_len : len;
_norflash_send_write_enable();
spi_cs_l();
spi_write_byte(WINBOND_PAGE_PROGRAM);
_norflash_send_addr(addr) ;
spi_dma_write(buf, first_page_len);
spi_cs_h();
reg = _norflash_wait_ok();
if (reg) {
return 1;
}
addr += first_page_len;
buf += first_page_len;
len -= first_page_len;
while (len) {
u32 cnt = len > 256 ? 256 : len;
_norflash_send_write_enable();
spi_cs_l();
spi_write_byte(WINBOND_PAGE_PROGRAM);
_norflash_send_addr(addr) ;
spi_dma_write(buf, cnt);
spi_cs_h();
reg = _norflash_wait_ok();
if (reg) {
return 1;
}
addr += cnt;
buf += cnt;
len -= cnt;
}
return 0;
}
Source: norflash.c
设计意图:每一页写前都必须单独发写使能命令,这是 W25X 系列芯片的硬件约束;先处理"首页非对齐"再按整页循环,保证任意起始地址、任意长度的写请求都能被正确切页。
擦除粒度
norflash.h 定义四种擦除粒度,与命令一一对应:
enum {
FLASH_PAGE_ERASER, // 0x81 WINBOND_PAGE_ERASE
FLASH_SECTOR_ERASER, // 0x20 WINBOND_SECTOR_ERASE
FLASH_BLOCK_ERASER, // 0xD8 WINBOND_BLOCK_ERASE
FLASH_CHIP_ERASER, // 0xC7 WINBOND_CHIP_ERASE
};
Source: norflash.h
_norflash_eraser(eraser, addr) 根据粒度选择命令并执行"写使能 → 发命令/地址 → 等待 BUSY"序列,供 ioctl 与缓存同步调用。
4KB 写缓存机制(FLASH_CACHE_ENABLE)
缓存是驱动最重要的优化手段之一,其状态变量:
#define FLASH_READ_NO_USE_CACHE 0
#define FLASH_READ_OCCUPY_CACHE 1
#define FLASH_READ_PREPARE_USE_CACHE 2
static volatile u8 flash_cache_sync_enter_critical_dis = 0;
static u8 flash_read_use_cache = FLASH_READ_NO_USE_CACHE;
static u32 flash_cache_addr;
static u8 *flash_cache_buf = NULL; //缓存4K的数据,与flash里的数据一样。
static u8 norfs_cache_buf[4096] __attribute__((aligned(4)))SEC(.norflash_cache_buf);
static u8 flash_cache_is_dirty;
static volatile u8 flash_cache_timer;
Source: norflash.c
工作方式:
- 读:
_norflash_read(addr, buf, len, cache=1)时若flash_cache_addr是 4KB 对齐且请求范围落在缓存窗口内,直接从flash_cache_buf拷贝,完全绕开 SPI;超出部分回退到真读。 - 写:上层写入先修改缓存(脏标记
flash_cache_is_dirty),不立即擦写 Flash。 - 同步:
_norflash_cache_sync_timer(sync_step)在定时器中检查脏标记,满足步进(NORFLASH_NO_SYS下idle_cnt >= sync_step,即 N×100ms)后,进入临界区抢占互斥锁,执行"擦除扇区 → 整页写回 4096 字节":- 若
SPI_SD_IO_REUSE(与 SD 卡复用 SPI IO),同步前先sd_io_reuse_suspend()抢占总线,完成后spi_flash_io_suspend()再sd_io_reuse_resume()归还; - 互斥锁获取失败(
query_flash_mutex_pend < 0)则本次放弃同步,等下一个周期。
- 若
- 关闭:
_norflash_close()在open_cnt归零时若缓存仍脏,先擦除再写回,然后释放缓存指针、关闭 SPI 并spi_cs_uninit()。
设计意图:NOR Flash 擦除时间长(扇区擦除可达数十毫秒)、擦写寿命有限。缓存把"每次小写"合并为"4KB 整扇区写回",显著减少擦除次数与总耗时;脏数据定时回写则兼顾了掉电数据安全与响应延迟。flash_cache_sync_enter_critical_dis 允许某些场景(如正在擦写期间)临时关闭临界区保护,避免死锁。
核心流程
设备打开与关闭流程
norflash_dev_open 的执行顺序(_norflash_open):
- 首次打开时初始化 CS 引脚与 SPI(
spi_cs_init/spi_init); - 发
WINBOND_JEDEC_ID(0x9F)读取 Flash ID 与容量,若 ID 为 0 或0xffffff则报错返回-EFAULT; - 对 S25FL 系列执行 4 字节地址模式切换(
S25FL512_enter_4byte_addr)或配置寄存器写回(_S25FLxxx_read_id); FLASH_CACHE_ENABLE时初始化缓存指针与地址;ASSERT(max_end_addr <= flash_capacity)校验分区边界;open_cnt++记录引用计数 —— 多任务共享同一设备时只有最后一个 close 才真正关 SPI。
sequenceDiagram
participant App as 上层应用/文件系统
participant Dev as 设备抽象层
participant Nf as norflash 驱动
participant Spi as SPI 层
participant Chip as NOR Flash 芯片
App->>Dev: dev_open("EXT_FLASH", 0)
Dev->>Nf: norflash_dev_open()
Nf->>Spi: spi_cs_init / spi_init
Nf->>Chip: JEDEC_ID (0x9F)
Chip-->>Nf: flash_id / capacity
Nf->>Nf: 校验分区边界 ASSERT
Nf-->>Dev: struct device *
Dev-->>App: 设备句柄
App->>Dev: dev_byte_read(buf, addr, len)
Dev->>Nf: norflash_byte_read()
Nf->>Nf: 4KB 缓存命中?
alt 命中
Nf-->>App: memcpy 直接返回
else 未命中
Nf->>Spi: FAST_READ (0x0B) + DMA
Spi->>Chip: 读数据
Chip-->>Nf: 数据
Nf-->>App: 返回数据
end
App->>Dev: dev_byte_write(buf, addr, len)
Dev->>Nf: norflash_byte_write()
Nf->>Nf: 更新 4KB 缓存, 置脏标记
Nf-->>App: 立即返回
Note over Nf: 定时器触发 _norflash_cache_sync_timer
Nf->>Nf: 抢互斥锁, sd_io_reuse_suspend (可选)
Nf->>Chip: SECTOR_ERASE (0x20)
Nf->>Chip: PAGE_PROGRAM ×16 (写回 4KB)
Nf->>Nf: 清脏标记, 释放总线
App->>Dev: dev_close()
Dev->>Nf: norflash_dev_close()
Nf->>Nf: open_cnt--, 缓存同步(若脏)
Nf->>Spi: spi_closed / spi_cs_uninit
读缓存命中判定
_norflash_read 中命中判定要求 flash_cache_addr 4KB 对齐,且请求区间 [addr, addr+len) 不越过缓存窗口末尾;窗口剩余长度 r_len = 4096 - (addr % 4096) 内的部分直接拷贝,超出部分继续走 SPI 真读。
使用示例
以下代码来自 norflash.c 末尾的测试函数(默认被 #if 0 关闭,仅作验证/示例用途),展示了块(bulk)接口与字节(byte)接口两种用法。
块操作示例(norflash_dev_ops 风格)
void norflash_test()
{
u32 i = 0;
struct device *device;
for (i = 0; i < n_n * 512; i++) {
w25q_write_buf[i] = i % 26 + 'a';
}
devices_init();
device = (struct device *)dev_open(__EXT_FLASH_NANE, 0);
if (device == NULL) {
log_error("w25q32 open fail!\n");
} else {
log_info("w25q32 open ok!\n");
#if 0 //块(512byte)操作测试---norflash_dev_ops
log_info("flash dev bulk write/read test!");
dev_bulk_write(device, w25q_write_buf, 0, n_n);
delay(10000);
memset(w25q_read_buf, 48, 512);
dev_bulk_read(device, w25q_read_buf, 0, n_n);
log_info("w25q bulk read buf:");
log_info_hexdump(w25q_read_buf, n_n * 512);
#else //字节(1byte)操作测试---norfs_dev_ops
log_info("flash dev byte write/read test!");
if (dev_ioctl(device, IOCTL_ERASE_SECTOR, 0)) { //返回0:擦出正常;1:擦出等待失败
log_error("erase err\n");
while (1);
}
dev_byte_read(device, w25q_read_buf, 0, n_n * 512);
dev_byte_write(device, w25q_write_buf, 0, n_n * 512);
delay(10000);
dev_byte_read(device, w25q_read_buf, 0, n_n * 512);
log_info(":%s\n", w25q_read_buf);
#endif
}
}
Source: norflash.c
要点:dev_open(__EXT_FLASH_NANE, 0) 拿到句柄后,先 dev_ioctl(IOCTL_ERASE_SECTOR, 0) 擦除目标扇区,再写后读验证;IOCTL_ERASE_SECTOR 返回 0 表示成功,1 表示等待 Flash 空闲失败。
大容量 Flash(S25FL256)示例:按地址选择擦除粒度
void s25_flash_test()
{
u32 s25fl_start_addr = 0x1020020;
u8 erase_sector = 0;
struct device *device;
devices_init();
device = (struct device *)dev_open("S25FL256", 0);
if (device == NULL) {
log_error("S25FL256 open fail!\n");
} else {
s25fl_start_addr = 0x1020020;//32个4k 扇区:0-0x20000; 510个64k扇区:0x20000-0x1ffffff
//手动擦出扇区
if (s25fl_start_addr < 0x20000) {
erase_sector = IOCTL_ERASE_SECTOR;
} else if (s25fl_start_addr < 0x1ffffff) {
erase_sector = IOCTL_ERASE_BLOCK;
} else while (1);
if (dev_ioctl(device, erase_sector, s25fl_start_addr)) {
log_error("erase err\n");
while (1);
}
dev_byte_read(device, s25_read_buf, s25fl_start_addr, srw_len);
dev_byte_write(device, s25_write_buf, s25fl_start_addr, srw_len);
delay(10000);
dev_byte_read(device, s25_read_buf, s25fl_start_addr, srw_len);
}
}
Source: norflash.c
要点:S25FL256 低地址区是 4KB 小扇区、高地址区是 64KB 大扇区,因此调用方需按目标地址选择 IOCTL_ERASE_SECTOR 还是 IOCTL_ERASE_BLOCK;注释明确说明"扇区为 256KB 时(如 S25FL512)没有擦除相关程序",即超大扇区芯片不支持该擦除命令。源码注释还注明"两种 flash 不能同时运行",同一编译单元内只能启用其中一种测试。
配置选项
以下宏/字段决定驱动行为,均在 norflash.h、norflash.c 或板级配置(app_config.h、board 文件)中定义:
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
EXT_FLASH_EN | 宏 | — | 置 1 时定义 TCFG_NORFLASH_DEV_ENABLE,启用 norflash 驱动编译(norflash.c 整体被 #if defined(TCFG_NORFLASH_DEV_ENABLE) 包裹) |
TCFG_FLASH_SPI_TYPE_SELECT | 宏 | — | 1:使用硬件 SPI(spi1.h);0:使用软件 SPI(spi_soft.h)。软件 SPI 仅支持 1BIT |
FLASH_CACHE_ENABLE | 宏 | — | 1:启用 4KB 读写缓存(norfs_cache_buf[4096] 置于 .norflash_cache_buf 段);0:直读直写 |
MALLOC_EN | 宏 | 0 | 1:缓存缓冲区用 malloc 动态分配;0:使用静态 norfs_cache_buf |
NORFLASH_NO_SYS | 宏 | 1 | 1:裸机模式,用自定义 flash_mutex(基于 jiffies+喂狗);0:使用操作系统互斥量 |
SPI_SD_IO_REUSE | 宏 | — | 1:SPI IO 与 SD 卡复用,访问 Flash 前需 sd_io_reuse_suspend() 抢占、结束后归还总线 |
MAX_NORFLASH_PART_NUM | 宏 | 4 | 静态分区表容量上限 |
spi_hw_num(平台数据) | s8 | — | SPI 控制器编号,只支持 SPI1 或 SPI2 |
spi_cs_port(平台数据) | u8 | — | CS 片选引脚 |
spi_read_width(平台数据) | u8 | — | 读数据线宽(SPI_MODE_BIDIR_1BIT / SPI_MODE_UNIDIR_2BIT 等),4BIT 在 SPI1 上不支持 |
start_addr / size(平台数据) | u32 | — | 该设备名对应的分区起始地址与大小 |
_norflash_wait_ok 超时 | u32 | 80000 次轮询 | 8 * 1000 * 1000 / 100,每次 delay(100),超时返回 1 并打印 norflash_wait_ok timeout! |
sync_step(_norflash_cache_sync_timer) | u32 | — | 缓存同步步进,N 表示 N×100ms 才回写一次脏缓存 |
命令集常量(norflash.h)汇总:
| 命令 | 值 | 用途 |
|---|---|---|
WINBOND_WRITE_ENABLE | 0x06 | 写使能(每次写/擦除前必须发送) |
WINBOND_READ_SR1 / READ_SR2 | 0x05 / 0x35 | 读状态寄存器 1/2(BUSY 位检测) |
WINBOND_WRITE_SR1 / WRITE_SR2 | 0x01 / 0x31 | 写状态寄存器 |
WINBOND_READ_DATA | 0x03 | 普通读 |
WINBOND_FAST_READ_DATA | 0x0B | 快速读(默认路径) |
WINBOND_FAST_READ_DUAL_OUTPUT | 0x3B | 双线快读(2BIT 模式) |
WINBOND_PAGE_PROGRAM | 0x02 | 页编程(256B/页) |
WINBOND_PAGE_ERASE | 0x81 | 页擦除(FLASH_PAGE_ERASER) |
WINBOND_SECTOR_ERASE | 0x20 | 扇区擦除(FLASH_SECTOR_ERASER) |
WINBOND_BLOCK_ERASE | 0xD8 | 块擦除(FLASH_BLOCK_ERASER) |
WINBOND_CHIP_ERASE | 0xC7 | 全片擦除(FLASH_CHIP_ERASER) |
WINBOND_JEDEC_ID | 0x9F | 读 JEDEC ID 与容量 |
WINBOND_POWER_DOWN | 0xB9 | 进入掉电模式(_norflash_power_down) |
WINBOND_RELEASE_POWER_DOWN | 0xAB | 释放掉电模式(_norflash_release_power_down) |
Source: norflash.h
API 参考
int _norflash_read(u32 addr, u8 *buf, u32 len, u8 cache)
按绝对地址从 Flash 读取数据。
参数:
addr(u32):Flash 绝对地址;buf(u8 *):接收缓冲区;len(u32):读取长度(字节);cache(u8):FLASH_CACHE_ENABLE时传 1 启用 4KB 缓存命中检查,0 强制真读。
返回: 0 成功(当前实现恒返回 0,SPI 层错误通过 NULL 参数丢弃)。
Source: norflash.c
static int _norflash_write_pages(u32 addr, u8 *buf, u32 len)
按绝对地址分页写入,自动切页、逐页发写使能。
参数: 同 _norflash_read(无 cache 参数)。
返回: 0 成功;1 某页编程后等待 BUSY 超时。
Source: norflash.c
int _norflash_eraser(u8 eraser, u32 addr)
执行指定粒度的擦除(页/扇区/块/芯片)。
参数:
eraser(u8):FLASH_PAGE_ERASER/FLASH_SECTOR_ERASER/FLASH_BLOCK_ERASER/FLASH_CHIP_ERASER;addr(u32):擦除地址(芯片擦除时忽略)。
返回: 0 成功;非 0 等待失败。
设备接口(上层通过设备抽象层调用)
| 接口 | 对应 ops 回调 | 说明 |
|---|---|---|
dev_open(name, arg) | norflash_dev_open | 打开设备,返回 struct device *;失败返回 NULL |
dev_byte_read(dev, buf, addr, len) | norflash_byte_read | 以 1 字节为单位的读 |
dev_byte_write(dev, buf, addr, len) | norflash_byte_write | 以 1 字节为单位的写(写前不擦除、不检查) |
dev_bulk_read(dev, buf, blk, cnt) | norflash_bulk_read | 以 512 字节块为单位的读 |
dev_bulk_write(dev, buf, blk, cnt) | norflash_bulk_write | 以 512 字节块为单位的写 |
dev_ioctl(dev, cmd, arg) | norflash_byte_ioctl | IOCTL_ERASE_SECTOR / IOCTL_ERASE_BLOCK / IOCTL_ERASE_CHIP / IOCTL_GET_CAPACITY 等 |
dev_close(dev) | norflash_dev_close | 关闭设备,引用计数归零时真正关 SPI |
IOCTL_GET_CAPACITY 语义(源码注释):多分区注册时返回分区大小;仅注册 1 个分区且起始地址为 0 时返回 Flash 总容量;起始地址非 0 时返回分区大小。返回长度以 1 字节为单位。
故障模式、边界情况与并发
互斥与并发访问
NORFLASH_NO_SYS(裸机)模式下,驱动用自定义信号量实现互斥:
typedef volatile u8 flash_mutex;
static inline void flash_mutex_create(flash_mutex *sem, u8 count) { *sem = count; }
static inline void flash_mutex_post(flash_mutex *sem) { (*sem) = 1; }
static inline s8 flash_mutex_pend(flash_mutex *sem, u32 timeout)// 当为timeout=0时,死等
{
u32 _timeout = timeout + jiffies;
extern void wdt_clear();
while (1) {
if (*sem) { (*sem) = 0; break; }
if ((timeout != 0) && (_timeout < jiffies)) {
return -1;
}
wdt_clear();
}
return 0;
}
static inline s8 query_flash_mutex_pend(flash_mutex *sem, u32 timeout)
{
while (1) {
if (*sem) { (*sem) = 0; break; }
return -1;
}
return 0;
}
Source: norflash.h
要点与隐患:
flash_mutex_pend在等待期间主动wdt_clear()喂狗,避免长时间等锁导致看门狗复位;timeout=0时死等。query_flash_mutex_pend是非阻塞版本(拿不到立即返回-1),被缓存同步定时器使用:拿不到锁就放弃本轮同步,而不是阻塞定时器。_norflash_read/_norflash_write_pages中的互斥调用被注释掉(/* flash_mutex_pend(...) */),说明当前字节接口的串行化主要依赖上层调用方与缓存同步的临界区;_norflash_cache_sync_timer在同步前用OS_ENTER_CRITICAL()+flash_mutex_pend双重保护。flash_cache_sync_enter_critical_dis可临时关闭临界区进入,用于避免在关中断的上下文中调用同步逻辑造成死锁——这是一处需要调用方与驱动配合的约定。
等待超时
_norflash_wait_ok 轮询 BUSY 位有固定超时;超时返回 1,上层应视为该操作失败。若擦除/编程期间掉电或芯片异常,BUSY 一直为 1,驱动会打印 norflash_wait_ok timeout! 并返回错误,不会死循环。
分区校验
- 地址重叠:
norflash_verify_part拒绝"起始地址落在其他分区范围内且名字不同"的分区; - 越界:open 时
ASSERT(_norflash.max_end_addr <= _norflash.flash_capacity),分区结束地址超过 Flash 容量直接触发断言(调试期即暴露配置错误)。
Flash ID 异常
open 时若 ID 为 0 或 0xffffff(读不到芯片 / 片选错误 / SPI 配置错误),返回 -EFAULT(errno-base.h)并打印 re-open norflash id error!。norflash_dev_ioctl 在 SPI_SD_IO_REUSE 下若 sd_io_reuse_suspend() 失败(SD 正忙),打印 sd io suspend fail 并直接返回错误,避免与 SD 卡控制器争抢总线。
掉电与脏缓存
脏缓存数据在关闭设备(_norflash_close)或定时同步时才回写。若在回写前掉电,最近写入的数据会丢失("擦除→写回"不是原子的)。这是"写合并提升寿命与性能"与"掉电一致性"之间的取舍,需要上层文件系统配合(如 journal、幂等写)。_norflash_close 中同步失败只打日志不阻塞返回(注释明确"错了不能 return,只提示出错")。
大容量 Flash 的 4 字节地址
S25FL 系列 >16MB 需要 4 字节地址模式:S25FL512_enter_4byte_addr 通过读/写 Bank Register(0x16/0x17)切换地址模式并回读验证,失败返回 1;_S25FLxxx_read_id 还通过配置寄存器(0x01 命令写 0x80)设置读时钟限制(<104MHz)并回读 0x35 验证。这些步骤必须在任何读写前完成,否则大容量地址访问越界。
性能与运维考虑
- 读路径:默认走
WINBOND_FAST_READ_DATA(0x0B)+ DMA,2BIT 模式启用双线快读;命中 4KB 缓存时零 SPI 开销。注意 4BIT 在 SPI1 上不支持,代码会打印错误并回退。 - 写路径:页编程每页都等待 BUSY(页编程典型数百微秒);
FLASH_CACHE_ENABLE把小写合并为整扇区擦写,写性能与寿命显著提升。sync_step可调同步频率(N×100ms),频率越低写放大越小、但掉电丢数据窗口越大。 - 定时同步抢占:缓存同步在定时器上下文执行且可能占用 SPI 总线数百毫秒(擦除+16 页写回),
SPI_SD_IO_REUSE时需先抢占 SD 总线;若 SD 繁忙则本轮放弃。对实时性敏感的任务需评估该阻塞窗口。 - 日志:驱动使用
LOG_TAG "[flash]",LOG_ERROR_ENABLE/LOG_INFO_ENABLE已开启;open success、close done、wait_ok timeout、sd io suspend fail等日志是排查 Flash 问题的主要线索。 - 内存占用:静态缓存
norfs_cache_buf[4096](4KB)放在.norflash_cache_buf段,MALLOC_EN=0时零动态分配;分区表、norflash_info均为静态,适合小内存 MCU。
扩展点
- 新增 Flash 型号适配:在
_norflash_open的 ID 分支中扩展(现有 S25FL 系列 4 字节地址/配置寄存器初始化即为此模式),或参照_S25FLxxx_read_id增加型号专用初始化序列。 - 新增分区/设备名:复用
norfs_dev_ops注册新的设备名并填写不同start_addr/size(平台数据),即可把同一物理 Flash 切分为多个逻辑设备(如存储区 + 升级区)。 - 新增 ops 变体:源码注释明确"如果需要驱动自己处理擦除……新建一个符合需求的 ops",即复制
norfs_dev_ops的结构,修改read/write回调中的cache实参即可实现"驱动内擦写"语义。 - 软硬件 SPI 切换:
TCFG_FLASH_SPI_TYPE_SELECT仅影响宏映射,驱动核心逻辑不变;新增 SPI 控制器时只需补全对应宏映射。
测试覆盖
norflash.c 内置两个测试函数(默认 #if 0 关闭,且"两种 flash 不能同时运行"):
norflash_test():覆盖dev_open(__EXT_FLASH_NANE)、dev_bulk_write/read(512B 块)、dev_ioctl(IOCTL_ERASE_SECTOR)、dev_byte_read/write(1B 粒度)及log_info_hexdump校验;s25_flash_test():覆盖dev_open("S25FL256")、按地址选择IOCTL_ERASE_SECTOR/IOCTL_ERASE_BLOCK的擦除路径与字节读写回读校验。
上层文件系统的读写压力测试、掉电测试需在对应文件系统目录中验证,本驱动页不赘述。
Related Links
- norflash.h(命令集、平台数据、互斥锁)
- norflash.c(驱动实现)
- ini_area_norflash.c(norflash 初始化区域)
- flash_wp.h(Flash 写保护)
- 文件系统层(norfs/fatfs 挂载与读写逻辑)——见文件系统目录页
- 构建与烧录布局(
post_build/*/dir_ex_flash、flash_write_protect/flash_wp_info.csv)——见构建/烧录目录页