【STM32】SPI Flash 驱动开发实战:阻塞模式驱动 W25Q64
一、前言
前面我们用极简 demo 的方式介绍完了 SPI 的阻塞通信接口,完成了理论与实践结合的练习。本文把重点放在纯实践层面:使用 STM32 的 SPI1 主机,以阻塞模式驱动 W25Q64 Flash 芯片,完成从 CubeMX 配置、工程搭建到驱动代码编写、功能测试的全流程。
编写该驱动之前需要掌握三件事:
- SPI 主机阻塞模式配置过程:CubeMX 软件相关配置以及生成代码的理解;
- SPI 主机阻塞模式数据发送、接收相关函数接口的使用方法(
HAL_SPI_Transmit/HAL_SPI_Receive);- W25Qxx Flash 的操作方法------本次的核心目标。
二、需求分析
完成 W25Q64 驱动开发,必须实现以下功能:
序号 功能 说明 1 读取芯片 ID JEDEC ID,用于识别厂商、类型、容量 2 读取 Flash 状态信息 状态寄存器 1,关注 BUSY / WEL 位 3 使能 Flash 写操作 写使能指令 06h,置 WEL=1 4 Flash 扇区擦除 擦除最小单位 4KB 5 Flash 整片擦除 擦除整个芯片 6 Flash 页编程 / 跨页写入 写入最大单位 256 字节,封装跨页 7 等待 Flash 写完成 轮询 BUSY 位 8 读 Flash 数据 任意地址连续读取 驱动完成后,对功能进行测试,并通过串口打印测试结果。
三、硬件准备
3.1 W25Q64 的 SPI1 接线关系
开发板上 SPI1 的信号线(SCK / MOSI / MISO)是被 LCD 触摸屏和 W25Q64 两个从设备统一共享的,它们的引脚功能固定:
MCU 引脚 SPI1 功能 连接设备 PA5 SPI1_SCK 时钟,两从设备并联 PA7 SPI1_MOSI 主机输出,两从设备并联 PA6 SPI1_MISO 主机输入,两从设备并联 PE6 FLASH_CS(软件片选) 片选 W25Q64 PE4 TC_CS(软件片选) 片选 LCD 触摸屏 ⚠️ 注意(这里有个大坑):
- 如果不把 PE4 配置并拉高,LCD 可能被误片选,导致片选出错,进而引发未知错误(默认电平不确定,必须保证确定性);
- 所以必须开启 GPIOE,PE4 和 PE6 默认都拉高,未来片选时只需拉低 PE6 即可选中 W25Q64。
3.2 选择 USART1 进行信息打印
- 串口复用 GPIOA:PA9 (USART1_TX)、PA10 (USART1_RX);
- 核心配置:异步模式、波特率 115200、9 位数据(含校验)、奇校验(Odd)、1 位停止位;
- 上位机串口助手需保持相同配置才能正确收发。
3.3 选择 LED 灯
使用两个 LED 指示状态:
LED1(灵犀板 LED4)指示数据正常通信,LED2(LED5)指示通信异常。
选择 PF8 (LED4)、PF9 (LED5),外部有上拉电源,
配置为开漏输出:导通 N-MOS → LED 点亮,关闭 N-MOS → LED 熄灭。
3.4 选择串口助手
默认使用 comtool 上位机软件。
四、CubeMX 软件设计
4.1 配置仿真器接口(SWD)
开发板预留的调试接口是 SWD(Serial Wire Debug),在 SYS → Debug 中选择 Serial Wire。
⚠️ 如果在 CubeMX 中错误配置或禁止了 debug 接口(如 JTAG/SWD),烧录完程序后将无法通过调试器连接目标 STM32。
4.2 配置晶振和时钟
- RCC → HSE 选择 Crystal / Ceramic Resonator(外部晶振);
- 开发板是 8MHz HSE 晶振,将系统运行最大频率配置到 72MHz(HSE ×9 → SYSCLK 72MHz)。
4.3 SPI1 接口配置
cppMode: Full Duplex Master Hardware NSS Signal: Disable ← 不用硬件NSS,采用软件NSS片选
参数 值 Frame Format Motorola(标准 SPI) Data Size 8 Bits First Bit MSB First Prescaler 64(波特率 1.125 MBits/s) Clock Polarity (CPOL) Low Clock Phase (CPHA) 1 Edge(即 Mode 0) NSS Signal Type Software 引脚自动分配:
PA5 = SPI1_SCK、PA6 = SPI1_MISO、
PA7 = SPI1_MOSI(复用功能、高速、无上下拉)。
片选引脚:
开启 GPIOE,PE4 常拉高(防 LCD 误选),
PE6 供片选 W25Q64,默认输出高,后续通过软件拉低进行选通。
4.4 配置打印串口(USART1)
异步模式、波特率 115200、9 位数据(含奇校验)、1 位停止位。
使能串口后会自动生成 PA9/PA10 的引脚配置,保持默认即可。
4.5 配置指示 LED 灯
开启 PF8、PF9,配置为通用输出开漏模式,默认输出电平 LOW(默认让 LED 点亮)。
4.6 配置项目管理 & 4.7 生成代码
- 工程名:
ISP_Flash_test,Toolchain/IDE:MDK-ARM V5.32;- 点击 GENERATE CODE 生成代码(生成的代码结构之前已看过,此处不再赘述)。
五、代码编写
5.1 代码结构与准备工作
- 构建驱动文件结构:在工程目录下新建
BSP_Drivers文件夹,内含inc(头文件)、src(源文件)两个子目录,然后添加到 Keil 工程;- 注意事项:Keil 中勾选 Use MicroLIB(配合 printf 重定向使用);
- 导入 LED 模块(操作略)。
细节:
BSP 全称 板级支持包(Board Support Package)。
文件命名带 BSP,表示访问的设备都是板子上外设的驱动功能。
5.2 初始化与驱动兼容性设计
我们希望驱动能够驱动基于 STM32 为主控的 W25Qxx 全系芯片,影响驱动兼容性的因素:
- W25Qxx 型号不同 → 页大小、扇区大小、块大小、容量大小的差异;
- 硬件电路板不同 → 控制片选的 IO 管脚可能不同;
- 硬件电路板不同 → 使用的 SPI 接口可能不同。
解决方案:通过结构体在初始化时传递差异性参数。
cpp/* Bsp_W25Q64.h */ typedef struct { uint32_t page_size; // 页大小 uint32_t sector_size; // 扇区大小 uint32_t block_size; // 块大小 uint32_t mem_size; // 总容量大小 uint32_t chip_id; // 芯片ID SPI_HandleTypeDef *spi_handler; // SPI句柄指针 GPIO_TypeDef *nss_port; // 软件片选引脚端口 uint16_t nss_pin; // 软件片选针脚 } flash_t;
cpp/* Bsp_W25Q64.c */ static flash_t w25qxx; // W25Qxx 对象 static bool w25qxx_init = false; // 初始化完成标志 bool BSP_W25Qxx_Init(flash_t *init) { if (init == NULL) return false; // 注意:读取ID依赖初始化状态,所以先置true,失败再改回false w25qxx_init = true; w25qxx.page_size = init->page_size; w25qxx.sector_size = init->sector_size; w25qxx.block_size = init->block_size; w25qxx.mem_size = init->mem_size; w25qxx.chip_id = init->chip_id; w25qxx.spi_handler = init->spi_handler; w25qxx.nss_port = init->nss_port; w25qxx.nss_pin = init->nss_pin; // 为了保证兼容性,只处理ID相同的芯片 uint32_t id = BSP_W25Qxx_Read_Id(); if (id != w25qxx.chip_id) { w25qxx_init = false; printf("w25qxx init failed, ID error, current id is : 0x%08x, your id is : 0x%08x\n", id, w25qxx.chip_id); return false; } printf("w25qxx init success, ID=0x%08x\n", id); return w25qxx_init; }main 中的初始化参数(W25Q64):
cppflash_t w25qxx_init = { .page_size = 256, // 页大小:256字节 .sector_size = 4096, // 扇区大小:4KB .block_size = 64 * 1024, // 块大小:64KB .mem_size = 8 * 1024 * 1024, // 内存大小:8MB .chip_id = 0xEF4017, // 芯片ID:W25Q64 = 0xEF4017 .spi_handler = &hspi1, // SPI句柄 .nss_port = GPIOE, // CS端口 .nss_pin = GPIO_PIN_6 // CS引脚 }; BSP_W25Qxx_Init(&w25qxx_init);灵犀板选择 GPIOE 端口位的 PE6 对 W25Q64 进行片选,默认输出高电平,拉低电平即选中从设备。
5.3 指令码定义
对 W25Qxx 芯片操作时,需要先发送一个字节的指令码,用于表示本次操作的作用(读数据、擦除扇区等)。常用标准 SPI 指令码(摘自 W25Q64JV 规格书 8.1.2):
指令 指令码 字节1 字节2 字节3 字节4 后续 Write Enable 06h --- --- --- --- --- Write Disable 04h --- --- --- --- --- Read Status Register-1 05h (S7-S0) 连续输出 --- --- --- Write Status Register-1 01h (S7-S0) --- --- --- --- Read Data 03h A23-A16 A15-A8 A7-A0 (D7-D0) 连续输出 Fast Read 0Bh A23-A16 A15-A8 A7-A0 Dummy (D7-D0) Page Program 02h A23-A16 A15-A8 A7-A0 D7-D0 D7-D0(n) Sector Erase (4KB) 20h A23-A16 A15-A8 A7-A0 --- --- Block Erase (32KB) 52h A23-A16 A15-A8 A7-A0 --- --- Block Erase (64KB) D8h A23-A16 A15-A8 A7-A0 --- --- Chip Erase C7h/60h --- --- --- --- --- JEDEC ID 9Fh (MF7-MF0) (ID15-ID8) (ID7-ID0) --- --- Manufacturer/Device ID 90h Dummy Dummy Dummy (MF7-MF0) (ID7-ID0) Read Unique ID 4Bh Dummy Dummy Dummy Dummy (UID63-0) Power down B9h --- --- --- --- --- Release Power-down ABh Dummy Dummy Dummy (ID7-ID0) --- Enable Reset 66h --- --- --- --- --- Reset Device 99h --- --- --- --- ---
5.4 从设备使能(片选控制)
操作 SPI 从机前,必须先通过片选信号选择设备。
我们选择软件使能 W25Q64,用完后再通过片选信号禁用。
再次强调:
当 SPI 片选信号(NSS)使用硬件模式时,SPI 外设自动控制 NSS;
使用软件模式时,需要用户通过 IO 口独立实现,灵活性更大。
cpp/* 使能:拉低CS,激活通信 */ static void BSP_W25Qxx_CS_Enable(void) { HAL_GPIO_WritePin(w25qxx.nss_port, w25qxx.nss_pin, GPIO_PIN_RESET); } /* 禁用:拉高CS,结束通信 */ static void BSP_W25Qxx_CS_Disable(void) { HAL_GPIO_WritePin(w25qxx.nss_port, w25qxx.nss_pin, GPIO_PIN_SET); }
5.5 读取芯片 ID(JEDEC ID)
获取步骤:
- 将 /CS 引脚拉低,选中设备;
- 发送指令码 0x9F 启动 ID 读取操作;
- 制造商 ID(0xEF)+ 两个设备 ID 字节(存储器类型 ID15-8、容量 ID7-0)在 CLK 下降沿移出,MSB 优先;
- 将 /CS 引脚拉高,禁止设备。
cppuint32_t BSP_W25Qxx_Read_Id(void) { if (!w25qxx_init) return 0; uint8_t cmd = W25QXX_JEDEC_ID; // 0x9F uint8_t id[3] = {0}; HAL_StatusTypeDef status = HAL_OK; BSP_W25Qxx_CS_Enable(); status = HAL_SPI_Transmit(w25qxx.spi_handler, &cmd, 1, HAL_MAX_DELAY); if (status != HAL_OK) { printf("w25qxx_read_id: write cmd(0x%02X) failed, status=%d\r\n", cmd, status); goto END; } status = HAL_SPI_Receive(w25qxx.spi_handler, id, 3, HAL_MAX_DELAY); if (status != HAL_OK) { memset(id, 0, sizeof(id)); printf("w25qxx_read_id: read id failed, status=%d\r\n", status); goto END; } END: BSP_W25Qxx_CS_Disable(); // 组合3字节为24位返回值:高8位制造商 + 中8位类型 + 低8位容量 return (id[0] << 16) | (id[1] << 8) | id[2]; }测试结果:
W25Q64 JEDEC ID : 0x00ef4017✅(EF = Winbond 制造商 ID,40 = 存储器类型,17 = 8Mbit→8MB 容量)
5.6 读取芯片状态
5.6.1 两个关键标志位
W25Q64 共有 3 个状态寄存器,我们只需关注 Status Register-1 的 BUSY 和 WEL 两个标志位:
位 名称 含义 S0 BUSY 1 = 芯片正在忙(页编程/擦除/写状态寄存器中),完成后自动清 0 S1 WEL 1 = 写使能锁存(执行 06h 后置 1),操作完成或上电后自动清 0 S2-S4 BP0-BP2 块保护位 S5 TB 顶部/底部保护 S6 SEC 扇区保护 S7 SRP 状态寄存器保护 高度总结:
- WEL(写使能)是"入场券"****:芯片默认锁定(WEL=0),防止意外误抹除。每次写/擦前必须手动"领票"(执行写使能指令),操作完票据自动作废;
- BUSY(忙碌位)是"进度条":芯片内部擦除/写入需要物理时间(电荷充放电),BUSY 就像门口挂的"施工中"牌子,告诉主控先等等。
开发中的逻辑:
写入前必须调一次 Write Enable(WEL=1);
写入后必须循环读状态寄存器,等 BUSY=0 才能进行下一步。
5.6.2 读取状态寄存器时序
指令码 05h:
/CS 拉低 → CLK 上升沿移入指令码 → 状态寄存器各位在 CLK 下降沿从 DO 移出,MSB 优先 → /CS 拉高结束。
细节:状态寄存器不是 8 位吗?为什么返回 16 位?
如果在输入 05h 后时钟没有停止,芯片会不断重复输出这 8 位数据。
这方便主控轮询(Polling):
不需要反复发 05h,拉低 CS 后一直读,直到 BUSY 位变为 0 为止。
所以我们只读 register1 的 8 位、关注 2 位就够了。
cppuint8_t BSP_W25Qxx_Status1_Reg(void) { if (!w25qxx_init) return 0xFF; uint8_t cmd = W25QXX_READ_STATUS_REG1; // 0x05 uint8_t status = 0xFF; HAL_StatusTypeDef state = HAL_OK; BSP_W25Qxx_CS_Enable(); state = HAL_SPI_Transmit(w25qxx.spi_handler, &cmd, 1, HAL_MAX_DELAY); if (state != HAL_OK) { printf("HAL_SPI_Transmit cmd 0x%02x error, state is: %d\n", cmd, state); goto END; } state = HAL_SPI_Receive(w25qxx.spi_handler, &status, 1, HAL_MAX_DELAY); if (state != HAL_OK) { status = 0xFF; printf("HAL_SPI_Receive cmd 0x%02x error, state is: %d\n", cmd, state); goto END; } END: BSP_W25Qxx_CS_Disable(); return status; }测试结果:
register1 status: 0x00→ 即默认 BUSY=0(不忙)、WEL=0(写保护)。
5.7 使能写操作
W25Q64 默认写保护。
每次执行页编程、扇区擦除、块擦除、芯片擦除和写状态寄存器指令之前,都必须先进行写使能(设置 WEL=1)。
cppvoid BSP_W25Qxx_Write_Enable(void) { if (!w25qxx_init) return; uint8_t cmd = W25QXX_WRITE_ENABLE; // 0x06 BSP_W25Qxx_CS_Enable(); HAL_StatusTypeDef state = HAL_SPI_Transmit(w25qxx.spi_handler, &cmd, 1, HAL_MAX_DELAY); if (state != HAL_OK) { printf("HAL_SPI_Transmit cmd 0x%02x error, state is: %d\n", cmd, state); } BSP_W25Qxx_CS_Disable(); }测试结果:
cppWrite_Enable before register status: 0x00 Write_Enable after register status: 0x02 ← WEL=1,关闭写保护
5.8 禁止写操作
指令码 04h。两种应用场景:
- 自动锁定(最常用):实际编程中很少手动发 04h,因为 W25Q64 非常"谨慎",每次完成写入或擦除任务都会自动执行写禁止,防止误操作;
- 手动锁定:发了写使能(06h)但突然不想写了,可以发 04h 手动"把门锁上"。
cppvoid BSP_W25Qxx_Write_Disable(void) { if (!w25qxx_init) return; uint8_t cmd = W25QXX_WRITE_DISABLE; // 0x04 BSP_W25Qxx_CS_Enable(); HAL_StatusTypeDef state = HAL_SPI_Transmit(w25qxx.spi_handler, &cmd, 1, HAL_MAX_DELAY); if (state != HAL_OK) { printf("HAL_SPI_Transmit cmd 0x%02x error, state is: %d\n", cmd, state); } BSP_W25Qxx_CS_Disable(); }上电保护:
芯片刚通电时默认就是"写禁止"状态,保证电源不稳时不会意外破坏数据。
断电重启后 Flash 默认恢复为"写禁止"。
5.9 等待写完成
对 W25Q64 进行编程或擦除时,操作需要几 ms ~ 几十 s 不等,必须等待操作顺利完成,本质就是检测 BUSY 标志位。
cpp/* 状态寄存器位定义 */ #define W25QXX_STATUS_BUSY 0x01 // 1 = 芯片正在忙(擦除或写入中) #define W25QXX_STATUS_WR_EN 0x02 // 1 = 芯片处于写使能状态 void BSP_W25Qxx_Waitfor_Write_Complete(void) { if (!w25qxx_init) return; while (BSP_W25Qxx_Status1_Reg() & W25QXX_STATUS_BUSY); }
- 读取状态标志位需要等待吗?不需要------返回是及时性的,而且"读 BUSY 还要等 BUSY"就是鸡生蛋蛋生鸡的问题了;
- 读取正常数据时一般也不需要等,但最好避开设备忙的时间。
补充两个检测函数:
cppbool BSP_W25Qxx_Is_Busy(void) { if (!w25qxx_init) return false; return BSP_W25Qxx_Status1_Reg() & W25QXX_STATUS_BUSY; } bool BSP_W25Qxx_Is_Writeable(void) { if (!w25qxx_init) return false; return BSP_W25Qxx_Status1_Reg() & W25QXX_STATUS_WR_EN; }
5.10 扇区擦除
单位 大小 W25Q64 参数 指令 特点 页 Page 256 Byte 8MB / 256B = 32768 页 02h Page Program 页编程 ✅写入单位; 不能跨页写; 单次最多写 256 字节; 不能擦页!没有擦页指令 扇区 Sector 4 KB(4096B) 8MB / 4KB = 2048 个扇区 20h Sector Erase 扇区擦除 ✅最小擦除单位; 擦除后内容变成 0xFF; 地址必须 4K 对齐; 典型擦除时间 45ms 块 Block (64KB) 64 KB 8MB / 64KB = 128 个块 D8h Block Erase 64K 块擦除 一次性擦除 16 个扇区; 擦除速度比逐个扇区快; 适合大批量擦除 整片 Chip 8 MB 1 颗 W25Q64 C7h Chip Erase 擦全部存储; ⚠️时间几十秒,谨慎使用 编程(写入)之前必须先擦除,再写入**。**
指令码 20h:
/CS 拉低 → 移入指令码 + 24 位扇区地址 → /CS 拉高,自定时擦除周期开始。
参数 典型值 最大值 Sector Erase Time (4KB) tSE 45 ms 400 ms
cppvoid BSP_W25Qxx_Sector_Erase(uint32_t sector_address) { if (!w25qxx_init) return; // 强制地址对齐:扇区地址低12位必须为0(4096的整数倍) if (sector_address & 0xFFF) { printf("BSP_W25Qxx_Sector_Erase: address 0x%x not sector aligned\n", sector_address); return; } // 地址不能超过总容量 if (sector_address >= w25qxx.mem_size) { printf("BSP_W25Qxx_Sector_Erase: address 0x%x out of range max memory size\n", sector_address); return; } // 构造擦除指令:指令码 + 24位地址(顺序来自时序图) uint8_t cmd[4] = { W25QXX_SECTOR_ERASE, // 0x20 (uint8_t)(sector_address >> 16), (uint8_t)(sector_address >> 8), (uint8_t)(sector_address) }; // ⚠️ 坑:擦除操作一定要先写使能,否则擦除不会执行 BSP_W25Qxx_Write_Enable(); BSP_W25Qxx_CS_Enable(); HAL_StatusTypeDef state = HAL_SPI_Transmit(w25qxx.spi_handler, cmd, 4, HAL_MAX_DELAY); if (state != HAL_OK) { printf("BSP_W25Qxx_Sector_Erase: sector erase cmd 0x%02x failed, status = %d\n", cmd[0], state); BSP_W25Qxx_CS_Disable(); return; } printf("BSP_W25Qxx_Sector_Erase: sector erase cmd 0x%02x done, status = %d\n", cmd[0], state); // 拉高CS才开始擦除,然后等待完成 BSP_W25Qxx_CS_Disable(); BSP_W25Qxx_Waitfor_Write_Complete(); }细节要点:
- 芯片内部擦除逻辑在 CS 上升沿触发:下发擦除指令后不会立即擦除,必须取消片选(拉高 CS),擦除才开始;
- 虽然是 24 位地址,但扇区擦除时芯片会自动忽略低 12 位,最佳实践:传入地址必须是 4096 的整数倍;
- 擦除一旦完成,WEL 自动变回 0。连续擦除两个扇区必须遵循:
写使能 → 擦除扇区1 → 等待BUSY → 写使能 → 擦除扇区2 → 等待BUSY的循环;- TYP 就是 Typical(典型值),通常是统计上的平均值。
⚠️ 格外注意:
除非特殊要求,千万不要把扇区擦除、块擦除、整片擦除写在 while 循环中。
Flash 是有寿命的,如果忘了,可能会导致 Flash 一直被擦除。
5.11 整片擦除
指令码 C7h / 60h,无地址,单字节指令。
参数 典型值 最大值 Chip Erase Time tCE 20 s 100 s ⚠️ 注意单位是秒(S)!
整片擦除细节和扇区擦除差不多,唯一要特别注意的是擦除时间单位。
cppvoid BSP_W25Qxx_Chip_Erase(void) { if (!w25qxx_init) return; uint8_t cmd = W25QXX_CHIP_ERASE; // 0xC7 // ⚠️ 坑:擦除前必须写使能 BSP_W25Qxx_Write_Enable(); BSP_W25Qxx_CS_Enable(); HAL_StatusTypeDef state = HAL_SPI_Transmit(w25qxx.spi_handler, &cmd, 1, HAL_MAX_DELAY); if (state != HAL_OK) { printf("BSP_W25Qxx_Chip_Erase: chip erase cmd 0x%02x failed, status = %d\n", cmd, state); BSP_W25Qxx_CS_Disable(); return; } printf("BSP_W25Qxx_Chip_Erase: chip erase 0x%02x done, status = %d\n", cmd, state); // 关闭片选,开始整片擦除 BSP_W25Qxx_CS_Disable(); BSP_W25Qxx_Waitfor_Write_Complete(); }测试时把等待函数改成带打印的版本,可以看到擦除过程约 19~20 秒:
cppErase ing ..., cnt is : 1 Erase ing ..., cnt is : 2 ... Erase ing ..., cnt is : 19 Erase Chip Done
5.12 页编程(写入的本质)
编程本质就是写入**。**
指令码 02h:
/CS 拉低 → 移入指令码 + 24 位地址 + 至少一个数据字节,
发送数据期间 /CS 必须全程保持低电平 → /CS 拉高,自定时页编程开始。
参数 典型值 最大值 Page Program Time tPP 0.4 ms 3 ms 关于页编程必须要知道的细节:
- Flash 的写入基本单位随类型不同而有差异:NOR Flash 可以按字节写入,单次不能超过 Page;NAND Flash 必须是整页写入。W25Q64 是 NOR Flash:
- W25Q64(NOR):1 ≤ 写入长度 ≤ Page(灵活,上限受限);
- NAND:写入长度 = Page(刚性,必须对齐);
- 地址回卷(Wrap):如果地址最后一个字节不为 0(即起始地址不是 256 对齐),且发送数据超过了该页剩余长度,地址将回卷到该页开头。可以把页理解为一个容量 256 的漏斗:可以只滴一滴水(1 字节),也可以倒满,但一次倒超过 256 就会溢出(卷绕);
- 为什么叫"页编程":SPI 协议中发送地址后持续发数据,芯片内部地址指针自动 +1,但自动 +1 的逻辑被限制在页边界内(低 8 位地址自增)。如果叫"随机编程",理论上能从 0x00FF 连续写到 0x0100;但因为是"页编程",它会在 0x00FF 之后跳回 0x0000。这种物理地址指针不进位的特性,决定了它必须以"页"为边界进行逻辑划分;
- 如果发送数据超过 256 字节,地址回卷到页起始位置并覆盖之前发送的数据。
cppvoid BSP_W25Qxx_Write_Page(uint32_t page_address, uint16_t page_inner_offset, uint8_t *wbuffer, uint16_t num_byte) { if (!w25qxx_init) return; if (wbuffer == NULL || num_byte == 0) return; // 参数检查:确保写入的数据不会超出页面边界(不能跨页) if (page_inner_offset + num_byte > w25qxx.page_size) return; // 目标地址 = 页号 * 页大小 + 页内偏移 uint32_t write_address = page_address * w25qxx.page_size + page_inner_offset; // 命令格式: PAGE_PROGRAM + 3字节地址(高位在前) uint8_t cmd[] = { W25QXX_PAGE_PROGRAM, // 0x02 (uint8_t)(write_address >> 16), (uint8_t)(write_address >> 8), (uint8_t)(write_address) }; HAL_StatusTypeDef state = HAL_OK; // 发送写使能命令,允许后续写入操作 BSP_W25Qxx_Write_Enable(); BSP_W25Qxx_CS_Enable(); // 发送页编程命令及地址信息 state = HAL_SPI_Transmit(w25qxx.spi_handler, cmd, 4, HAL_MAX_DELAY); if (state != HAL_OK) { printf("BSP_W25Qxx_Write_Page: write cmd 0x%02x failed, status = %d\n", cmd[0], state); BSP_W25Qxx_CS_Disable(); return; } printf("BSP_W25Qxx_Write_Page: write cmd 0x%02x done, status = %d\n", cmd[0], state); // 通过SPI发送要写入的数据 state = HAL_SPI_Transmit(w25qxx.spi_handler, wbuffer, num_byte, HAL_MAX_DELAY); if (state != HAL_OK) { printf("BSP_W25Qxx_Write_Page: write data 0x%02x failed, status = %d\n", cmd[0], state); BSP_W25Qxx_CS_Disable(); return; } printf("BSP_W25Qxx_Write_Page: write data 0x%02x done, status = %d\n", cmd[0], state); // 禁用片选信号并启动写入过程 BSP_W25Qxx_CS_Disable(); // 等待写入操作完成 BSP_W25Qxx_Waitfor_Write_Complete(); }
5.13 读数据
指令码 03h:
/CS 拉低 → 移入指令码 + 24 位地址 → 数据在 CLK 下降沿从 DO 移出(MSB 优先),每移出一个字节地址自动 +1,可实现连续读取 → /CS 拉高结束。
读取操作的三大特性:
- 不用考虑 page / sector / block,在容量范围内没有大小限制;
- 读取具有实时性,不用阻塞等待,没有擦除/页编程"操作完成时长"的概念;
- 读取数据指令不需要使用 WEL 标志。
若在擦除、编程或写入周期中(BUSY=1)发送读取指令,该指令会被忽略,且不会对当前周期产生影响。另外 03h 仅支持标准 SPI 模式。
cppvoid BSP_W25Qxx_Read_Data(uint32_t read_address, uint8_t *rbuffer, uint32_t rsize) { if (!w25qxx_init) return; if (rbuffer == NULL || rsize == 0) return; // 检查读取地址+长度是否超出Flash容量范围 if (read_address + rsize > w25qxx.mem_size) return; // 如果设备正在执行其他操作(如擦除或写入),等待其完成 if (BSP_W25Qxx_Is_Busy()) BSP_W25Qxx_Waitfor_Write_Complete(); // 构建读取命令序列:命令码 + 3字节地址 uint8_t cmd[] = { W25QXX_READ_DATA, // 0x03 (uint8_t)(read_address >> 16), (uint8_t)(read_address >> 8), (uint8_t)(read_address) }; HAL_StatusTypeDef state = HAL_OK; BSP_W25Qxx_CS_Enable(); state = HAL_SPI_Transmit(w25qxx.spi_handler, cmd, 4, HAL_MAX_DELAY); if (state != HAL_OK) { printf("BSP_W25Qxx_Read_Data: send read cmd 0x%02x failed, status = %d\n", cmd[0], state); goto END; } printf("BSP_W25Qxx_Read_Data: send read cmd 0x%02x done, status = %d\n", cmd[0], state); state = HAL_SPI_Receive(w25qxx.spi_handler, rbuffer, rsize, HAL_MAX_DELAY); if (state != HAL_OK) { printf("BSP_W25Qxx_Read_Data: read data 0x%02x failed, status = %d\n", cmd[0], state); goto END; } printf("BSP_W25Qxx_Read_Data: read data 0x%02x done, status = %d\n", cmd[0], state); END: BSP_W25Qxx_CS_Disable(); }擦除 + 写入 + 读取的完整测试流程:
cpp// 1. 先擦除,才能后续写入 printf("----------------Erase Begin----------------\n"); BSP_W25Qxx_Sector_Erase(4096); printf("erase 4096 address done.\n"); HAL_Delay(2000); printf("----------------Erase Done----------------\r\n\r\n"); // 2. 读取确认(擦除后应为全0xFF) uint8_t rbuffer[256]; BSP_W25Qxx_Read_Data(4096, rbuffer, 256); // ... 打印 ... // 3. 写入字符串(写入第二个扇区的第一个page,页内偏移0) uint8_t *message = (uint8_t*)"hello world, hello bit!"; uint32_t len = strlen((const char*)message) + 1; // 把\0也带上 BSP_W25Qxx_Write_Page(16, 0, message, len); printf("Write message: %s\n", message); // 4. 再读出来验证 BSP_W25Qxx_Read_Data(4096, rbuffer, len); printf("read message: %s\n", rbuffer); Led4_On(); Led5_On();串口输出:
cppw25qxx init success, ID=00ef4017 Bsp_W25Qxx_Sector_Erase: Sector Erase cmd 0x20 done, status = 0 erase 4096 address done. Erase Done ----------------Read Begin Bsp_W25Qxx_Read_Data: send read cmd 0x03 done, status = 0 read data: 00000000000000000000000000000000000000000000... ----------------Write Begin Bsp_W25Qxx_Write_Page: write cmd 0x02 done, status = 0 Bsp_W25Qxx_Write_Page: write data 0x02 done, status = 0 write message: hello world, hello bit! Write Done ----------------Read Begin read message: hello world, hello bit! ← 写入-读取验证通过 ✅ Read Done
5.14 写数据(跨页写入封装)
测试通过了读取和页写入,但使用上存在一个痛点:写入时必须指明写到哪个 page(256 字节),且最多写一个 page。
我们想要的是在任意起始地址处写入任意长度的数据。
于是基于底层页编程函数
BSP_W25Qxx_Write_Page封装**BSP_W25Qxx_Write_Data,内部自动处理分页逻辑:**
cppvoid BSP_W25Qxx_Write_Data(uint32_t address, uint8_t *wbuffer, uint32_t wsize) { if (!w25qxx_init) return; if (wbuffer == NULL || wsize == 0) { printf("BSP_W25Qxx_Write_Data: Invalid params, wbuffer=%p, wsize=%d\n", wbuffer, wsize); return; } if (address + wsize >= w25qxx.mem_size) { printf("BSP_W25Qxx_Write_Data: address+wsize 0x%X out of range\r\n", address + wsize); return; } uint32_t bytes_written = 0; // 已写入字节计数器 while (bytes_written < wsize) { // 循环写入直到全部写完 uint32_t remaining_data = wsize - bytes_written; // 剩余未写入的数据量 uint16_t page_number = address / w25qxx.page_size; // 当前页号 uint16_t page_offset = address % w25qxx.page_size; // 页内偏移 uint16_t remaining_space = w25qxx.page_size - page_offset; // 当前页剩余空间 // 本次写入量 = min(剩余数据量, 当前页剩余空间) // 保证绝对不会跨页;需要跨页就下一次循环再写 uint16_t to_writen = remaining_data > remaining_space ? remaining_space : remaining_data; BSP_W25Qxx_Write_Page(page_number, page_offset, &wbuffer[bytes_written], to_writen); bytes_written += to_writen; address += to_writen; } }至此,驱动所需的全部函数已完成。
六、Flash 测试
实际写代码的时候,最好边写边测。
6.1 功能测试项
序号 测试项 ① W25Q64 整片擦除测试 ② W25Q64 随机写入测试 ③ W25Q64 随机读取测试 ④ W25Q64 扇区擦除测试 ⑤ W25Q64 页写入测试 ⑥ W25Q64 页读取测试 ⑦ W25Q64 周期性单字节读取测试 测试代码要点(main.c):
cpp/* USER CODE BEGIN PD */ #define W25QXX_PAGE_SIZE 256 // 为了测试 /* USER CODE END PD */ /* USER CODE BEGIN PV */ uint8_t rx_buffer[W25QXX_PAGE_SIZE]; // 读取缓存 uint8_t tx_buffer[W25QXX_PAGE_SIZE]; // 发送缓存 uint32_t first_page_address = 0; // 第一个page起始地址 uint32_t last_page_address = 0x7FFF00; // 最后一个page起始地址 uint32_t first_sector_address = 0; // 第一个sector起始地址 uint32_t last_sector_address = 0x7FF000; // 最后一个sector起始地址 /* USER CODE END PV */
- 整片擦除测试:
BSP_W25Qxx_Chip_Erase()→ 读首末页,应全为0xff;- 随机写测试:
BSP_W25Qxx_Write_Data(first_page_address, "hello, bit!", ...)和Write_Data(last_page_address, "w25q64 flash.", ...);- 随机读测试:分别读回首末页内容验证;
- 扇区擦除测试:
Sector_Erase(first_sector_address)和Sector_Erase(last_sector_address),擦除后读回全0xff;- 页写入测试:
tx_buffer[idx] = idx(首页 00FF)和tx_buffer[idx] = 255 - idx(末页 FF00),写满一页;- 页读取测试:按 16 字节一行十六进制打印,首页应为
00 01 02 ... FF,末页应为FF FE ... 00;- 周期性单字节读取:while(1) 中
Read_Data(addr0, &data0, 1),offset 自增,验证任意地址单字节读取。
6.2 下载验证(串口输出节选)
cppw25qxx init success, ID=0x00ef4017 1. W25Qxx Test Begin: 2. W25Qxx EraseChip Begin: Read Page First: 0xff,0xff | 0xff, 0xff ← 整片擦除成功 Read Page Last: 0xff,0xff | 0xff, 0xff 3. W25Qxx Random Write Begin: Write First Page: hello, bit! Write Last Page: w25q64 flash. 4. W25Qxx Random Read Begin: Read First Page: hello, bit! ← 随机读写一致 Read Last Page: w25q64 flash. 5.W25Qxx PageErase Begin: Erase First Sector Read Page First: 0xff,0xff | 0xff, 0xff ← 扇区擦除成功 ... 6.W25Qxx PageWrite Begin: Write Page First: 0x00,0x01 | 0xfe, 0xff Write Page Last: 0xff,0xfe | 0x01, 0x00 7.W25Qxx PageRead Begin: Read Page First: 00 01 02 ... FD FE FF ← 页写入读取一致 Read Page Last: FF FE FD ... 02 01 00 Read: first(00000000)=00, last(000001ff)=ff ← 周期单字节读取 Read: first(00000001)=01, last(000001ff)=fe ...
6.3 回卷(Wrap Around)验证
验证"地址超过页边界后回卷到页开头"的特性。测试前需临时去掉
BSP_W25Qxx_Write_Page中的页边界安全检查(page_inner_offset + num_byte > page_size的检测)。
cpp// 先擦除,然后才能写入 BSP_W25Qxx_Sector_Erase(0); // 欺骗W25Q64:前256字节全写0xFF(写入0xFF等于没写),再追加4字节 uint8_t wrap_around[260] = {0}; for (uint32_t idx = 0; idx < 256; idx++) wrap_around[idx] = 0xFF; wrap_around[256] = 0x1; wrap_around[257] = 0x2; wrap_around[258] = 0x3; wrap_around[259] = 0x4; // 一次写入260字节 → 超过页容量,发生回卷 BSP_W25Qxx_Write_Page(0, 0, wrap_around, 260); // 读取该页验证 uint8_t wrap_around_test[256] = {0}; BSP_W25Qxx_Read_Data(0, wrap_around_test, 256); for (uint32_t idx = 0; idx < 256; idx++) { printf("%03d ", wrap_around_test[idx]); if ((idx + 1) % 16 == 0) printf("\n"); }串口结果:
001 002 003 004 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 ...现象很明显:
第 257~260 字节(01 02 03 04)被回卷写到了页的开头位置,覆盖了原来的 0xFF。这就是"地址回卷"的直观证据。
七、附录:完整指令码定义(Bsp_W25Q64.h)
cpp/** * @brief W25Qxx系列Flash芯片命令定义 */ #define W25QXX_WRITE_ENABLE 0x06 // 写使能命令:允许对Flash进行写入操作 #define W25QXX_WRITE_DISABLE 0x04 // 写禁用命令:禁止对Flash进行写入操作 #define W25QXX_READ_STATUS_REG1 0x05 // 读取状态寄存器1命令 #define W25QXX_READ_STATUS_REG2 0x35 // 读取状态寄存器2命令 #define W25QXX_WRITE_STATUS_REG 0x01 // 写入状态寄存器命令 #define W25QXX_PAGE_PROGRAM 0x02 // 页编程命令:写入一页数据(最大256字节) #define W25QXX_QUAD_PAGE_PROGRAM 0x32 // 四通道页编程命令 #define W25QXX_BLOCK_ERASE 0xD8 // 块擦除命令:擦除一个块(通常为64KB) #define W25QXX_SECTOR_ERASE 0x20 // 扇区擦除命令:擦除一个扇区(通常为4KB) #define W25QXX_CHIP_ERASE 0xC7 // 整片擦除命令:擦除整个Flash芯片 #define W25QXX_ERASE_SUSPEND 0x75 // 擦除暂停命令 #define W25QXX_ERASE_RESUME 0x7A // 擦除恢复命令 #define W25QXX_POWER_DOWN 0xB9 // 掉电模式命令 #define W25QXX_HIGH_PERF_MODE 0xA3 // 高性能模式命令 #define W25QXX_CONTINUOUS_READ 0x03 // 连续读取命令 #define W25QXX_FAST_READ 0x0B // 快速读取命令 #define W25QXX_READ_DATA 0x03 // 读取数据命令(同连续读取) #define W25QXX_FAST_READ_DUAL 0x3B // 双输出快速读取命令 #define W25QXX_JEDEC_ID 0x9F // 读取JEDEC ID命令 #define W25QXX_RELEASE_PDWN 0xAB // 退出掉电模式命令 /* 状态寄存器位定义 */ #define W25QXX_STATUS_BUSY 0x01 // 1 = 芯片正在忙(擦除或写入操作中) #define W25QXX_STATUS_WR_EN 0x02 // 1 = 芯片已处于写使能状态
八、总结
一次完整的 SPI Flash 驱动开发,核心套路其实很固定:
cppflowchart TD A["写使能 06h(WEL=1,入场券)"] --> B["拉低 CS 选中设备"] B --> C["发送 指令码 + 24位地址 + 数据"] C --> D["拉高 CS(操作在 CS 上升沿触发)"] D --> E["轮询读状态寄存器 05h"] E --> F{"BUSY = 0 ?"} F -- "否" --> E F -- "是" --> G["操作完成,WEL 自动清零"]要点回顾:
- 先使能后操作:任何写/擦操作前必须
Write_Enable(),操作完 WEL 自动清零;- 等 BUSY:擦除/编程后必须
Waitfor_Write_Complete()轮询等待;- 擦除单位:扇区 4KB(低 12 位对齐)、块 64KB、整片约 20s;
- 写入单位:页 256 字节,不能跨页,跨页写入由
Write_Data分页封装;- 地址回卷:单次写入超过页剩余空间会回卷到页开头并覆盖;
- 擦除是写入前提:NOR Flash 只能把 1 写为 0,想写数据必须先把目标区域擦成全 1(0xFF);
- 别把擦除写进循环:Flash 有寿命,防止误擦除导致 Flash 报废。
工程文件结构
bashSPI_Flash_test/ ├── BSP_Drivers/ │ ├── inc/BSP_W25Q64.h ← 驱动头文件 │ └── src/BSP_W25Q64.c ← 驱动源文件 ├── APP_Test/ │ ├── App_Test.h ← 测试函数声明 │ └── App_Test.c ← 测试函数实现 └── Core/ ├── Inc/main.h ← CubeMX 生成,无需修改 └── Src/main.c ← 主函数(含外设初始化)
BSP_Drivers/inc/BSP_W25Q64.h
cpp#ifndef __BSP_W25Q64_H #define __BSP_W25Q64_H #ifdef __cplusplus extern "C" { #endif #include "stm32f1xx_hal.h" #include <stdio.h> #include <stdint.h> #include <stdbool.h> #include <string.h> #define CMD_W25QXX_JEDEC_ID 0x9F #define CMD_W25QXX_READ_STATUS_REG1 0x05 #define CMD_W25QXX_WRITE_ENABLE 0x06 #define CMD_W25QXX_WRITE_DISABLE 0x04 #define CMD_W25QXX_SECTOR_ERASE 0x20 #define CMD_W25QXX_READ_DATA 0x03 #define CMD_W25QXX_PAGE_PROGRAM 0x02 #define CMD_W25QXX_CHIP_ERASE 0xC7 // 设置BUSY检测位 #define W25Qxx_STATUS_BUSY 0x01 // 0000 0001 #define W25Qxx_STATUS_WEL 0x02 // 0000 0010 #define SECTOR_ADDRESS_MASK 0xFFF typedef struct { uint32_t page_size; // 页大小 uint32_t sector_size; // 扇区大小 uint32_t block_size; // 块大小 uint32_t mem_total_size; // 总大小 uint32_t chip_id; // 芯片ID SPI_HandleTypeDef *hspi; // spi句柄指针 GPIO_TypeDef *gpio_port; // 从设备选择引脚端口GPIOE uint16_t nss_pin; // 片选针脚 }spi_flash_t; bool BSP_W25Qxx_Init(spi_flash_t *init); uint32_t BSP_W25Qxx_ID(void); uint8_t BSP_W25Qxx_Statue1_Reg(void); void BSP_W25Qxx_Write_Enable(void); void BSP_W25Qxx_Write_Disable(void); void BSP_W25Qxx_Waitfor_Write_Complete(void); void BSP_W25Qxx_Waitfor_Write_Complete_V2(void); void BSP_W25Qxx_Sector_Erase(uint32_t sector_address); void BSP_W25Qxx_Chip_Erase(void); // void BSP_W25Qxx_Block_Erase(void); bool BSP_W25Qxx_Page_Program(uint32_t page_address, uint16_t page_inner_offset, uint8_t *wbuffer, uint16_t wsize); bool BSP_W25Qxx_Read_Data(uint32_t read_address, uint8_t *rbuffer, uint32_t rsize); bool BSP_W25Qxx_Write_Data(uint32_t write_address, uint8_t *wbuffer, uint32_t wsize); #ifdef __cplusplus } #endif #endif
BSP_Drivers/src/BSP_W25Q64.c
cpp#include "BSP_W25Q64.h" #include "BSP_LED.h" spi_flash_t g_w25qxx; static bool g_is_init = false; const uint8_t dummy = 0x00; // GPIOE, PIN6, 拉低片选 static void BSP_W25Qxx_CS_Enable(void) { if (!g_is_init) return; HAL_GPIO_WritePin(g_w25qxx.gpio_port, g_w25qxx.nss_pin, GPIO_PIN_RESET); } static void BSP_W25Qxx_CS_Disable(void) { if (!g_is_init) return; HAL_GPIO_WritePin(g_w25qxx.gpio_port, g_w25qxx.nss_pin, GPIO_PIN_SET); } static bool BSP_W25Qxx_Is_busy(void) { return BSP_W25Qxx_Statue1_Reg() & W25Qxx_STATUS_BUSY; } static bool BSP_W25Qxx_Is_Wel_Enable(void) { return BSP_W25Qxx_Statue1_Reg() & W25Qxx_STATUS_WEL; } bool BSP_W25Qxx_Init(spi_flash_t *init) { if (init == NULL) return g_is_init; g_w25qxx.page_size = init->page_size; g_w25qxx.sector_size = init->sector_size; g_w25qxx.block_size = init->block_size; g_w25qxx.mem_total_size = init->mem_total_size; g_w25qxx.chip_id = init->chip_id; g_w25qxx.hspi = init->hspi; g_w25qxx.gpio_port = init->gpio_port; g_w25qxx.nss_pin = init->nss_pin; g_is_init = true; // TODO uint32_t id = BSP_W25Qxx_ID(); // 获取SPI flash内部的id if (g_w25qxx.chip_id != id) { g_is_init = false; printf(">> BSP_W25Qxx_Init error, ID error, set ID is : 0x%x, current ID is: 0x%x\n", g_w25qxx.chip_id, id); return g_is_init; } printf(">> BSP_W25Qxx_Init success! current ID is: 0x%x\n", id); return g_is_init; } uint32_t BSP_W25Qxx_ID(void) { if (!g_is_init) return 0x0; BSP_W25Qxx_CS_Enable(); uint8_t cmd = CMD_W25QXX_JEDEC_ID; uint8_t id[3] = {0}; HAL_StatusTypeDef state = HAL_SPI_Transmit(g_w25qxx.hspi, &cmd, sizeof(cmd), HAL_MAX_DELAY); if (state != HAL_OK) { printf(">> BSP_W25Qxx_ID, HAL_SPI_Transmit error, cmd is : 0x%x failed, state: 0x%x\n", cmd, state); memset(id, 0, sizeof(id)); goto END; } state = HAL_SPI_Receive(g_w25qxx.hspi, id, sizeof(id), HAL_MAX_DELAY); if (state != HAL_OK) { printf(">> BSP_W25Qxx_ID, HAL_SPI_Receive error, cmd is : 0x%x failed, state: 0x%x\n", cmd, state); memset(id, 0, sizeof(id)); goto END; } END: BSP_W25Qxx_CS_Disable(); return ((id[0] << 16) | (id[1] << 8) | (id[2])); // 0000 0000 0000 0000 0000 0000 0000 0000 } uint8_t BSP_W25Qxx_Statue1_Reg(void) { if (!g_is_init) return 0xFF; uint8_t cmd = CMD_W25QXX_READ_STATUS_REG1; uint8_t status_reg = 0xFF; BSP_W25Qxx_CS_Enable(); HAL_StatusTypeDef state = HAL_SPI_Transmit(g_w25qxx.hspi, &cmd, sizeof(cmd), HAL_MAX_DELAY); if (state != HAL_OK) { printf(">> BSP_W25Qxx_Statue1_Reg HAL_SPI_Transmit error, cmd is : 0x%x failed, state: 0x%x\n", cmd, state); goto END; } state = HAL_SPI_Receive(g_w25qxx.hspi, &status_reg, sizeof(status_reg), HAL_MAX_DELAY); if (state != HAL_OK) { status_reg = 0xFF; printf(">> BSP_W25Qxx_Statue1_Reg HAL_SPI_Receive error, cmd is : 0x%x failed, state: 0x%x\n", cmd, state); goto END; } END: BSP_W25Qxx_CS_Disable(); return status_reg; } void BSP_W25Qxx_Write_Enable(void) { if (!g_is_init) return; uint8_t cmd = CMD_W25QXX_WRITE_ENABLE; BSP_W25Qxx_CS_Enable(); HAL_StatusTypeDef state = HAL_SPI_Transmit(g_w25qxx.hspi, &cmd, sizeof(cmd), HAL_MAX_DELAY); if (state != HAL_OK) { printf(">> BSP_W25Qxx_Write_Enable HAL_SPI_Transmit error, cmd is : 0x%x failed, state: 0x%x\n", cmd, state); } BSP_W25Qxx_CS_Disable(); } void BSP_W25Qxx_Write_Disable(void) { if (!g_is_init) return; uint8_t cmd = CMD_W25QXX_WRITE_DISABLE; BSP_W25Qxx_CS_Enable(); HAL_StatusTypeDef state = HAL_SPI_Transmit(g_w25qxx.hspi, &cmd, sizeof(cmd), HAL_MAX_DELAY); if (state != HAL_OK) { printf(">> BSP_W25Qxx_Write_Enable HAL_SPI_Transmit error, cmd is : 0x%x failed, state: 0x%x\n", cmd, state); } BSP_W25Qxx_CS_Disable(); } void BSP_W25Qxx_Waitfor_Write_Complete(void) { if (!g_is_init) return; // while (BSP_W25Qxx_Statue1_Reg() & W25Qxx_STATUS_BUSY) // ; int cnt = 0; printf(">> 整盘擦除中\n"); while (BSP_W25Qxx_Statue1_Reg() & W25Qxx_STATUS_BUSY) { printf("."); cnt++; LED_Blink(LED1, 1, 500); } printf("\n>> 整盘擦除完成, 花费时间: %dS\n", cnt); } void BSP_W25Qxx_Waitfor_Write_Complete_V2(void) { if (!g_is_init) return; uint8_t cmd = CMD_W25QXX_READ_STATUS_REG1; uint8_t reg1 = 0xFF; BSP_W25Qxx_CS_Enable(); HAL_StatusTypeDef state = HAL_SPI_Transmit(g_w25qxx.hspi, &cmd, sizeof(cmd), HAL_MAX_DELAY); if (state != HAL_OK) { printf(">> BSP_W25Qxx_Waitfor_Write_Complete_V2 HAL_SPI_Transmit error, cmd is : 0x%x failed, state: 0x%x\n", cmd, state); goto END; } do { HAL_SPI_TransmitReceive(g_w25qxx.hspi, &dummy, ®1, sizeof(dummy), HAL_MAX_DELAY); } while (reg1 & W25Qxx_STATUS_BUSY); END: BSP_W25Qxx_CS_Disable(); } void BSP_W25Qxx_Sector_Erase(uint32_t sector_address) { if (!g_is_init) return; if (sector_address & SECTOR_ADDRESS_MASK) { printf(">>> BSP_W25Qxx_Sector_Erase error, address 0x%x not sector aligned\n", sector_address); return; } if (sector_address >= g_w25qxx.mem_total_size) { printf(">>> BSP_W25Qxx_Sector_Erase error, address 0x%x out of range max flash size\n", sector_address); return; } // cmd[0] = CMD_W25QXX_SECTOR_ERASE; // cmd[1] = (sector_address>>16) // 1. 检测WEL位是否被设置1,如果为0才需要设置 2. 设置 if (!BSP_W25Qxx_Is_Wel_Enable()) { BSP_W25Qxx_Write_Enable(); } uint8_t cmd[4] = {CMD_W25QXX_SECTOR_ERASE, (sector_address >> 16), (sector_address >> 8), sector_address}; BSP_W25Qxx_CS_Enable(); HAL_StatusTypeDef state = HAL_SPI_Transmit(g_w25qxx.hspi, cmd, sizeof(cmd) / sizeof(cmd[0]), HAL_MAX_DELAY); if (state != HAL_OK) { printf(">> BSP_W25Qxx_Sector_Erase HAL_SPI_Transmit error, cmd is : 0x%x failed, state: 0x%x\n", cmd[0], state); goto END; } printf(">> BSP_W25Qxx_Sector_Erase success\n"); // 擦除其实没有成功!!!! END: BSP_W25Qxx_CS_Disable(); if (state == HAL_OK) BSP_W25Qxx_Waitfor_Write_Complete_V2(); } void BSP_W25Qxx_Chip_Erase(void) { if (!g_is_init) return; // 1. 检测WEL位是否被设置1,如果为0才需要设置 2. 设置 if (!BSP_W25Qxx_Is_Wel_Enable()) { BSP_W25Qxx_Write_Enable(); } // 正片擦除的逻辑 uint8_t cmd = CMD_W25QXX_CHIP_ERASE; BSP_W25Qxx_CS_Enable(); HAL_StatusTypeDef state = HAL_SPI_Transmit(g_w25qxx.hspi, &cmd, sizeof(cmd), HAL_MAX_DELAY); if (state != HAL_OK) { printf(">> BSP_W25Qxx_Chip_Erase HAL_SPI_Transmit error, cmd is : 0x%x failed, state: 0x%x\n", cmd, state); } BSP_W25Qxx_CS_Disable(); if (state == HAL_OK) BSP_W25Qxx_Waitfor_Write_Complete(); } bool BSP_W25Qxx_Read_Data(uint32_t read_address, uint8_t *rbuffer, uint32_t rsize) { if (!g_is_init) return false; if (rbuffer == NULL || rsize == 0) return false; if (read_address + rsize > g_w25qxx.mem_total_size) return false; bool ret = true; // 1. 检测W25qxx是否在忙 2. 等待忙完 if (BSP_W25Qxx_Is_busy()) BSP_W25Qxx_Waitfor_Write_Complete_V2(); uint8_t cmd[] = {CMD_W25QXX_READ_DATA, (uint8_t)(read_address >> 16), (uint8_t)(read_address >> 8), (uint8_t)(read_address)}; uint16_t cmdlen = sizeof(cmd) / sizeof(cmd[0]); // 2. 发送 BSP_W25Qxx_CS_Enable(); HAL_StatusTypeDef state = HAL_SPI_Transmit(g_w25qxx.hspi, cmd, cmdlen, HAL_MAX_DELAY); if (state != HAL_OK) { ret = false; printf(">> BSP_W25Qxx_Read_Data HAL_SPI_Transmit error, cmd is : 0x%x failed, state: 0x%x\n", cmd[0], state); goto END; } state = HAL_SPI_Receive(g_w25qxx.hspi, rbuffer, (uint16_t)rsize, HAL_MAX_DELAY); if (state != HAL_OK) { ret = false; printf(">> BSP_W25Qxx_Read_Data HAL_SPI_Receive error, cmd is : 0x%x failed, state: 0x%x\n", cmd[0], state); goto END; } printf(">> BSP_W25Qxx_Read_Data success, cmd is : 0x%x, state: 0x%x\n", cmd[0], state); END: BSP_W25Qxx_CS_Disable(); return ret; } // 这个是对页进行写入的函数 bool BSP_W25Qxx_Page_Program(uint32_t page_address, uint16_t page_inner_offset, uint8_t *wbuffer, uint16_t wsize) { if (!g_is_init) return false; if (wbuffer == NULL || wsize == 0) return false; // 不能超过一页大小 if (page_inner_offset + wsize > g_w25qxx.page_size) return false; // 真实的写入地址 uint32_t write_address = page_address * g_w25qxx.page_size + page_inner_offset; bool ret = true; // 构建指令 uint8_t cmd[] = {CMD_W25QXX_PAGE_PROGRAM, (uint8_t)(write_address >> 16), (uint8_t)(write_address >> 8), (uint8_t)(write_address)}; uint16_t cmdlen = sizeof(cmd) / sizeof(cmd[0]); // 发送指令,首选需要关闭禁止写入 // 1. 检测WEL位是否被设置1,如果为0才需要设置 2. 设置 if (!BSP_W25Qxx_Is_Wel_Enable()) { BSP_W25Qxx_Write_Enable(); } BSP_W25Qxx_CS_Enable(); HAL_StatusTypeDef state = HAL_SPI_Transmit(g_w25qxx.hspi, cmd, cmdlen, HAL_MAX_DELAY); if (state != HAL_OK) { ret = false; printf(">> BSP_W25Qxx_Page_Program HAL_SPI_Transmit cmd error, cmd is : 0x%x failed, state: 0x%x\n", cmd[0], state); goto END; } // 发送数据 state = HAL_SPI_Transmit(g_w25qxx.hspi, wbuffer, wsize, HAL_MAX_DELAY); if (state != HAL_OK) { ret = false; printf(">> BSP_W25Qxx_Page_Program HAL_SPI_Transmit data error, cmd is : 0x%x failed, state: 0x%x\n", cmd[0], state); goto END; } printf(">> BSP_W25Qxx_Page_Program HAL_SPI_Transmit cmd && data success, cmd is : 0x%x, state: 0x%x\n", cmd[0], state); END: BSP_W25Qxx_CS_Disable(); if (state == HAL_OK) BSP_W25Qxx_Waitfor_Write_Complete_V2(); return ret; } bool BSP_W25Qxx_Write_Data(uint32_t write_address, uint8_t *wbuffer, uint32_t wsize) { if (!g_is_init) return false; if (wbuffer == NULL || wsize == 0) return false; if (write_address + wsize > g_w25qxx.mem_total_size) { printf("BSP_W25Qxx_Write_Data error, address out range!\n"); return false; } // 已经写入的字节数 uint32_t written_bytes = 0; while (written_bytes < wsize) { // 计算剩余数据 uint32_t remaining_data = wsize - written_bytes; // 计算写入的页号 uint16_t page_number = write_address / g_w25qxx.page_size; // 计算页内偏移 uint16_t page_inner_offset = write_address % g_w25qxx.page_size; // 当前页的剩余空间 uint16_t remaining_space = g_w25qxx.page_size - page_inner_offset; // 计算本次写入的数据量 uint16_t to_written = remaining_data > remaining_space ? remaining_space : remaining_data; // 进行页编程 bool ret = BSP_W25Qxx_Page_Program(page_number, page_inner_offset, &wbuffer[written_bytes], to_written); (void)ret; // 更新已经写入的数据量 written_bytes += to_written; // 更新当前写入地址 write_address += to_written; } return true; }
APP_Test/App_Test.h
cpp#ifndef ___APP_TEST_H #define ___APP_TEST_H #ifdef __cplusplus extern "C" { #endif #include "BSP_W25Q64.h" void Test_W25Q64_Driver_Init(SPI_HandleTypeDef *hspi, GPIO_TypeDef *port, uint16_t pin); void Test_W25Q64_Driver_Reg1(void); void Test_W25Q64_Driver_Write_Enable_Disable(void); void Test_W25Q64_Driver_Erase_Read(void); void Test_W25Q64_Driver_ChipErase_Any_Read_Write(void); void For_SPI_Test(SPI_HandleTypeDef *hspi, GPIO_TypeDef *port, uint16_t pin); #ifdef __cplusplus } #endif #endif
APP_Test/App_Test.c
cpp#include "App_Test.h" void Test_W25Q64_Driver_Init(SPI_HandleTypeDef *hspi, GPIO_TypeDef *port, uint16_t pin) { printf("--------------Test_W25Q64_Driver_Init-----------\n"); spi_flash_t init = { .page_size = 256, .sector_size = 256 * 16, .block_size = 256 * 16 * 16, .mem_total_size = 256 * 16 * 16 * 128, .chip_id = 0xEF4017, .hspi = hspi, .gpio_port = port, .nss_pin = pin}; BSP_W25Qxx_Init(&init); } void Test_W25Q64_Driver_Reg1(void) { printf("--------------Test_W25Q64_Driver_Reg1-----------\n"); uint8_t reg1 = BSP_W25Qxx_Statue1_Reg(); printf(">> register1 value is : 0x%x\n", reg1); } void Test_W25Q64_Driver_Write_Enable_Disable(void) { printf("--------------Test_W25Q64_Driver_Write_Enable-----------\n"); uint8_t reg1 = BSP_W25Qxx_Statue1_Reg(); printf(">> register1 value is : 0x%x\n", reg1); BSP_W25Qxx_Write_Enable(); reg1 = BSP_W25Qxx_Statue1_Reg(); printf(">> register1 value is : 0x%x\n", reg1); BSP_W25Qxx_Write_Disable(); reg1 = BSP_W25Qxx_Statue1_Reg(); printf(">> register1 value is : 0x%x\n", reg1); } void Test_W25Q64_Driver_Erase_Read(void) { printf("--------------Test_W25Q64_Driver_Erase_Read-----------\n"); // 1. 先读取数据 && 打印 uint32_t start_address = 0; uint32_t end_address = 4096 - 4; uint32_t rx_buffer_first = 0; uint32_t rx_buffer_last = 0; // 1.1 把page0前4个字节清零 uint32_t data = 0; BSP_W25Qxx_Page_Program(0, 0, (uint8_t *)&data, sizeof(data)); BSP_W25Qxx_Read_Data(start_address, (uint8_t *)&rx_buffer_first, sizeof(rx_buffer_first)); BSP_W25Qxx_Read_Data(end_address, (uint8_t *)&rx_buffer_last, sizeof(rx_buffer_last)); printf(">> 擦除前: rx_buffer_first: 0x%x, rx_buffer_last: 0x%x\n", rx_buffer_first, rx_buffer_last); // 2. 擦除 BSP_W25Qxx_Sector_Erase(start_address); // 3. 先读取数据 && 打印 rx_buffer_first = 0; rx_buffer_last = 0; BSP_W25Qxx_Read_Data(start_address, (uint8_t *)&rx_buffer_first, sizeof(rx_buffer_first)); BSP_W25Qxx_Read_Data(end_address, (uint8_t *)&rx_buffer_last, sizeof(rx_buffer_last)); printf(">> 擦除后: rx_buffer_first: 0x%x, rx_buffer_last: 0x%x\n", rx_buffer_first, rx_buffer_last); } uint8_t buffer[4096 * 2]; static void PrintData(int step) { printf("------------------Step: %d-------------\n", step); // 先打印前8个字节 for (int i = 0; i < 8; i++) printf("0x%x ", buffer[i]); printf("\n"); // 先打印后8个字节 for (int i = 4096 * 2 - 8; i < 4096 * 2; i++) printf("0x%x ", buffer[i]); printf("\n"); } void Test_W25Q64_Driver_ChipErase_Any_Read_Write(void) { // 1. 确定两个sector uint32_t start_address = 0x0; uint16_t size = 4096 * 2; // 2. 先读取 BSP_W25Qxx_Read_Data(start_address, buffer, size); // 3. 挑选若干数据 PrintData(1); // 4. 擦除两页 BSP_W25Qxx_Sector_Erase(0); BSP_W25Qxx_Sector_Erase(4096); BSP_W25Qxx_Read_Data(start_address, buffer, size); PrintData(2); // 5. 写入数据 memset(buffer, 0, sizeof(buffer)); BSP_W25Qxx_Write_Data(start_address, buffer, size); BSP_W25Qxx_Read_Data(start_address, buffer, size); PrintData(3); // 6. 整盘擦除 BSP_W25Qxx_Chip_Erase(); // 7. 读取 BSP_W25Qxx_Read_Data(start_address, buffer, size); PrintData(3); // } void For_SPI_Test(SPI_HandleTypeDef *hspi, GPIO_TypeDef *port, uint16_t pin) { Test_W25Q64_Driver_Init(hspi, port, pin); Test_W25Q64_Driver_Reg1(); Test_W25Q64_Driver_Write_Enable_Disable(); Test_W25Q64_Driver_Erase_Read(); Test_W25Q64_Driver_ChipErase_Any_Read_Write(); }

