SPI总线深度解析:从四线协议原理到STM32/Linux/FPGA实战与避坑指南
2026/9/16 8:09:22 网站建设 项目流程

干了这么多年嵌入式,手里过过的接口协议少说也有十几种,但要说哪个用得最多、最顺手,我脑子里第一个浮现的永远是SPI。身边老有刚入行的朋友问我:SPI到底难在哪?怎么时序老是对不上?硬件片选和软件片选到底该用哪个?这个词条下面也堆满了五花八门的提问,从ESP8266能不能挂SPI芯片,到要不要用两个DMA,再到T卡要不要上拉电阻,看得出大家对这条“线”的感情很复杂。

别担心,这条总线之所以让人觉得绕,99%的情况不是你不会,而是没把它的底裤看清。今天我就用一篇长文,把SPI总线从头到尾给你捋一遍,从协议原理到实操配置,从STM32到Linux再到FPGA,把我这些年踩过的坑、总结的经验一次性倒给你。

1. SPI到底是什么:从四根线说起

1.1 四个字母背后的主从架构

SPI全称是Serial Peripheral Interface,串行外设接口,最早是Motorola搞出来的。它的核心思想就八个字:一主多从,同步全双工。和UART那种你一句我一句的聊天不同,SPI通信更像指挥家带着乐队,所有乐手听同一根指挥棒的节拍来演奏。

SPI总线物理上就四根线:

  • SCK(Serial Clock):主设备产生的时钟信号,SPI是同步通信,这个时钟谁来打、打多快,完全由主设备说了算。
  • MOSI(Master Out Slave In):主设备输出、从设备输入的数据线。有的芯片标SDI(Serial Data In)或者DIN,看从设备的角度。
  • MISO(Master In Slave Out):从设备输出、主设备输入的数据线。从设备角度叫SDO或者DOUT。
  • CS/SS(Chip Select / Slave Select):片选信号。这根线承担着“点名”的职责,低电平有效,主设备把哪个从设备的CS拉低,哪个从设备就知道轮到自己上班了。

我一直觉得SPI最适合拿公交车来打比方:SCK是发车时间表,MOSI是从总站开出去的线路,MISO是从各个站点开回总站的线路,CS则是报站员手里的喇叭,喊到哪一站哪一站才有人应答。这样一比喻,所谓“带片选的主从同步通信”其实一点都不玄乎。

1.2 四种工作模式:CPOL和CPHA别再死记硬背

这是很多新手的第一道坎,其实理解透了就是两个开关的组合。

  • CPOL(Clock Polarity,时钟极性):决定空闲时SCK是高电平还是低电平。CPOL=0,空闲时SCK为低;CPOL=1,空闲时SCK为高。
  • CPHA(Clock Phase,时钟相位):决定数据在哪个时钟沿被采样。CPHA=0,在第一个边沿采样;CPHA=1,在第二个边沿采样。

两个开关一组合,就是大家常说的Mode 0~3:

模式CPOLCPHA空闲SCK电平数据采样边沿
Mode 000上升沿
Mode 101下降沿
Mode 210下降沿
Mode 311上升沿

注意,从设备的datasheet里一般都会明确写“Data is latched on the rising edge”或者“Shifted out on falling edge”之类的话,跟着手册选就对了。遇到最头疼的情况就是主从都支持多种模式,你只在Mode 0下试通了,换了芯片或换了固件版本就白屏——这时候不要怀疑硬件坏了,先查CPOL和CPHA。

1.3 没有标准速率:想快就快,降速万能

我见过不少从I2C转过来的人问:“SPI最快的速率是多少?”答案是:没有标准答案。和I2C板上钉钉地标称最高400k或1M不同,SPI的速率完全取决于三点:主设备时钟频率能分频到多少、从设备datasheet里标的最大SCK频率、以及你的PCB走线和电平匹配能扛住多少。换句话说,器件说它支持50M,但你的杜邦线又长又乱,那实际上10M就可能出错了。

所以我的经验是:先按datasheet的最大值跑,不行就一档一档往下降,降到稳定为止。千万不要觉得用低速就很丢人,稳定压倒一切。

2. SPI时序与通信细节:读写本质上是换数据

2.1 全双工的真相:一次传输同时“收发”

SPI和I2C、UART最大的不同在于:它是真正的同时收发。主设备往MOSI上移动一个bit的同时,从设备也在MISO上返回一个bit。所以,如果你用一个数组发数据,你几乎总能在同一个周期里从MISO上读回一样多的数据。

这也就解释了为什么很多SPI外设(比如Flash、SD卡、传感器)的读操作都是这样的:先发几个byte的命令和地址,然后再发若干个任意值(通常发0xFF或0x00),把从设备的数据“顶”出来。你要是理解不了这一步,后面调任何SPI外设都会被“为什么我读不到数据”“为什么读回来全是0xFF”这种问题折磨。

实操时有个细节:很多MCU的HAL库接口里发送和接收都是同一个数组,比如HAL_SPI_TransmitReceive,传入的pTxData和pRxData可以是同一个指针,这样一边发一边收,数据自动就换位了。这个特性用的熟了,读寄存器、读FIFO都特别顺手。

2.2 数据格式与字节序

SPI协议本身没有规定字节序,所以你得认真看从设备手册。大多数芯片默认是高字节在前(MSB First),比如发送0x9F读取Flash的JEDEC ID,0x9F就是命令。但也有一部分器件,尤其是某些音频芯片、LCD驱动IC,会支持或要求低字节在前(LSB First)。这玩意儿一旦搞反,表现出的故障又隐蔽又讨厌:偶尔一次对,大部分时间错乱,而且代码逻辑看起来完全没问题。

在寄存器层面,无论STM32的SPI_CR1里的LSBFIRST位,还是Linux spi驱动里的spi->mode标志位,都只需要设一个bit就能切换。建议你在驱动里把这个位写成一个可通过宏切换的选项,调试时能省一大半时间。

2.3 片选信号的时序:硬件片选与软件片选的恩怨

片选是整个SPI系统里最容易出幺蛾子的地方,值得单独拎出来说。

硬件片选(Hardware CS)

MCU自带一个NSS引脚(比如STM32的PA4对应SPI1_NSS),当SPI外设使能时,由外设硬件自动拉低、拉高。优点是响应快,不太占用CPU;缺点也很明显:

  • 如果你启用了SSOE(Slave Select Output Enable),主设备每次传输结束,片选就会自动释放。有些从设备要求CS保持整个事务周期为低,比如“发命令+发地址+读数据”必须一气呵成,如果片选在命令发完就拉高了,从设备立刻复位,啥都读不到。
  • 硬件片选引脚通常是复用固定的,一旦PCB布局冲突,板子改版的周期会拖到让人崩溃。
软件片选(软件拉CS/GPIO模拟CS)

就是用一个普通GPIO来当CS,传输前自己拉低,传输完再自己拉高。灵活性极高,随便找个引脚就能用,而且想什么时候拉就什么时候拉。绝大多数嵌入式工程师在实际项目里用的都是软件片选,尤其是在操作Flash、SD卡、OLED这类需要“连续帧事务”的器件时,软件片选几乎是标配。

我个人的习惯是:哪怕是硬件NSS,我也喜欢关掉NSS硬件输出功能,改用GPIO软件控制。这样能规避所有和片选时序相关的坑,代价只是每次传输前后各多一条GPIO写操作,几十个纳秒而已,相比稳定性提升,这点开销完全可以接受。

Linux下的软件拉片选

在Linux环境里,软件片选的思路也适用。设备树里可以用cs-gpios来指定一个GPIO作为片选,这样内核在发起SPI传输时,会自动操作这个GPIO,不用挂到SPI控制器原生的CS引脚上。很多国产平台、香橙派这类开发板上跑SPI外设,用的都是这个方案。比如:

spi0: spi@... { cs-gpios = <&gpio0 20 0>; ... };

如果发现默认硬件CS行为不对(比如片选释放过早、电平极性不对),直接在设备树里切到cs-gpios,问题往往立刻就能解决。

3. 别傻傻分不清:SPI与UART、I2C、CAN的关键区别

3.1 四大总线横向对比

这些词在热搜里同时出现不是偶然,很多人就是被这一堆“xx总线”绕晕的。我做了个表,照着看最直观:

特性SPII2CUARTCAN
线数4根(可裁剪)2根(SDA+SCL)2根(TX+RX)2根(CANH+CANL,差分)
同步/异步同步同步异步异步
多设备支持,靠CS片选支持,靠地址点对点为主支持,靠仲裁
速率通常最高(几十M很常见)通常较低(几百k~几M)中等(常用115200~几十M)中等(常用500k/1M,CAN FD更高)
全双工半双工全双工半双工
错误处理弱,基本靠主设备自己判断弱,有ACK但有限弱,靠帧格式和校验强,自带错误帧、重发机制
抗干扰一般一般一般强,物理层差分
典型场景板内高速外设:Flash、传感器、LCD板内低速外设:EEPROM、RTC板间调试、模块通信车载、工业现场

3.2 为什么我建议板内高速外设优先选SPI

I2C的点名方式是通过地址来寻址,所以只需要两根线,效率高,但在主从有一套复杂的ACK/时钟拉伸机制,提速困难。而SPI牺牲了引脚数量换来了速度和简单性。这也解释了为什么现在对性能有要求的板级外设,基本全走SPI。比如W25Q128这种Flash、各类加速度计、TFT LCD屏驱动,无一例外都是SPI接口。

3.3 “SPI能替代CAN吗”:这个问题本身就是伪命题

不少做机器人、机械臂的朋友问过我,SPI能不能拿来当总线舵机用的通信总线,或者替代CAN。这通常来自一个误解:SPI快,所以想一快遮百丑。但你要明白,CAN的看家本领不是速度,而是可靠的容错机制、多主仲裁、以及在工业/车载恶劣电磁环境下的差分物理层。SPI没有这个级别的抗干扰和错误恢复机制,你把SPI线拉到一米开外,大概率就会出现丢bit、错位。总线舵机、机械臂关节这种分布式场景,老老实实用CAN或RS485,省得后面天天头疼。

4. 实操第一站:用STM32和CubeMX把SPI跑起来

4.1 CubeMX配置里的关键选项

很多新手打开CubeMX就懵,SPI配置页里一堆下拉框该选什么。我以STM32F103 + SPI1,驱动一个25Q系列Flash为例,说说我的标准选择:

  • Mode:Full-Duplex Master(全双工主模式),除非你是做从设备。
  • Hardware NSS Signal:Disable。原因前面说了,我习惯用GPIO软件片选。
  • Clock Prescaler:先选一个保守的,比如64分频或16分频,先让通信跑起来再说。
  • Clock Polarity (CPOL):Low。
  • Clock Phase (CPHA):1 Edge(对应Mode 0)。
  • Data Size:8 Bits。
  • First Bit:MSB First。
  • CRC Computation:Disable,不是所有CRC应用都复用SPI硬件CRC功能,多数时候它只会给您添乱。

这套配置适用于绝大多数SPI外设,跑不通的时候再根据器件手册微调CPOL/CPHA和分频系数。

4.2 HAL库的SPI读写三板斧

HAL库下SPI读写就三个函数,记牢就够用:

HAL_StatusTypeDef HAL_SPI_Transmit(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout); HAL_StatusTypeDef HAL_SPI_Receive(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout); HAL_StatusTypeDef HAL_SPI_TransmitReceive(SPI_HandleTypeDef *hspi, uint8_t *pTxData, uint8_t *pRxData, uint16_t Size, uint32_t Timeout);

读Flash的时候,完整流程是这样的:

uint8_t cmd[4]; uint8_t data[4]; // 读取JEDEC ID cmd[0] = 0x9F; // READ JEDEC ID命令 // 先拉低CS HAL_GPIO_WritePin(FLASH_CS_GPIO_Port, FLASH_CS_Pin, GPIO_PIN_RESET); HAL_SPI_Transmit(&hspi1, cmd, 1, 100); HAL_SPI_Receive(&hspi1, data, 3, 100); // 从设备连续回3个字节:厂商、型号、容量 // 拉高CS HAL_GPIO_WritePin(FLASH_CS_GPIO_Port, FLASH_CS_Pin, GPIO_PIN_SET);

注意看,HAL_SPI_Receive看起来只收不发,但在字节层面上,主设备SCK一直在跑,MOSI上其实在发0xFF或者0x00,等于“用全双工的便利把数据顶出来”。理解了这一点,你就不会再纠结“为毛收数据要发0xFF”。

如果要用DMA,则用对应的带_DMA后缀函数。HAL库里打开DMA传输后,CPU不阻塞,传输完成触发回调HAL_SPI_TxRxCpltCallback,然后别忘了在错误回调里处理超时和复位。

4.3 SPI需要两个DMA吗:一个明确的答案

热搜词里出现的“spi需要两个dma吗”,我直接说结论:传统上SPI要真正做到不占CPU的全双工收与发,确实是两个DMA通道,一个从内存到SPI_TX_DR,一个从SPI_RX_DR到内存。对于STM32F1系列,SPI1的TX和RX分别映射到DMA1或DMA2的不同通道,你得各分配一个。很多HAL工程里TX和RX用的就是两个不同的DMA流。

但注意,不一定非得两个都用。如果只是发命令、发配置,一个TX DMA就够;如果只是接收,一个RX DMA就够;只有收发都想后台化、双缓冲乒乓操作,才会真正起用两个DMA。另外一个常见误区是:开DMA收数据时,如果不发数据,SCK谁来提供?DMA收也得配合一个“发送缓冲”,哪怕里面全塞0xFF,否则主设备不产生SCK,从设备啥也返回不了。

4.4 实战:用CubeMX驱动NRF24L01的小经验

NRF24L01是典型的SPI无线模块,很多人在它上面栽跟头。我总结三条最值得分享的经验:

  • NRF24L01的IRQ引脚是低有效,别把它当普通GPIO中断读到高电平就以为没事。
  • 操作NRF24L01时要严格保证CSN(片选)在整组命令期间保持低电平,读完状态寄存器后立刻拉高。这就是前文说的为什么软件片选更稳。
  • CE引脚和CSN引脚千万别连错,CE负责进入收发模式,CSN才管SPI传输。两个整反了,发数据几乎必然失败。

5. 更广阔的战场:Linux与FPGA上的SPI

5.1 Linux下的SPI驱动框架:设备树、spidev与用户态控制

Linux对SPI的支持非常成熟,内核里从SPI核心到具体控制器驱动层层封装。如果只是想拿SPI先验证板子,最快的方式是用“spidev”这个通用驱动。设备树里把SPI从设备节点配成compatible = "rohm,dh2228fv"(代表使用spidev)或者直接在节点里加spidev,然后在用户态用open/ioctl/read/write来操作。

关于片选这里再给一个关键提示:很多Linux SPI控制器驱动默认的行为与你预期不同,比如片选在每次消息结束后自动释放。如果你要操作的是Flash类器件,需要在一个spi_transfer里把整个“命令+地址+数据”串起来,保证CS不会中途断掉。实在控制不了的时候,就用cs-gpios软控制,这是最稳妥的退路。

5.2 FPGA上的SPI:从零写一个还是用IP核

FPGA处理SPI是个很有意思的话题。你说难吧,SPI时序在FPGA里算简单;说简单吧,高速下时序收敛没那么轻松。

如果只是做一个SPI Slave,逻辑其实就三大块:检测SCK边沿、移入移出寄存器、根据CS状态判断事务起始。有一段Verilog来演示这一小核心逻辑再清晰不过:

module spi_slave ( input wire sclk, input wire cs_n, input wire mosi, output reg miso, input wire [7:0] tx_data, output reg [7:0] rx_data ); reg [3:0] bit_cnt; reg [7:0] shift_reg; always @(posedge sclk or negedge cs_n) begin if (!cs_n) begin bit_cnt <= 4'd0; end else if (bit_cnt < 4'd8) begin shift_reg <= {shift_reg[6:0], mosi}; bit_cnt <= bit_cnt + 1'b1; end end always @(*) begin miso = tx_data[7 - bit_cnt]; end always @(posedge sclk) begin if (bit_cnt == 4'd8) begin rx_data <= shift_reg; end end endmodule

这段代码对应的是Mode 0(空闲低、上升沿采样),实际工程里还要补上跨时钟域处理、fifo缓冲、寄存器组。要是用Xilinx FPGA,直接调AXI Quad SPI这个IP核就行。它把SPI控制逻辑封装成AXI接口,配合Vivado的Block Design,CPU通过AXI总线读写寄存器,再由IP核产生SPI时序。这就是热搜里“axi quad spi”和“amba总线”能关联在一起的原因。理清这条链路,你对“总线”这个词的理解会上升一个层次。

5.3 DSPI和QSPI:SPI的“非主流变体”

  • DSPI(Double SPI)也叫Dual SPI:把MOSI和MISO合并成SIO0和SIO1两根双向数据线,一次传两个bit。常见于NOR Flash的读加速。
  • QSPI(Quad SPI):四根双向数据线,SIO0~SIO3,一个时钟沿传4个bit,速度直接拉满。如今的高性能NOR Flash,比如GD25Q256、W25Q256,都支持QPI模式。
  • 还有OSPI(Octal SPI):八根数据线,那就是另一个世界了。

用QSPI Flash的时候,初始化通常要发一条命令把设备从SPI模式切到QPI模式,之后再发的数据才会用四线格式。这个“模式切换”命令往往比普通SPI读慢,但读大数据块时优势巨大。

5.4 别忽略AMBA、APB、AHB、AXI这些“上层总线”

很多写MCU代码的朋友看到“AMBA总线”“APB总线”这类词会懵:这跟SPI有关系吗?有关系,而且关系很大。当你在SoC里运行Linux或裸机系统时,CPU并不会直接去操作SPI外设的引脚,软件先读写的是挂在APB或AXI总线上的SPI控制器的寄存器。也就是说,SPI是SoC“内部总线”通往外部世界的一扇门,AMBA/AHB/APB/AXI是门内的大厅和走廊。理解了这一层,再看Vivado里的AXI Quad SPI,再看STM32参考手册里的APB2外设时钟树,你就能把整条链路串起来。

6. 排查避坑与独门经验:从示波器到上下拉

6.1 数据对不上,先从波形找原因

遇到SPI通信不对,我调试的第一动作永远是拿逻辑分析仪抓波形。SPI四根线抓起来非常直观,一看SCK有没有时钟,二看CS有没有正确拉低,三看数据线上的bit是否符合预期。主从模式不匹配时,你可以在波形里清楚看到MISO上采到的数据刚好错位了半拍;没有上拉时,MISO空闲状态飘忽不定,直接看波形就能判断。

6.2 那几个容易忽视的“物理层”问题

  • 电平不匹配:3.3V器件和5V MCU直连,MOSI方向通常是安全的(输入高阻),但MISO方向如果没有电平转换芯片,5V MCU的高电平可能会反向灌进3.3V器件。轻则数据错乱,重则烧芯片。
  • 上拉电阻:SPI的MOSI和SCK一般由主设备推挽驱动,不爱加上拉也能工作。但像TF卡(SD卡)这类从设备,DAT/CMD引脚内部可能是开漏或高阻,不加上拉就极不稳定。这也是热搜词里“tf卡spi需要上拉吗”的真实来源。答案:需要,按卡规范通常在10k~100k,推荐47k上下,卡架子上常预留位置。
  • 走线太长和高速:SPI本意是板内通信,杜邦线超过20cm建议速率降到1M以内,否则反射和串扰会让调试变得很痛苦。
  • 地线回流:MISO和MOSI共地不牢固,数据传输错位会以很微妙的方式出现,时好时坏最坑人。

6.3 寄存器配置与软件细节里的坑

  • 读寄存器时记得发一个“虚拟字节”:很多人写SPI读操作时只发命令和地址,忘记发0xFF来产生SCK。解决方法是读操作在发送阶段就直接带上0xFF或0x00占位。
  • 从设备要求“写使能”或“状态检查”:操作Flash、AD7124这类芯片时,写寄存器之前必须先发Write Enable命令,写完再读状态寄存器确认,很多问题就出在没做写保护解除。
  • SPI的DMA回调标志位:DMA传输完成中断里有一堆Flag要清,不清的话第二次传输就卡在超时等待。HAL库里建议在错误回调里临时禁用SPI再重新初始化,比手动清一堆标志位省事得多。

6.4 ESP8266这类小资源设备能不能挂SPI芯片

热搜里“esp8266模块能连接spi接口芯片吗”的答案是可以,但要分情况。ESP8266的硬件SPI(HSPI)资源是有的,Arduino环境下直接SPI.begin()就能用。如果你觉得硬件SPI资源的引脚冲突了或者你只是想让OLED、Flash这类芯片转起来,用GPIO软模拟SPI完全可行。给ESP8266用软件模拟SPI时,注意三个点:一是GPIO的翻转速度有上限,想把模拟SPI刷到20M是不现实的,稳一点的模拟SPI上10M就是极限;二是很多模块同时把某些GPIO用作启动模式选择,比如GPIO15、GPIO0、GPIO2,别把CS或SCK接到这些脚上,上电可能会进不了运行模式;三是把IO口全部在初始化时明确设置成推挽输出或浮空输入,ESP8266上电瞬间IO状态非常混乱,不加处理很容易出现“上电乱发数据”的问题。

6.5 一些独门心得

最后分享几条压箱底的经验:

  • 给每个SPI设备建一个“读写状态机”:别把一大坨收发逻辑堆在main里,写成一个SPI_Device_ReadRegSPI_Device_WriteReg的接口,长期维护会轻松很多。
  • 调试时几十毫秒一个周期把寄存器值打到串口上,比示波器抓波形定位逻辑问题更快。
  • 如果同一总线上挂两个不同厂商的从设备,一定要保证它们的空闲SCK电平一致。否则,总线切换时会出现“上一颗芯片的下拉把时钟电平耗到半高”这种玄学。
  • 在FPGA里用IP核时,确认一下IP的fifo深度是否足够。SPI突发数据一长,fifo满了又没及时读走,就会丢数据。
  • 换芯片型号后第一件事不是改驱动,而是重新读一遍新芯片手册的“SPI时序特性”页。很多芯片看着Pin和寄存器兼容,但CPOL/CPHA的默认方向可能截然不同,甚至连读地址都要加dummy cycle。

SPI这条路,说到底是“时序”和“电平”的艺术。把CPOL/CPHA搞懂,把片选时序搞对,再弄明白数据交换的底层机制,剩下的就是不断在下板实践中积累手感。上面这些细节,每一行都是我实打实跑过、量过、修过才梳理出来的。初次接触SPI的朋友,建议先拿一块常见的Flash或OLED从零调一遍,吃亏了再回来看这篇文章,很多字句你会有更深的体感。至于已经踩过SPI坑的老手,希望这篇帖子能帮你沉淀出一套更利落的调试套路,让大家少熬几个抓波形的夜。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询