干了这么多年嵌入式,手里过过的接口协议少说也有十几种,但要说哪个用得最多、最顺手,我脑子里第一个浮现的永远是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:
| 模式 | CPOL | CPHA | 空闲SCK电平 | 数据采样边沿 |
|---|---|---|---|---|
| Mode 0 | 0 | 0 | 低 | 上升沿 |
| Mode 1 | 0 | 1 | 低 | 下降沿 |
| Mode 2 | 1 | 0 | 高 | 下降沿 |
| Mode 3 | 1 | 1 | 高 | 上升沿 |
注意,从设备的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总线”绕晕的。我做了个表,照着看最直观:
| 特性 | SPI | I2C | UART | CAN |
|---|---|---|---|---|
| 线数 | 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_ReadReg、SPI_Device_WriteReg的接口,长期维护会轻松很多。 - 调试时几十毫秒一个周期把寄存器值打到串口上,比示波器抓波形定位逻辑问题更快。
- 如果同一总线上挂两个不同厂商的从设备,一定要保证它们的空闲SCK电平一致。否则,总线切换时会出现“上一颗芯片的下拉把时钟电平耗到半高”这种玄学。
- 在FPGA里用IP核时,确认一下IP的fifo深度是否足够。SPI突发数据一长,fifo满了又没及时读走,就会丢数据。
- 换芯片型号后第一件事不是改驱动,而是重新读一遍新芯片手册的“SPI时序特性”页。很多芯片看着Pin和寄存器兼容,但CPOL/CPHA的默认方向可能截然不同,甚至连读地址都要加dummy cycle。
SPI这条路,说到底是“时序”和“电平”的艺术。把CPOL/CPHA搞懂,把片选时序搞对,再弄明白数据交换的底层机制,剩下的就是不断在下板实践中积累手感。上面这些细节,每一行都是我实打实跑过、量过、修过才梳理出来的。初次接触SPI的朋友,建议先拿一块常见的Flash或OLED从零调一遍,吃亏了再回来看这篇文章,很多字句你会有更深的体感。至于已经踩过SPI坑的老手,希望这篇帖子能帮你沉淀出一套更利落的调试套路,让大家少熬几个抓波形的夜。