十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SPI通信协议从原理到实战:时序、片选、DMA与FPGA应用全解析

SPI通信协议从原理到实战:时序、片选、DMA与FPGA应用全解析 我一直觉得嵌入式工程师入行门槛不算高但真正区分“能跑”和“能卖”的往往就是通信协议这几板斧。UART、I2C、SPI再加上CAN和USB基本覆盖了绝大多数产品的数据通路。而这里面SPI是我个人认为“性能上限最高、同时翻车姿势也最丰富”的一个协议。它没有复杂的应答机制没有仲裁电气结构简单速度却能轻松跑到几十兆甚至上百兆简直是传感器、Flash、屏幕、ADC这些外设的黄金搭档。但这玩意儿简单是表象真要在项目里把它调稳、调快、调得不出错你得把时序、极性、相位、片选策略、DMA协同这些硬骨头全部啃明白。这篇文章不打算给你念协议手册我想从一个常年跟SPI打交道的工程师视角把SPI通信从原理到实战、从STM32到FPGA、从配置到踩坑完整串一遍。适合刚入门想弄懂SPI到底怎么工作的新手也适合那些已经能点灯但老在SPI上栽跟头的进阶开发者。你不需要有很深的数字电路基础但最好手里有一块开发板边看边试效果最好。1. 项目概述为什么嵌入式里处处都是SPI先聊点实际的。我们做的很多产品主控芯片的引脚是有限的但外设的胃口是无限的。传感器要读数Flash要存日志屏幕要刷新画面ADC要采波形每一个都嗷嗷待哺等着和主控对话。通信协议就是这场对话的“语言规范”。而SPI之所以能在这么多协议里胜出成为高速外设的默认选择核心就三个字快、简、省。快说的是速度。标准SPI的时钟轻松上10MHz高性能器件跑40MHz、50MHz也不稀奇一个时钟周期传输一个bit换算下来每秒能搬好几兆字节的数据这是I2C那种标准模式下100kHz、快速模式400kHz完全没法比的。简说的是逻辑简单。它不像I2C那样有设备地址、读写位、ACK应答这些繁文缛节也没有UART那样需要双方精确约定波特率只要主从双方约定好时钟极性和相位剩下的就是纯粹的“移位寄存器对接移位寄存器”。省说的是引脚开销可控——四根线搞定一切比并口动辄十几根线省钱省PCB面积多了。但SPI也有它的小脾气。它是一对多的主从结构主机主动、从机被动没有从机主动上报数据的能力。它的接收和发送是同时进行的所以很多新手第一次调试时会被“读回来的数据全是FF”这种问题搞到怀疑人生。另外SPI的时序参数非常敏感上升沿采样还是下降沿采样、空闲电平是高还是低只要配置错一个bit整条链路的数据全是错的。这也正是我想写这篇文章的原因。SPI这东西上限很高下限也很低。用好了它是最趁手的数据搬运工用不好它能把你折磨到重新审视人生。接下来我按照从原理到实战的顺序把整个SPI通信涉及的要点全部拆开讲透。2. 核心原理解读SPI的通信机制与四线架构2.1 主从结构与四根线的角色分配先看SPI的整体架构。SPI总线采用主从Master-Slave模式一个主机可以挂多个从机但同一时刻只能有一个从机被“激活”。这种结构下主机掌控全局时钟由主机产生所有数据的传输节奏都跟着主机走。所以SPI从机的实现通常比主机简单很多——它只需要被动响应。SPI通信靠四根信号线完成它们的分工非常明确信号线全称方向职责SCLKSerial Clock主机→从机串行时钟决定数据传输的节奏MOSIMaster Out Slave In主机→从机主机发送数据给从机MISOMaster In Slave Out从机→主机从机发送数据给主机CSChip Select主机→从机片选信号低电平有效选中目标从机你可以把这四根线理解成一个工厂的流水线SCLK是产线节拍器MOSI是主机递给从机的物料MISO是从机加工完回传给主机的成品CS就是“老王你过来干活”的那一声点名。只有当片选信号拉低时从机才会“听到”SCLK和MOSI上的信号其他时间它都在装睡。这里有个很重要的细节MOSI叫“Master Out Slave In”但在主机视角里它通常也标注为“主发数据线”MISO则相反。很多人看原理图时容易搞混一根线到底该接哪其实只需要记住一个口诀——同类相连。主机的MISO接从机的MISO引脚或者叫DOUT主机的MOSI接从机的MOSI或者叫DINSCLK接SCLKCS接CS。不同厂家芯片的命名习惯不太一样有的用MOSI/MISO有的用SDO/SDI有的用DO/DI但只要认准方向就不会接错。2.2 移位寄存器SPI数据传输的本质说清楚四根线之后我们来聊SPI数据传输的本质。很多教科书一来就给你讲时序图新手看完一头雾水。我换个方式用移位寄存器来解释SPI的工作机制。SPI的核心是一个“双向移位寄存器”的对接。主机内部有一个N位通常是8位的移位寄存器从机内部也有一个N位的移位寄存器。通信开始后主机把CS拉低然后在SCLK的每个时钟沿到来时主机移位寄存器的最低位或最高位取决于配置从MOSI线上移出同时从机的那一位从MISO线上移入主机。一个时钟周期内主机发出1 bit也从从机收回1 bit这就是SPI的“全双工”含义。举一个具体例子。主机要发送0xA5二进制10100101给从机同时从机也正在发送自己的0x5A01011010回来。主机移位寄存器里的0xA5一位一位从MOSI送出去而从机的0x5A一位一位从MISO送回来。8个时钟之后主机拿到的是从机发来的0x5A从机拿到的是主机发来的0xA5。换句话说SPI的“发送”和“接收”在物理上是同一个动作——你每发出一个字节就必然收回一个字节哪怕收回的数据你没兴趣。这个机制带来一个非常经典的坑读Flash、读传感器、读屏幕状态时主机想读到数据就必须先“假装发送”一些字节。比如你要读SPI Flash某个地址的内容你得先发命令码0x03再发24位地址三个字节这四个发送动作的过程中主机的MISO线上收到的全是垃圾数据等到主机继续发第5个字节随便发个0x00时Flash才把要读的数据一个bit一个bit地放到MISO线上主机在时钟沿把这8个bit收回来才真正拿到了数据。理解了这个机制你调试任何SPI器件时都会从容很多。2.3 SPI的优点与固有短板再补充一些SPI的特点帮你判断它在你的项目里到底合不合适。首先SPI没有ACK机制。发送方把数据发出去了至于对方有没有收到、收到的是不是对的SPI本身不负责。你需要在协议层自己加校验比如CRC、校验和或者至少加上“读回对比”之类的动作。这一点和I2C很不一样——I2C每收到一个字节都强制从机回一个ACK错了主机立刻就能感知。其次SPI没有标准的寻址机制。I2C靠器件地址区分设备而SPI靠片选线区分设备。每个从机独占一根CS引脚主机要选哪个从机就把对应的CS拉低即可。所以SPI挂多从机时主控引脚消耗比较大——每多一个从机就多占用一个GPIO。这个问题在后面讲片选策略时我会展开。再次SPI的传输是连续的数据流没有帧起始位和停止位。数据的边界全靠CS的电平变化来界定CS拉低一帧开始CS拉高一帧结束。这带来一个很实际的问题——主机必须在正确的时机把CS拉低和拉高拉早了或拉晚了从机就可能解析出错误的数据帧。最后SPI的极性和相位配置一旦不匹配读回来的数据全错。这个问题太经典了可以说十个SPI调试故障里五个都是极性相位没配对造成的。我专门用下一节来讲透它。3. SPI时序详解极性、相位与四种工作模式3.1 CPOL与CPHA到底在配置什么SPI时序里最核心的两个参数是CPOLClock Polarity时钟极性和CPHAClock Phase时钟相位。它们共同决定了SCLK在空闲时是高还是低以及数据是在时钟的上升沿还是下降沿被采样。看它们组合出来的4种模式就是SPI Mode 0到Mode 3。先看CPOL。CPOL 0时SCLK空闲时为低电平CPOL 1时SCLK空闲时为高电平。就这么简单它只决定空闲状态不决定采样边沿。再看CPHA。CPHA 0时数据在时钟的第一个边沿从空闲态到有效态的跳变沿被采样CPHA 1时数据在时钟的第二个边沿被采样。判断“第一个边沿”还是“第二个边沿”要从CS拉低后的第一个时钟沿开始数。把CPOL和CPHA交叉组合就得到下表SPI模式CPOLCPHA空闲电平数据采样边沿Mode 000低电平上升沿第一个边沿Mode 101低电平下降沿第二个边沿Mode 210高电平下降沿第一个边沿Mode 311高电平上升沿第二个边沿Mode 0是最常见的模式市面上绝大多数SPI器件默认就跑这个。ST7789屏幕、W25Q系列Flash、MPU6050传感器、SD卡等等数据手册里写的默认模式基本都是Mode 0。但这个“默认”不代表你可以不看手册因为总有一些器件不按套路出牌比如某些射频芯片喜欢用Mode 3某些触摸控制器在Mode 1下才能稳定工作。3.2 为什么主从模式不匹配时数据全是错的很多人不理解模式没配对不就是时钟边沿错了嘛数据真的会全错吗答案是真的会全错而且错得毫无规律。原因是这样的。SPI收发双方靠SCLK的边沿来对齐数据。主机在某个边沿把数据放到线上从机默认在同一个边沿去采样。如果主从配置的CPHA不一致就可能出现从机在主机还没把数据放好时就采样了——采到的是上一个bit的残留值甚至是不确定电平等真正采到位时一个bit的周期已经过去了数据错位。一次错位后面每个bit都跟着错最终收到的字节和发送的完全对不上。更隐蔽的情况是如果主机的CPHA配置错误数据在SCLK的两个边沿之间变化而采样边沿恰好落在了数据变化的过程中采到的可能是高、可能低、可能是不确定的毛刺状态。这种故障最讨厌因为它不是稳定的“全FF”或“全00”而是每次读回的数据都随机变动非常难排查。我自己的经验是拿到一个陌生的SPI器件第一步不要急着调寄存器先查数据手册里有没有写“SPI Mode”。大部分器件手册的时序图会明确标出CPOL和CPHA或者直接用文字写“Mode 0”或“Mode 3”。如果手册没写就用逻辑分析仪挂上去分别试4种模式看哪种模式下读回的器件ID是正确的。用排除法解决通常十分钟内能定位。3.3 一个典型SPI读时序的实操拆解为了让你更直观地理解SPI时序到底长什么样我拆一个最常见的操作读SPI Flash的厂商ID。以W25Q128为例读ID的命令是0x90后面跟三个字节的地址0x000000然后主机再发一个哑字节就能在MISO上读回2个字节的ID信息。在Mode 0下的完整时序是这样的主机将CS拉低从机被选中开始工作。主机在SCLK的上升沿逐bit发送0x90命令码共8个SCLK。主机继续在上升沿逐bit发送0x00、0x00、0x0024位地址共24个SCLK。主机继续产生8个SCLK同时在这8个时钟内从机将厂商ID的高字节例如0xEF放到MISO线上主机在上升沿采样。主机再产生8个SCLK从机将厂商ID的低字节例如0x40放到MISO线上主机采样。主机收到完整的2字节ID后将CS拉高一次读操作结束。注意第4和第5步的关键点——从机并不是把数据“推”给主机而是主机主动产生时钟从机顺势把数据放到MISO上。如果主机不产生时钟从机什么也做不了。所以这个过程中那8个“哑时钟”是必须的你不能说“我只想读数据不想发东西”——不发时钟数据根本不会出现在线上。这就是前面说的SPI“读写一体”机制在时序层面的落地表现。我在实际项目里读Flash ID、读传感器寄存器、读屏幕状态时几乎都会写一个通用的SPI读写函数大概长这样uint8_t spi_read_byte(uint8_t tx_data) { uint8_t rx_data; // 等待发送缓冲区空 while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) RESET); // 发送一个字节同时启动接收 SPI_I2S_SendData(SPI1, tx_data); // 等待接收缓冲区非空 while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) RESET); // 读取收到的字节 rx_data SPI_I2S_ReceiveData(SPI1); return rx_data; }每一次调用既发送了tx_data也接收了从机返回的一个字节。用这个函数去拼读ID的流程就是连续调用多次最后一两次返回的才是真正有用的ID数据。逻辑上很顺但如果你不理解“发送哑字节才能读到数据”这个前提很容易在这里卡壳半天。4. 片选策略深入分析硬件片选与软件片选怎么选4.1 硬件片选与软件片选的本质区别SPI通信里片选这根线看似只是一个GPIO拉高拉低的动作里面的讲究其实不少。具体到工程实现片选有两种玩法硬件片选和软件片选。硬件片选就是把SPI外设的NSS引脚或者叫CS引脚直接配置成硬件自动控制。主机启动传输时外设自动把NSS拉低传输结束自动拉高。全程不需要CPU干预代码里只需要保证数据写入发送寄存器即可。这种方式的好处是响应速度快、时序精确CPU负担小。坏处是灵活性差——你没法在一帧数据中间自定义片选的拉高时机。软件片选就是把CS引脚当作普通GPIO来操作。需要开始传输时手动拉低CS要结束传输时手动拉高CS中间想停顿多久就停顿多久。这种方式极其灵活是实际项目里用得最多的做法。几乎我接触过的每个量产固件CS都是软件控制的GPIO而不是硬件NSS。为什么硬件片选在复杂场景下不够用举个最典型的场景很多SPI从机要求“命令帧结束”和“数据帧开始”之间CS必须拉高一小段时间再拉低。比如某些触摸控制器连续发送两个命令帧中间间隔不够第二个命令就被吞掉了。用软件CS你想拉高多久就拉高多久完全可编程用硬件CS片选时机由外设说了算你很难精准插入这个间隔。4.2 两种片选方式的优劣对比对比项硬件片选软件片选CPU开销低稍高每次都要操作GPIO响应速度快取决于GPIO操作耗时灵活性差片选时机由外设决定高完全可控多从机扩展需要切换NSS映射麻烦每个从机一个GPIO即可中断/轮询配合需要额外处理更直观代码可读性一般好容易理解这里额外说一个很多人问过的问题能不能用硬件片选挂多个从机答案是能但要十分小心。STM32的NSS引脚可以做引脚复用映射你可以在不同时刻把NSS映射到不同引脚但切换映射本身需要时间而且一旦在传输过程中误切换数据就乱了。所以多从机场景下我基本只用软件片选每个从机接一个独立GPIO主机的NSS引脚干脆做普通GPIO用。4.3 多从机共享SPI总线的三种连接方式SPI挂多个从机时连接方式主要有三种独立片选、菊花链、以及用GPIO扩展片选。独立片选是最常见的做法。所有从机的SCLK、MOSI、MISO并联在一起每个从机独占一根CS线。主机同一时刻只拉低一个CS其他从机的CS保持高电平。这种方式的优点是简单直观任意时刻只有一个从机在总线上回话不会冲突。缺点是每加一个从机就多一个GPIO主控引脚紧张时比较难受。菊花链结构在寄存器配置类的芯片里偶尔出现。它的连接方式是主机MOSI接第一个从机的MOSI第一个从机的MISO接第二个从机的MOSI以此类推最后一个从机的MISO接主机MISO。数据像排队一样一个一个往后传。这种方式省CS线但延迟和控制复杂度都上去了而且不是所有从机都支持菊花链。实际项目里如果遇到数据量大的场景我一般不建议用菊花链做高速传输。GPIO扩展片选是在主控GPIO不够时的变通方案。用一个74HC595或者PCF8574之类的IO扩展芯片把CS信号串行输出到多个引脚主机只需要2根线数据线时钟线就能控制任意一个从机的CS。代价是片选切速度慢毕竟每个CS状态都要通过串行方式刷新一遍。这个方案适合从机切换频率不高的场景比如频繁读Flash的场合就不太适合。4.4 片选设计中的几个实操细节无论哪种方案片选设计里都有几个细节容易翻车我单独拎出来强调一下。第一CS信号必须提前于SCLK稳定。主机拉低CS之后至少要留出几十纳秒的建立时间才能开始产生SCLK。如果CS刚拉低就立刻发时钟有些要求严格的从机可能还没准备好第一个bit就被吃掉了。第二CS的上升沿要干净。如果CS线上有毛刺从机可能误判为一次传输的结束和另一次传输的开始导致帧边界错误。硬件上可以在CS引脚加一个10kΩ左右的上拉电阻软件上可以启动引脚滤波功能。第三主机的MISO引脚如果配成开漏输出必须外部上拉。很多SPI从机是推挽输出这没问题但如果从机支持多主模式MISO浮空时会引入噪声加一个4.7kΩ或10kΩ的上拉电阻更保险。第四CS不要和SCLK、MOSI走线靠得太近。高频SPI信号之间会产生串扰CS上稍微串进来一个毛刺整个传输帧就废了。PCB布局时SPI四根线尽量包地或拉开间距特别是CS这种异步控制的信号线。5. STM32 SPI配置与DMA协同的实战落地5.1 CubeMX里SPI外设的关键配置项STM32是目前工程师群体里用得最多的MCU系列之一所以我把STM32侧SPI的完整配置流程拆开讲。CubeMX的SPI配置界面看起来项目很多其实关键的就那么几个。第一个是数据宽度。STM32的SPI支持8位和16位两种帧格式。大多数外设是8位的但有些屏幕驱动、Flash芯片支持16位模式能明显减少寄存器操作次数。这里注意一点STM32的数据寄存器是16位的8位模式下写入数据时要放在低8位读取时也只用低8位。第二个是帧格式即MSB First还是LSB First。SPI协议规定MSB优先是常态几乎所有器件都遵循这个规则。但总有例外比如某些射频芯片要求LSB First。这个配置错了读回来的字节就会“左右颠倒”——比特顺序是反的数值完全不对。需要特别说明的是这并不等同于字节序大小端而是比特级的倒序。第三个是波特率预分频。SPI时钟不是想多快就多快的它来源于MCU外设时钟PCLK再经过预分频得到。以STM32F1系列为例APB2时钟通常是72MHzSPI1挂着这个时钟最高可以1分频得到36MHz的SPI时钟。但实际能跑多快还要看从机的上限。W25Q128最高支持60MHz的时钟但STM32F1的SPI1最高就36MHz所以直接配1分频即可。如果从机是普通传感器建议从4分频或8分频开始试稳定后再逐步提速。5.2 软件片选模式下GPIO的配置要点CubeMX里有一个非常经典的坑当你使用软件片选时SPI外设的NSS引脚配置会直接影响通信。如果你把NSS配成了硬件模式那么即使你把某个GPIO拉低SPI外设也可能因为NSS的状态不对而不启动传输。所以正确做法是在CubeMX里把SPI的NSS配置为“Disabled”或者“Software”让外设完全不干涉NSS引脚然后随便选一个空闲GPIO配置为普通推挽输出初始状态拉高用它来做CS。代码里发送前拉低发送完拉高。说到底软件片选的核心就是把CS的控制权从外设手里抢过来完全交给固件。这样做的另外一大好处是你可以在任意时刻修改CS比如遇到特殊外设要求帧间隔时随时加延时根本不需要改外设配置。我的习惯是把片选操作封装成两个内联函数#define SPI_CS_LOW() GPIO_ResetBits(CS_GPIO_PORT, CS_PIN) #define SPI_CS_HIGH() GPIO_SetBits(CS_GPIO_PORT, CS_PIN)然后每次传输的模板都是SPI_CS_LOW(); // 可选的小延时确保CS建立时间 for (volatile int i 0; i 10; i); // 发送命令、地址、数据 spi_write_and_read(buf, len); // 传输完成拉高CS SPI_CS_HIGH();5.3 DMA传输把CPU从字节搬运里解放出来SPI痛点之二就是阻塞式发送太费CPU。假如刷新一块320x240的屏幕一帧数据就是320×240×2 ≈ 150KB。就算SPI跑36MHz一个字节8个时钟理论带宽4.5MB/s刷一帧也要约34ms。这股时间里CPU如果死等SPI发送完成那啥也别干了就剩刷屏了。所以高速数据显示场景必须上DMA。DMADirect Memory Access直接存储器访问的作用就是让数据在外设和存储器之间直接搬运搬完了再叫CPU一声。STM32的SPI1发送DMA请求映射通常是SPI1_TX和SPI1_RX。配置好DMA通道后你要发送的数据放在一个连续的内存缓冲区里然后启动DMADMA会把数据分批次写入SPI发送寄存器SPI外设自动发出全部发完触发完成中断。CPU在启动DMA后立刻就可以去干别的。实际项目里我是这么配置的SPI的发送和接收各挂一个DMA流优先级都设成High数据宽度看SPI配置是8位还是16位。发送DMA模式选Normal接收DMA也选Normal每次传输前重新设置要搬运的长度。特别注意DMA传输的总长度不要超过65535字节16位计数器上限一次超过就得分块发送。DMA版本的Flash页写入函数伪代码大概是void flash_page_program_dma(uint32_t addr, uint8_t *data, uint32_t len) { // 选择Flash并拉低CS FLASH_CS_LOW(); // 发送页编程命令 0x02 和 24位地址如果是标准3字节命令 spi_write_byte(0x02); spi_write_byte((addr 16) 0xFF); spi_write_byte((addr 8) 0xFF); spi_write_byte(addr 0xFF); // 启动DMA发送数据 SPI_DMA_Start_TX(data, len); // 等待DMA发送完成中断或标志位 while (DMA_GetFlagStatus(DMA_TX_CH, DMA_FLAG_TC) RESET); // 拉高CS FLASH_CS_HIGH(); // 等Flash内部写入完成 while (flash_read_busy_bit() BUSY); }使用DMA之后同样刷新一帧屏幕CPU占用从“满载死等”降到“启动DMA后几乎零负担”帧率还稳定了很多。唯一需要注意的是DMA传输完成标志和SPI外设真正发完最后一个bit之间存在一个极短的时间差拉高CS时要确保已经收到DMA传输完成中断千万不要在DMA还没发完时就拉高CS否则最后一个或几个字节会被截断。5.4 实际调试中常被忽略的NSS与BSY标志最后补一个STM32 SPI最刁钻的细节。STM32的SPI外设有一个忙标志BSY当SPI正在传输时BSY会保持置位。但很多人不知道在最后一次数据传输完成后BSY并不会立刻清零而是会延迟大约一个半SPI时钟周期。如果代码在发送完最后一位后立刻检查BSY则会发现BSY依然为0的情况或者在BSY还没清除时就拉高CS导致最后一个字节的数据没有完整地移出。所以正统的收尾流程应该是先确认TXE发送缓冲区空再确认BSY清零之后才拉高CS。顺序错了就会出现“数据发送了但从机收到的最后一个字节是错的”这种诡异现象。// 等待发送缓冲区空 while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) RESET); // 等待SPI不忙关键步骤 while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_BSY) SET); // 传输真正结束此时才允许拉高CS SPI_CS_HIGH();6. 从CubeMX到FPGASPI设备驱动的进阶实现思路6.1 如何在STM32上用SPI驱动ST7789屏幕演进到实际项目SPI最常见的使用场景之一就是驱动彩屏。ST7789是入门常用的SPI接口TFT屏驱动芯片240x240分辨率单色深16位用STM32的SPI接口就能驱动。初始化流程大概是硬件复位RES引脚拉低再拉高、开显示SLPOUT命令、设置显示方向MADCTL、写显存。这些初始化命令都是SPI写操作主机通过MOSI发送命令码和数据CS全程保持低电平D/C引脚如果使用4线SPIDC的话用于区分命令和数据。这里有个性能决策点值得说一说屏幕刷新时数据量极大要不要用DMA我用过两种方案。第一种是裸机下SPIDMA刷新每行或每半屏启动一次DMA发送发送期间CPU可以去计算下一块图像数据。第二种是屏幕和SD卡共享SPI总线两个器件分时复用。这种场景下DMA的意义就更大了——没有DMACPU长时间被SPI占用连从SD卡读文件的时间窗口都挤不出来。如果你用的是ESP32这类双核芯片还可以做一个更极端的方案核心0负责SPI刷屏核心1负责逻辑计算和数据准备两边通过队列通信。这种多核共享SPI的设计能让整条数据链路像流水线一样跑起来瓶颈只出现在总线带宽上。6.2 FPGA侧用Verilog实现SPI从机的核心逻辑MCU之外FPGA也是SPI协议的主战场。很多FPGA外接ADC、Flash、传感器时都得自己用Verilog实现SPI主机或从机。这里我以SPI从机为例讲讲核心状态机的设计思路。一个SPI从机要处理的事情其实非常单纯监听CS下降沿、接收命令、接收地址、接收数据或发送数据。在Verilog里最简实现就是三段式状态机。第一个状态空闲等待CS拉低第二个状态接收命令字节根据命令决定后续是读还是写第三个状态传输数据逐bit移入或移出。下面是一个SPI从机的简化框架module spi_slave ( input wire clk, // 系统时钟用于同步采样 input wire rst_n, // 复位 input wire spi_cs_n, // SPI片选低有效 input wire spi_sclk, // SPI时钟 input wire spi_mosi, // SPI主机输出数据 output reg spi_miso, // SPI从机输出数据 output reg [7:0] reg_addr, // 解析出的寄存器地址 output reg [31:0] reg_data_out // 读出的寄存器数据 ); localparam IDLE 2b00; localparam CMD 2b01; localparam TRANS 2b10; reg [1:0] state; reg [3:0] bit_cnt; reg [7:0] shift_reg; always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; bit_cnt 4d0; shift_reg 8h00; spi_miso 1b0; reg_addr 8h00; end else begin case (state) IDLE: begin if (!spi_cs_n) begin state CMD; bit_cnt 4d0; end end CMD: begin // 在SPI时钟上升沿采样MOSI拼装命令字节 if (!spi_cs_n spi_sclk) begin shift_reg {shift_reg[6:0], spi_mosi}; bit_cnt bit_cnt 1b1; if (bit_cnt 4d7) begin reg_addr {shift_reg[6:0], spi_mosi}; // 这里仅为示意 state TRANS; bit_cnt 4d0; end end if (spi_cs_n) begin state IDLE; end end TRANS: begin // 类似地传输8个bit ... end endcase end end endmodule写SPI从机最容易犯的错误是直接用SPI_SLCK作为采样时钟而不是用系统时钟做同步采样。如果SPI时钟太快或者抖动较大整个逻辑就会不稳定。正确的做法是用系统时钟对外部信号做打拍同步和边沿检测然后根据检测到的边沿去采样或移出数据。道理和MCU读GPIO要防抖没什么区别只是时序要求更严格。6.3 SPI在FPGA远程升级场景中的应用细节SPI在FPGA领域一个很实用也很有分量的应用场景就是基于SPI Flash的远程升级。FPGA的配置文件bitstream通常存放在外部的SPI Flash里上电时FPGA从SPI Flash加载配置。如果产品要支持远程升级就要在运行状态下通过某种接口比如网口或串口拿到新固件然后写进SPI Flash并在下次上电时让FPGA加载新版本。这个流程看着简单但工程上全是细节。第一个细节是FPGA运行中读写SPI Flash时不能让FPGA的“配置加载接口”和“用户逻辑的SPI接口”打架。很多FPGA有专用的SPI配置引脚同时还允许用户逻辑使用一个SPI控制器访问同一颗Flash两个控制器必须分时复用而且要做切换互斥否则两边同时访问Flash数据就乱了。第二个细节是坏块处理。SPI Flash写之前要先擦除擦除以扇区为单位最小擦除单位通常是4KB。升级固件的大小可能跨越多个扇区必须先把目标扇区的旧数据读出来备份、改写或者干脆整扇区擦除后按新文件写入。如果每一条都严格按Flash的数据手册来顺序错一个就可能把原有固件擦没了。第三个细节是掉电保护。远程升级过程中如果突然断电Flash里可能留着半个新固件旧固件已经被覆盖产品变砖。常见的做法是设计双备份启动——固件写入Flash的A分区运行期间如果校验失败回退到B分区的旧版本。这种机制在量产产品里几乎是标配。我参与过的一个FPGA项目就是用SPI Flash做多分区引导。每一次升级流程大致是上位机通过UART下发固件包 → MCU或FPGA内的软核将固件写入SPI Flash的空闲分区 → 写入完成后校验CRC → 校验通过则切换启动标志分区 → 触发重启加载新固件。整个过程最关键的一点就是每一步都要校验而且CS操作用软件手动管避免硬件NSS自动化在某些边界情况下出错。6.4 CubeMX生成代码后的二次梳理与DMA中断协同再回到STM32侧。CubeMX能帮你把外设初始化的骨架生成好但真正稳定的业务代码还是要靠你在初始化之后做一些必要的“二次梳理”。我举个例子。CubeMX中如果你同时使能了DMA发送和DMA接收它默认会在SPI初始化时把两个DMA通道都绑好。但如果你只用发送DMA接收完全靠轮询那么接收DMA通道虽然配好了却可能在你读取接收数据寄存器时干扰你的轮询逻辑。我的习惯是把用不到的DMA通道直接停用只保留实际使用的通道。另外就是DMA中断和忙标志之间的配合。DMA传输完成中断触发不代表SPI外设已经把数据全部送出。必须像前面说的那样先等TXE再等BSY清零然后才关CS。如果DMA中断和CS拉高之间不加这个等待低概率的截断问题早晚会在你客户现场爆发一次。7. 常见SPI通信故障与排查技巧实录7.1 故障速查表从现象到原因快速定位SPI通信出问题时现象往往五花八门。我把常见故障整理成一个速查表排查时可以先对号入座能省不少时间。故障现象可能原因排查建议读回全是0xFF从机没被选中或者没响应检查CS是否拉低、SCLK是否真的产生读回全是0x00MOSI/MISO接反或者从机无输出驱动核对四根线的连接顺序数据随机变化无规律极性和相位CPOL/CPHA不匹配用逻辑分析仪抓时序对比数据手册只有第一个字节正确后面全错CS拉高过早或数据帧边界错乱检查传输完成后的BSY等待偶尔出现一个错误bit信号完整性差走线过长/有干扰降低SPI时钟频率加滤波电容某个从机正常其他从机混乱片选逻辑冲突多个CS同时拉低检查软件片选的互斥逻辑7.2 经典排查场景为什么读传感器寄存器读到FF我给你讲一个我亲身踩过的坑。有一款气压传感器通过SPI读取压力数据数据手册明确写了建议使用Mode 3。这是我犯的第一个错误——我偷懒直接用了默认的Mode 0结果读回来的寄存器值全是FF我一度怀疑芯片坏了。后来用逻辑分析仪抓波形发现主机在SCLK下降沿采样而从机在SCLK上升沿更新数据——两边边沿相差半个周期采样点正好落在数据变化区。改成Mode 3之后数据立刻正常了。这个案例说明SPI调试时最好一开始就抓波形。肉眼看到主机发出的波形和器件手册上的时序图一对比是极性错了、相位错了还是CS时序不对一目了然。没有逻辑分析仪时可以用示波器双通道同时抓SCLK和MOSI但抓MISO时需要先让主机连续发送时钟否则从机不会输出数据。还有一次更隐蔽的故障。我在一块自己画的板子上调试W25Q32 Flash读ID时发现ID对了一半另外一半错了一个bit。查了很久没查出问题后来用示波器探到MOSI和MISO之间串扰严重。原因是这两根线在PCB上平行走了很长一段间距又小。我把SPI时钟从36MHz降到4MHz测试一切正常。这就说明是信号完整性问题。最后通过调整走线间距和降低SPI速率解决了。在高频SPI设计中速率不是越高越好能稳定工作是第一位的。7.3 排查工具选择与逻辑分析仪使用心得排查SPI问题工欲善其事必先利其器。我个人是强烈建议SPI调试至少准备一个逻辑分析仪哪怕是几十块钱的8通道USB逻辑分析仪也够用采样率至少要到24MHz以上建议50MHz。因为SPI时钟动不动就是10MHz以上以24MHz采样率去抓10MHz的SPI信号每个bit只能采到2个点时序细节会丢失。50MHz以上能比较可靠地还原波形。逻辑分析仪接好之后重点是看几个东西SCLK频率和实际预期是否一致、CS拉低到第一个SCLK边沿的建立时间是否足够、数据在SCLK的哪个边沿变化、是否有毛刺。把主机发送命令的波形和从机数据手册的时序图对照着看90%的SPI问题都能一眼揪出来。高级一点的工具是带协议解析功能的逻辑分析仪它能直接把SPI波形解析成你发的命令码和数据序列。调试SPI Flash时我经常用这个功能回放整段通信记录定位是哪一帧出了问题、哪个字节被吃了。7.4 软件层面的三个排查技巧没有硬件工具时软件层面也有一些排查技巧关键时刻能救命。第一个技巧用回环测试验证外设本身的好坏。很多MCU的SPI外设支持环回模式LoopBack发送的数据直接回到接收端。把SPI配置成环回模式发一串测试数据看能否原样收回来。能收回说明MCU的SPI外设基本正常问题大概率出在外部连线或从机侧。不能收回先查外设配置。第二个技巧手动翻转GPIO模拟SPI时序。当怀疑SPI外设配置有大问题时可以退回到用GPIO模拟简单波形来验证从机是否响应。比如初始化一个SPI从机最简单的做法就是先把时钟线和数据线全部配置为GPIO输出手动拉高拉低来产生波形。虽然慢但每个bit的变化你都能看见非常适合定位“从机完全没反应”的问题。第三个技巧检查SPI从机的数据手册里的初始化命令序列。很多SPI器件上电后并不是立刻就能通信而是需要一组初始化命令、需要延时、需要等待内部状态稳定。如果漏了这些步骤SPI读写就会莫名其妙地失败。我调试ST7789屏幕时初始化序列少写了一条SLPOUT命令屏幕就一直黑屏读状态寄存器也读不到正确值。所以排查SPI问题时别忘了回头检查你的初始化流程是否完整。8. 项目复盘我常用的SPI总线设计规范与避坑清单这一节我整理一套自己在多个量产项目里验证过的SPI总线设计规范分硬件和软件两块算是避坑清单。硬件设计上SPI走线尽量短主控到从机的距离控制在5cm以内超过这个距离就要考虑降低时钟频率。SCLK、MOSI、MISO三根信号线之间预留足够间距最好加地线隔离。CS信号线单独走避免和SCLK长距离平行。如果SPI和SD卡这类需要热插拔的器件相连MISO上串一个33Ω左右的电阻做保护防止插拔瞬间的电压尖峰损坏主控。每个SPI从机的电源脚附近都加一个100nF的退耦电容高速开关时电源纹波会影响信号边沿的干净程度。如果从机支持CS上最好加上拉电阻避免CS在主机复位期间浮空导致从机误触发。软件设计上SPI初始化完成后先做一次回环或读ID测试确认链路正常后再进入业务逻辑。片选操作统一封装禁止在业务代码里散落式地操作CS引脚。传输帧尽量设计成统一的结构命令地址数据校验方便后期维护。数据缓冲区要注意内存对齐DMA传输时缓冲区地址和长度都要满足外设的对齐要求。写到这里我想把一个埋了很久的观点再说透一点SPI协议本身看起来简单但真正决定一个嵌入式工程师SPI功力上限的不是会不会初始化外设、能不能收发几个字节而是对“时序边界条件”的把控能力。CS的建立时间够不够、SCLK最后一个边沿之后等了多久才拉高CS、DMA传输完成中断和BSY标志之间的时间差会不会造成截断——每一个都是数据手册不会教你、只有靠反复实测才能总结出来的东西。我刚入行那会儿也觉得SPI简单配置完寄存器就能用。后来做了几款量产品遇到各种低速时没问题、高速时偶发错误的诡异故障才慢慢意识到SPI的每一次稳定传输背后都是无数细节共同作用的结果。希望这篇从原理到实战、从STM32到FPGA、从配置到排查的梳理能让你少走一些我当年走过的弯路。如果之后你在具体芯片的SPI调试里遇到什么奇葩问题欢迎带着波形图和时序截图来交流咱们一起把SPI这个老朋友玩得更明白。
返回列表