尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
STM32硬件SPI驱动W25Q128:CubeMX配置与DMA高速读写实战
我玩STM32这几年W25Q128基本是绕不开的一个片子。不管是做数据记录仪、GUI缓存的图片字库还是OTA固件备份这颗128Mbit的SPI NOR Flash可以说是性价比极高的选择。但很多朋友一开始都是拿GPIO模拟SPI去驱动它慢不说还容易在时序上踩坑。这次我直接把实际项目中验证过的方案分享出来基于STM32硬件SPI外设从CubeMX图形化配置到DMA高速读写整个过程完整走一遍最后附上可以直接套用的代码。这篇文章适合正在用F1/F4/G0系列做产品开发、或者刚接触SPI Flash想搞清楚硬件SPI和DMA配合逻辑的朋友。你会搞清楚几个关键问题为什么CubeMX里SPI参数要那样填、DMA的Normal和Circular模式到底选哪个、W25Q128的读和写为什么在时序上完全不同。读完你就能从CubeMX生成工程到自己写驱动再到用DMA把读取速率拉满整个过程不会有那种“教程能跑但换成自己的板子就废”的割裂感。1. 整体方案拆解为什么是硬件SPI加DMA而不是模拟时序1.1 软件模拟SPI和硬件SPI的本质差异先说个很多新手容易忽略的问题SPI通信本身并不复杂就是四根线CLK、MOSI、MISO、CS按时钟节奏一位一位往外送数据。用GPIO模拟SPI完全可行甚至网上很多例程都这么干。但模拟SPI的性能瓶颈在于每个bit都要CPU去翻转电平、读电平一个字节8个bit再加上延时和循环CPU几乎被占满。以W25Q128为例它支持最高104MHz的时钟频率。如果主频72MHz的STM32F103用GPIO模拟能把SPI时钟跑到1-2MHz就已经很吃力了再高就会出现波形畸变、采样错位。而硬件SPI外设是芯片内部专门为串行通信设计的电路只要配置好分频系数它自己就能产生精确的时钟波形CPU只需要把数据丢给数据寄存器就行。对于W25Q128这种大容量Flash来说动不动就要读写几十KB甚至几MB的数据用软件模拟SPI去传一次固件升级能等得人怀疑人生。硬件SPI加上DMA之后数据传输完全不用CPU干预CPU可以腾出手去做别的事情,比如刷新LCD、处理按键扫描、跑通信协议栈。这也是为什么产品级的代码里几乎没人用GPIO模拟SPI去驱动W25Q128。1.2 W25Q128内部结构对驱动设计的影响W25Q128的容量是128Mbit换算过来是16MByte分成256个64KB的Block每个Block又分成16个4KB的Sector最小的擦除单位是4KB的Sector最小的编程单位是1Byte但实际上按Page256Byte编程效率最高。这个结构决定了驱动设计上的几个要点。第一写数据之前必须先擦除而且擦除是按Sector/Block/整片三种粒度进行的。第二擦除操作非常耗时擦除一个Sector大概要150ms典型值擦除一个Block要1秒左右整片擦除更是要40到160秒。第三Flash的写入不是随便写的只能把1写成0要把0恢复成1只能靠擦除。这意味着如果你的应用程序要频繁更新小数据必须设计好磨损均衡策略不能每次都擦同一个Sector否则这颗芯片的10万次擦写寿命很快就会被耗尽。还有一个容易踩坑的点W25Q128上电默认是读不了数据的。它上电后处于power-down状态或者标准SPI模式但这不代表CS一拉低就能读ID。必须发送0x9FRead JEDEC ID指令芯片才会把制造商ID和设备ID返回给你。如果读到ID不对后面所有操作都是空中楼阁。1.3 DMA参与后系统架构的变化加入DMA之后整个数据通路就变了。传统模式下CPU把数据逐个写入SPI-DR寄存器读的时候又逐个从SPI-DR读取每个字节都要经历“判断发送寄存器空”、“判断接收寄存器非空”这两个标志位的轮询。一个字节还好几万个字节下来CPU时间全耗在等待上了。DMA模式下CPU只需要做三件事启动DMA传输、等待DMA传输完成中断、处理接收缓冲区里的数据。以读取W25Q128的8KB数据为例传统轮询方式下CPU全程被占用期间不能响应其他中断这在实时性要求高的系统里几乎是灾难。而DMA方式下CPU启动传输后就可以干别的等DMA把数据全部搬完再通知你。不过这也会带来一个新的问题DMA和CPU谁先访问内存如果DMA正在往内存搬运数据CPU同时又去读同一个缓冲区就会发生总线竞争。好在STM32的DMA控制器和内核之间有仲裁机制但为了稳妥起见我还是建议读取操作使用双缓冲或者等待DMA传输完成标志后再处理数据。后面代码部分会展示这种安全处理方式。2. CubeMX配置一步步来从新建工程到SPI参数全解2.1 新建工程与时钟树设置CubeMX配置的第一步是选择芯片型号。我用的是STM32F103RCT664pin封装Flash是256KBRAM是48KB。这个芯片在项目里非常常见淘宝上几块钱一片做产品样机足够了。如果你是F407或者G030之类的芯片配置思路完全一样只是DMA通道号和中断号要按参考手册对应。时钟树的部分我直接把HSE设为8MHz外部晶振主频拉满到72MHz。SPI1挂在APB2总线上APB2的最高频率就是72MHz所以SPI1的时钟源也是72MHz。这里有个常识STM32F103的SPI1和SPI2挂在不同的总线上SPI1在APB272MHzSPI2在APB136MHz。同样的分频系数下SPI1能跑到的最高时钟就是SPI2的两倍所以高速读写W25Q128优先选择SPI1。时钟树配置里还要注意一个坑如果你把APB2的分频器配置成了2分频那SPI1的时钟源就只有36MHz了。CubeMX里有个“Reset Configuration”按钮点一下就会全部打回默认状态重新配PLL和分频避免因为之前的配置残留导致SPI频率不对。2.2 SPI参数面板的每一项怎么填进入SPI1的参数配置界面有这几个关键参数需要逐个说清楚第一个是Frame Format选Motorola模式也就是最经典的SPI Mode 0到Mode 3。W25Q128的数据手册明确写了支持SPI Mode 0CPOL0CPHA0和Mode 3CPOL1CPHA1。我习惯用Mode 0也就是空闲时钟为低电平数据在第一个时钟沿采样。这个模式下时钟空闲是低发送方在时钟上升沿把数据送出去接收方在上升沿采样。CubeMX里对应设置就是Clock PolarityCPOL为LowClock PhaseCPHA为1 Edge。第二个关键参数是Data Size选8bit。W25Q128的一条指令有8位指令码、24位地址、后续数据本质上都是字节。虽然它支持Quad模式四线IO但普通SPI操作下就是8位一个单位。第三个是First Bit选MSB First。W25Q128的所有指令都是高位先发这个一定不能选错选成LSB First的话ID读出来都是字节颠倒的数据写入后读出来也是乱的。第四个是Prescaler分频系数。SPI1的输入时钟是72MHz目标是让SCLK频率尽量高但不能超过W25Q128的极限104MHz。我把分频系数设为2也就是SPI时钟跑到36MHz。这个频率已经在安全范围内而且比很多开发板标称的18MHz要快不少。如果需要更保守的验证环境可以设为4也就是18MHz。实测下来短走线5cm以内的杜邦线连接36MHz跑读写没问题如果飞线很长或者用了面包板建议降到18MHz否则波形质量会比较差。最后一个比较容易被忽略的是NSS片选设置。CubeMX里默认可能把NSS配置为Hardware Mode但这样的话片选信号由硬件自动控制每次传输前硬件会自动拉低CS传输完自动拉高。问题在于W25Q128的很多操作比如写状态寄存器需要持续拉低CS来保持通信会话硬件自动控制的话时序对不上会导致指令执行失败。我在实际项目里用的是软件控制CS也就是把NSS引脚配置成普通GPIO输出手动拉低和拉高。虽然CubeMX的硬件NSS也能用软件去干预但用GPIO控制会简单直观很多调试起来也方便。后面代码里会看到SS_GPIO_Port和CS_Pin的宏定义。2.3 DMA通道配置的几个关键点CubeMX配置DMA这一步我见过不少朋友卡在这里。通道Channel和请求Request的概念要先搞清楚。F103的SPI1_TX和SPI1_RX分别占用DMA1的不同通道。CubeMX里你只需要选择SPI1_TX然后它默认会指向DMA1 Channel 3SPI1_RX指向DMA1 Channel 2。在代码里你会看到类似于hspi1.hdmatx hdma_spi1_tx这样的赋值这个关联就是CubeMX自动生成的。DMA的两个核心参数是Direction和Mode。Direction选择Memory To Peripheral发送或Peripheral To Memory接收。Mode这里有两种Normal和Circular。驱动W25Q128这种一次性读取固定长度数据的场景用Normal就够了也就是DMA传输完指定长度后自动停止。Circular模式适用于持续不断的数据流比如ADC连续采样输出SPI Flash的读写很少用到。还有Priority DMA优先级我一般设成High因为SPI数据传输如果被打断可能导致帧同步错误。当然这不是绝对的如果你的系统里还有UART、USB等高速外设可以根据实际需求调整。CubeMX里设置好后生成代码时会自动把DMA初始化函数放在MX_DMA_Init()里注意这个函数会被main()中的HAL_Init()之后调用不要自己手动注释掉。注意生成代码后MX_DMA_Init()必须在MX_SPI1_Init()之前调用否则DMA和SPI的关联不会生效。CubeMX默认排列顺序就是这样但如果你手动调整过初始化顺序务必检查这一点。3. 驱动代码实现从底层读写函数到DMA高速版本3.1 硬件SPI底层接口封装CubeMX生成的HAL库代码里SPI的收发有几个现成函数HAL_SPI_Transmit()、HAL_SPI_Receive()、HAL_SPI_TransmitReceive()还有它们的中断和DMA版本。对于W25Q128的操作我封装了三个核心接口SPI读写一个字节、SPI写入一段数据、SPI读取一段数据。先看最基础的读写一个字节的函数uint8_t SPI_ExchangeByte(uint8_t byte) { uint8_t rxdata; HAL_SPI_TransmitReceive(hspi1, byte, rxdata, 1, 100); return rxdata; }这个函数内部调用的是HAL_SPI_TransmitReceive()它同时发送一个字节并接收一个字节正好适合SPI全双工的特性。发送和接收在同一个时钟周期内完成。这个函数的超时时间我设成了100ms正常情况下一个字节的SPI传输是微秒级的100ms完全够但出现异常时又能及时退出不会卡死系统。再来是读取一段数据到缓冲区。这里有个关键点SPI读取数据时主机必须持续发送时钟。所以读操作的实现一般是先发送一个任意字节通常是0x00或0xFF来产生时钟同时把从机返回的数据收进来。我封装了一个函数void SPI_ReadBuffer(uint8_t *pBuffer, uint16_t len) { HAL_SPI_Receive(hspi1, pBuffer, len, 1000); }HAL库的HAL_SPI_Receive()在硬件上会自动处理时钟生成不需要你手动发送哑字节。但要注意这个函数只负责纯接收场景如果你的代码里先调用了HAL_SPI_Transmit()发送指令紧接着又要HAL_SPI_Receive()读取响应中间CS必须一直是低电平否则W25Q128会认为一次命令结束了。3.2 W25Q128的指令集与关键寄存器操作W25Q128最核心的指令无非那么几条读ID0x9F、写使能0x06、读状态寄存器0x05、页编程0x02、扇区擦除0x20、读数据0x03。读ID是上电后第一个要做的验证操作uint16_t W25Q_ReadID(void) { uint16_t id 0; uint8_t d1, d2; CS_LOW(); SPI_ExchangeByte(0x9F); // Read JEDEC ID指令 d1 SPI_ExchangeByte(0xFF); // 制造商标识 d2 SPI_ExchangeByte(0xFF); // 设备标识 CS_HIGH(); id (d1 8) | d2; return id; }W25Q128的JEDEC ID是0xEF 0x40 0x18其中制造商ID是0xEFWinbond设备ID的两个字节分别是0x40和0x18。所以读出来的16位应该是0x4018。实际调试时经常有人读到0xFFFF或者0x0000那基本就是接线问题或者SPI配置问题而不是芯片坏了。这块我在后面的问题排查里详细说。写使能是写入操作的前提。因为Flash芯片内部有个写使能锁存器每次写操作之前必须发送0x06指令把锁存器置位void W25Q_WriteEnable(void) { CS_LOW(); SPI_ExchangeByte(0x06); CS_HIGH(); }写状态寄存器0x01和读状态寄存器0x05也很常用。擦除和编程都是异步操作芯片在执行的时候忙标志位BUSY会置1你需要轮询状态寄存器的bit0来判断是否完成void W25Q_WaitBusy(void) { uint8_t status; CS_LOW(); SPI_ExchangeByte(0x05); do { status SPI_ExchangeByte(0xFF); } while (status 0x01); CS_HIGH(); }注意等待忙状态时CS必须保持低电平。有些新手在循环里反复拉高拉低CS每次重新发送0x05指令也能工作但效率会低一些。3.3 读写擦除三大核心函数实现读数据是最常用的操作。W25Q128的0x03指令是普通读模式最快支持到50MHz时钟。后面跟24位地址然后芯片就会持续把该地址开始的数据从MISO输出只要CS保持低可以连续读任意多字节读到Flash末尾会自动回卷到0地址void W25Q_Read(uint32_t addr, uint8_t *pData, uint32_t len) { uint32_t i; CS_LOW(); SPI_ExchangeByte(0x03); SPI_ExchangeByte((uint8_t)(addr 16)); SPI_ExchangeByte((uint8_t)(addr 8)); SPI_ExchangeByte((uint8_t)(addr)); for (i 0; i len; i) { pData[i] SPI_ExchangeByte(0xFF); } CS_HIGH(); }页编程要小心一点。0x02指令后面跟24位地址然后是要写入的数据。一次最多写256个字节这个256字节必须限制在一个Page内也就是说如果起始地址不是Page对齐的写入长度超过256字节就会写到当前页的边界后自动回卷到该页开头导致数据覆盖。正确做法是判断剩余字节数和当前页剩余空间分段写入void W25Q_WritePage(uint32_t addr, const uint8_t *pData, uint32_t len) { uint32_t i; W25Q_WriteEnable(); CS_LOW(); SPI_ExchangeByte(0x02); SPI_ExchangeByte((uint8_t)(addr 16)); SPI_ExchangeByte((uint8_t)(addr 8)); SPI_ExchangeByte((uint8_t)(addr)); for (i 0; i len; i) { SPI_ExchangeByte(pData[i]); } CS_HIGH(); W25Q_WaitBusy(); }扇区擦除是最常用的擦除粒度。4KB的Sector擦除后全部变成0xFF。擦除之前同样要先发写使能然后发0x20指令和24位地址。Sector地址必须是4KB对齐也就是地址的低12位必须是0void W25Q_EraseSector(uint32_t sector_addr) { W25Q_WriteEnable(); CS_LOW(); SPI_ExchangeByte(0x20); SPI_ExchangeByte((uint8_t)(sector_addr 16)); SPI_ExchangeByte((uint8_t)(sector_addr 8)); SPI_ExchangeByte((uint8_t)(sector_addr)); CS_HIGH(); W25Q_WaitBusy(); }3.4 DMA版本的高效读写接下来是重头戏DMA版本的读写函数。DMA模式下数据不再通过CPU逐字节搬运而是由DMA控制器直接完成SPI外设和内存之间的传输。对于发送方向你要做的就是先把数据准备好然后调用HAL_SPI_Transmit_DMA()DMA会把内存缓冲区里的数据搬到SPI的发送寄存器对于接收方向调用HAL_SPI_Receive_DMA()DMA会把SPI的接收寄存器里的数据搬到内存缓冲区。不过这里有个小坑W25Q128的读操作需要先发送指令和地址然后才能读数据。所以DMA读数据其实是分两个阶段。第一阶段用普通的轮询或DMA发送指令第二阶段再用DMA接收数据。如果你的DMA接收用了Normal模式一次传输完指定长度后DMA会自动停止这是对的。但注意在调用DMA接收之前CS必须保持低电平不能让W25Q128认为指令会话结束了。下面是DMA版本的关键代码void W25Q_Read_DMA(uint32_t addr, uint8_t *pData, uint32_t len) { uint8_t cmd[4]; cmd[0] 0x03; cmd[1] (uint8_t)(addr 16); cmd[2] (uint8_t)(addr 8); cmd[3] (uint8_t)(addr); CS_LOW(); HAL_SPI_Transmit(hspi1, cmd, 4, 100); HAL_SPI_Receive_DMA(hspi1, pData, len); // 必须等DMA传输完成否则CS拉高太快数据没拿完 while (HAL_SPI_GetState(hspi1) ! HAL_SPI_STATE_READY); CS_HIGH(); }HAL_SPI_GetState()返回HAL_SPI_STATE_READY时说明DMA传输已经完成SPI外设空闲了。这个等待逻辑比较粗暴但配合DMA传输完成中断其实是更好的方式。在中断回调里置一个标志位主循环判断标志位再拉高CS这样不会阻塞CPU。DMA写的思路类似命令和地址还是要先发然后数据部分用DMA发送。但要注意W25Q128的页编程一次最多256字节DMA发送也一样超过256字节就得拆分void W25Q_Write_DMA(uint32_t addr, const uint8_t *pData, uint32_t len) { while (len 0) { uint16_t chunk (len 256) ? 256 : len; // 检查跨页边界如果剩余字节数超过当前页末尾就截断 uint16_t page_remain 256 - (addr % 256); if (chunk page_remain) { chunk page_remain; } uint8_t cmd[4]; cmd[0] 0x02; cmd[1] (uint8_t)(addr 16); cmd[2] (uint8_t)(addr 8); cmd[3] (uint8_t)(addr); W25Q_WriteEnable(); CS_LOW(); HAL_SPI_Transmit(hspi1, cmd, 4, 100); HAL_SPI_Transmit_DMA(hspi1, (uint8_t *)pData, chunk); while (HAL_SPI_GetState(hspi1) ! HAL_SPI_STATE_READY); CS_HIGH(); W25Q_WaitBusy(); addr chunk; pData chunk; len - chunk; } }页对齐这个计算是必须的。假设addr是300那么当前页剩余空间就是256 - (300 % 256) 256 - 44 212字节。如果你一次要写300字节第一段只写212字节写完地址变成512正好是下一页的起始地址第二段再写88字节。这样就能避免跨页回卷导致的数据灾难。3.5 擦除的粒度选择对产品寿命的影响擦除效率直接影响到Flash的寿命和使用体验。W25Q128支持三种擦除方式4KB Sector擦除、32KB Block擦除、64KB Block擦除、整片擦除。整片擦除最慢但一次性清空所有数据很方便Sector擦除最灵活适合频繁更新的小数据存储。实际项目中我总结出的经验是频繁更新的小数据用Sector擦除比如存储设备运行参数、WiFi配置信息每4KB一个槽位轮询使用中等大小的数据比如OTA固件包可以按64KB Block擦除减少擦除指令的次数因为每次发擦除命令前都要等上一次擦除完成。如果一块固件有512KB用Sector擦除要发128次命令用64KB Block擦除只需要8次省下的时间还是很可观的。4. 实测性能表现与常见问题排查4.1 DMA高速读写的实测数据我用逻辑分析仪抓过波形也用了HAL_GetTick()做过毫秒级计时。在SPI时钟36MHz、开启DMA的条件下实测读取1MB数据大概耗时约290ms左右换算下来读取速率在3.5MB/s。这个速度和W25Q128理论上限还有差距因为W25Q128在标准SPI模式下每8位数据需要至少8个时钟周期36MHz时钟理论上是4.5MB/s考虑到指令地址和命令开销实测3.5MB/s已经很接近极限了。页编程的速度会慢很多因为每写一页256字节芯片内部都需要程序化时间典型值在0.7ms到3ms之间这部分是芯片物理特性决定的不管SPI时钟多快都没用。实测连续写1MB数据耗时大约在3到4秒平局速率0.3MB/s左右。如果你需要更高的读速度可以开启W25Q128的快速读模式0x0B指令这个支持到104MHz时钟但前提是你的STM32 SPI时钟能跑到那么高。F103的SPI1最高36MHz72MHz主频2分频F4系列SPI可以跑到45MHz左右再往上就受限了。所以对于F1平台来说36MHz是性价比最高的档位没必要牺牲稳定性追更高频率。4.2 常见故障读ID不对、一直忙、数据错乱读ID不对是玩W25Q128最高频的问题。ID读出来是0xFFFFFF或者0x000000大概率是硬件连接问题。排查顺序是这样的先量VCC和GND确保供电正常W25Q128的工作电压是2.7V到3.6V3.3V供电没问题然后查CS、CLK、MOSI、MISO四根线有没有接反尤其是MOSI和MISO接反了数据完全不通再用示波器或者逻辑分析仪看SPI时钟波形确认主控侧确实有时钟输出。软件方面检查SPI的CPOL和CPHA是否配置正确。W25Q128的Mode 0对应CPOL0、CPHA0意思是空闲时CLK为低数据在第一个时钟沿采样。CubeMX配置界面里把Clock Polarity选为LowClock Phase选为1 Edge就是你想要的Mode 0。第二种常见问题是指令发出去后读取状态寄存器一直显示忙BUSY位一直为1。这个问题大多是CS时序不对导致的。擦除或编程过程中CS应该保持高电平让芯片内部操作但如果你在等待忙的过程中不小心拉低了CS又拉高了芯片可能会误判为一次新的指令开始导致状态混乱一直卡在忙状态出不来。解决方法是严格按照长度操作等待忙状态时CS保持低并轮询或者用延时函数先等一段时间再查忙。第三种问题比较隐蔽就是写入的数据读出来是错误的最常见的是数据错位几个字节。这个大概率是SPI帧格式或者数据大小配置错了。Data Size一定要是8bitFirst Bit一定要是MSB。还有一个容易被忽略的点如果你用了DMA发送数据发送缓冲区的数据格式和被DMA搬移的字节顺序在Cortex-M3这样的平台上是小端模式和SPI的MSB-first顺序没有任何冲突但如果你的数据是多字节变量比如uint16_t或uint32_t数组要注意字节序问题发送前把数据转换成字节流。4.3 一个调试小技巧错误注入法这个方法是我在实际项目里摸索出来的排查问题时特别好用。那就是故意写错一个的关键参数观察故障现象如果现象和你预期的错误类型吻合说明系统链路是通的问题出在软件参数上如果现象完全不对那大概率是硬件或者初始化的问题。举个例子调试的时候把CPOL从Low改成High如果读ID立刻从正确的0xEF4018变成0xFFFFFF说明你的SPI外设和W25Q128的通信链路是通的只是模式不匹配。如果改成High后读出来的ID还是0xEF4018那就有意思了——说明你的W25Q128可能工作在Dual或者Quad模式或者硬件上CS的控制有问题跟时钟极性没关系。这种调试思路比盲猜有效得多能帮助你快速缩小问题范围不用一上来就把所有参数全改一遍。4.4 布线注意事项和极端情况最后说说布线。很多开发板和传感器模块是用杜邦线连接的SPI时钟频率一高杜邦线就变成天线了。实测中用杜邦线连接时36MHz时钟下波形已经有明显过冲和振铃边缘变得很差。如果你只是验证功能18MHz分频Prescaler4会更稳妥如果你要做产品或者需要高速率PCB上SPI走线尽量短且线和线之间拉开距离信号线不交叉。还有一点要特别注意当W25Q128处于写状态并且正在页编程或者扇区擦除时不要突然断电。如果断电发生在擦除过程中Flash内部的数据可能损坏严重时可能导致整个扇区无法继续擦写。工业产品里一般会在电源上加掉电检测和延时断电电路起码保证掉电前能有机会完成擦除操作或者保存关键数据。另外W25Q128虽然支持每个扇区10万次擦写寿命但频繁擦写同一个扇区寿命很快会耗尽。我设计数据存储方案时通常是安排多个扇区轮换写入每个扇区写满后再切换下一个应用程序记录当前写入的扇区号这样能大大延长Flash的实际使用寿命。5. 写在最后的个人经验做嵌入式开发这些年W25Q128这颗芯片真的陪我走过不少项目。从最早用模拟SPI慢慢读到后来换硬件SPI再到把DMA加进去整个性能提升的过程非常直观。性能提升是一方面更重要的是DMA让系统架构变得更合理——主控不用一直傻等Flash传输完成可以把时间花在更重要的逻辑上。有一个小细节我一直想强调CubeMX只是生成初始代码的起点不代表它生成的代码就是最优的。比如DMA的中断回调函数CubeMX默认生成的HAL_SPI_TxRxCpltCallback()是__weak修饰的真正的处理逻辑需要你自己在用户文件里重写。很多人漏了这一步导致DMA传输完了程序却不知道传输已经结束白白浪费了DMA的优势。如果你照着这篇文章动手做一遍我建议你在自己的板子上先跑一个最简单的测试读ID擦除一个扇区写入256字节再读回来校验。这个过程跑通了剩下的无非就是在这个基础上加功能。踩过坑、调过波形之后你才算真正掌握了SPI和W25Q128这套组合以后再遇到别的SPI设备比如LCD屏、SD卡、传感器底层逻辑都是相通的。最后再分享一个小技巧调试W25Q128的时候多利用Flash的“读状态寄存器”指令。只要这条命令能返回正确状态基本可以判断SPI通信是通的如果连状态寄存器都读不到数据问题大概率在硬件连接或者CubeMX的基础配置上不用纠结是不是DMA配置错了。
RELATED

相关推荐

云开发实现大学食堂点餐预约小程序:从数据模型到防超卖实战

云开发实现大学食堂点餐预约小程序:从数据模型到防超卖实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/10/5 6:23:51
V4L2采集报ENOSPC?不是磁盘满,而是USB等时带宽不够

V4L2采集报ENOSPC?不是磁盘满,而是USB等时带宽不够

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/10/5 6:23:51
RT-Thread Studio实战:STM32F407工程创建与程序下载全流程

RT-Thread Studio实战:STM32F407工程创建与程序下载全流程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/10/5 6:23:51
MORE NEWS

更多资讯

📰

华为OD机试采样过滤题详解:状态机思路与多语言实现

这两年华为OD机试的题目越来越卷,但本质考的还是那几个老生常谈的东西:逻辑拆解、边界处理、多语言实现能力。C卷的“采样过滤”这道题,乍一看就是个数组遍历加状态标记,但真上手写的时候,不少人在“连续无效数据的起点…

📰

libevent编译成功运行却报错?一文搞懂Linux动态链接库加载机制

1. 从一次诡异的“运行失败”说起大概两年前,我在一台刚装好的 CentOS 服务器上部署一个基于 libevent 的事件驱动服务。代码是用 CMake 写的,编译过程非常顺利,二进制文件也成功产出。结果一运行,终端直接甩出一行:er…

📰

华为路由器交换机VLAN配置实例:从VLAN划分到ACL排错

简介:面向网络工程师与华为设备初学者的VLAN配置实战资料,用一个由4台PC、华为R2621路由器及S3026e交换机组网的小型案例,演示VLAN划分、路由器网关设置、防火墙策略与ACL访问控制的具体命令。资源共1个PDF文件,压缩包约141KB&…

📰

MongoDB热点数据缓存击穿:从识别到两级缓存优化实战

上周五晚上十点,运营那边报了个紧急问题:积分排行榜接口的RT从60ms一路涨到3s,MongoDB主节点的CPU直接冲到99%,慢查询日志刷了满满一页。我翻了下日志,发现其中一条记录特别刺眼——同一个文档ID在十分钟内被查了48万次…

📰

DeepSeek 15天精通指南:API调用、本地部署与工具链集成全攻略

简介:这是一份关于DeepSeek人工智能平台的实战操作手册,以15天从入门到精通为主线,适合办公人士、科研人员、自媒体创作者、学生及编程爱好者学习。内容涵盖账号注册与界面认识、高效提问方法、文档解析与代码生成,以及学术论文辅…

📰

Zeroboot源码深度解析:CPU状态恢复的严格顺序、vmstate解析与KVM开发避坑

Zeroboot源码深度解析:CPU状态恢复的严格顺序、vmstate解析与KVM开发避坑 【免费下载链接】zeroboot Sub-millisecond VM sandboxes for AI agents via copy-on-write forking 项目地址: https://gitcode.com/gh_mirrors/ze/zeroboot Zeroboot 是一个面向 AI…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬