资讯动态

SPI总线实战:CPOL/CPHA、片选、STM32 DMA与Flash排错

发布时间:2026/9/18 17:55:47 来源:尧图企业网站定制
逻辑分析仪上时钟跳得挺欢片选也乖乖拉低了MOSI那根线却像睡着了一样——这是我第一次调SPI时最抓狂的一幕。后来发现原因很朴素CPOL和CPHA配反了从机根本没把我的数据当回事。SPI总线看着简单四条线、不带地址、没有应答教科书三页纸就能讲完可真到了板子上时序、片选、DMA、上拉这些细节一个没照顾到读回来的就是一串0xFF或者0x00。这篇把SPI总线从物理层拆到代码层CPOL/CPHA怎么定Mode、硬件片选和软件片选怎么选、STM32F103上用CubeMX配SPI加DMA收发怎么跑通、SPI Flash和TF卡这类有脾气的从机怎么写以及时钟跑到十几兆之后波形和信号完整性会出什么幺蛾子。刚接触单片机的新手能照着抄调过几轮的老手也能从排查顺序里省点时间。1. SPI总线在真实项目里扮演的角色四条线撑起的板内高速通道1.1 SCLK、MOSI、MISO、CS各自负责什么SPI的全称是串行外设接口本质上就是一条带时钟的移位寄存器通道。主机和从机各自有一个移位寄存器SCLK每打一拍双方同时把最高位推出去、把对方推过来的位收进来。听上去很绕其实类比一下就很清楚两个人面对面站在传送带两端一个人每敲一下桌子就同时把手里的牌推出去一张、接住对面推过来的一张。敲桌子的节奏就是SCLK推出的牌就是MOSI和MISO而由谁参与这场游戏由CS决定。SCLK时钟永远由主机产生。从机没有自己的时钟概念它完全跟着SCLK的节拍走。MOSI主机输出、从机输入数据方向固定。MISO主机输入、从机输出。这里的门道不少——从机没被选中的时候MISO必须是高阻态否则多个从机同时驱动一根线轻则读到乱码重则两边输出级互相灌电流发热。CS/NSS/SS片选低电平有效居多。SPI没有地址字段靠的就是这片选线来点名拉低谁就跟谁说话。这里有两个点很容易被忽略。第一SPI协议本身不强制四根线很多器件是残缺版的只收数据的DAC可能只有SCLK、MOSI、CS三根只发数据的传感器可能省掉CS上电就一直往外吐数据。遇到这种器件别死盯着四线不放翻手册看引脚定义。第二SPI没有应答机制。I2C发完一个字节从机会拉低SDA一个ACK位告诉你收到了SPI什么都没有。这意味着一件事SPI出问题的时候从机不会给你任何反馈你只能靠波形和返回值自己判断。这也是为什么调SPI一定要有逻辑分析仪哪怕是几十块钱的USB逻辑分析仪也比盲猜强得多。1.2 把UART、I2C、SPI摆在同一张桌上比新手最容易问的就是这几个串行总线到底啥区别、我该用哪个。下面这张表是我自己总结的对照基本能覆盖选型时九成的问题维度UARTI2CSPI线数2TX/RX2SCL/SDA4SCLK/MOSI/MISO/CS时钟异步无时钟线同步主机产生同步主机产生拓扑点对点多主多从靠地址寻址一主多从靠片选寻址速率常见115200可上Mbps标准100k、快速400k、高速3.4M几M到几十M都很常见应答无有ACK/NACK无输出方式推挽开漏必须上拉推挽典型距离板内、短线板内、短线板内、短线典型器件模块通信、调试口传感器、EEPROM、RTCFlash、屏、SD卡、ADC、无线芯片选型的逻辑其实不复杂要点对点、双方速率不高、走线要走长一点的用UART器件多但速率要求低、引脚紧张、想省线的用I2C要吞吐量、要实时性、板内布线宽裕的用SPI。UART最大的优势是不需要时钟线双方各自按约定的波特率采样所以跨板走线、经过接插件都不太怕几米甚至十几米用普通线也能凑合再远就得上差分方案。I2C的好处是两根线能挂一堆器件地址冲突除外但开漏输出加上总线电容速率一高波形就塌而且总线被某个器件拉死的时候排查起来非常头疼。SPI是推挽驱动边沿干脆跑得快代价就是引脚多——挂八个从机就要八根片选线主控引脚不够的时候很多人会被迫回到I2C或者加个I2C转SPI的扩展芯片。1.3 项目里你真会碰到的那些SPI器件说几个具体的方便有直观印象SPI FlashW25Q系列、GD25Q128这类是重灾区几乎每个带外部存储的板子都有一颗TFT液晶屏和OLED走SPI的版本特别多尤其是引脚少的小屏SD卡和TF卡支持SPI模式很多低成本方案干脆不跑SDIO直接用SPI省事ADC、DAC、数字电位器、部分IMU和气压计也用SPI无线芯片里nRF24L01、部分LoRa模块、以太网控制器ENC28J60、独立的CAN控制器MCP2515都是SPI挂在主控下面的还有一类容易被忽视——电调、总线舵机这类执行机构的驱动芯片很多配置寄存器也是走SPI的。这里的经验是每个器件都要单独确认它的SPI Mode、最大时钟频率、片选有效电平和数据位序。我见过同一块板子上三颗SPI器件分别是Mode 0、Mode 3和Mode 1的这种情况下SPI外设的初始化参数必须按器件切换或者干脆给每颗器件单独分配一路SPI。前者省引脚但要小心切换时机后者省心但吃外设资源怎么取舍看你板子上还剩下几路SPI。2. 把CPOL和CPHA彻底搞清楚SPI Mode不是背出来的2.1 CPOL管空闲电平CPHA管采样边沿CPOL和CPHA这两个词看着抽象拆开看特别好理解。CPOLClock Polarity只描述一件事时钟空闲的时候是高还是低。CPOL0表示空闲为低CPOL1表示空闲为高。CPHAClock Phase描述的是数据在第几个边沿被采样。把两个参数组合起来就是四个ModeMode编号 CPOL×2 CPHA。很多人试图背Mode 0是低电平空闲、第一个沿采样这种口诀结果一换器件就糊涂。我的建议是别背数字记住边沿ModeCPOLCPHA空闲电平采样边沿移出边沿000低第一个上升下降沿101低第二个下降上升沿210高第一个下降上升沿311高第二个上升下降沿记住一个原则就行时钟从空闲态出发第1个跳变沿如果是采样沿那就是CPHA0如果是移出沿那就是CPHA1。因为CPHA0的时候数据必须在第一个边沿到来之前就已经稳定在线上所以主机通常要在CS拉低之后、时钟动起来之前就把第一位摆好而CPHA1的时候第一个边沿负责把数据推出去第二个边沿才收数据中间有半个周期的时间去建立。2.2 采样沿和移出沿的关系以及一个不用背的推导法上面这句话值得再嚼一遍。SPI是全双工同一时刻主从双方都在推、也都在收。如果采样沿和移出沿是同一个边沿那就会出现把数据推出去的瞬间同时去采对方的尴尬——线还没稳定就被采样了。所以协议上采样沿和移出沿必然错开半个周期。CPHA0和CPHA1的区别本质就是这半个周期错在哪个位置。推导Mode的时候我通常这么干先看手册时序图找到画着MISO Valid或者带箭头的那个采样位置看它对应的是时钟的第几个边沿再看时钟线上标着空闲电平的那段是高点还是低点。这两个信息一凑Mode立刻出来不用管编号是几。还有个实用的办法是看器件手册里给的参数表很多手册会直接写支持SPI Mode 0和Mode 3因为这两个Mode空闲态分别是低和高但对采样沿的要求都是先摆数据再采样从机实现起来最简单。如果你手头器件手册明确写了Mode 0/3那就优先用Mode 0把CPOL和CPHA都设成0出错概率最低。2.3 上板实测三分钟内锁定从机的Mode手册找不到、或者手册里时序图画得太抽象的时候就用试的。方法很朴素先把时钟降下来。预分频拉到最大让SCLK跑到几百kHz。低速下时序余量大很多原本因为边沿太快导致的问题会直接消失。发一条读取身份的命令。绝大多数SPI Flash、传感器都有读ID或者读寄存器的命令比如Flash发0x9F某些传感器发0x80再读回。依次试四个Mode看哪个Mode能读回一个像是真的的值。读回全0x00或者全0xFF基本就是没通。真实ID一般是0xEF、0xC8这种有特征的值。一旦通了立刻用逻辑分析仪存一份波形。这份波形以后就是你的参考基准改动代码之后拿出来对比一眼能看出哪里变了。这里有个小坑有些器件在MISO上是三态弱上拉或者三态弱下拉的设计。如果主机这侧的MISO引脚内部上拉/下拉被CubeMX默认打开从机没被选中时你读到的可能是一个稳定的电平而不是悬空噪声。判断从机到底有没有回应要看CS有效期间MISO上有没有跟着数据变化的跳变而不是看静止电平。2.4 位序、时钟频率和手册里那些ns级参数位序这块95%以上的器件是MSB First也就是高位先出。但确实有例外少数转换器、少数老式存储是LSB First。CubeMX里有个First Bit选项默认MSB不是特殊器件别动它。时钟频率的坑在于手册给的最大频率是在特定条件下的理论值实际能跑多快取决于走线长度、上拉强度、从机的建立保持时间。同一颗Flash短走线可能20MHz很稳排线拉到十厘米就得降到10MHz。我的习惯是先按从机最大频率的一半配好跑通功能之后再用逻辑分析仪看采样沿位置的数据是否居中慢慢往上加。如果看到MISO的数据跳变贴着采样沿那就是余量快没了果断降速。另外提一个容易踩的点STM32F103的SPI1挂在APB2上最高72MHzSPI2和SPI3挂在APB1上最高36MHz。分频系数是2、4、8一直到256所以SPI2能分出来的最高时钟是18MHz。如果你按72MHz去算SPI2的分频算出来的实际速率会翻倍从机直接扛不住。3. 片选是SPI里最容易被轻视的一环硬件NSS、软件片选与多从机3.1 STM32的硬件NSS在主机模式下为什么容易出问题STM32的SPI外设自带NSS引脚CubeMX里可以选择Hardware NSS Signal。但我踩过坑之后基本不再在主机模式下用它。原因在于F1系列SPI的硬件NSS逻辑有个众所周知的行为当NSS被外部拉低时SPI会自动切换到从机模式。主机模式下如果NSS引脚配置成了输入一旦这条线被其他逻辑干扰拉低SPI外设会默默从主机变成从机此时时钟不再输出通信直接中断而且这个状态在寄存器的Mode Fault标志位里不去查根本不知道发生了什么。稳妥的做法是把NSS引脚当成普通GPIO用CubeMX里选SPI_NSS_Soft然后在代码里自己控制这个GPIO。这样片选的拉低拉高时机完全掌握在自己手里还能顺手控制片选和第一个时钟沿之间的间隔。代价就是每个从机要占用一个GPIO别的没什么。送出数据的代码里片选的包裹位置很关键FLASH_CS_LOW(); /* 片选拉低 */ HAL_SPI_Transmit(hspi1, cmd, 4, 100); /* 命令 地址 */ HAL_SPI_Receive(hspi1, buf, len, 100); /* 读数据 */ FLASH_CS_HIGH(); /* 片选拉高一次事务结束 */千万不要在中间随便把CS拉起来因为很多从机把CS上升沿当作一次事务的结束标志中途抬一下会让它的内部状态机复位后面发的东西全被当成新命令的第一字节。3.2 多从机怎么接独立片选、菊花链以及MISO的冲突最常见的接法是独立片选SCLK、MOSI、MISO 三根线并联到所有从机每颗从机单独占一根CS。拉低哪根主机就跟哪颗说话。这个方案的好处是软件简单坏处是引脚消耗大。这里有个必须确认的点是从机未选中时MISO是否高阻。我遇到过一颗传感器它的MISO输出级在未选中时并不是高阻而是弱驱动结果跟另一颗Flash挂在一起Flash回数据的时候这颗传感器在往同一个网络里灌电平读回来的数据时不时错一位。这种器件的解决办法要么是给它单独一路SPI要么在MISO上加一个三态缓冲或者串一个小电阻做隔离不能硬挂。另一种接法是菊花链数据从第一颗的MISO进第二颗的MOSI一路串下去最后从最后一颗的MISO回到主机。很多LED驱动芯片、部分ADC支持这种方式。它的好处是片选只需要一根坏处是发送的数据长度等于所有级联器件位宽之和而且中间某个器件挂了整条链都不通。选它之前先确认器件手册明确支持Daisy Chain别自己想当然。还有一种情况值得单独说同一路SPI上挂不同Mode的器件。控制器切换CPOL/CPHA需要在CS全部拉高、总线空闲的时候改配置改完之后再重新拉片选。如果你的代码在传输过程中才想起来改那基本必挂。3.3 CS拉低到第一个时钟沿之间的那点时间这个细节大多数教程都不讲但它经常是单独调一颗器件能通、多颗挂一起就不通的元凶。器件手册里通常有个参数叫tCSSChip Select Setup Time意思是片选有效之后至少要等这么久时钟才能动。有的器件要求几十纳秒有的慢速器件比如某些ADC、某些老式EEPROM要求几百纳秒甚至微秒级。如果主控拉低片选之后立刻发时钟慢速器件还没准备好接收第一个字节就会丢。对于STM32F103这种72MHz的芯片一条GPIO写操作大概几十纳秒如果从机要求1微秒的建立时间光靠函数调用之间的间隔是不够的。解决办法有两种一是干脆在拉低片选之后插一个短延时比如__NOP()循环几次或者用DWT做个微秒延时二是在低速率场景下把SCLK分频调低间接拉长位周期让建立时间相对宽裕。我一般是在片选操作封装里加一个可配置的延时参数调试阶段先用一个比较大的值保证能通功能稳定之后再慢慢往下砍砍到刚好够用为止。还有一个反过来的参数是tCSHChip Select Hold Time片选拉高之前要保证最后一个时钟沿之后数据已经稳定。这个在DMA传输完成的回调里要特别注意回调触发意味着最后一个字节已经移出但如果你在回调里立刻把CS拉高大多数器件是能接受的如果某些器件要求更长的保持时间两级串联的器件就会掉数据。遇到这类问题时在回调里加几个NOP或者在CS拉高前插一小段延时问题往往就消失了。4. STM32F103加CubeMXSPI DMA收发从配置到跑通4.1 先搞清楚SPI挂在哪条APB上分频怎么算配置之前先把时钟关系理清楚这是最容易出错的地方。STM32F103有三路SPISPI1挂在APB2最高时钟72MHzSPI2和SPI3挂在APB1最高36MHz。而SPI外设能输出的SCLK是APB时钟除以分频系数分频系数只有2、4、8、16、32、64、128、256这八档。所以SPI1在72MHz下最小分频得是2最高SCLK是36MHzSPI2在36MHz下最小分频是2最高SCLK是18MHz想要8MHz的SCLK、用SPI1的话72÷89没有9这档只能选16分频得到4.5MHz或者选8分频得到9MHz。也就是说实际能配出来的时钟频率是离散的不是你想要多少就有多少。这个在选型阶段就要算清楚别等到代码写完才发现从机要5MHz而SPI1只能给你4.5或9。4.2 CubeMX里逐项过一遍哪些勾必须动打开CubeMXSPI配置页里的选项不算多但每一项都有意义。我按自己的习惯列一遍ModeFull-Duplex Master。只发不收的器件可以用Half-Duplex或者只开Transmit Only能省一个引脚但我一般保持全双工通用性好。Hardware NSS SignalDisable。原因上面讲过了用软件片选。Frame FormatMotorola。TI的SSP格式是另一种时序只有明确支持的器件才选。Data Size8 Bits。有些DAC要16位一次发完这时候改成16位能少一次收发。First BitMSB First。Prescaler按上面算出来的档位选。Clock Polarity / Clock Phase按从机手册定。CRC CalculationDisable。SPI的CRC基本没什么器件用开了反而多占时间。DMA Settings这里要加两路SPIx_TX和SPIx_RX加密优先级中等即可。NVIC Settings要把对应的DMA通道中断打开否则回调函数永远不会进。DMA通道的对应关系必须查参考手册F103上很容易记混SPI外设TX使用的DMA通道RX使用的DMA通道SPI1DMA1 Channel3DMA1 Channel2SPI2DMA1 Channel5DMA1 Channel4SPI3DMA1 Channel1DMA1 Channel2CubeMX选好SPI外设之后会自动带出正确的通道自己手写寄存器配置的时候才需要查表。这里有个坑SPI3的RX和SPI1的RX共用DMA1 Channel2所以这两路SPI不可能同时用DMA收数据同一个时刻只能有一个通道绑定。做多路SPI方案的时候一定要提前看这张表。4.3 Normal和CircularDMA模式的选择依据DMA的循环模式Circular和普通模式Normal选哪个取决于你的数据流形态。一问一答式的场景比如发命令读Flash的一页、读一次传感器寄存器用Normal最合适。传输完成触发一次回调你在回调里处理数据、拉高片选逻辑清晰不会出现上一帧还没处理完下一帧就覆盖缓冲区的情况。连续数据流的场景比如某颗传感器上电后按固定速率往外吐数据、或者用SPI接ADC连续采样这时候接收侧用Circular更合适。DMA一直在后台把收到的数据往环形缓冲区里放CPU只需要定期去取不需要每次都重启DMA。配合半传输中断Half Transfer和传输完成中断还能做成乒乓缓冲半满的时候处理前半段全满的时候处理后半段CPU处理和数据接收完全并行。需要提醒的是Circular模式下的回调触发频率很高如果你在回调里做了耗时操作比如printf、写Flash很容易造成缓冲区被覆盖。回调里只做搬数据和置标志位真正的处理放到主循环里做这是基本纪律。4.4 一份能直接用的收发代码骨架下面这段是读Flash数据的典型写法用的是TransmitReceive_DMA发送和接收一起走DMA长度必须一致F1的HAL库里这两者共用同一个长度参数#define FLASH_BUF_LEN 64 static uint8_t txBuf[FLASH_BUF_LEN]; static uint8_t rxBuf[FLASH_BUF_LEN]; static uint8_t *appDst; /* 应用层目标缓冲 */ static uint16_t appLen; static volatile uint8_t flashBusy 0; void Flash_Read_DMA(uint32_t addr, uint8_t *dst, uint16_t len) { if (flashBusy) return; /* 上一次还没完直接丢弃 */ flashBusy 1; appDst dst; appLen len; txBuf[0] 0x03; /* Read Data 命令 */ txBuf[1] (addr 16) 0xFF; txBuf[2] (addr 8) 0xFF; txBuf[3] addr 0xFF; memset(txBuf[4], 0xFF, len); /* 后面全是空字节靠它们把时钟撑起来 */ FLASH_CS_LOW(); HAL_SPI_TransmitReceive_DMA(hspi1, txBuf, rxBuf, len 4); } void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi-Instance SPI1) { FLASH_CS_HIGH(); memcpy(appDst, rxBuf[4], appLen); /* 前4字节是命令和地址要跳掉 */ flashBusy 0; } }这段代码里有三个细节值得单独讲。第一为什么要发0xFF做填充。SPI是全双工主机不按时钟从机就不会动而主机的时钟是靠往DR里写数据产生的。读Flash的时候MOSI上发什么从机并不关心所以习惯上用0xFF填。发0x00也能跑但0xFF在某些器件上更安全。第二为什么长度是len4。因为命令和3字节地址也是靠着时钟移出去的它们在同一个DMA事务里接收缓冲区的前4个字节是垃圾数据取数据时要从偏移4开始。第三flashBusy这个标志位不能省。DMA传输是异步的如果主循环不检查就直接发起下一次HAL库会返回HAL_BUSY你的标志位和实际状态就对不上了后面一次片选永远拉不高总线直接锁死。如果只用发送不用接收比如写命令、写寄存器可以直接用HAL_SPI_Transmit_DMA。这里有个容易忽视的点主机只发不收的时候从机回过来的数据同样会被移进接收寄存器如果不及时读走OVR溢出标志会置位之后HAL库会报错。HAL的Transmit函数内部是逐字节读DR来清标志的所以用HAL库一般不会有这个问题但你要是自己写寄存器版的SPI驱动就必须记得发一个字节读一次DR。4.5 波形不对时的排查顺序调SPI出问题的时候不要乱改代码按下面的顺序往下走效率最高先量四根线的波形。片选有没有动、时钟有没有、MOSI上有没有数据、MISO有没有回应。四根线都在问题在时序参数上缺哪个问题在配置或者硬件连接上。这一步能省掉后面一半的猜测。时钟没有检查GPIO是否配置成了复用推挽、SPI外设时钟是否使能、有没有Mode Fault。时钟有了但CS不动检查片选GPIO的初始电平是不是设成了高上电时CS如果默认低从机可能被误触发进入异常状态。读回全0xFF最常见的原因是从机没供电、MISO没接好、或者从机压根没被选中。读回全0x00可能是MISO被下拉了或者从机处于复位状态或者Mode不对导致它在错误的边沿采样。数据偶尔错位优先怀疑时钟太快、建立保持时间不够、或者片选建立时间不足。DMA传不完、回调不触发检查DMA通道中断是否使能、通道和SPI是否匹配、优先级是否被别的中断长期抢占。这套流程我在几个项目里反复用过基本上第七步之前都能定位到问题。5. 伺候好SPI Flash和TF卡指令集、忙等待与上拉电阻5.1 SPI Flash的指令集和一次完整读写的时序SPI Flash是所有SPI器件里最典型的指令集也不复杂。以常见的GD25Q128这类128Mbit16MB的芯片为例常用命令就这么几条0x9F读JEDEC ID、0x03读数据、0x02页编程、0x06写使能、0x05读状态寄存器、0x20扇区擦除4KB、0xD8块擦除64KB、0xC7整片擦除。读数据的时序是CS拉低发0x03发24位地址然后时钟继续打数据从MISO按位出来CS拉高结束。整个过程不需要任何握手全靠主机自己数够时钟。读ID是上电自检的第一步非常值得在初始化里加一段static uint8_t id[3]; void Flash_Init(void) { uint8_t cmd 0x9F; FLASH_CS_LOW(); HAL_SPI_Transmit(hspi1, cmd, 1, 100); HAL_SPI_Receive(hspi1, id, 3, 100); FLASH_CS_HIGH(); /* 常见的厂商ID0xEF(华邦)、0xC8(兆易)、0x20(镁光)、0x01(Spansion) */ }如果这一步读回来的不是已知厂商ID后面所有读写都不用试了先解决硬件和Mode问题。5.2 写之前必须WREN写完必须等BUSYFlash的写操作有两个铁律违反了就是数据丢失。第一任何擦除或编程之前必须先发0x06写使能。这条命令只设置一个内部锁存位不设置的话后面的编程命令会被直接忽略而且整颗芯片不会有任何报错——你会以为写进去了读回来还是老数据。第二擦除和编程是慢操作扇区擦除典型几十毫秒、最坏几百毫秒页编程几百微秒。在这段时间里芯片不响应任何命令必须通过读状态寄存器的bit0WIPWrite In Progress去轮询等待。static void Flash_WriteEnable(void) { uint8_t cmd 0x06; FLASH_CS_LOW(); HAL_SPI_Transmit(hspi1, cmd, 1, 100); FLASH_CS_HIGH(); } static void Flash_WaitBusy(void) { uint8_t cmd 0x05, sr 0; uint32_t t0 HAL_GetTick(); do { FLASH_CS_LOW(); HAL_SPI_Transmit(hspi1, cmd, 1, 100); HAL_SPI_Receive(hspi1, sr, 1, 100); FLASH_CS_HIGH(); } while ((sr 0x01) (HAL_GetTick() - t0 3000)); }还有一个Flash特有的性质NOR Flash只能把位从1写成0不能从0写成1。所以写之前必须先擦除擦除后整个区域是全0xFF。另外页编程不能跨页一次最多写256字节且必须落在同一个256字节页内跨页的部分会绕回页首把开头的数据覆盖掉这种错误特别隐蔽因为读回来大部分数据是对的只有边界那一小段是错的。我的做法是在写函数里主动按页边界切分应用程序只调用一个从任意地址写任意长度的封装切页的逻辑藏在里面。5.3 TF卡SPI模式为什么要上拉初始化为什么必须降速TF卡MicroSD支持SPI模式引脚对应关系是卡的DAT3对应CSCMD对应MOSIDAT0对应MISOCLK对应SCLK。用SPI模式最大的好处是所有主控都能驱动不用SDIO外设。但硬件上有个必须做的动作上拉电阻。信号线是否上拉原因CSDAT3建议上拉卡内部在SPI模式下把它当片选上电乱跳会误入命令MOSICMD建议上拉保证空闲时为高电平MISODAT0强烈建议上拉卡在初始化阶段可能不驱动该线没有上拉会读到乱码SCLK视情况低速时通常不用高速且走线长可考虑串阻上拉阻值一般用10k到47k之间10k是比较保险的选择。我见过太多TF卡识别不了的案例最后原因就是MISO没上拉卡在初始化握手阶段回的数据主机根本读不到。初始化流程上还有一个硬性要求上电后必须先给至少74个时钟而且整个初始化阶段SCLK必须低于400kHz。原因是卡内部有个上电复位过程需要足够多的时钟完成内部初始化而这个阶段的通信速率必须足够低才能兼容所有卡。所以代码里要先配一个256分频的慢速时钟走完CMD0、CMD8、ACMD41这些命令、确认卡进入就绪状态、读到CID和CSD之后再把SPI重新配置成高速。很多人卡在卡能识别但读写大量数据就出错问题往往出在初始化成功后没切换时钟几百kHz去读几MB的数据要等到天荒地老。5.4 ESP8266能不能直接挂SPI把模块的SPI分清楚这个问题被问得非常多。答案是ESP8266模块上引出来的SPI接口不是给用户用的那是它挂外部Flash用的。ESP8266芯片本身有个SPI接口出厂固件用它连接模块板载的那颗小容量Flash存放程序用户通过AT指令或者烧录固件都走的是UART接不上别的SPI从机。如果你的需求是用主控通过SPI去控制一个带无线功能的模块那要找的是原生支持SPI从机模式的芯片ESP32系列里有些型号开放了SPI slave接口一些无线收发芯片比如nRF24L01、部分LoRa模组也是标准的SPI从机直接挂上去就行。这个例子其实说明一个通用原则看到器件上有SPI引脚先翻数据手册确认它支持的是SPI主机、SPI从机还是仅内部使用。主控要有主控的样子从机要有从机的样子两个都只想当主机是连不上的。同理有些芯片的SPI接口只支持特定的帧格式或者固定Mode这些约束都要在选型阶段确认别等PCB打回来才发现。6. 速度往上走之后双DMA、信号完整性与FPGA侧的SPI实现6.1 收发各挂一个DMA到底是不是必须的这个问题在论坛上出现频率很高答案取决于数据量和实时性要求。如果只是偶尔发几十个字节DMA完全没必要HAL_SPI_Transmit阻塞式调用简单可靠出错还好查。如果是一次收几百字节以上的数据块接收侧必须上DMA因为SPI的接收是逐字节进DR寄存器的字节间隔只有几个微秒靠中断一个一个搬中断响应加上压栈出栈的开销很可能跟不上一跟不上就OVR数据就丢了。发送侧要不要DMA呢看情况。发送是主机自己控制节奏的如果数据量不大用阻塞式发送完全没问题还能省一个DMA通道。但要注意前面提到的那个细节只要时钟在跑从机的数据就会进来如果接收寄存器满了没读走OVR会置位。HAL库的Transmit函数内部会读DR清标志所以不会有问题如果用HAL_SPI_TransmitReceive_DMA收发配对那就更省心了。我自己的判断标准很简单接收数据超过64字节或者单位时间内要做多次传输就上双DMA否则单侧DMA或者纯阻塞式就够。很多项目盲目上了双DMA结果因为回调处理和缓冲区管理没写好反而引入了新的时序问题得不偿失。6.2 十几兆时钟下的波形问题串阻、走线、探头地线SPI速率一旦超过10MHz它就不再是接上就能通的接口了开始变成一个信号完整性问题。这时候最常见的现象是低速下一切正常提速之后读回来的数据随机出错或者某些位固定错。几个我踩过并验证有效的办法串联电阻。在SCLK和MOSI的源端串一个22到33欧的电阻能明显抑制过冲和振铃。代价是边沿变缓所以阻值不能太大33欧是个常见起点。探头地线要短。用逻辑分析仪看十几兆的信号如果用的是那种十几厘米长的鳄鱼夹地线你看到的波形里有一大半是探头自己引入的振铃。换成带弹簧地针的探头波形立刻干净一大截。这一点非常反直觉——你以为是电路有问题其实是测量方法有问题。走线要短、参考地要完整。SPI的三根信号线尽量走在一起下面有一整层地做参考。如果从主控到从机中间隔了接插件或者排线能降速就降速非高速不可的话在两条线之间穿插地线做隔离。多个从机共用SCLK时走线会有分叉分叉点就是反射源。这种情况下把片选线当成给哪颗器件发时钟的开关分叉影响会小一些但根本解法还是缩短主干、控制支线长度。6.3 FPGA做SPI主机或从机时的跨时钟域处理FPGA上实现SPI和单片机上完全不是一回事因为FPGA里没有现成的SPI外设你得自己写状态机。做主机相对简单用一个系统时钟分频产生SCLK用状态机控制数据移出和采样逻辑都是同一个时钟域没什么坑。做从机就麻烦了。SCLK是外部来的跟你的系统时钟没有任何关系直接拿它去驱动内部逻辑会带来两个问题时钟域不统一导致时序分析通不过以及毛刺可能引起的状态机乱跳。正确做法是把SCLK、CS、MOSI这些外部信号先用系统时钟打两拍做同步再用边沿检测电路找出SCLK的上升沿和下降沿在检测到边沿的那个系统时钟周期里去采样或者更新数据。这样整个设计只有一个时钟域稳定得多。还有一种更省事的做法是用厂商提供的SPI IP核配置好Mode、位宽、FIFO深度就能用缺点是灵活性差一些特殊时序需求比如字节间插入延迟实现不了。FPGA的方案里还有个细节是CS的处理从机必须在CS无效时把MISO置成高阻这个用三态缓冲原语实现仿真的时候也要验证。最后说个跨领域的观察做车载电子或者工业控制的岗位招聘要求里通常会同时列出CAN、LIN、SPI、I2C这些总线因为实际项目里这几个经常一起出现——CAN负责整车网络SPI负责主控和外围芯片之间的高速通信各司其职。把SPI这一层的时序和排查方法吃透对理解其他同步串行总线也有直接帮助很多坑是相通的。我个人的体会是SPI调不通的时候九成的答案在波形里剩下的一成在手册里。把逻辑分析仪接上、把手册的时序图翻出来一起看比在代码里反复改参数快得多。另外养成一个习惯先把时钟降到你确定能通的频率让功能跑起来再往上提速。很多新手一上来就按手册最大频率配结果卡在时序问题上好几天其实降速跑通再提速半天就能定位到真正的瓶颈在哪儿。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价