文章目录
-
- [我为什么放弃了软件模拟 SPI](#我为什么放弃了软件模拟 SPI)
- [SPI 到底怎么传数据](#SPI 到底怎么传数据)
- [把 W25Q128 接上开发板](#把 W25Q128 接上开发板)
- [CubeMX 里的关键配置](#CubeMX 里的关键配置)
- [写一个能用的 W25Q 驱动](#写一个能用的 W25Q 驱动)
-
- [读 ID 与忙等待](#读 ID 与忙等待)
- 写使能、扇区擦除与页编程
- [读数据:轮询与 DMA 两版](#读数据:轮询与 DMA 两版)
- [实测:DMA 到底快多少](#实测:DMA 到底快多少)
- [我踩过的五个坑(SPI Flash 故障排查)](#我踩过的五个坑(SPI Flash 故障排查))
- 总结与下一步
- 版本备注
- 参考资料
摘要:嵌入式项目需存配置或字库,片上 EEPROM 太小、SD 卡过重,W25Q SPI Flash 低价提供 MB 级容量,新手首调常卡在"读回 ID 全是 0xFF"。本文基于 STM32F103C8T6 + W25Q128 搭可复用驱动:CubeMX 配 SPI 全双工主 + DMA 双通道,覆盖读 ID、擦除、页编程与 DMA 大块读,对比轮询与 DMA 搬运。实测读 1MB:轮询 3.2s、CPU100%;SPI+DMA 降至 0.95s、CPU<5%,吞吐率 0.31→1.05MB/s。
我为什么放弃了软件模拟 SPI
最早学 SPI 那会儿,图省事用 GPIO 翻转模拟时序(所谓"软 SPI"),代码里一堆 for 循环配合 delay_us 凑时钟。写个 256 字节页编程要占住 CPU 好几毫秒,主循环直接卡成幻灯片。更糟的是软 SPI 的时钟频率完全取决于循环延时,一旦换编译器优化等级或主频,时序就飘。
硬件 SPI 把时钟、移位、收发全交给外设硬件,CPU 只管往数据寄存器丢字节。对 W25Q 这种"写一次要等几十毫秒"的慢速器件,恰好是 DMA 发挥价值的最佳场景------发完指令就把总线交给 DMA,CPU 转头去干别的。
本文完整工程代码可在 CSDN 下载频道 获取(VIP 免费)。
读完这篇你能拿到:一个不依赖具体 HAL 版本、可直接移植到 F1/F4 的 W25Q 驱动框架;一套经过实测的 CubeMX 关键参数;以及把"读 ID 全是 0xFF""写了读出来还是旧值"这类问题一次性排干净的清单。
SPI 到底怎么传数据
SPI 是四线全双工同步总线:SCK 是主设备给的时钟,MOSI 主发从收、MISO 主收从发,CS(片选)拉低表示"现在跟你通信"。核心要记牢一点------SPI 没有"只收不发",时钟每跳一格,主从双方就同时移位一位。所以"读 Flash"的硬件动作其实是:主设备一边发 dummy 字节(通常是 0xFF)制造时钟,一边从 MISO 收数据。
W25Q128(从) STM32(主) W25Q128(从) STM32(主) #mermaid-svg-DHgCX2LxyKGpls3w{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-DHgCX2LxyKGpls3w .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-DHgCX2LxyKGpls3w .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-DHgCX2LxyKGpls3w .error-icon{fill:#552222;}#mermaid-svg-DHgCX2LxyKGpls3w .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-DHgCX2LxyKGpls3w .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-DHgCX2LxyKGpls3w .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-DHgCX2LxyKGpls3w .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-DHgCX2LxyKGpls3w .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-DHgCX2LxyKGpls3w .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-DHgCX2LxyKGpls3w .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-DHgCX2LxyKGpls3w .marker{fill:#333333;stroke:#333333;}#mermaid-svg-DHgCX2LxyKGpls3w .marker.cross{stroke:#333333;}#mermaid-svg-DHgCX2LxyKGpls3w svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-DHgCX2LxyKGpls3w p{margin:0;}#mermaid-svg-DHgCX2LxyKGpls3w .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-DHgCX2LxyKGpls3w text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-DHgCX2LxyKGpls3w .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-DHgCX2LxyKGpls3w .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-DHgCX2LxyKGpls3w .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-DHgCX2LxyKGpls3w .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-DHgCX2LxyKGpls3w #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-DHgCX2LxyKGpls3w .sequenceNumber{fill:white;}#mermaid-svg-DHgCX2LxyKGpls3w #sequencenumber{fill:#333;}#mermaid-svg-DHgCX2LxyKGpls3w #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-DHgCX2LxyKGpls3w .messageText{fill:#333;stroke:none;}#mermaid-svg-DHgCX2LxyKGpls3w .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-DHgCX2LxyKGpls3w .labelText,#mermaid-svg-DHgCX2LxyKGpls3w .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-DHgCX2LxyKGpls3w .loopText,#mermaid-svg-DHgCX2LxyKGpls3w .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-DHgCX2LxyKGpls3w .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-DHgCX2LxyKGpls3w .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-DHgCX2LxyKGpls3w .noteText,#mermaid-svg-DHgCX2LxyKGpls3w .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-DHgCX2LxyKGpls3w .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-DHgCX2LxyKGpls3w .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-DHgCX2LxyKGpls3w .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-DHgCX2LxyKGpls3w .actorPopupMenu{position:absolute;}#mermaid-svg-DHgCX2LxyKGpls3w .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-DHgCX2LxyKGpls3w .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-DHgCX2LxyKGpls3w .actor-man circle,#mermaid-svg-DHgCX2LxyKGpls3w line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-DHgCX2LxyKGpls3w :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} loop 每个时钟沿 CS 拉低 发送指令字节(如 0x03 读) 发送 3 字节地址 发 dummy(0xFF) 产生 SCK 返回 1 字节数据 CS 拉高, 结束本次访问
时钟极性相位:最容易翻车的地方
CPOL 决定空闲时 SCK 是高还是低,CPHA 决定在时钟的第一个还是第二个边沿采样。四组组合对应 SPI 模式 0~3。W25Q128 支持模式 0(CPOL=0, CPHA=0)和模式 3,绝大多数驱动用模式 0。我第一次配 CubeMX 时把 CPHA 错设成 2Edge,读回来的 ID 全是 0xFF------因为采样沿和数据变化沿对不上,错了一位后面全乱。
三种搬运方式怎么选
| 方式 | CPU 占用 | 适用场景 | 我的判断 |
|---|---|---|---|
轮询 HAL_SPI_TransmitReceive |
100%,全程阻塞 | 单字节读 ID、发指令 | 只适合短指令,长数据会卡死主循环 |
中断 HAL_SPI_TransmitReceive_IT |
中,每字节进 ISR | 中等数据量 | 比轮询好,但几千字节仍频繁打断 |
DMA HAL_SPI_TransmitReceive_DMA |
<5% | 大块读写(页/扇区) | 本项目主用,CPU 几乎零参与 |
设计决策很直接:读 ID、发擦除指令这种几十字节的操作,轮询最省事;但读 1MB 字库时,必须用 DMA,否则界面刷新会卡顿。所以最终驱动是"短指令轮询 + 大块数据 DMA"混合方案。
相关阅读:STM32硬件SPI驱动W25Q128实战:从CubeMX配置到DMA高速读写 --- 看别人如何处理 DMA 通道与 CPOL/CPHA 的配置细节。
把 W25Q128 接上开发板
W25Q128 是标准 SPI 接口,接线比 I2C 还简单,但 CS 和电源有两个坑。
| STM32F103 引脚 | W25Q128 引脚 | 说明 |
|---|---|---|
| PA5 | SCK | SPI 时钟,3.3V |
| PA7 | MOSI | 主发从收 |
| PA6 | MISO | 主收从发 |
| PA4 | CS | 片选,GPIO 软件控制,初始高电平 |
| 3.3V | VCC | 必须 3.3V,不要接 5V |
| GND | GND | 与 MCU 共地 |
| --- | WP / HOLD | 本驱动悬空(写保护关掉即可) |
设计决策:为什么用 GPIO 软件控制 CS,而不是硬件 NSS?因为 STM32 的硬件 NSS 在"脉冲模式"下会在每个字节之间把 CS 拉高 ,而 W25Q 要求整条指令(指令字节 + 地址 + 数据)期间 CS 必须一直保持低电平。一旦中途拉高,Flash 就认为指令结束、重新开始等待。DMA 多缓冲场景下 HAL 更不会替你维持 CS,所以老老实实用 GPIO_WritePin 手动拉 CS 最稳。
⚠️ 风险提示 :W25Q 系列 VCC 标称 2.7~3.6V,绝对不能接 5V,否则芯片直接烧毁。VCC 旁务必并联 0.1μF 陶瓷电容抑制电源噪声;CS 引脚建议加 4.7kΩ 上拉电阻,确保上电瞬间不被误触发。
CubeMX 里的关键配置
新建工程后,按下面步骤配,少一步都可能读不到 ID。
1. SPI1 基础配置(Connectivity → SPI1)
- Mode:
Full-Duplex Master(全双工主) - Hardware NSS Signal:
Disable(我们用 GPIO 控 CS) - Parameter Settings:
- Clock Prescaler:
8(APB2=72MHz → SPI 时钟 9MHz,先低速调通再提速) - CPOL:
Low,CPHA:1 Edge(即 SPI 模式 0) - Data Size:
8 Bits - First Bit:
MSB First
- Clock Prescaler:
2. DMA 双通道(SPI1 的 DMA Settings 选项卡,Add 两个请求)
SPI1_TX:DirectionMemory To Peripheral,ModeNormal,PriorityMediumSPI1_RX:DirectionPeripheral To Memory,ModeNormal,PriorityMedium
3. CS 引脚 :把 PA4 设为 GPIO_Output,初始 High,标签改成 FLASH_CS。
设计决策:DMA 的 TX 和 RX 通道必须同时开,哪怕你只想读数据。这是 SPI 全双工决定的------读的时候主设备得不停发 dummy 时钟,不发就没时钟、收不到东西。我见过有人只配 RX 通道,结果数据死活读不出来,调了一整天。
相关阅读:STM32CubeMX配置SPI驱动W25Q128实战:从硬件连接到DMA优化 --- 里面给了 HAL 生成的 SPI 初始化代码,可对照你 CubeMX 导出的结果。
写一个能用的 W25Q 驱动
先在 main.h 里定义 CS 操作宏,后面所有函数共用:
c
/* main.h 中追加 */
#define FLASH_CS_PIN GPIO_PIN_4
#define FLASH_CS_PORT GPIOA
#define FLASH_CS_LOW() HAL_GPIO_WritePin(FLASH_CS_PORT, FLASH_CS_PIN, GPIO_PIN_RESET)
#define FLASH_CS_HIGH() HAL_GPIO_WritePin(FLASH_CS_PORT, FLASH_CS_PIN, GPIO_PIN_SET)
读 ID 与忙等待
读 ID 指令 0x9F 返回 3 字节:厂商(W25Q 为 0xEF)、内存类型(0x40)、容量(0x18 表示 128Mbit)。这是验证硬件接线的第一道关卡。
c
/* 读厂商/设备 ID,正常返回 0xEF */
uint8_t W25Q_ReadID(void) {
uint8_t tx[4] = {0x9F, 0x00, 0x00, 0x00};
uint8_t rx[4] = {0};
FLASH_CS_LOW();
HAL_SPI_TransmitReceive(&hspi1, tx, rx, 4, HAL_MAX_DELAY);
FLASH_CS_HIGH();
return rx[1]; /* 0xEF 即 Winbond */
}
/* 读状态寄存器 1,BUSY 位为 bit0 */
uint8_t W25Q_ReadSR(void) {
uint8_t tx[2] = {0x05, 0x00};
uint8_t rx[2] = {0};
FLASH_CS_LOW();
HAL_SPI_TransmitReceive(&hspi1, tx, rx, 2, HAL_MAX_DELAY);
FLASH_CS_HIGH();
return rx[1];
}
/* 阻塞等待擦除/编程完成 */
void W25Q_WaitBusy(void) {
while (W25Q_ReadSR() & 0x01) { /* BUSY=1 时原地等 */ }
}
写使能、扇区擦除与页编程
Flash 有个反直觉特性:写入前必须先擦除,而擦除的最小单位是 4KB 扇区。擦除后该扇区位全变 1,页编程(256 字节)只能把 1 写成 0。所以"写了读出来还是旧值",八成是忘了擦除,或者擦除没等完就写。
c
void W25Q_WriteEnable(void) {
uint8_t cmd = 0x06;
FLASH_CS_LOW();
HAL_SPI_Transmit(&hspi1, &cmd, 1, HAL_MAX_DELAY);
FLASH_CS_HIGH();
}
/* 4KB 扇区擦除,addr 取扇区首地址即可 */
void W25Q_SectorErase(uint32_t addr) {
W25Q_WriteEnable();
uint8_t tx[4] = {0x20,
(addr >> 16) & 0xFF,
(addr >> 8) & 0xFF,
addr & 0xFF};
FLASH_CS_LOW();
HAL_SPI_Transmit(&hspi1, tx, 4, HAL_MAX_DELAY);
FLASH_CS_HIGH();
W25Q_WaitBusy(); /* 擦除约 45ms,必须等 */
}
/* 页编程,单次最多 256 字节,且不能跨页 */
void W25Q_PageProgram(uint32_t addr, uint8_t *buf, uint16_t len) {
if (len > 256) len = 256; /* 超出会绕回页首覆盖 */
W25Q_WriteEnable();
uint8_t header[4] = {0x02,
(addr >> 16) & 0xFF,
(addr >> 8) & 0xFF,
addr & 0xFF};
FLASH_CS_LOW();
HAL_SPI_Transmit(&hspi1, header, 4, HAL_MAX_DELAY);
HAL_SPI_Transmit(&hspi1, buf, len, HAL_MAX_DELAY);
FLASH_CS_HIGH();
W25Q_WaitBusy();
}
读数据:轮询与 DMA 两版
轮询版最简单,适合随机小读取:
c
void W25Q_Read(uint32_t addr, uint8_t *buf, uint32_t len) {
uint8_t header[4] = {0x03,
(addr >> 16) & 0xFF,
(addr >> 8) & 0xFF,
addr & 0xFF};
FLASH_CS_LOW();
HAL_SPI_Transmit(&hspi1, header, 4, HAL_MAX_DELAY);
HAL_SPI_Receive(&hspi1, buf, len, HAL_MAX_DELAY);
FLASH_CS_HIGH();
}
读 1MB 字库时换 DMA 版,CPU 几乎完全释放。注意 SPI 全双工要求收发同时,所以用 TransmitReceive_DMA 并在 TX 缓冲塞 0xFF 当 dummy 时钟:
c
#define W25Q_DMA_BUF 4096
static uint8_t g_dma_tx[W25Q_DMA_BUF]; /* 全填 0xFF 作 dummy */
volatile uint8_t g_dma_done = 0;
/* 大块读:先发 4 字节读指令(轮询),再 DMA 收数据 */
void W25Q_Read_DMA(uint32_t addr, uint8_t *buf, uint16_t len) {
uint8_t header[4] = {0x03,
(addr >> 16) & 0xFF,
(addr >> 8) & 0xFF,
addr & 0xFF};
FLASH_CS_LOW();
HAL_SPI_Transmit(&hspi1, header, 4, HAL_MAX_DELAY);
memset(g_dma_tx, 0xFF, len);
g_dma_done = 0;
HAL_SPI_TransmitReceive_DMA(&hspi1, g_dma_tx, buf, len);
while (!g_dma_done); /* 也可改回调通知,避免忙等 */
FLASH_CS_HIGH();
}
/* DMA 完成回调里拉高 CS,结束本次访问 */
void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi) {
if (hspi->Instance == SPI1) {
g_dma_done = 1;
}
}
相关阅读:STM32cube之SPI+DMA高效搬运W25Q海量数据的实战与调优 --- 讲清了 DMA 内存对齐、循环模式取舍,以及 CPU 占用从 90% 降到 10% 以下的实测。
实测:DMA 到底快多少
测试环境:STM32F103C8T6,SPI 时钟 9MHz,从 W25Q128 偏移 0 处读取累计 1MB 数据,写测试用 256KB(1024 页 + 64 个 4KB 扇区擦除)。
读取 1MB 三种方式对比:
| 方式 | 耗时 | CPU 占用 | 吞吐率 |
|---|---|---|---|
轮询 HAL_SPI_Receive |
3.2 s | 100% | 0.31 MB/s |
中断 HAL_SPI_Receive_IT |
1.8 s | 约 40% | 0.56 MB/s |
DMA TransmitReceive_DMA |
0.95 s | <5% | 1.05 MB/s |
| 理论极限(9MHz/8bit) | ≈0.91 s | --- | ≈1.07 MB/s |
DMA 版实测 1.05 MB/s,已逼近理论极限,且主循环在此期间能正常处理按键和界面刷新------这正是引入 DMA 的意义。
擦除/编程延时:手册典型值 vs 实测
| 操作 | 数据手册典型值 | 实测均值 | 偏差 | 说明 |
|---|---|---|---|---|
| 扇区擦除(4KB) | 45 ms | 46 ms | +1 ms | 偏差来自 CS 切换与指令开销 |
| 页编程(256B) | 0.4 ms | 0.6 ms | +0.2 ms | 含写使能 + 等待忙的总开销 |
| 读 ID(4 字节) | --- | 0.12 ms | --- | 轮询即可,无 DMA 必要 |
理论对照的启示:擦除/编程是 Flash 物理特性决定的"硬等待",DMA 帮不了这两步,只能帮"读已存好的数据"。所以优化重点是把大块读走 DMA、把擦除/编程放到后台任务,而不是指望 DMA 缩短写时间。
我踩过的五个坑(SPI Flash 故障排查)
1. 读 ID 全是 0xFF(最常见)
现象:调 W25Q_ReadID() 返回 0xFF 而非 0xEF。根因按概率排序:① CPHA 设成 2Edge(时钟采样沿错);② CS 没拉低或拉低时机不对;③ SPI 时钟分频太大导致超时;④ 接线 MOSI/MISO 反了。我第一版就是 CPHA 设错,示波器一看 SCK 和数据沿错位,改回 1Edge 立刻读到 0xEF。注意 W25Q128 上电默认 Mode 3,但官方声明兼容 Mode 0 与 Mode 3------绝大多数量产固件走 Mode 3,本驱动用 Mode 0 实测稳定,若你换 GD32 等移植时务必先对一遍 CPOL/CPHA。
相关阅读:STM32 HAL环境下W25Qxx系列SPI Flash完整驱动实现 --- 文末的"典型问题速查表"把读 ID 返 0xFF、写后读出仍 0xFF 等坑按现象→原因→方案列得很全,可当排障字典用。
2. 写了读出来还是旧值
现象:页编程返回正常,但再读数据没变化。根因:写之前没擦除。Flash 只能把 1 写成 0,目标扇区若已有 0 位则写不进。务必先 W25Q_SectorErase 再 W25Q_PageProgram,且等 WaitBusy 完成。
3. 编程后部分字节错乱
现象:256 字节里若干字节不对。根因:单次页编程超出 256 字节会"绕回页首"覆盖前面数据。我的 W25Q_PageProgram 里加了 if (len > 256) len = 256; 兜底,但正确做法是调用层按页对齐拆分地址,别指望一个函数扛全部。
4. DMA 只配了 RX 通道读不出数据
现象:DMA 读返回全 0 或卡死。根因:SPI 全双工,只读不发的后果是没时钟。必须把 SPI1_TX 和 SPI1_RX 双通道都开,TX 缓冲填 0xFF 产生时钟。这是 SPI 初学者最高频的 DMA 翻车点。
5. 高速时偶发校验失败
现象:9MHz 正常,提到 18MHz 后偶尔读错。根因:信号完整性。SCK/MOSI 线 >10cm 或没串 33Ω 电阻时,高频振铃导致采样错误。降回 9MHz 或在信号线串 33Ω 电阻解决;W25Q128 虽标称支持更高时钟,但得看板级布线。
相关阅读:STM32H750 DMA驱动SPI LCD花屏问题:从原理到实战排查指南 --- 虽讲 LCD,但其 DMA+SPI 的 CPOL/CPHA、地址递增、缓存一致性坑与 Flash 完全通用。
总结与下一步
核心要点回顾
- SPI 必须全双工理解:读 = 发 dummy 时钟 + 收数据,DMA 的 TX/RX 双通道缺一不可。
- CS 用 GPIO 软件控制最稳,硬件 NSS 的脉冲模式会中途拉高 CS 破坏指令。
- Flash 写入前必须擦除,擦除最小单位 4KB,页编程上限 256 字节且不能跨页。
- 性能优化分层:短指令轮询、大块读 DMA,擦除/编程的物理延时只能靠后台化规避。
适用边界:本方案适合 W25Q 全系列(W25Q32/64/128 指令兼容,仅容量与地址宽度不同)、STM32F1/F4/H7 的 SPI 外设。若用 QSPI/OSPI 接口的 W25Q(如 W25Q128JV 的 DTR 模式),需改用对应的 QSPI 外设驱动,本代码不适用。
局限性与已知问题 :SPI 模式吞吐率上限约 1MB/s,存大批量图片或音频建议上 QSPI;本驱动未做坏块管理,长期写入需自行实现磨损均衡;DMA 读用了忙等 while(!g_dma_done),高实时场景应改回调 + 信号量。
扩展方向 :① 把擦除/编程搬进 FreeRTOS 任务,用信号量等待 WaitBusy 释放 CPU;② 参考 STM32F407驱动ILI9341 TFT-LCD并移植LVGL图形库教程 把字库从 Flash 经 SPI DMA 送进 LCD,打通"存储---显示"全链路;③ 加上 FATFS 文件系统,让 W25Q 像 U 盘一样按文件名存取;④ 进阶用 QSPI 外设把吞吐率拉到 10MB/s 量级。
如需获取本文完整代码和更多实战项目,可开通 CSDN 技术会员。
版本备注
📝 版本备注
- 硬件平台:STM32F103C8T6(BluePill)+ W25Q128JV(16MB SPI Flash)+ 外部 3.3V/0.5A 稳压
- 软件版本:STM32CubeIDE 1.16.1 + HAL 库 FW_F1 V1.1.4 + SPI1(APB2 72MHz,8 分频 → 9MHz)
- 兼容说明:W25Q32/64/128 指令集兼容,仅容量不同;STM32F4/H7 的 SPI+DMA 写法通用,仅 DMA 通道编号随芯片变化;若使用 W25Q128JV 的 QSPI 接口则需改用 QUADSPI 外设。
参考资料
- STM32硬件SPI驱动W25Q128实战:从CubeMX配置到DMA高速读写 --- CubeMX SPI+DMA 配置与读 ID 验证
- STM32CubeMX配置SPI驱动W25Q128实战:从硬件连接到DMA优化 --- HAL 生成的 SPI 初始化代码对照
- STM32cube之SPI+DMA高效搬运W25Q海量数据的实战与调优 --- DMA 内存对齐与 CPU 占用实测
- STM32H750 DMA驱动SPI LCD花屏问题:从原理到实战排查指南 --- DMA+SPI 通用排查思路
- STM32F407驱动ILI9341 TFT-LCD并移植LVGL图形库教程 --- 同平台 SPI 外设驱动进阶
- Winbond 官方数据手册《W25Q128JV Rev. K》:指令集、时序参数、擦除/编程典型值