5.25 SFlash SPI Flash设备
5.25 SFlash SPI Flash设备
📚 本节导读
学习时长: 约 35 分钟
难度级别: ⭐⭐⭐⭐☆
前置知识: C 语言基础、SPI 总线基础、5.1 设备驱动框架
本节讲解 OneOS 的 SFlash(SPI Flash)设备。SFlash 是一套通过 SPI/QSPI 接口访问 NOR Flash 芯片的驱动框架。它最大的亮点是 SFDP(Serial Flash Discoverable Parameters,JESD216 标准)自动探测——很多新款 Flash 芯片会通过 SFDP 表"自我介绍"容量、页大小、擦除大小等参数,SFlash 读取 SFDP 就能免去手动配置芯片参数。
本节会带你:
- 理解 SFlash → SFBus → SPI 硬件层 的分层架构;
- 看懂 SFDP 自动探测 如何在
os_sflash_init中按"芯片 ID → SFDP → 默认信息"三级兜底获取芯片参数; - 逐个拆解
os_sflash_init/os_sflash_read_page/os_sflash_write_page/os_sflash_erase_block的完整源码; - 掌握页读写、块擦除的标准操作顺序(解锁 → 等待空闲 → 操作 → 等待空闲)。
说明:本节所有源码均取自项目
drivers/sflash/sflash.c,与项目源码逐字一致(含原始英文注释)。为便于阅读,个别重复结构已省略,但不影响对核心逻辑的理解。
🎯 学习目标
- 理解 SFlash SPI Flash 驱动框架的分层架构
- 掌握 SFDP 自动探测机制与"芯片 ID → SFDP → 默认信息"三级兜底策略
- 理解 SFBus 总线抽象层的作用
- 逐个拆解初始化、页读写、块擦除的完整源码
- 掌握页读写、块擦除的标准操作顺序
一、概述
1.1 SFlash 解决什么问题
一句话核心:SFlash 设备适用于需要通过 SPI/QSPI 访问 NOR Flash 芯片进行存储的场景。它让你"初始化时自动识别芯片,然后用统一接口读写擦除",无需关心底层 SPI 指令细节。
为什么用 SFlash 框架:
- 直接操作 SPI 发送指令(读 ID、读 SFDP、页编程、块擦除)繁琐易错,SFlash 封装成
read_page/write_page/erase_block三个标准接口。 - 通过 SFBus 总线抽象屏蔽 SPI/QSPI/多线模式的差异,同一份驱动代码可适配不同总线。
- 通过 SFDP 自动探测,让驱动能"识别"多种芯片,减少手动配置。
1.2 分层架构
应用层(用户代码)
│
▼
SFlash 设备层(sflash.c)
os_sflash_init / read_page / write_page / erase_block
│
▼
SFBus 总线层(sfbus.c)
os_sfbus_attach / os_sfbus_transfer(适配多线模式)
│
▼
SPI/QSPI 硬件层(具体芯片 SPI 驱动)关键理解:SFlash 不直接操作 SPI,而是通过 os_sfbus 抽象。os_sflash_init(bus_name, cs) 里调用 os_sfbus_attach(bus_name, cs) 挂到指定 SPI 总线,之后所有收发都走 os_sfbus_transfer。
1.3 SFDP 自动探测与三级兜底
在 os_sflash_init 中,芯片参数的获取顺序是 "芯片 ID → SFDP → 默认信息" 三级兜底:
os_sflash_init
│
├─ 1. 读芯片 ID(os_sflash_read_id)
│ ├─ 若 OS_SFLASH_SUPPORTED_CHIP:按 ID 在 os_sflash_info 段查表
│ └─ 找到 → sflash->info = 该芯片信息
│
├─ 2. 若上一步没找到(info == OS_NULL)
│ ├─ 若 OS_SFLASH_SUPPORT_SFDP:读取并解码 SFDP 表
│ └─ 得到 → sflash->info = SFDP 信息
│
└─ 3. 若仍没找到(info == OS_NULL)→ 用 sflash_unknown_info 默认信息关键理解 —— 为什么需要三级兜底:
- 芯片 ID:最精确,但需要
OS_SFLASH_SUPPORTED_CHIP(编译期内置已知芯片表)。 - SFDP:芯片自带的参数表,适用于表里没有的新芯片,无需改代码。
- 默认信息:兜底,至少能读能写,但参数是保守值。
1.4 默认指令配置 default_cmds
来源:drivers/sflash/sflash.c
static const struct os_xspi_message_cfg default_cmds[] = {
{
.type = OS_XSPI_MESSAGE_CFG_TYPE_READ,
.instruction = 0x03,
.instruction_lines = 1,
.address_size = 3,
.address_lines = 1,
.data_lines = 1,
},
{
.type = OS_XSPI_MESSAGE_CFG_TYPE_WRITE,
.instruction = 0x02,
.instruction_lines = 1,
.address_size = 3,
.address_lines = 1,
.data_lines = 1,
.priv = 256, /* page size */
},
{
.type = OS_XSPI_MESSAGE_CFG_TYPE_ERASE,
.instruction = 0x20,
.instruction_lines = 1,
.address_size = 3,
.address_lines = 1,
.priv = 4096, /* erase size */
},
{
.type = OS_XSPI_MESSAGE_CFG_TYPE_STATUS,
.instruction = 0x05,
.instruction_lines = 1,
.data_lines = 1,
.priv = (0 << 4) | 1, /* busy bit, busy status */
},
{
.type = OS_XSPI_MESSAGE_CFG_TYPE_UNLOCK,
.instruction = 0x06,
.instruction_lines = 1,
},
};关键理解:
default_cmds定义了最通用的 SPI NOR 指令:读0x03、写0x02、擦除0x20、状态0x05、解锁0x06。.priv字段承载额外信息:写指令的priv = 256是页大小,擦除指令的priv = 4096是擦除块大小,状态指令的priv = (0<<4)|1是"忙位 = bit0、忙时电平 = 1"。- 这些指令会在
os_sflash_prepare_cmds中按总线支持能力(几线)与芯片能力合并进sflash->cmds。
数值演算(状态指令 priv 解码):
| 字段 | 值 | 计算 | 含义 |
|---|---|---|---|
busy_bit | (0<<4)>>4 | (0<<4)>>4 = 0 | 忙位在 bit0 |
busy_sta | priv & 1 | 1 & 1 = 1 | 忙时该位电平为 1 |
os_sflash_wait_busy会不断读状态寄存器,直到"忙位不为忙电平"(即 bit0 == 0)。
1.5 SFlash 使用场景
1.5.1 一句话核心
SFlash 设备适用于通过 SPI/QSPI 访问 NOR Flash 进行代码或数据存储的场景,尤其适合希望自动识别芯片、不用手动配参数的项目。
1.5.2 典型应用场景
场景一:启动时初始化并打印芯片信息
> (上电自动初始化)
sflash: GigaDevice, W25Q128, id: 0xef4018os_sflash_init 会在 OS_INIT_LEVEL_DEVICE 阶段自动执行(见源码 OS_INIT_CALL(sflash_init, OS_INIT_LEVEL_DEVICE, OS_INIT_SUBLEVEL_MIDDLE)),打印厂商、型号、ID。
为什么用 SFlash:初始化时自动读 ID / SFDP 识别芯片,产线不用针对不同芯片改配置。
场景二:数据存储的页读写擦除
把 256 字节数据写入 Flash 第 0 页:
os_sflash_erase_block(sflash, 0); /* 先擦除块 0 */
memset(buf, 0xAA, 256); /* 填数据 */
os_sflash_write_page(sflash, 0, buf, 256); /* 写一页 */
os_sflash_read_page(sflash, 0, buf, 256); /* 读回 */为什么用 SFlash:read_page/write_page/erase_block 内部自动处理解锁、等待空闲、收发指令,用户只需给出地址和数据。
1.5.3 选型边界表(何时不该用它)
| 场景 | 该用什么 | 为什么 |
|---|---|---|
| 已用 FAL 分区的应用 | 直接通过 FAL API 访问 | SFlash 在 OS_USING_FAL 下会自动注册为 FAL Flash,用 FAL 分区更上层 |
| 超大容量并行 NAND | NAND 设备(5.28) | SFlash 是 SPI NOR,NAND 用不同框架 |
| 掉电保持配置的小数据 | EEPROM(5.26) | EEPROM 按字节擦写、寿命更长,适合频繁小量改写的配置数据 |
二、核心数据结构
2.1 SFlash 设备结构体 struct os_sflash
来源:drivers/sflash/sflash.h
struct os_sflash
{
struct os_device parent;
struct os_sfbus *sfbus;
struct os_spi_configuration config;
int cs;
const struct os_sflash_info *info;
struct os_sflash_commands cmds;
};| 字段 | 说明 |
|---|---|
parent | 继承的标准设备对象 |
sfbus | SFBus 总线对象(挂到具体 SPI 总线) |
config | SPI 配置(数据宽度、模式、频率) |
cs | 片选引脚号 |
info | 芯片信息(由 ID/SFDP/默认三级获得) |
cmds | 合并后的操作指令集(读/写/擦除/状态/解锁) |
2.2 Flash 设备信息 struct os_sflash_info
struct os_sflash_info
{
uint8_t mf; /* 厂商 ID */
uint8_t id; /* 设备 ID */
const char *name; /* 设备名称 */
const struct os_xspi_message_cfg *supported_cmds;
int supported_cmds_nr;
uint32_t capacity; /* 容量(字节) */
uint32_t page_size; /* 页大小(字节) */
uint8_t addr_bytes; /* 地址字节数 */
};字段详解(源码无注释,下表补充说明,不属于源码):
| 字段 | 含义 |
|---|---|
mf | 厂商 ID(如 Winbond=0xEF) |
id | 设备 ID(如 W25Q128 的设备号) |
name | 芯片名称(打印用,如 "W25Q128") |
supported_cmds | 该芯片自定义的指令集(可覆盖默认指令) |
supported_cmds_nr | 该芯片指令数量 |
capacity | 容量(字节) |
page_size | 页大小(字节,默认 256) |
addr_bytes | 地址字节数(3 或 4;>16MB 芯片用 4) |
关键理解 —— 芯片表如何定义:每个已知芯片用
OS_SFLASH_INFO宏(见 2.4)声明一个os_sflash_info实例放进os_sflash_info链接器段。os_sflash_find_flash_info_by_id用读到的(id<<8)|mf在段中查找匹配项(见 3.2)。supported_cmds可为芯片指定专有指令(如更快的擦除指令),框架会把它们与default_cmds合并。
2.3 SFlash 指令集结构体 struct os_sflash_commands
来源:drivers/sflash/sflash.h
struct os_sflash_commands
{
struct os_xspi_message_cfg status;
struct os_xspi_message_cfg unlock;
struct os_xspi_message_cfg read;
struct os_xspi_message_cfg write;
struct os_xspi_message_cfg erase;
};字段详解(源码无注释,下表补充说明,不属于源码):
| 字段 | 含义 |
|---|---|
status | 状态寄存器读取指令(默认 0x05,用于 os_sflash_wait_busy 轮询忙位) |
unlock | 解锁/写使能指令(默认 0x06,Write Enable,写/擦除前必须发) |
read | 读数据指令(默认 0x03,3 字节地址、1 线) |
write | 页编程指令(默认 0x02,priv=页大小 256) |
erase | 块擦除指令(默认 0x20,priv=擦除块大小 4096) |
关键理解 ——
cmds是"合并后的最终指令集":os_sflash_init里os_sflash_prepare_cmds把default_cmds和芯片info->supported_cmds合并进sflash->cmds,且只采用总线支持的线数。后续read_page/write_page/erase_block都直接引用sflash->cmds.read/write/erase构造传输消息。各指令的priv承载额外信息:write 的priv=页大小、erase 的priv=块大小、status 的priv=忙位编码(见 1.4 数值演算)。
2.4 设备信息声明宏
#define OS_SFLASH_INFO static OS_USED OS_SECTION("os_sflash_info") const struct os_sflash_infoOS_SFLASH_INFO 把声明放入 os_sflash_info 链接器段,os_sflash_find_flash_info_by_id 遍历该段按 ID 查找(见 3.2 节)。
三、核心 API 详解
说明:以下源码均取自
drivers/sflash/sflash.c,与项目源码逐字一致(含原始英文注释)。
3.1 os_sflash_init —— 初始化 SPI Flash
函数签名:
struct os_sflash *os_sflash_init(const char *bus_name, int cs);| 参数 | 说明 |
|---|---|
bus_name | SFBus/SPI 总线名称(如 "spi0") |
cs | 片选引脚号 |
| 返回 | 初始化成功返回 os_sflash* 指针,失败返回 OS_NULL |
完整源码(drivers/sflash/sflash.c,与项目一致):
struct os_sflash *os_sflash_init(const char *bus_name, int cs)
{
struct os_sflash *sflash = os_calloc(1, sizeof(struct os_sflash));
sflash->cs = cs;
sflash->sfbus = os_sfbus_attach(bus_name, cs);
struct os_spi_configuration cfg;
cfg.data_width = 8;
cfg.mode = OS_SPI_MODE_0 | OS_SPI_MSB;
cfg.max_hz = OS_SFLASH_FREQ;
os_sflash_configure(sflash, &cfg);
os_sflash_reset(sflash);
os_sflash_dummy(sflash);
uint16_t flash_id = os_sflash_read_id(sflash);
#ifdef OS_SFLASH_SUPPORTED_CHIP
sflash->info = os_sflash_find_flash_info_by_id(flash_id);
#endif
#ifdef OS_SFLASH_SUPPORT_SFDP
struct os_sfdp *sfdp = OS_NULL;
if (sflash->info == OS_NULL)
{
sfdp = os_sflash_find_flash_info_by_sfdp(sflash);
if (sfdp != OS_NULL)
{
sflash->info = &sfdp->info;
}
}
#endif
if (sflash->info == OS_NULL)
{
sflash->info = &sflash_unknown_info;
}
os_sflash_prepare_cmds(sflash, default_cmds, OS_ARRAY_SIZE(default_cmds));
os_sflash_prepare_cmds(sflash, sflash->info->supported_cmds, sflash->info->supported_cmds_nr);
if (sflash->info->addr_bytes == 4)
{
struct os_xspi_message xmsg, *xmessage = &xmsg;
sflash->cmds.read.address_size = sflash->info->addr_bytes;
sflash->cmds.write.address_size = sflash->info->addr_bytes;
sflash->cmds.erase.address_size = sflash->info->addr_bytes;
sflash_write_unlock(sflash);
memset(xmessage, 0, sizeof(struct os_xspi_message));
xmessage_instruction(xmessage) = 0xb7;
xmessage_instruction_lines(xmessage) = 1;
#ifdef OS_SFLASH_SUPPORT_SFDP
if (sfdp == OS_NULL)
{
os_sfbus_transfer(sflash, xmessage);
}
#else
os_sfbus_transfer(sflash, xmessage);
#endif
}
if (sflash->info->page_size != 0 && sflash->info->page_size < sflash->cmds.write.priv)
{
sflash->cmds.write.priv = sflash->info->page_size;
}
#ifdef OS_SFLASH_SUPPORT_SFDP
if (sfdp != OS_NULL)
{
os_sfdp_init_sflash(sflash, sfdp);
}
#endif
OS_ASSERT(sflash->cmds.read.type != OS_XSPI_MESSAGE_CFG_TYPE_NONE);
OS_ASSERT(sflash->cmds.write.type != OS_XSPI_MESSAGE_CFG_TYPE_NONE);
OS_ASSERT(sflash->cmds.write.priv != 0);
OS_ASSERT(sflash->cmds.erase.type != OS_XSPI_MESSAGE_CFG_TYPE_NONE);
OS_ASSERT(sflash->cmds.erase.priv != 0);
/* sflash manufacturer */
int i;
struct sflash_manufacturer_info *minfo = &gs_manufacturer_info[0];
for (i = 0; i < OS_ARRAY_SIZE(gs_manufacturer_info) - 1; i++, minfo++)
{
if (minfo->mf == (flash_id & 0xff))
{
break;
}
}
os_kprintf("sflash: %s, %s, id: 0x%x\r\n",
minfo->name ? minfo->name : "unknow",
sflash->info->name ? sflash->info->name : "unknow",
flash_id);
/* unprotected blocks */
os_sflash_unlock_blocks(sflash);
return sflash;
}分步详解:
第 1 步:申请结构体并挂到 SFBus(无条件执行)
struct os_sflash *sflash = os_calloc(1, sizeof(struct os_sflash));
sflash->cs = cs;
sflash->sfbus = os_sfbus_attach(bus_name, cs);详细解释:os_calloc 申请 SFlash 对象并清零。记录片选 cs。os_sfbus_attach(bus_name, cs) 打开名为 bus_name 的 SPI 总线并返回 os_sfbus*,之后所有收发都经过它。
第 2 步:配置 SPI(无条件执行)
struct os_spi_configuration cfg;
cfg.data_width = 8;
cfg.mode = OS_SPI_MODE_0 | OS_SPI_MSB;
cfg.max_hz = OS_SFLASH_FREQ;
os_sflash_configure(sflash, &cfg);详细解释:设置 SPI 为 8 位数据宽度、MODE_0 | MSB、最高频率 OS_SFLASH_FREQ,并调用 os_sflash_configure 应用配置。
第 3 步:复位芯片并读 ID(无条件执行)
os_sflash_reset(sflash);
os_sflash_dummy(sflash);
uint16_t flash_id = os_sflash_read_id(sflash);详细解释:os_sflash_reset 发送复位指令 0x66/0x99(按支持线数),os_sflash_dummy 发 2 个哑字节。os_sflash_read_id 发 0x9f 读 3 字节 ID 并合并返回 16 位 ID。
第 4 步:按 ID 查芯片表(条件 OS_SFLASH_SUPPORTED_CHIP)
#ifdef OS_SFLASH_SUPPORTED_CHIP
sflash->info = os_sflash_find_flash_info_by_id(flash_id);
#endif详细解释:若编译了内置芯片表(OS_SFLASH_SUPPORTED_CHIP),用读到的 ID 在 os_sflash_info 段中查找匹配的芯片信息。这是三级兜底的第 1 级。
第 5 步:ID 没找到则用 SFDP 探测(条件 info == OS_NULL 且支持 SFDP)
#ifdef OS_SFLASH_SUPPORT_SFDP
struct os_sfdp *sfdp = OS_NULL;
if (sflash->info == OS_NULL)
{
sfdp = os_sflash_find_flash_info_by_sfdp(sflash);
if (sfdp != OS_NULL)
{
sflash->info = &sfdp->info;
}
}
#endif详细解释:三级兜底的第 2 级。若内置表没找到,读取并解码 SFDP 表(JESD216),得到芯片的容量、页大小等参数。
第 6 步:都用不上则用默认信息(条件 info == OS_NULL)
if (sflash->info == OS_NULL)
{
sflash->info = &sflash_unknown_info;
}详细解释:三级兜底的第 3 级。用 sflash_unknown_info(默认 16MB、标准指令)兜底,保证至少能读写。
第 7 步:合并指令集(无条件执行)
os_sflash_prepare_cmds(sflash, default_cmds, OS_ARRAY_SIZE(default_cmds));
os_sflash_prepare_cmds(sflash, sflash->info->supported_cmds, sflash->info->supported_cmds_nr);详细解释:把默认指令集和芯片自带指令集合并进 sflash->cmds。os_sflash_prepare_cmds 会检查总线支持几线(os_sflash_bus_support_lines),只采用总线能支持的指令(见 3.2 节)。
第 8 步:4 字节地址模式处理(条件 addr_bytes == 4)
if (sflash->info->addr_bytes == 4)
{
struct os_xspi_message xmsg, *xmessage = &xmsg;
sflash->cmds.read.address_size = sflash->info->addr_bytes;
sflash->cmds.write.address_size = sflash->info->addr_bytes;
sflash->cmds.erase.address_size = sflash->info->addr_bytes;
sflash_write_unlock(sflash);
memset(xmessage, 0, sizeof(struct os_xspi_message));
xmessage_instruction(xmessage) = 0xb7;
xmessage_instruction_lines(xmessage) = 1;
#ifdef OS_SFLASH_SUPPORT_SFDP
if (sfdp == OS_NULL)
{
os_sfbus_transfer(sflash, xmessage);
}
#else
os_sfbus_transfer(sflash, xmessage);
#endif
}详细解释:若芯片地址是 4 字节(大容量 >16MB),把读/写/擦除指令的地址大小设为 4,解锁后发送 0xb7(Enter 4-Byte Address Mode)指令进入 4 字节寻址模式。
第 9 步:按芯片页大小收紧写页大小(条件 芯片页大小更小)
if (sflash->info->page_size != 0 && sflash->info->page_size < sflash->cmds.write.priv)
{
sflash->cmds.write.priv = sflash->info->page_size;
}详细解释:若芯片报告的页大小小于默认的 256,用更小的页大小,避免跨页写溢出。
第 10 步:SFDP 额外初始化并校验(条件 SFDP 存在;无条件断言)
#ifdef OS_SFLASH_SUPPORT_SFDP
if (sfdp != OS_NULL)
{
os_sfdp_init_sflash(sflash, sfdp);
}
#endif
OS_ASSERT(sflash->cmds.read.type != OS_XSPI_MESSAGE_CFG_TYPE_NONE);
OS_ASSERT(sflash->cmds.write.type != OS_XSPI_MESSAGE_CFG_TYPE_NONE);
OS_ASSERT(sflash->cmds.write.priv != 0);
OS_ASSERT(sflash->cmds.erase.type != OS_XSPI_MESSAGE_CFG_TYPE_NONE);
OS_ASSERT(sflash->cmds.erase.priv != 0);详细解释:若有 SFDP,调用 os_sfdp_init_sflash 完成芯片级初始化(如进入 QPI 模式)。随后用 OS_ASSERT 断言读/写/擦除指令及其 priv(页大小/擦除大小)都已正确配置。
第 11 步:打印芯片信息并解锁块(无条件执行)
int i;
struct sflash_manufacturer_info *minfo = &gs_manufacturer_info[0];
for (i = 0; i < OS_ARRAY_SIZE(gs_manufacturer_info) - 1; i++, minfo++)
{
if (minfo->mf == (flash_id & 0xff))
{
break;
}
}
os_kprintf("sflash: %s, %s, id: 0x%x\r\n",
minfo->name ? minfo->name : "unknow",
sflash->info->name ? sflash->info->name : "unknow",
flash_id);
/* unprotected blocks */
os_sflash_unlock_blocks(sflash);
return sflash;
}详细解释:在厂商表 gs_manufacturer_info 中按厂商 ID(flash_id & 0xff)查找厂商名,打印 sflash: <厂商>, <型号>, id: 0x...。最后 os_sflash_unlock_blocks 把各块解除写保护,返回 SFlash 对象。
os_sflash_init() 条件走向总结:
os_sflash_init() 条件走向总结:
├── 申请对象、挂 SFBus、配置 SPI
├── 复位 + 读 ID
├── #if OS_SFLASH_SUPPORTED_CHIP → 按 ID 查表(第 1 级)
├── #if OS_SFLASH_SUPPORT_SFDP
│ └── info == OS_NULL → 读 SFDP(第 2 级)
├── info == OS_NULL → 用默认信息(第 3 级)
├── 合并指令集(默认 + 芯片自带)
├── addr_bytes == 4 → 设 4 字节地址、发 0xb7 进入 4B 模式
├── 芯片页大小更小 → 收紧写页大小
├── #if SFDP 且 sfdp != OS_NULL → os_sfdp_init_sflash
├── OS_ASSERT 校验读/写/擦除指令配置
├── 查找厂商名并打印 "sflash: <厂商>, <型号>, id: 0x..."
├── os_sflash_unlock_blocks
└── 返回 sflash3.2 指令准备 os_sflash_prepare_cmds 与总线支持检测
一句话定位:把指令集合并进 sflash->cmds,只采用总线支持线数的指令。
核心源码 os_sflash_prepare_cmds(drivers/sflash/sflash.c,与项目逐字一致):
static void os_sflash_prepare_cmds(struct os_sflash *sflash, const struct os_xspi_message_cfg *cmds, int cmds_nr)
{
int i;
struct os_sfbus *sfbus = sflash->sfbus;
const struct os_xspi_message_cfg *cfg = cmds;
for (i = 0; i < cmds_nr; i++, cfg++)
{
OS_ASSERT(cfg->type != OS_XSPI_MESSAGE_CFG_TYPE_NONE);
if (cfg->type == OS_XSPI_MESSAGE_CFG_TYPE_READ && os_sflash_bus_support_lines(sfbus, cfg))
{
os_sflash_cmd_set(&sflash->cmds.read, cfg);
}
if (cfg->type == OS_XSPI_MESSAGE_CFG_TYPE_WRITE && os_sflash_bus_support_lines(sfbus, cfg))
{
os_sflash_cmd_set(&sflash->cmds.write, cfg);
}
if (cfg->type == OS_XSPI_MESSAGE_CFG_TYPE_ERASE && os_sflash_bus_support_lines(sfbus, cfg))
{
os_sflash_cmd_set(&sflash->cmds.erase, cfg);
}
if (cfg->type == OS_XSPI_MESSAGE_CFG_TYPE_UNLOCK && os_sflash_bus_support_lines(sfbus, cfg))
{
os_sflash_cmd_set(&sflash->cmds.unlock, cfg);
}
if (cfg->type == OS_XSPI_MESSAGE_CFG_TYPE_STATUS && os_sflash_bus_support_lines(sfbus, cfg))
{
os_sflash_cmd_set(&sflash->cmds.status, cfg);
}
}
}分步详解:
第 1 步:遍历指令集(无条件执行)
for (i = 0; i < cmds_nr; i++, cfg++)
{
OS_ASSERT(cfg->type != OS_XSPI_MESSAGE_CFG_TYPE_NONE);详细解释:遍历 cmds 数组的每个指令配置。断言指令类型非 NONE(无效指令直接暴露)。
第 2 步:按类型分发到对应槽位(条件 类型匹配 且 总线支持)
if (cfg->type == OS_XSPI_MESSAGE_CFG_TYPE_READ && os_sflash_bus_support_lines(sfbus, cfg))
{
os_sflash_cmd_set(&sflash->cmds.read, cfg);
}
/* WRITE / ERASE / UNLOCK / STATUS 同理 */详细解释:对每种类型(READ/WRITE/ERASE/UNLOCK/STATUS),先 os_sflash_bus_support_lines 确认总线支持该指令的线数,再 os_sflash_cmd_set 把指令合并进 sflash->cmds 对应槽位。两次 os_sflash_prepare_cmds 调用(默认 + 芯片)会把更好的指令覆盖进 cmds。
os_sflash_cmd_set(指令合并,os_sflash_cmd_set 辅助函数,逐字一致):
static void os_sflash_cmd_set(struct os_xspi_message_cfg *cmd, const struct os_xspi_message_cfg *cfg)
{
if (cfg->instruction_lines != 1)
{
return;
}
if (cmd->type == OS_XSPI_MESSAGE_CFG_TYPE_NONE)
{
}
else if (cfg->type == OS_XSPI_MESSAGE_CFG_TYPE_READ)
{
if (!((cmd->data_lines == 1 && cmd->address_lines == 1 && cmd->instruction_lines == 1) ||
cmd->data_lines < cfg->data_lines ||
(cmd->data_lines == cfg->data_lines && cmd->address_lines < cfg->address_lines) ||
(cmd->data_lines == cfg->data_lines && cmd->address_lines == cfg->address_lines &&
cmd->instruction_lines < cfg->instruction_lines)))
{
return;
}
}
else if (cfg->type == OS_XSPI_MESSAGE_CFG_TYPE_WRITE)
{
/* priv is page size */
if (cmd->priv < cfg->priv)
{
return;
}
}
else if (cfg->type == OS_XSPI_MESSAGE_CFG_TYPE_ERASE)
{
/* priv is erase size */
if (cmd->priv < cfg->priv)
{
return;
}
}
*cmd = *cfg;
}os_sflash_cmd_set 分步详解:
第 1 步:指令必须 1 线(条件 instruction_lines != 1 直接返回)
if (cfg->instruction_lines != 1)
{
return;
}详细解释:指令(命令字节)总是 1 线发送(即使是 QSPI 模式,命令码也是单线),非 1 线指令直接忽略。
第 2 步:读指令按"更优线数"决定是否替换(条件 读类型)
else if (cfg->type == OS_XSPI_MESSAGE_CFG_TYPE_READ)
{
if (!((cmd->data_lines == 1 && cmd->address_lines == 1 && cmd->instruction_lines == 1) ||
cmd->data_lines < cfg->data_lines ||
(cmd->data_lines == cfg->data_lines && cmd->address_lines < cfg->address_lines) ||
(cmd->data_lines == cfg->data_lines && cmd->address_lines == cfg->address_lines &&
cmd->instruction_lines < cfg->instruction_lines)))
{
return;
}
}详细解释:读指令的"更好"判断——只有当新指令 cfg 比已设置的 cmd 线数更多/更优时才覆盖(保持当前已有指令为空或更差时)。这样芯片自带的 QSPI 高速读指令能替换默认 1 线读。
第 3 步:写/擦除按"更大 priv"决定(条件 写/擦除类型)
else if (cfg->type == OS_XSPI_MESSAGE_CFG_TYPE_WRITE)
{
/* priv is page size */
if (cmd->priv < cfg->priv)
{
return;
}
}详细解释:写指令的 priv 是页大小——若已有指令页大小更大,说明当前指令更优(能一次写更多),新指令不覆盖;否则覆盖。擦除同理(priv 是擦除块大小)。
第 4 步:覆盖(无条件执行,走到这里即覆盖)
*cmd = *cfg;
}详细解释:只有前面的条件都通过(当前指令更差/为空)才执行 *cmd = *cfg,把新指令配置覆盖进 sflash->cmds 对应槽位。
os_sflash_bus_support_lines(总线线数检测,含辅助宏,逐字一致):
#define OS_SFLASH_BUS_CHECK_SUPPORT_N_LINES(sfbus, cfg, n) \
do \
{ \
if (cfg->instruction_lines == n && sfbus->support_##n##_line == 0) \
return OS_FALSE; \
if (cfg->address_lines == n && sfbus->support_##n##_line == 0) \
return OS_FALSE; \
if (cfg->alternate_lines == n && sfbus->support_##n##_line == 0) \
return OS_FALSE; \
if (cfg->data_lines == n && sfbus->support_##n##_line == 0) \
return OS_FALSE; \
} while (0)
static os_bool_t os_sflash_bus_support_lines(struct os_sfbus *sfbus, const struct os_xspi_message_cfg *cfg)
{
OS_SFLASH_BUS_CHECK_SUPPORT_N_LINES(sfbus, cfg, 8);
OS_SFLASH_BUS_CHECK_SUPPORT_N_LINES(sfbus, cfg, 4);
OS_SFLASH_BUS_CHECK_SUPPORT_N_LINES(sfbus, cfg, 2);
OS_SFLASH_BUS_CHECK_SUPPORT_N_LINES(sfbus, cfg, 1);
return OS_TRUE;
}分步详解:
第 1 步:逐一检查各线数支持(无条件执行)
OS_SFLASH_BUS_CHECK_SUPPORT_N_LINES(sfbus, cfg, 8);
OS_SFLASH_BUS_CHECK_SUPPORT_N_LINES(sfbus, cfg, 4);
OS_SFLASH_BUS_CHECK_SUPPORT_N_LINES(sfbus, cfg, 2);
OS_SFLASH_BUS_CHECK_SUPPORT_N_LINES(sfbus, cfg, 1);
return OS_TRUE;详细解释:宏 OS_SFLASH_BUS_CHECK_SUPPORT_N_LINES(sfbus, cfg, n) 检查:若指令/地址/备用/数据任一线数等于 n 而总线 support_n_line == 0(不支持 n 线),直接返回 OS_FALSE。对 8/4/2/1 线都检查完才返回 OS_TRUE。
os_sflash_bus_support_lines() 条件走向总结:
os_sflash_bus_support_lines() 条件走向总结:
├── 检查 8 线:指令/地址/备用/数据任一为 8 线且总线不支持 → 返回 OS_FALSE
├── 检查 4 线:任一为 4 线且总线不支持 → 返回 OS_FALSE
├── 检查 2 线:任一为 2 线且总线不支持 → 返回 OS_FALSE
├── 检查 1 线:任一为 1 线且总线不支持 → 返回 OS_FALSE
└── 全部支持 → 返回 OS_TRUE3.3 os_sflash_read_page —— 读一页数据
函数签名:
int os_sflash_read_page(struct os_sflash *sflash, uint32_t offset, uint8_t *buf, os_size_t size);| 参数 | 说明 |
|---|---|
sflash | SFlash 对象 |
offset | 读的起始地址(字节偏移) |
buf | 读出的数据缓冲区 |
size | 读取字节数 |
| 返回 | 传输结果(0 成功) |
完整源码(drivers/sflash/sflash.c,与项目一致):
int os_sflash_read_page(struct os_sflash *sflash, uint32_t offset, uint8_t *buf, os_size_t size)
{
OS_ASSERT(sflash != OS_NULL);
struct os_xspi_message xmsg, *xmessage = &xmsg;
memset(xmessage, 0, sizeof(struct os_xspi_message));
xmessage->xcfg = sflash->cmds.read;
xmessage_address(xmessage) = offset;
xmessage_data_size(xmessage) = size;
xmessage_data_dir(xmessage) = OS_XSPI_DATA_DIR_DEVICE_TO_HOST;
xmessage_data(xmessage) = buf;
return os_sfbus_transfer(sflash, xmessage);
}分步详解:
第 1 步:构造读消息(无条件执行)
struct os_xspi_message xmsg, *xmessage = &xmsg;
memset(xmessage, 0, sizeof(struct os_xspi_message));
xmessage->xcfg = sflash->cmds.read;
xmessage_address(xmessage) = offset;
xmessage_data_size(xmessage) = size;
xmessage_data_dir(xmessage) = OS_XSPI_DATA_DIR_DEVICE_TO_HOST;
xmessage_data(xmessage) = buf;详细解释:用 sflash->cmds.read(已合并好的读指令配置,含指令码/线数/地址大小)作为 xcfg。设置地址 offset、数据大小 size、数据方向为设备→主机(DEVICE_TO_HOST,即读)、数据指针 buf。
第 2 步:通过 SFBus 传输(无条件执行)
return os_sfbus_transfer(sflash, xmessage);
}详细解释:调用 os_sfbus_transfer 把消息交给 SFBus 发送到 Flash,返回传输结果。
3.4 os_sflash_write_page —— 写一页数据
函数签名:
int os_sflash_write_page(struct os_sflash *sflash, uint32_t offset, const uint8_t *buf, os_size_t size);| 参数 | 说明 |
|---|---|
sflash | SFlash 对象 |
offset | 写的起始地址(字节偏移) |
buf | 待写入的数据缓冲区 |
size | 写入字节数 |
| 返回 | 传输结果(0 成功) |
完整源码(drivers/sflash/sflash.c,与项目一致):
int os_sflash_write_page(struct os_sflash *sflash, uint32_t offset, const uint8_t *buf, os_size_t size)
{
OS_ASSERT(sflash != OS_NULL);
int ret;
struct os_xspi_message xmsg, *xmessage = &xmsg;
/* unlock */
if (sflash->cmds.unlock.type != OS_XSPI_MESSAGE_CFG_TYPE_NONE)
{
memset(xmessage, 0, sizeof(struct os_xspi_message));
xmessage->xcfg = sflash->cmds.unlock;
os_sflash_wait_busy(sflash);
os_sfbus_transfer(sflash, xmessage);
}
/* write */
memset(xmessage, 0, sizeof(struct os_xspi_message));
xmessage->xcfg = sflash->cmds.write;
xmessage_address(xmessage) = offset;
xmessage_data_size(xmessage) = size;
xmessage_data_dir(xmessage) = OS_XSPI_DATA_DIR_HOST_TO_DEVICE;
xmessage_data(xmessage) = (uint8_t *)buf;
os_sflash_wait_busy(sflash);
ret = os_sfbus_transfer(sflash, xmessage);
os_sflash_wait_busy(sflash);
return ret;
}分步详解:
第 1 步:先解锁(条件 有解锁指令)
/* unlock */
if (sflash->cmds.unlock.type != OS_XSPI_MESSAGE_CFG_TYPE_NONE)
{
memset(xmessage, 0, sizeof(struct os_xspi_message));
xmessage->xcfg = sflash->cmds.unlock;
os_sflash_wait_busy(sflash);
os_sfbus_transfer(sflash, xmessage);
}详细解释:若配置了解锁指令(unlock.type != NONE),先等待 Flash 空闲(os_sflash_wait_busy),再发解锁指令 0x06(Write Enable)。为什么写前要解锁:NOR Flash 默认写保护,必须先发 Write Enable 才能编程。
第 2 步:构造并发送写消息(无条件执行)
/* write */
memset(xmessage, 0, sizeof(struct os_xspi_message));
xmessage->xcfg = sflash->cmds.write;
xmessage_address(xmessage) = offset;
xmessage_data_size(xmessage) = size;
xmessage_data_dir(xmessage) = OS_XSPI_DATA_DIR_HOST_TO_DEVICE;
xmessage_data(xmessage) = (uint8_t *)buf;
os_sflash_wait_busy(sflash);
ret = os_sfbus_transfer(sflash, xmessage);
os_sflash_wait_busy(sflash);
return ret;
}详细解释:用写指令配置(cmds.write,指令 0x02),数据方向为主机→设备(HOST_TO_DEVICE)。写前等待空闲、写后等待空闲(等待编程完成),返回传输结果。为什么写后要等待:Flash 编程耗时,wait_busy 保证下一操作前编程完成。
3.5 os_sflash_erase_block —— 擦除一个块
函数签名:
int os_sflash_erase_block(struct os_sflash *sflash, uint32_t offset);| 参数 | 说明 |
|---|---|
sflash | SFlash 对象 |
offset | 要擦除块的起始地址(字节偏移) |
| 返回 | 传输结果(0 成功) |
完整源码(drivers/sflash/sflash.c,与项目一致):
int os_sflash_erase_block(struct os_sflash *sflash, uint32_t offset)
{
OS_ASSERT(sflash != OS_NULL);
int ret;
struct os_xspi_message xmsg, *xmessage = &xmsg;
/* unlock */
if (sflash->cmds.unlock.type != OS_XSPI_MESSAGE_CFG_TYPE_NONE)
{
memset(xmessage, 0, sizeof(struct os_xspi_message));
xmessage->xcfg = sflash->cmds.unlock;
os_sflash_wait_busy(sflash);
os_sfbus_transfer(sflash, xmessage);
}
/* erase */
memset(xmessage, 0, sizeof(struct os_xspi_message));
xmessage->xcfg = sflash->cmds.erase;
xmessage_address(xmessage) = offset;
os_sflash_wait_busy(sflash);
ret = os_sfbus_transfer(sflash, xmessage);
os_sflash_wait_busy(sflash);
return ret;
}分步详解:
第 1 步:先解锁(条件 有解锁指令)
/* unlock */
if (sflash->cmds.unlock.type != OS_XSPI_MESSAGE_CFG_TYPE_NONE)
{
memset(xmessage, 0, sizeof(struct os_xspi_message));
xmessage->xcfg = sflash->cmds.unlock;
os_sflash_wait_busy(sflash);
os_sfbus_transfer(sflash, xmessage);
}详细解释:与写操作相同,先解锁(Write Enable)。为什么擦除也要解锁:擦除是破坏性写操作,同样受写保护约束。
第 2 步:构造并发送擦除消息(无条件执行)
/* erase */
memset(xmessage, 0, sizeof(struct os_xspi_message));
xmessage->xcfg = sflash->cmds.erase;
xmessage_address(xmessage) = offset;
os_sflash_wait_busy(sflash);
ret = os_sfbus_transfer(sflash, xmessage);
os_sflash_wait_busy(sflash);
return ret;
}详细解释:用擦除指令配置(cmds.erase,指令 0x20,块大小 4096),设置擦除地址。擦除前等待空闲、擦除后等待空闲(等待擦除完成),返回结果。
关键理解 —— 读/写/擦除的标准操作顺序:
| 操作 | 顺序 |
|---|---|
| 读 | 无需解锁(read_page 直接构造消息传输) |
| 写 | 解锁 → 等待空闲 → 写 → 等待空闲 |
| 擦除 | 解锁 → 等待空闲 → 擦除 → 等待空闲 |
读不需要解锁是因为读不会改写 Flash;写/擦除是破坏性写操作,必须先 Write Enable(解锁),并等 Flash 空闲后再操作。
四、完整使用示例
示例:初始化并读写擦除
#include <sflash.h>
#include <string.h>
void sflash_demo(void)
{
struct os_sflash *sflash;
uint8_t buf[256];
int ret;
/* 1. 初始化:挂到 spi0 总线、片选 0,自动读 ID/SFDP 识别芯片 */
sflash = os_sflash_init("spi0", 0);
if (sflash == OS_NULL)
{
os_kprintf("sflash init failed\r\n");
return;
}
/* 2. 擦除块 0(地址 0,擦除 4096 字节) */
ret = os_sflash_erase_block(sflash, 0);
if (ret != 0)
{
os_kprintf("erase failed\r\n");
return;
}
/* 3. 填充一页数据并写入页 0 */
memset(buf, 0xAA, sizeof(buf));
ret = os_sflash_write_page(sflash, 0, buf, sizeof(buf));
if (ret != 0)
{
os_kprintf("write failed\r\n");
return;
}
/* 4. 读回并校验 */
memset(buf, 0x00, sizeof(buf));
ret = os_sflash_read_page(sflash, 0, buf, sizeof(buf));
if (ret != 0)
{
os_kprintf("read failed\r\n");
return;
}
/* 5. 校验数据是否写入正确 */
for (int i = 0; i < (int)sizeof(buf); i++)
{
if (buf[i] != 0xAA)
{
os_kprintf("verify failed at %d\r\n", i);
return;
}
}
os_kprintf("sflash write/read/verify ok\r\n");
}说明:示例展示 SFlash 标准用法:
init → erase → write → read → verify。实际效果:擦除把块 0 清成0xff,写把0xAA写入,读回校验一致则打印ok。注意read_page无需解锁,write_page/erase_block内部自动解锁。
注意:
os_sflash_init里用到了OS_SFLASH_BUS_NAME/OS_SFLASH_CS_PIN(sflash_init的 FAL 注册路径)或用户手动传入总线名。手动初始化时bus_name需与板级 SPI 总线名一致。
五、SFlash 的适配(接入新 Flash 芯片 / 新 SPI 总线)
5.1 先认识适配层:SFlash 框架分层
drivers/sflash/sflash.c 是跨平台统一框架(os_sflash_init + read_page/write_page/erase_block + SFDP 探测),不碰具体寄存器。真正操作硬件的是 SFBus 层(drivers/sfbus/,封装 SPI/QSPI 多线传输)和底层 SPI 驱动。接入新 Flash 芯片或新 SPI 总线,都无需改框架。
适配全景图:
┌──────────────────────────────────────────────────────────┐
│ 应用层(read_page / write_page / erase_block)│
└──────────────────────────────┬───────────────────────────┘
▼
┌──────────────────────────────────────────────────────────┐
│ SFlash 框架层(drivers/sflash/sflash.c,通用不改) │
│ os_sflash_init(ID→SFDP→默认三级找参数)+ 读写擦 │
└───────────────┬──────────────────────────┬────────────────┘
│ ①实现 chip_info(可选) │ ②SFBus 总线
▼ ▼
┌──────────────────────────────────────────────────────────┐
│ SFBus 层(drivers/sfbus/sfbus.c)+ 底层 SPI/QSPI 驱动 │
│ os_sfbus_attach / os_sfbus_transfer(多线模式) │
└───────────────┬──────────────────────────┬────────────────┘
│ SPI 驱动 │
▼ ▼
┌──────────────────────────────────────────────────────────┐
│ 硬件(SPI Flash 芯片 + MCU SPI/QSPI 外设) │
└──────────────────────────────────────────────────────────┘适配的关键洞察:
- 换 SPI Flash 芯片:若芯片支持 SFDP(
OS_SFLASH_SUPPORT_SFDP),无需改代码——os_sflash_init自动读 SFDP 识别。若不支持 SFDP,用OS_SFLASH_INFO定义一个os_sflash_info(含 ID/容量/页大小/指令)放入芯片表即可。 - 换 SPI 总线/MCU:SFlash 通过
os_sfbus_attach(bus_name, cs)挂到 SFBus,SFBus 再走底层 SPI/QSPI 驱动。接入新 SPI 总线只需保证os_sfbus_attach能找到对应总线设备。
5.2 接入新 Flash 芯片(无 SFDP 芯片)的标准流程
第 1 步:查芯片手册——确认厂商 ID(mf)、设备 ID(id)、容量、页大小、地址字节数(3/4)、支持的指令。
第 2 步:定义 os_sflash_info——用 OS_SFLASH_INFO 宏在芯片表文件(如 sflash_info.c)声明实例,填入 mf/id/name/capacity/page_size/addr_bytes,supported_cmds 可留空(用默认指令)或填芯片专有指令。
/* sflash_info.c:定义 W25Q128 芯片信息 */
OS_SFLASH_INFO w25q128_info = {
.mf = 0xef, /* Winbond */
.id = 0x18, /* W25Q128 设备号 */
.name = "W25Q128",
.capacity = 16 * 1024 * 1024, /* 16MB */
.page_size = 256,
.addr_bytes = 3,
};第 3 步:确认链接脚本含 os_sflash_info 段——OS_SFLASH_INFO 用 OS_SECTION("os_sflash_info"),MDK/IAR/GCC 链接脚本都要有该段,否则芯片表进不去、按 ID 查不到。
第 4 步:测试——os_sflash_init 后看打印的厂商/型号/ID 是否正确,读写擦验证。
5.3 适配检查清单(快速自查)
| 检查项 | 说明 |
|---|---|
芯片 mf/id 与手册一致? | os_sflash_find_flash_info_by_id 按 `(id<<8) |
链接脚本有 os_sflash_info 段? | 无段则 OS_SECTION 不生效,芯片表进不了段 |
| 地址字节数 3/4 对? | >16MB 芯片必须 addr_bytes=4,否则 os_sflash_init 不进入 4B 模式、高地址读写错 |
| 总线支持线数对? | 配置了 QSPI 读但总线只支持 1 线,os_sflash_bus_support_lines 会拒绝,自动回退默认指令 |
OS_SFLASH_SUPPORTED_CHIP 开了? | 开了才编译内置表查找路径;不开仅靠 SFDP/默认 |
📝 本节小结
核心要点回顾
本节围绕 SFlash SPI Flash 设备展开,核心要点如下:
1. 分层架构
- 应用层 → SFlash 设备层 → SFBus 总线层 → SPI 硬件层。
- SFlash 通过
os_sfbus抽象,不直接操作 SPI。
2. SFDP 自动探测与三级兜底
- 获取芯片参数顺序:芯片 ID → SFDP → 默认信息。
- 内置表精确、SFDP 免配置、默认信息兜底。
3. 四个核心函数
| 函数 | 作用 | 关键点 |
|---|---|---|
os_sflash_init | 初始化并识别芯片 | 复位→读 ID→三级找参数→合并指令→解锁块 |
os_sflash_read_page | 读一页 | 无需解锁,直接传输 |
os_sflash_write_page | 写一页 | 解锁→等待→写→等待 |
os_sflash_erase_block | 擦除一块 | 解锁→等待→擦→等待 |
4. 指令配置
- 默认指令:读
0x03、写0x02、擦除0x20、状态0x05、解锁0x06。 os_sflash_prepare_cmds按总线支持线数合并指令;os_sflash_cmd_set按"更优线数/更大 priv"决定是否覆盖。
5. 硬件适配
- 换芯片:SFDP 芯片免配置;无 SFDP 芯片用
OS_SFLASH_INFO定义芯片表(须确认链接脚本含os_sflash_info段)。 - 换总线:SFlash 走
os_sfbus_attach挂到 SFBus,接入新 SPI/QSPI 总线无需改框架。
关键易错点速查
| 易错点 | 正确做法 |
|---|---|
| 忘记擦除就直接写 | NOR Flash 只能把 1 写为 0,写前必须先擦除对应块,否则写入失败或数据错 |
| 写/擦除前未解锁 | 写/擦除内部会先解锁(0x06 Write Enable),但前提是配置了 unlock 指令 |
| 忽略总线线数支持 | 若总线不支持 4 线,却配置 4 线指令,os_sflash_bus_support_lines 会拒绝该指令 |
| 跨页写入 | 页大小由 cmds.write.priv 决定,一次写入不应超过页大小,否则可能跨页失败 |
| 误以为所有芯片都要配表 | 支持 SFDP 的芯片在 OS_SFLASH_SUPPORT_SFDP 下可自动识别,无需内置表 |
| 混淆读与写的解锁 | 读不需要解锁;写/擦除必须先解锁并等待空闲 |
芯片 mf/id 填错 | os_sflash_find_flash_info_by_id 按 `(id<<8) |
大容量芯片 addr_bytes 没设 4 | >16MB 芯片必须 addr_bytes=4,否则 os_sflash_init 不进入 4B 模式、高地址读写错 |
下一步
掌握了 SFlash SPI Flash 设备后,接下来学习 EEPROM 存储设备:
- 5.26 EEPROM设备:了解 OneOS 如何通过 I2C 总线访问 AT24Cxx 系列 EEPROM,并集成到 FAL。