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

资讯详情

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

SPI总线深度解析:从四线协议原理到STM32/Linux/FPGA实战与避坑指南

SPI总线深度解析:从四线协议原理到STM32/Linux/FPGA实战与避坑指南 干了这么多年嵌入式手里过过的接口协议少说也有十几种但要说哪个用得最多、最顺手我脑子里第一个浮现的永远是SPI。身边老有刚入行的朋友问我SPI到底难在哪怎么时序老是对不上硬件片选和软件片选到底该用哪个这个词条下面也堆满了五花八门的提问从ESP8266能不能挂SPI芯片到要不要用两个DMA再到T卡要不要上拉电阻看得出大家对这条“线”的感情很复杂。别担心这条总线之所以让人觉得绕99%的情况不是你不会而是没把它的底裤看清。今天我就用一篇长文把SPI总线从头到尾给你捋一遍从协议原理到实操配置从STM32到Linux再到FPGA把我这些年踩过的坑、总结的经验一次性倒给你。1. SPI到底是什么从四根线说起1.1 四个字母背后的主从架构SPI全称是Serial Peripheral Interface串行外设接口最早是Motorola搞出来的。它的核心思想就八个字一主多从同步全双工。和UART那种你一句我一句的聊天不同SPI通信更像指挥家带着乐队所有乐手听同一根指挥棒的节拍来演奏。SPI总线物理上就四根线SCKSerial Clock主设备产生的时钟信号SPI是同步通信这个时钟谁来打、打多快完全由主设备说了算。MOSIMaster Out Slave In主设备输出、从设备输入的数据线。有的芯片标SDISerial Data In或者DIN看从设备的角度。MISOMaster In Slave Out从设备输出、主设备输入的数据线。从设备角度叫SDO或者DOUT。CS/SSChip Select / Slave Select片选信号。这根线承担着“点名”的职责低电平有效主设备把哪个从设备的CS拉低哪个从设备就知道轮到自己上班了。我一直觉得SPI最适合拿公交车来打比方SCK是发车时间表MOSI是从总站开出去的线路MISO是从各个站点开回总站的线路CS则是报站员手里的喇叭喊到哪一站哪一站才有人应答。这样一比喻所谓“带片选的主从同步通信”其实一点都不玄乎。1.2 四种工作模式CPOL和CPHA别再死记硬背这是很多新手的第一道坎其实理解透了就是两个开关的组合。CPOLClock Polarity时钟极性决定空闲时SCK是高电平还是低电平。CPOL0空闲时SCK为低CPOL1空闲时SCK为高。CPHAClock Phase时钟相位决定数据在哪个时钟沿被采样。CPHA0在第一个边沿采样CPHA1在第二个边沿采样。两个开关一组合就是大家常说的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 ID0x9F就是命令。但也有一部分器件尤其是某些音频芯片、LCD驱动IC会支持或要求低字节在前LSB First。这玩意儿一旦搞反表现出的故障又隐蔽又讨厌偶尔一次对大部分时间错乱而且代码逻辑看起来完全没问题。在寄存器层面无论STM32的SPI_CR1里的LSBFIRST位还是Linux spi驱动里的spi-mode标志位都只需要设一个bit就能切换。建议你在驱动里把这个位写成一个可通过宏切换的选项调试时能省一大半时间。2.3 片选信号的时序硬件片选与软件片选的恩怨片选是整个SPI系统里最容易出幺蛾子的地方值得单独拎出来说。硬件片选Hardware CSMCU自带一个NSS引脚比如STM32的PA4对应SPI1_NSS当SPI外设使能时由外设硬件自动拉低、拉高。优点是响应快不太占用CPU缺点也很明显如果你启用了SSOESlave 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根SDASCL2根TXRX2根CANHCANL差分同步/异步同步同步异步异步多设备支持靠CS片选支持靠地址点对点为主支持靠仲裁速率通常最高几十M很常见通常较低几百k~几M中等常用115200~几十M中等常用500k/1MCAN FD更高全双工是半双工全双工半双工错误处理弱基本靠主设备自己判断弱有ACK但有限弱靠帧格式和校验强自带错误帧、重发机制抗干扰一般一般一般强物理层差分典型场景板内高速外设Flash、传感器、LCD板内低速外设EEPROM、RTC板间调试、模块通信车载、工业现场3.2 为什么我建议板内高速外设优先选SPII2C的点名方式是通过地址来寻址所以只需要两根线效率高但在主从有一套复杂的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为例说说我的标准选择ModeFull-Duplex Master全双工主模式除非你是做从设备。Hardware NSS SignalDisable。原因前面说了我习惯用GPIO软件片选。Clock Prescaler先选一个保守的比如64分频或16分频先让通信跑起来再说。Clock Polarity (CPOL)Low。Clock Phase (CPHA)1 Edge对应Mode 0。Data Size8 Bits。First BitMSB First。CRC ComputationDisable不是所有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上的SPI5.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 4d0; end else if (bit_cnt 4d8) begin shift_reg {shift_reg[6:0], mosi}; bit_cnt bit_cnt 1b1; end end always (*) begin miso tx_data[7 - bit_cnt]; end always (posedge sclk) begin if (bit_cnt 4d8) begin rx_data shift_reg; end end endmodule这段代码对应的是Mode 0空闲低、上升沿采样实际工程里还要补上跨时钟域处理、fifo缓冲、寄存器组。要是用Xilinx FPGA直接调AXI Quad SPI这个IP核就行。它把SPI控制逻辑封装成AXI接口配合Vivado的Block DesignCPU通过AXI总线读写寄存器再由IP核产生SPI时序。这就是热搜里“axi quad spi”和“amba总线”能关联在一起的原因。理清这条链路你对“总线”这个词的理解会上升一个层次。5.3 DSPI和QSPISPI的“非主流变体”DSPIDouble SPI也叫Dual SPI把MOSI和MISO合并成SIO0和SIO1两根双向数据线一次传两个bit。常见于NOR Flash的读加速。QSPIQuad SPI四根双向数据线SIO0~SIO3一个时钟沿传4个bit速度直接拉满。如今的高性能NOR Flash比如GD25Q256、W25Q256都支持QPI模式。还有OSPIOctal 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的硬件SPIHSPI资源是有的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坑的老手希望这篇帖子能帮你沉淀出一套更利落的调试套路让大家少熬几个抓波形的夜。
返回列表