STM32 硬件SPI驱动W25Q Flash实战:从CubeMX配置到DMA高速读写

文章目录

    • [我为什么放弃了软件模拟 SPI](#我为什么放弃了软件模拟 SPI)
    • [SPI 到底怎么传数据](#SPI 到底怎么传数据)
    • [把 W25Q128 接上开发板](#把 W25Q128 接上开发板)
    • [CubeMX 里的关键配置](#CubeMX 里的关键配置)
    • [写一个能用的 W25Q 驱动](#写一个能用的 W25Q 驱动)
    • [实测: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

2. DMA 双通道(SPI1 的 DMA Settings 选项卡,Add 两个请求)

  • SPI1_TX:Direction Memory To Peripheral,Mode Normal,Priority Medium
  • SPI1_RX:Direction Peripheral To Memory,Mode Normal,Priority Medium

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_SectorEraseW25Q_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 完全通用。

总结与下一步

核心要点回顾

  1. SPI 必须全双工理解:读 = 发 dummy 时钟 + 收数据,DMA 的 TX/RX 双通道缺一不可。
  2. CS 用 GPIO 软件控制最稳,硬件 NSS 的脉冲模式会中途拉高 CS 破坏指令。
  3. Flash 写入前必须擦除,擦除最小单位 4KB,页编程上限 256 字节且不能跨页。
  4. 性能优化分层:短指令轮询、大块读 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 外设。

参考资料

相关推荐
HRTOS1 小时前
HRTOS应用示例:信号量机制详解
c语言·人工智能·嵌入式硬件·51单片机
从零开始的嵌入式之旅2 小时前
day36
linux·c语言·经验分享·笔记·嵌入式硬件
HRTOS2 小时前
HRTOS应用示例:Mutex互斥锁机制详解
c语言·单片机·嵌入式硬件·51单片机
铅笔小新z2 小时前
【stm32】I2C 阻塞通信实战与 HAL 源码
stm32·单片机·嵌入式硬件
码上上岸4 小时前
基于stm32的智能荷兰豆生长舱监控系统
stm32·单片机·嵌入式硬件·物联网
星栖与芯5 小时前
STM32MP157 M4 指针避坑(一):先看清“客栈“——内存布局与指针基础
stm32·单片机·嵌入式硬件
国科安芯5 小时前
星载数据处理单元中单粒子翻转防护机制的设计考量与实现路径
网络·单片机·嵌入式硬件·架构·抗辐射·星载数据处理·单粒子
沐欣工作室_lvyiyi6 小时前
基于物联网的智慧路灯监控系统设计(论文+源码)
单片机·物联网·智能路灯
北京迅为6 小时前
【迅为开发板专属工具③】告别万用表串口助手|BoardLab一站式硬件测试平台
单片机·嵌入式硬件