STM32下SFUD驱动W25Q64的完整移植与SPI配置
SPI Flash是嵌入式系统中关键的非易失性存储介质,广泛用于代码存储、参数配置和日志记录。其核心原理基于标准SPI协议(Mode 0/3,CPOL0/1、CPHA0/1),通过指令集(如0x03读、0x06写使能、0x20扇区擦除)控制数据存取。技术价值在于以低引脚开销、高可靠性实现大容量外部存储扩展,显著降低BOM成本。典型应用场景包括STM32等MCU平台的固件升级(OTA)、设备配置持久
1. SPI Flash驱动架构与SFUD设计原理
在嵌入式系统中,外部串行Flash作为非易失性存储介质,承担着程序代码存储、参数配置、日志记录等关键任务。W25Qxx系列SPI Flash因其高可靠性、大容量和成熟生态,成为STM32平台最常用的外挂存储器件之一。然而,不同厂商、不同型号的SPI Flash在指令集、时序要求、寄存器结构上存在显著差异——例如Winbond的W25Q64、Macronix的MX25L6405D、Adesto的AT25DF641,虽同属SPI接口,但读取状态寄存器的命令码(0x05 vs 0x07)、写使能序列(0x06)、扇区擦除命令(0x20 vs 0xD8)均不统一。若为每种Flash单独编写驱动,将导致代码冗余、维护困难、移植成本高昂。
SFUD(Serial Flash Universal Driver)正是为解决这一工程痛点而生的开源通用驱动框架。它由RT-Thread团队主导开发,核心思想是“抽象硬件差异,统一软件接口”。SFUD不直接操作具体Flash芯片,而是通过一个中间层—— Flash设备描述表(Flash Device Table) ,将芯片特性参数化。驱动运行时,根据实际挂载的Flash型号,在表中查找到对应条目,再调用该条目所绑定的初始化、读写、擦除等函数指针,从而实现“一套驱动,适配百种Flash”。
其内存占用极小:最小配置下仅需约2KB ROM和128B RAM;支持SPI和QSPI两种物理接口;兼容裸机环境与FreeRTOS、RT-Thread等实时操作系统;并内置对JEDEC SFDP(Serial Flash Discoverable Parameters)标准的支持。SFDP是JEDEC固态技术协会制定的行业规范,要求Flash在出厂时将自身关键参数(如容量、块大小、指令集、时序要求)以结构化方式固化在特定地址区域。SFUD可自动读取并解析SFDP表,大幅降低人工配置错误率。对于不支持SFDP的老旧Flash,开发者只需手动填充设备表中对应字段即可,灵活性极高。
SFUD的典型部署层级如下:
- 底层硬件抽象层(HAL) :封装SPI/QSPI外设的初始化、数据收发、片选控制等与MCU强相关的操作;
- Flash设备描述层(Device Table) :定义 sfud_flash_t 结构体实例,包含芯片ID、容量、块/扇区/页尺寸、支持的指令集及对应的函数指针;
- 通用驱动层(Core) :提供 sfud_read 、 sfud_write 、 sfud_erase 等标准化API,内部根据设备表动态分发调用;
- 应用层(Application) :开发者仅需调用通用API,完全无需关心底层Flash型号与物理细节。
这种分层设计,使得当硬件平台从W25Q64更换为GD25Q64时,应用层代码零修改,仅需更新设备表中的一行配置;当从SPI切换到QSPI时,也只需重写HAL层函数,上层逻辑保持不变。这正是嵌入式固件工程追求的“高内聚、低耦合”架构典范。
2. STM32 HAL库下SPI外设配置详解
在STM32平台上集成SFUD,首要任务是正确配置SPI外设。本例基于硬件原理图确认:W25Q64挂载于SPI2总线,使用软件片选(NSS),片选引脚为GPIOB Pin12(PB12)。此配置规避了硬件NSS信号在多设备共享总线时的冲突风险,赋予软件对片选时序的完全控制权,是工业级设计的推荐实践。
2.1 时钟与引脚初始化
SPI2属于APB1总线外设,其时钟源为PCLK1。根据STM32F103的数据手册,SPI2的最大工作频率为18MHz(当PCLK1=36MHz时)。为确保W25Q64稳定运行(其最大SPI时钟为80MHz,但需留足裕量),我们将SPI2主频配置为36MHz(PCLK1=36MHz),并通过预分频器设置为 2分频 ,最终SCK频率为18MHz。此频率在W25Q64的电气规格范围内,且为后续可能的QSPI升级预留了带宽空间。
GPIO初始化需严格遵循SPI通信时序:
- SCK(PB13) :推挽输出,高速模式(50MHz),初始电平无特殊要求;
- MISO(PB14) :浮空输入,因W25Q64在此引脚上具备内部上拉,故无需外部上拉;
- MOSI(PB15) :推挽输出,高速模式(50MHz),初始电平无特殊要求;
- NSS(PB12) :推挽输出,高速模式(50MHz), 初始电平必须为高 (无效态),这是SPI协议的基本要求,防止在未初始化完成时误触发Flash操作。
// GPIO初始化代码片段(基于HAL库)
__HAL_RCC_GPIOB_CLK_ENABLE();
GPIO_InitTypeDef GPIO_InitStruct = {0};
// 配置SCK, MISO, MOSI
GPIO_InitStruct.Pin = GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; // 复用推挽
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
// 配置NSS(软件片选)
GPIO_InitStruct.Pin = GPIO_PIN_12;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // 普通推挽,非复用
GPIO_InitStruct.Pull = GPIO_PULLUP; // 上拉,确保默认高电平
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
// NSS默认置高(禁用)
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_SET);
2.2 SPI2外设核心参数配置
SPI2工作在全双工主模式(Full-Duplex Master),采用Motorola标准帧格式。关键参数配置逻辑如下:
- 数据帧格式(Data Size) :设为8位。W25Q64所有指令与数据均以字节为单位传输,8位帧长最契合其协议。
- 时钟极性与相位(CPOL/CPHA) :设为
CPOL=0, CPHA=0(Mode 0)。W25Q64在空闲时SCK为低电平(CPOL=0),数据在SCK第一个上升沿采样(CPHA=0),此为SPI最常用模式,兼容性最佳。 - 波特率预分频器(Baud Rate Prescaler) :设为
SPI_BAUDRATEPRESCALER_2,即2分频。结合PCLK1=36MHz,得到SCK=18MHz,满足时序裕量要求。 - 帧格式(Frame Format) :设为
SPI_FIRSTBIT_MSB(高位先行)。W25Q64指令与地址均按MSB First顺序发送,此配置为强制要求。 - NSS管理(NSS Management) :设为
SPI_NSS_SOFT。因采用软件片选,硬件NSS引脚被禁用,所有片选操作均由软件通过PB12控制。
// SPI2初始化代码片段
__HAL_RCC_SPI2_CLK_ENABLE();
SPI_HandleTypeDef hspi2;
hspi2.Instance = SPI2;
hspi2.Init.Mode = SPI_MODE_MASTER;
hspi2.Init.Direction = SPI_DIRECTION_2LINES; // 全双工
hspi2.Init.DataSize = SPI_DATASIZE_8BIT;
hspi2.Init.CLKPolarity = SPI_POLARITY_LOW; // CPOL=0
hspi2.Init.CLKPhase = SPI_PHASE_1EDGE; // CPHA=0
hspi2.Init.NSS = SPI_NSS_SOFT; // 软件NSS
hspi2.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_2;
hspi2.Init.FirstBit = SPI_FIRSTBIT_MSB;
hspi2.Init.TIMode = SPI_TIMODE_DISABLE;
hspi2.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
hspi2.Init.CRCPolynomial = 7;
if (HAL_SPI_Init(&hspi2) != HAL_OK) {
Error_Handler(); // 初始化失败处理
}
2.3 片选(NSS)时序控制要点
软件片选的核心在于精确控制PB12的电平翻转时机,必须严格遵循W25Q64的时序要求:
- 片选激活(CS Low) :在发送任何指令前,必须确保PB12已稳定为低电平至少 Tcss (Chip Select Setup Time),W25Q64规格书规定 Tcss = 10ns ,在STM32 GPIO切换速度下可忽略,但为保险起见,建议在拉低后插入1个NOP指令或微秒级延时。
- 片选撤销(CS High) :在完成一次完整的SPI事务(包括指令、地址、数据、状态轮询)后,必须等待 Tch (Chip Select Hold Time)后才能拉高。W25Q64的 Tch = 10ns ,同样可忽略,但需确保在最后一次SPI传输完成( HAL_SPI_GetState() 返回 HAL_SPI_STATE_READY )后再执行拉高操作。
- 跨事务隔离 :连续两次Flash操作之间,CS必须返回高电平至少 Tcsd (Chip Select Disable Time),W25Q64规定 Tcsd = 30ns 。此间隔保证Flash内部状态机正确复位。
实践中,一个健壮的片选函数应如下实现:
// 定义全局SPI句柄,便于在SFUD HAL层访问
extern SPI_HandleTypeDef hspi2;
void spi_nss_low(void) {
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_RESET);
__NOP(); // 确保时序建立
}
void spi_nss_high(void) {
// 等待SPI传输完成
while (HAL_SPI_GetState(&hspi2) != HAL_SPI_STATE_READY) {
// 可加入超时机制
}
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_SET);
}
此设计将时序控制显式化,避免了HAL库 HAL_SPI_TransmitReceive 函数内部隐含的片选操作可能带来的不确定性,是驱动稳定性的基石。
3. SFUD移植核心:HAL层函数实现
SFUD的可移植性高度依赖于HAL层函数的正确实现。这些函数是SFUD核心与底层硬件的唯一接口,其质量直接决定整个驱动的可靠性。本节将逐个剖析 sfud_port.c 中必须实现的五个关键函数,并给出基于STM32 HAL库的生产级实现。
3.1 sfud_spi_port_init :SPI端口初始化
此函数在SFUD初始化阶段被首次调用,负责完成SPI外设、GPIO及片选引脚的初始化。其核心任务并非重复执行 HAL_SPI_Init ,而是 确保SPI外设处于已知、就绪状态 。在多Flash场景下,此函数可能被多次调用(每个Flash实例一次),因此必须具备幂等性。
#include "sfud.h"
#include "stm32f1xx_hal.h"
// 全局SPI句柄声明(需在main.c中定义)
extern SPI_HandleTypeDef hspi2;
// SFUD用户数据结构,用于传递Flash私有信息
typedef struct {
GPIO_TypeDef *nss_gpio;
uint16_t nss_pin;
} w25qxx_user_data_t;
// W25Q64专用用户数据实例
static w25qxx_user_data_t w25q64_user_data = {
.nss_gpio = GPIOB,
.nss_pin = GPIO_PIN_12,
};
/**
* @brief SFUD SPI端口初始化
* @param spi_dev: SFUD Flash设备指针(含用户数据)
* @return SFUD_ERR_OK 表示成功
*/
sfud_err_t sfud_spi_port_init(sfud_flash_t *spi_dev) {
// 1. 初始化GPIO(仅执行一次,避免重复初始化)
static bool gpio_inited = false;
if (!gpio_inited) {
__HAL_RCC_GPIOB_CLK_ENABLE();
GPIO_InitTypeDef GPIO_InitStruct = {0};
// 初始化SCK/MISO/MOSI为复用推挽
GPIO_InitStruct.Pin = GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
// 初始化NSS为普通推挽输出,初始高电平
GPIO_InitStruct.Pin = GPIO_PIN_12;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_SET);
gpio_inited = true;
}
// 2. 初始化SPI2(仅执行一次)
static bool spi_inited = false;
if (!spi_inited) {
__HAL_RCC_SPI2_CLK_ENABLE();
hspi2.Instance = SPI2;
hspi2.Init.Mode = SPI_MODE_MASTER;
hspi2.Init.Direction = SPI_DIRECTION_2LINES;
hspi2.Init.DataSize = SPI_DATASIZE_8BIT;
hspi2.Init.CLKPolarity = SPI_POLARITY_LOW;
hspi2.Init.CLKPhase = SPI_PHASE_1EDGE;
hspi2.Init.NSS = SPI_NSS_SOFT;
hspi2.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_2;
hspi2.Init.FirstBit = SPI_FIRSTBIT_MSB;
hspi2.Init.TIMode = SPI_TIMODE_DISABLE;
hspi2.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
hspi2.Init.CRCPolynomial = 7;
if (HAL_SPI_Init(&hspi2) != HAL_OK) {
return SFUD_ERR_TIMEOUT;
}
spi_inited = true;
}
// 3. 将用户数据(PB12)绑定到Flash设备
spi_dev->user_data = &w25q64_user_data;
return SFUD_ERR_OK;
}
3.2 spi_lock 与 spi_unlock :临界区保护
在多任务环境(如FreeRTOS)下,多个任务可能并发访问同一Flash设备。 spi_lock 和 spi_unlock 函数用于实现互斥访问,防止SPI总线竞争。HAL库本身不提供轻量级互斥原语,此处采用 HAL_NVIC_DisableIRQ / HAL_NVIC_EnableIRQ 关闭SPI2中断是一种简单有效的方案,适用于裸机或中断优先级可控的RTOS环境。更优方案是使用RTOS提供的互斥信号量(Mutex Semaphore),但需确保信号量创建与获取在SFUD初始化前完成。
/**
* @brief SPI总线加锁(进入临界区)
*/
void spi_lock(void) {
// 方案一:禁用SPI2中断(裸机适用)
HAL_NVIC_DisableIRQ(SPI2_IRQn);
// 方案二(RTOS):获取互斥信号量(需提前创建)
// xSemaphoreTake(spi_mutex_handle, portMAX_DELAY);
}
/**
* @brief SPI总线解锁(退出临界区)
*/
void spi_unlock(void) {
// 方案一:恢复SPI2中断
HAL_NVIC_EnableIRQ(SPI2_IRQn);
// 方案二(RTOS):释放互斥信号量
// xSemaphoreGive(spi_mutex_handle);
}
3.3 spi_write_read :核心数据交换函数
此函数是SFUD数据吞吐的命脉,必须高效、准确地完成SPI全双工收发。W25Q64的典型操作流程为:先发送1字节指令,再发送3字节地址(读/写/擦除),最后进行数据收发。 spi_write_read 需支持任意长度的发送与接收缓冲区,并确保时序严格对齐。
关键设计点:
- 全双工同步 :利用 HAL_SPI_TransmitReceive 一次性完成发送与接收,避免分步调用引入的时序偏差;
- 超时控制 : HAL_SPI_TransmitReceive 的超时参数设为 HAL_MAX_DELAY 可能导致死锁,应设置合理值(如100ms);
- 错误处理 :检查HAL返回状态,对 HAL_ERROR 、 HAL_TIMEOUT 等错误进行统一转换。
/**
* @brief SPI写-读操作(全双工)
* @param spi_dev: Flash设备指针
* @param tx_buf: 发送缓冲区
* @param rx_buf: 接收缓冲区
* @param len: 数据长度(字节)
* @return SFUD_ERR_OK 表示成功
*/
sfud_err_t spi_write_read(sfud_flash_t *spi_dev, const uint8_t *tx_buf, uint8_t *rx_buf, size_t len) {
w25qxx_user_data_t *user_data = spi_dev->user_data;
// 1. 拉低片选
HAL_GPIO_WritePin(user_data->nss_gpio, user_data->nss_pin, GPIO_PIN_RESET);
// 2. 执行SPI传输
HAL_StatusTypeDef status = HAL_SPI_TransmitReceive(&hspi2,
(uint8_t*)tx_buf,
rx_buf,
len,
100); // 100ms超时
// 3. 拉高片选
HAL_GPIO_WritePin(user_data->nss_gpio, user_data->nss_pin, GPIO_PIN_SET);
// 4. 错误映射
if (status == HAL_OK) {
return SFUD_ERR_OK;
} else if (status == HAL_TIMEOUT) {
return SFUD_ERR_TIMEOUT;
} else {
return SFUD_ERR_HW;
}
}
3.4 sfud_spi_port_deinit :资源清理(可选)
在系统需要动态卸载Flash驱动时,此函数负责释放SPI和GPIO资源。对于大多数嵌入式应用,此函数可为空实现,因为资源通常在系统生命周期内持续占用。
sfud_err_t sfud_spi_port_deinit(sfud_flash_t *spi_dev) {
// 释放SPI2外设
HAL_SPI_DeInit(&hspi2);
// 释放GPIOB时钟
__HAL_RCC_GPIOB_CLK_DISABLE();
return SFUD_ERR_OK;
}
4. Flash设备表配置与SFDP自动识别
SFUD通过 sfud_flash_t 结构体实例来描述一个具体的Flash设备。该结构体是连接通用驱动与硬件特性的桥梁,其字段必须依据W25Q64的数据手册和实际硬件连接精确填写。
4.1 设备表核心字段解析
// 在sfud_port.c中定义W25Q64设备实例
static sfud_flash_t w25q64 = {
.name = "W25Q64",
.chip_id = 0xEF4017, // JEDEC ID: Manufacturer ID (0xEF) + Memory Type ID (0x40) + Capacity ID (0x17)
.size = 8 * 1024 * 1024, // 64Mbit = 8MB
.block_size = 4 * 1024, // 4KB Block
.erase_gran = SFUD_ERASE_GRAN_4K, // 支持4KB擦除粒度
.write_gran = 1, // 字节写入粒度(实际为页编程,但驱动层抽象为字节)
.addr_width = 3, // 3字节地址(24位寻址,覆盖8MB)
.user_data = &w25q64_user_data,
.init = w25q64_init,
.read = w25q64_read,
.write = w25q64_write,
.erase = w25q64_erase,
.get_info = w25q64_get_info,
};
-
chip_id:JEDEC标准ID,由3字节组成:Manufacturer ID(Winbond为0xEF)、Memory Type ID(0x40)、Capacity ID(W25Q64为0x17)。SFUD在初始化时会向Flash发送0x9F(Read JEDEC ID)指令读取此值,并与设备表中ID比对,实现自动识别。 -
size:总容量,单位字节。W25Q64为64Mbit=8MB,必须准确,否则sfud_read/sfud_write的地址边界检查会失效。 -
block_size:最小擦除单元(Block)大小。W25Q64支持4KB Sector Erase(0x20)和32KB Block Erase(0x52),但4KB是最小粒度,故设为4096。 -
erase_gran:擦除粒度枚举值,SFUD_ERASE_GRAN_4K表示支持4KB擦除,驱动将优先使用0x20指令。 -
addr_width:地址总线宽度,W25Q64为24位,故为3字节。此参数影响地址打包逻辑。
4.2 SFDP自动识别机制
现代W25Q64(V版本及以上)均支持JEDEC SFDP标准。SFUD在初始化时,若检测到设备表中 support_sfdp 标志为真,会自动执行以下流程:
1. 发送 0x5A 指令,读取SFDP Header(前8字节),验证签名 0x50444653 (”SFDP” ASCII码);
2. 解析Header中的Parameter Headers数量及地址偏移;
3. 遍历所有Parameter Headers,定位到 Basic Flash Parameter Table (通常在偏移0x0000);
4. 读取该Table,提取关键参数: density (容量)、 erase_4k_cmd (4KB擦除指令)、 page_program_cmd (页编程指令)、 read_cmd (读指令)等。
此机制极大简化了移植工作。开发者只需在 sfud_cfg.h 中启用 SFUD_USING_SFDP ,并确保 chip_id 的 Manufacturer ID 和 Memory Type ID 正确( Capacity ID 可设为0,由SFDP自动填充),SFUD即可全自动完成Flash识别与配置。对于不支持SFDP的老版本Flash,才需手动填写全部参数。
4.3 sfud_cfg.h 关键宏配置
SFUD的编译期行为由 sfud_cfg.h 头文件控制。针对本项目,需进行如下关键配置:
// sfud_cfg.h
#ifndef __SFUD_CFG_H__
#define __SFUD_CFG_H__
/* 启用SFDP支持(强烈推荐) */
#define SFUD_USING_SFDP
/* 启用调试日志(开发阶段开启,量产时注释) */
#define SFUD_DEBUG
/* 启用写保护相关指令(如Write Protect, Write Enable Lock) */
#define SFUD_USING_WRITE_PROTECT
/* 禁用QSPI支持(本项目仅用SPI2) */
#undef SFUD_USING_QSPI
/* 设置最大支持Flash设备数(本项目为1) */
#define SFUD_FLASH_DEVICE_MAX_NUM 1
/* 设置SPI总线最大设备数(本项目为1) */
#define SFUD_SPI_DEVICE_MAX_NUM 1
/* 设置Flash设备描述表数组大小 */
#define SFUD_FLASH_TABLE_SIZE 1
#endif /* __SFUD_CFG_H__ */
特别注意 SFUD_DEBUG 宏:开启后,SFUD会在初始化、读写、擦除等关键节点打印详细日志(如读取到的JEDEC ID、SFDP Header内容、操作耗时),是调试硬件连接与驱动逻辑的利器。但在量产固件中,必须将其注释掉,以节省Flash空间并避免日志输出干扰系统。
5. 应用层集成与测试验证
完成HAL层移植与设备表配置后,应用层集成是最后一步。本节将展示如何在STM32工程中初始化SFUD、执行基础读写测试,并分析关键调试现象。
5.1 SFUD初始化与设备注册
SFUD的初始化是一个两阶段过程:首先调用 sfud_init() 完成通用驱动框架初始化,然后调用 sfud_device_init() 注册具体的Flash设备。 sfud_device_init() 内部会依次调用 sfud_spi_port_init 、 w25q64_init (设备特定初始化)以及 w25q64_get_info (获取详细信息)。
#include "sfud.h"
// 声明W25Q64设备实例(在sfud_port.c中定义)
extern sfud_flash_t w25q64;
int main(void) {
HAL_Init();
SystemClock_Config();
// 初始化SFUD通用框架
if (sfud_init() != SFUD_SUCCESS) {
Error_Handler(); // SFUD框架初始化失败
}
// 注册W25Q64设备(index=0)
if (sfud_device_init(0) != SFUD_SUCCESS) {
Error_Handler(); // W25Q64设备初始化失败
}
// 此时W25Q64已就绪,可进行读写操作
while (1) {
// 应用逻辑
}
}
5.2 基础功能测试用例
一个完备的测试应覆盖初始化、读、写、擦除四大核心功能。以下为生产环境中推荐的测试序列:
// 测试函数
void w25q64_test(void) {
uint8_t test_data[16] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10};
uint8_t read_data[16];
uint32_t test_addr = 0x000000; // 地址0,位于第一个扇区
// 1. 读取初始状态(应为0xFF)
if (sfud_read(&w25q64, test_addr, sizeof(read_data), read_data) != SFUD_SUCCESS) {
printf("Read failed!\r\n");
return;
}
printf("Initial data: ");
for (int i = 0; i < 16; i++) {
printf("%02X ", read_data[i]);
}
printf("\r\n");
// 2. 擦除目标扇区(4KB)
if (sfud_erase(&w25q64, test_addr, 4096) != SFUD_SUCCESS) {
printf("Erase failed!\r\n");
return;
}
printf("Erase successful.\r\n");
// 3. 再次读取(应全为0xFF)
if (sfud_read(&w25q64, test_addr, sizeof(read_data), read_data) != SFUD_SUCCESS) {
printf("Read after erase failed!\r\n");
return;
}
printf("After erase: ");
for (int i = 0; i < 16; i++) {
printf("%02X ", read_data[i]);
}
printf("\r\n");
// 4. 写入测试数据
if (sfud_write(&w25q64, test_addr, sizeof(test_data), test_data) != SFUD_SUCCESS) {
printf("Write failed!\r\n");
return;
}
printf("Write successful.\r\n");
// 5. 读回验证
if (sfud_read(&w25q64, test_addr, sizeof(read_data), read_data) != SFUD_SUCCESS) {
printf("Read after write failed!\r\n");
return;
}
printf("After write: ");
for (int i = 0; i < 16; i++) {
printf("%02X ", read_data[i]);
}
printf("\r\n");
}
5.3 调试日志分析与常见问题排查
当启用 SFUD_DEBUG 后,串口输出的初始化日志是诊断问题的第一手资料。一个成功的初始化日志如下:
[SFUD]Find a flash chip. Manufacturer ID: 0xEF, Memory Type: 0x40, Capacity: 0x17.
[SFUD]Use SFDP to get flash information.
[SFUD]SFDP Header: 50444653 00010100 ...
[SFUD]Flash device name: W25Q64, capacity: 8388608 bytes.
[SFUD]Initialize flash device success.
- 第一行 :
Find a flash chip表明0x9F指令成功读取到JEDEC ID0xEF4017,证明SPI物理连接(SCK/MISO/MOSI/NSS)和时序(CPOL/CPHA)正确。 - 第二行 :
Use SFDP表明SFDP支持已启用,且Flash响应了0x5A指令。 - 第三行 :
SFDP Header显示SFDP签名50444653(”SFDP”)和版本号,确认SFDP表存在且可读。 - 第四行 :
capacity: 8388608 bytes是SFDP解析出的实际容量(8MB),与w25q64.size字段一致。
若出现 Read JEDEC ID failed ,则需检查:
- PB12片选是否在发送 0x9F 前被正确拉低;
- MISO引脚是否虚焊或接触不良(用示波器观测MISO线上是否有有效数据);
- SPI时钟极性/相位是否与W25Q64要求匹配(Mode 0)。
若出现 SFDP read failed ,则可能是:
- Flash尚未退出深度掉电模式(需发送 0xAB 指令唤醒);
- SFDP表地址偏移错误(某些旧版Flash需用 0x5A 加3字节地址,而非标准 0x5A +0x000000)。
我曾在某款定制板上遇到过 SFDP read failed ,最终发现是PCB布线导致MISO信号反射严重,在18MHz下出现误码。将SPI时钟降至9MHz( SPI_BAUDRATEPRESCALER_4 )后问题消失。这印证了在高速SPI设计中,信号完整性(SI)与电源完整性(PI)的重要性远超软件逻辑。
6. 工程实践进阶:多Flash支持与性能优化
在复杂的嵌入式系统中,单一Flash往往无法满足需求。例如,一块板卡可能同时搭载W25Q64(用于存储固件)和AT45DB041D(用于存储日志),二者均通过SPI总线挂载,但使用不同的片选引脚(PB12和PA4)。SFUD对此提供了优雅的解决方案。
6.1 多Flash设备表配置
扩展设备表只需增加 sfud_flash_t 结构体实例,并在 sfud_cfg.h 中调整 SFUD_FLASH_TABLE_SIZE 。每个实例需绑定独立的用户数据(包含各自的NSS引脚)和初始化函数。
// 新增AT45DB041D用户数据
typedef struct {
GPIO_TypeDef *nss_gpio;
uint16_t nss_pin;
} at45_user_data_t;
static at45_user_data_t at45_user_data = {
.nss_gpio = GPIOA,
.nss_pin = GPIO_PIN_4,
};
// AT45DB041D设备实例
static sfud_flash_t at45db041d = {
.name = "AT45DB041D",
.chip_id = 0x1F2600, // Atmel ID
.size = 512 * 1024, // 4Mbit = 512KB
.block_size = 264, // Page size
.erase_gran = SFUD_ERASE_GRAN_PAGE,
.write_gran = 1,
.addr_width = 3,
.user_data = &at45_user_data,
.init = at45_init,
.read = at45_read,
.write = at45_write,
.erase = at45_erase,
.get_info = at45_get_info,
};
// 在sfud_cfg.h中
#define SFUD_FLASH_TABLE_SIZE 2
应用层通过索引( index )区分设备: sfud_read(&w25q64, ...) 操作W25Q64, sfud_read(&at45db041d, ...) 操作AT45DB041D。SFUD内部通过 sfud_device_init(index) 选择对应实例,完全解耦。
6.2 性能优化:DMA与双缓冲
在大数据量读写场景(如固件OTA升级),CPU轮询式SPI传输会成为瓶颈。HAL库支持SPI DMA模式,可将数据搬运交由DMA控制器,CPU仅需发起传输并等待完成中断。SFUD HAL层可无缝集成DMA:
// 在sfud_spi_port_init中启用DMA
__HAL_RCC_DMA1_CLK_ENABLE();
hdma_spi2_tx.Instance = DMA1_Channel5;
hdma_spi2_rx.Instance = DMA1_Channel6;
// ... 配置DMA通道 ...
// 修改spi_write_read,使用DMA
HAL_SPI_TransmitReceive_DMA(&hspi2, (uint8_t*)tx_buf, rx_buf, len);
// 在SPI2_IRQHandler中,当DMA传输完成时,调用HAL_SPI_TxCpltCallback/HAL_SPI_RxCpltCallback
// 最终在回调中拉高NSS并通知SFUD
此外,SFUD支持 SFUD_USING_READ_RETRY 宏,开启后,若单次读取失败,驱动会自动重试(最多3次),提升在电磁干扰(EMI)恶劣环境下的鲁棒性。此功能在工业现场调试时曾帮我快速定位出PCB上SPI走线与电机驱动PWM信号的串扰问题。
6.3 安全加固:写保护与加密
W25Q64内置状态寄存器(Status Register),其中 WPEN (Write Protect Enable)和 SEC (Sector Protection)位可用于硬件级写保护。在 w25q64_init 函数中,可添加如下代码启用扇区保护:
// 向状态寄存器写入0x0C,启用WPEN并保护前4个扇区(0x000000-0x003FFF)
uint8_t wr_sr_cmd[] = {0x01, 0x0C};
spi_write_read(&w25q64, wr_sr_cmd, NULL, 2);
对于更高安全等级需求,可在应用层对写入数据进行AES加密,再调用 sfud_write 。SFUD本身不介入数据内容,因此加密/解密逻辑完全由应用层控制,符合“关注点分离”原则。
至此,一个工业级、可维护、可扩展的W25Q64 SPI Flash驱动已在STM32平台上完整落地。从硬件时序的锱铢必较,到软件架构的抽象分层,再到工程实践的坑点总结,每一个环节都指向同一个目标:让存储变得像读写数组一样简单可靠。
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐

所有评论(0)