024、SPI高级应用:双线SPI、四线QSPI、DMA环形传输与多从机管理 📅 2026/6/18 19:51:53 024、SPI高级应用双线SPI、四线QSPI、DMA环形传输与多从机管理一、从一次诡异的Flash读写失败说起去年做一款工业数据记录仪主控是STM32H743外挂一颗W25Q64JV的QSPI Flash。板子打样回来单板测试一切正常读写擦除都没问题。结果整机装进金属外壳上电跑半小时日志里开始出现“Flash校验失败”的报错。用示波器抓QSPI的CLK和IO线发现CLK上偶尔会多出一个毛刺脉冲——不是外部干扰是DMA传输完成后的时钟残留。这个坑让我意识到SPI的高级玩法远不是调通一个轮询收发那么简单。双线、四线、DMA、多从机每一个特性背后都藏着硬件工程师用示波器才能看到的“鬼故事”。二、双线SPI别被“半双工”骗了很多人以为双线SPI就是省掉一根MISO线其实不是。双线SPIDual SPI是在标准四线基础上把MOSI和MISO复用成双向数据线同时传输两个bit。标准SPI一个时钟传1bit双线SPI一个时钟传2bit。实际工程中的坑某次用双线SPI读一个温湿度传感器手册说支持双线模式我直接配了寄存器。结果读回来的数据每隔几个字节就跳变一次。查了半天发现是MCU的SPI控制器在双线模式下数据线的方向切换需要额外的时钟周期。传感器那边没等MCU切完方向就发了数据导致采样错位。解决办法在每次读写切换时插入一个dummy时钟周期。代码里这样写// 这里踩过坑双线模式下方向切换必须插入等待voiddual_spi_read(uint8_t*buf,uint32_tlen){// 先发命令然后立刻切方向SPI1-CR1|SPI_CR1_BIDIMODE;// 启用双向模式SPI1-CR1|SPI_CR1_BIDIOE;// 输出使能// 发送读命令...while(SPI1-SRSPI_SR_BSY);// 等发送完成// 别这样写直接切输入然后开始读// SPI1-CR1 ~SPI_CR1_BIDIOE; // 切输入// 上面这行会导致时钟错位// 正确做法先发一个dummy字节让从机知道我们要切方向了SPI1-DR0x00;// 发dummywhile(SPI1-SRSPI_SR_BSY);SPI1-CR1~SPI_CR1_BIDIOE;// 现在切输入// 然后开始读...}双线SPI的带宽提升是实打实的但代价是控制逻辑变复杂。如果你的从机芯片手册里没写清楚方向切换时序建议先用标准模式调通再用逻辑分析仪抓出时序图再切双线。三、四线QSPI不是配个寄存器就能跑QSPIQuad SPI在双线基础上再加两根数据线一个时钟传4bit。很多MCU的QSPI控制器支持memory-mapped模式可以把Flash映射到地址空间直接当内存访问。听起来很美但实际调试时问题一堆。第一个坑DDR模式下的建立时间某项目用QSPI读一个Octal Flash其实是八线但原理类似为了追求速度开了DDR模式双沿采样。结果读回来的数据低4bit总是对的高4bit偶尔出错。用示波器看发现数据线的建立时间在DDR模式下不够——MCU在时钟上升沿采样低4bit下降沿采样高4bit但高4bit的数据在下降沿到来时还没稳定。解决办法降低QSPI时钟频率或者调整采样相位。但更根本的解法是不要盲目追求DDR。对于大多数嵌入式应用SDR模式单沿采样配合合理的时钟频率带宽已经够用。DDR模式对PCB走线等长、阻抗匹配的要求极高不是四层板能搞定的。第二个坑memory-mapped模式下的Cache一致性把QSPI Flash映射到地址空间后用指针直接读数据速度确实快。但如果你同时用DMA往Flash里写数据读出来的可能是Cache里的旧数据。这个问题在STM32上尤其明显——CPU的Cache和QSPI控制器之间没有硬件一致性协议。// 这里踩过坑memory-mapped模式下读之前必须清Cachevoidqspi_read_mapped(uint32_taddr,uint8_t*buf,uint32_tlen){// 先清掉Cache里的旧数据SCB_InvalidateDCache_by_Addr((uint32_t*)addr,len);// 然后直接从映射地址读memcpy(buf,(uint8_t*)addr,len);// 别这样写直接memcpy数据可能是错的// memcpy(buf, (uint8_t*)addr, len); // 可能读到Cache里的脏数据}四、DMA环形传输让SPI自己跑起来DMA环形传输Circular DMA是SPI高性能应用的核心。配置好之后DMA自动在内存和SPI之间搬运数据CPU完全解放。但环形DMA的配置细节手册里往往一笔带过。实际工程中的配置要点缓冲区大小必须是2的幂环形DMA的地址回绕依赖于地址掩码如果缓冲区大小不是2的幂回绕时会出问题。我见过有人设了100字节的环形缓冲区结果DMA跑到第64字节就回绕了——因为硬件只认低6位地址。中断触发时机环形DMA通常支持半传输中断和完全传输中断。半传输中断用来处理前半段数据完全传输中断处理后半段。但要注意半传输中断触发时DMA正在写后半段此时读前半段是安全的。反过来完全传输中断触发时DMA正在写前半段读后半段也是安全的。// 环形DMA配置示例这里踩过坑#defineSPI_RX_BUF_SIZE256// 必须是2的幂uint8_tspi_rx_buf[SPI_RX_BUF_SIZE]__attribute__((aligned(32)));voidspi_dma_circular_init(void){// 配置DMA为环形模式DMA1_Stream3-CR|DMA_SxCR_CIRC;// 使能环形模式// 设置传输长度DMA1_Stream3-NDTRSPI_RX_BUF_SIZE;// 别这样写直接设NDTR为256然后启动// 如果SPI_RX_BUF_SIZE不是2的幂DMA回绕会出错// 正确做法确保缓冲区大小是2的幂且对齐到32字节// 因为DMA的FIFO可能要求对齐}环形DMA的典型应用场景连续采集ADC数据、实时音频流处理。在这些场景下CPU只需要在中断里处理“半满”和“全满”的数据块不需要关心每个字节的收发。五、多从机管理片选信号的“战争”多从机SPI的常见做法是每个从机一根片选线共享SCK、MOSI、MISO。但实际工程中片选信号的时序管理是最大的坑。第一个坑片选信号的毛刺MCU的GPIO在输出片选信号时如果GPIO的驱动能力不够或者PCB走线过长片选信号上会出现毛刺。这些毛刺可能被从机误识别为片选有效导致数据错乱。解决办法在片选信号上加一个10kΩ上拉电阻或者用施密特触发器整形。更彻底的做法是用硬件片选不要用GPIO模拟。很多MCU的SPI控制器支持硬件片选由硬件自动控制片选时序比GPIO模拟可靠得多。第二个坑片选信号的“粘滞”某次调试两个从机共享SPI总线一个Flash一个传感器。读Flash时一切正常读传感器时偶尔读到Flash的数据。查了半天发现是Flash的片选信号在传输结束后没有完全拉高——GPIO输出高电平的驱动能力不够导致片选线上残留了一个低电平的“尾巴”。传感器误以为片选有效开始响应。解决办法在每次SPI传输结束后强制将片选GPIO配置为推挽输出并输出高电平。如果GPIO的驱动能力不够可以加一个三极管或MOSFET来增强驱动。// 多从机片选管理这里踩过坑voidspi_select_slave(uint8_tslave_id){// 先取消所有片选GPIOA-BSRR(14)|(15)|(16);// 拉高所有片选// 别这样写直接拉低目标片选// GPIOA-BRR (1 slave_id); // 可能残留其他片选信号// 正确做法先拉高所有再拉低目标// 加一个短暂延时让片选信号稳定__NOP();__NOP();__NOP();GPIOA-BRR(1slave_id);// 再等片选建立时间delay_us(1);// 根据从机手册调整}第三个坑多从机时的总线竞争如果两个从机同时被选中片选同时为低它们会同时驱动MISO线导致总线竞争。轻则数据错误重则烧毁从机IO口。绝对不要同时选中两个从机这是SPI多从机管理的铁律。六、个人经验性建议SPI高级功能先从示波器开始不要相信手册上的时序图用示波器抓出实际波形确认建立时间、保持时间、时钟极性都符合要求。我见过太多“手册上说支持实际跑不通”的案例。QSPI的时钟频率保守一点对于W25Q系列Flash80MHz的QSPI时钟在四层板上基本是极限。如果PCB走线超过5cm建议降到60MHz以下。DDR模式更是如此除非你有阻抗控制的能力。DMA环形传输缓冲区大小设成512或1024太小了中断太频繁CPU负担重太大了延迟高实时性差。512字节对于大多数音频和传感器应用是黄金平衡点。多从机管理硬件片选优于GPIO模拟如果MCU的SPI控制器支持硬件片选优先使用。硬件片选的时序由硬件保证不会出现毛刺和粘滞问题。如果必须用GPIO模拟记得加上拉电阻和施密特触发器。最后一条也是最重要的一条SPI的高级功能能不用就不用。双线、四线、DMA环形传输每一个特性都增加了系统的复杂度。如果你的应用场景标准SPI就能满足带宽需求就别折腾这些高级功能。嵌入式系统的第一原则是“稳定压倒一切”而不是“性能跑分”。那次Flash读写失败的最终原因是DMA传输完成后QSPI控制器的时钟线没有完全拉低导致Flash误以为还在传输中。解决办法是在DMA传输完成中断里手动复位QSPI控制器。这个bug查了三天最后是在数据手册的“Errata”章节里找到的——永远记得看芯片的勘误表。