搞嵌入式这么多年,要说哪几个通信接口用得最多,SPI绝对排在前三。不管你是玩STM32、ESP32,还是刚入门单片机准备点亮一块屏幕、挂一个Flash存储芯片,SPI几乎是绕不开的。这块协议本身不复杂,但实际调起来,时钟极性、片选时序、DMA配合,哪个环节没搞对都能卡你半天。这篇就把SPI从原理到实操完整捋一遍,全是实际项目里验证过的经验。
先说清楚这篇文章适合谁:刚接触单片机通信、准备用SPI驱动外设的初学者;已经在用SPI但经常遇到时序和片选问题、想搞明白底层原理的进阶开发者;以及做FPGA、Linux驱动需要快速回顾SPI细节的同行。无论哪种情况,理解SPI背后的设计逻辑,比死记硬背时序图更有用。
1. 先搞清SPI的四根线和主从关系
SPI全称Serial Peripheral Interface,串行外设接口,由Motorola在80年代提出。它在嵌入式领域能活这么多年,核心原因就一个字:简单。没有复杂的地址帧、应答帧,就是纯粹的“谁提供时钟,谁说了算”的主从结构。整个通信只依赖四根线,任何MCU上的SPI外设都是围绕这四根信号线展开的。
1.1 四根线各自到底干什么
- SCLK(Serial Clock):串行时钟,由主机产生并驱动整个通信节拍。它决定了数据传输速率,比如1MHz的SCLK就是每秒传输1M个bit。
- MOSI(Master Out Slave In):主机输出、从机输入。主机发送数据到从机走这根线。
- MISO(Master In Slave Out):从机输出、主机输入。从机回传数据走这根线。
- CS/SS(Chip Select / Slave Select):片选信号,低电平有效。主机拉低某个从机的CS,等于告诉这个从机“准备接收数据”。
其中CS是最容易被忽略但又最关键的信号。SPI总线上可以挂多个从机,它们共享SCLK、MOSI、MISO三根线,唯独CS必须“一线一从机”。主机想和哪个设备通信,就把哪个设备的CS拉低。这样设计的好处是省引脚,坏处是从机一多、CS引脚数量就上去了。
1.2 主从模式和数据流向的底层逻辑
SPI是同步全双工通信,意思是主机给从机发数据的同时,从机也能给主机回数据。你可能会问:这不就是两根独立的数据线吗,有什么稀奇的?关键在于SPI的收发是同一个时钟驱动的:每一个SCLK边沿,MOSI线上发送一位数据,同时MISO线上采样一位数据。主机发出1个字节,同时收到1个字节,两者是绑定发生的。
举个例子,读Flash芯片的JEDEC ID指令,第一步发送0x9F指令字节,这一步主机其实“顺带”收到了从机返回的1个字节;第二步到第四步继续发3个空字节,从机才把真正的3字节ID数据返回来。所以SPI驱动里常见的“读操作先写指令,再写空字节收数据”,本质上就是因为收发同拍、必须靠写空字节制造时钟来实现读。
SPI的结构决定了它没有I2C那种应答机制(ACK/NACK),主机也不关心从机是否真的“听懂”了指令。从机是否就绪、数据是否有效,完全依赖协议层自行处理。这对设计者的要求反而更高:你需要熟读外设手册,搞清楚“要发几条指令”“主时钟要跑多久从机才准备好”。很多新手第一次调SPI读数据读出一堆0xFF,大概率就是没给从机足够的响应时间。
2. 时钟极性和相位,四种模式别傻傻分不清
SPI最让人头大的就是四种工作模式:SPI Mode 0到Mode 3。很多MCU的SPI外设初始化代码里都有CPOL和CPHA两个参数,搞不明白这两个参数,配置就全靠猜。这里把它们的本质讲透。
2.1 CPOL和CPHA的四种组合
CPOL(Clock Polarity,时钟极性)决定空闲状态下SCLK是高电平还是低电平:
- CPOL = 0:空闲时SCLK为低电平。
- CPOL = 1:空闲时SCLK为高电平。
CPHA(Clock Phase,时钟相位)决定数据在哪个边沿被采样:
- CPHA = 0:第一个边沿采样数据(也就是“前沿采样”)。
- CPHA = 1:第二个边沿采样数据(也就是“后沿采样”)。
组合起来就是下表:
| 模式 | CPOL | CPHA | 空闲电平 | 数据采样边沿 |
|---|---|---|---|---|
| Mode 0 | 0 | 0 | 低 | 第一个边沿(上升沿) |
| Mode 1 | 0 | 1 | 低 | 第二个边沿(下降沿) |
| Mode 2 | 1 | 0 | 高 | 第一个边沿(下降沿) |
| Mode 3 | 1 | 1 | 高 | 第二个边沿(上升沿) |
为了记起来方便,业内有个经验口诀:Mode 0和Mode 3用得多,Mode 0对应CPOL=0/CPHA=0,Mode 3对应CPOL=1/CPHA=1。倒不是因为这两个模式“更高级”,而是很多常见外设(比如W25Q系列Flash、ST7789屏幕、MAX31865等)默认就支持这两种。剩下两个模式不是没人用,只是相对来说少。
2.2 实际项目里怎么判断该用哪个模式
判断方法其实很简单:看从机数据手册里的时序图。手册会明确标出SCLK空闲电平、数据建立时间、采样边沿。比如看到图里SCLK默认低电平、数据在上升沿被锁存,那就是Mode 0;看到SCLK默认高电平、数据在上升沿被锁存,那就是Mode 3。
假如手头没有手册、只有一颗未知芯片,也有笨办法:先用逻辑分析仪抓取示波器波形,把SCLK空闲电平、数据变化边沿和SCLK边沿对照,基本一眼就确定模式。没有逻辑分析仪的话,就逐个模式试,哪个模式能读出正常数据就用哪个。我在调试某些国产传感器时就这么“暴力”试过,三个模式读出来都是垃圾数据,唯独Mode 1是正常的。所以不要迷信网上默认Mode 0的说法,一切以实际芯片手册为准。
提醒一点:SPI模式必须主机和从机完全一致。MCU配置成Mode 0、从机却是Mode 2,轻则数据错位,重则通信完全失败。而且这种问题不报错、不中断,只输出一堆看似随机但实际有规律的错误数据,排查起来非常费劲。
3. CubeMX配置SPI,从参数到实操一次说清
STM32用户绝大多数都是先开CubeMX生成初始化代码,再填业务逻辑。CubeMX里的SPI配置页面看着复杂,其实每一项都对应于前面讲的原理。把参数和硬件机制对应起来,配置就是水到渠成的事。
3.1 参数逐项解释
- Prescaler(预分频器):决定SPI时钟频率。例如STM32F103的APB2总线时钟为72MHz,SPI1挂在这个总线上,预分频为16时,SPI时钟就是72/16=4.5MHz。具体分频多少,取决于从机支持的最高频率。
- Data Size(数据大小):选8位或16位。绝大多数SPI外设(Flash、SD卡、屏幕控制器)默认8位。改成16位之前务必确认从机支持。
- First Bit:MSB First还是LSB First。一般选MSB First(最高位先发),因为大多数SPI协议都是从高字节开始定义指令位顺序。
- CPOL和CPHA:结合上文选择正确的模式。
- NSS(片选)模式:这个是软件片选和硬件片选的分岔路,后面单独细说。
- RX Only / Transmit Only / Full-Duplex:收发模式选择。读Flash、读传感器一般用Full-Duplex;只写不读的LCD、LED驱动芯片可以选Transmit Only,省掉MISO引脚。
3.2 代码层面如何收发数据
CubeMX生成的HAL库代码,最常用的收发接口就几个:
// 发送+接收(全双工) HAL_SPI_TransmitReceive(&hspi1, tx_buffer, rx_buffer, size, timeout); // 只发送 HAL_SPI_Transmit(&hspi1, tx_buffer, size, timeout); // 只接收(配合手动控制CS) HAL_SPI_Receive(&hspi1, rx_buffer, size, timeout);这里有个很多新手都会踩的坑:HAL_SPI_Receive并不是“只接收不发送”,它内部照样会发送垃圾数据。因为SPI收发同拍,主机不输出时钟就没有办法让数据“流”过来。所以HAL库内部的HAL_SPI_Receive调用的是同一个全双工底层函数,发送缓冲区填的是0xFF之类的默认值。要读芯片内部数据,最干净的方式是全部用HAL_SPI_TransmitReceive,发送的字节根据协议来:该发指令发指令,该发空字节发空字节。
再看一个具体例子,读W25Q256 Flash的Manufacturer ID:
uint8_t cmd = 0x90; // 读 Manufacturer/Device ID 指令 uint8_t dummy = 0x00; // 空字节 uint8_t rx_data[2] = {0}; HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); // 拉低CS,选中从机 HAL_SPI_Transmit(&hspi1, &cmd, 1, 100); // 发送指令 HAL_SPI_Transmit(&hspi1, &dummy, 1, 100); // 发送地址高字节(多为0x00) HAL_SPI_Transmit(&hspi1, &dummy, 1, 100); // 发送地址低字节(多为0x00) HAL_SPI_Receive(&hspi1, rx_data, 2, 100); // 接收2字节ID HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); // 拉高CS,释放从机这个代码没问题,但请你同时注意几点:每次通信结束必须拉高CS,否则Flash认为总线还占着,后续指令全部忽略;如果从机对时序要求严格,CS拉低和第一个SCLK之间需要一定的建立时间,硬件上可能没问题,但有些严格的从机对CS建立时间有明确要求,需要查询数据手册确认没有低于最小值。
4. 硬件片选和软件片选,怎么选才是最优解
片选信号是SPI最容易出问题的地方,因为它有两种控制方式,各有利弊:硬件片选(NSS Hardware)和软件片选(NSS Software)。这个概念经常在CubeMX里直接出现,对新手来说特别容易纠结。
4.1 两类片选的工作机制
硬件片选由SPI外设硬件控制NSS引脚。配置成硬件输出时,只要调用HAL_SPI_Transmit,外设会自动拉低NSS,通信结束后自动拉高。听上去很省心,但实际里坑不少:在多从机总线中,硬件NSS可能会产生多余的片选脉冲,有些从机受不了这种毛刺;而且硬件控制的是固定引脚,想换GPIO就要改外设配置,灵活性差很多。
软件片选就是GPIO软件控制:在通信开始前手动拉低CS,通信结束后手动拉高CS。这相当于把片选的控制权完全交到你自己手里,灵活度最大。项目里95%以上的情况我推荐用软件片选。
4.2 什么时候用硬件片选更稳
硬件片选适合单从机、从机对CS时序要求特别高的场景。比如某些高速SD卡,在SPI模式下如果CS拉高时机不对或者拉低期间有抖动,可能直接进入错误状态。这时候用硬件NSS自动控制,可以减少软件时序误差。此外在SPI DMA配合下,硬件NSS能避免软件中途干预导致CS时序不准确,因为DMA传输的结束中断可能存在延迟,如果等中断里再软件拉高CS,从机早就因为空闲超时而误判了。
但即使使用硬件片选,也建议在外部IO上增加微秒级的延时确保CS建立时间,或者干脆在硬件设计上留出软件控制的冗余方案。很多工程师最终都趋向于软件片选,因为绝大多数从机标准协议并不那么挑剔CS抖动,只要保证正常的时序范围即可。
我的习惯做法:一旦发现通信偶发异常,第一步不是动SCLK频率,而是检查CS时序。用逻辑分析仪抓下拉低到第一个时钟的间隔,以及最后一个时钟到拉高的间隔,很多时候问题就出在这里。
5. 进阶玩法:DMA、中断和多从机共享
SPI的进阶用法主要集中在性能和架构层面。数据量小、对实时性要求不高的时候,阻塞式收发足够。一旦遇到屏幕刷新、Flash读写大量数据、SD卡日志这类场景,DMA和中断就派上用场了。
5.1 SPI DMA到底快在哪
阻塞收发在收发期间CPU全程参与,比如发1024字节到Flash,CPU就干等着SPI慢慢挪数据。而DMA模式下,CPU只需配置好DMA通道、告诉它“数据从哪来、要去哪、传多少”,然后SPI硬件就接管搬运工作,数据搬完后DMA再触发中断通知CPU。CPU空出来的时间可以去跑算法、刷UI、处理其他外设。
以STM32为例,在CubeMX里配置SPI DMA很简单:勾选SPI的TX和RX两个DMA请求,选择Normal模式即可。但实践中有几个容易踩的坑:
- 在HAL库中,使用
HAL_SPI_Transmit_DMA之后,必须等传输完成后再进行下一步操作,因为在DMA传输过程中再次操作SPI寄存器可能导致总线冲突。 - DMA中断优先级要合理设置。如果中断优先级过低,高频通信时可能丢失完成标志,数据对不上。
- 使用FreeRTOS时,DMA中断回调里不要直接调用
osDelay之类的阻塞函数,否则系统调度会被打乱。
读Flash多字节数据是DMA最常见的场景。比如读1MB数据,阻塞式可能要几百毫秒,DMA模式只需要启动传输,完成后中断通知,时间开销能大幅降低。实际测试中,STM32F4在40MHz SPI时钟下,DMA读W25Q256的读取速度能跑到约4MB/s,而阻塞式一般只有2MB/s左右,差距非常明显。
5.2 ESP32屏幕和SD卡共享SPI总线的处理思路
热搜词里出现了“ESP32屏幕与SD卡共享SPI哪个好”,这是个实战性很强的问题。很多ESP32项目要用TFT屏幕显示图片,同时要把抓拍的数据存到SD卡,两者都接SPI,共享总线几乎是必然选择。
共享SPI总线的要点是:分时复用,不能同时进行两个传输。
| 外设 | SCLK | MOSI | MISO | CS |
|---|---|---|---|---|
| TFT屏幕(ST7789) | 共用 | 共用 | 通常不用/共用 | GPIO 15 |
| SD卡(SPI模式) | 共用 | 共用 | 共用 | GPIO 4 |
ESP32上挂两个SPI从机,软件上唯一要杜绝的是“交叉传输”。在切换外设时,最好先等当前DMA/SPI传输完全停止,再操作另一个外设的CS。实际项目中我见过因为SD卡写入还没完成就拉低屏幕CS,导致SD卡写入失败、文件系统损坏的情况,数据直接报废。
至于“哪个好”,共享SPI肯定是更省引脚、更省线的方案,缺点是吞吐率受限于总线频率,而且两个外设不能并行刷新。如果项目对显示帧率和SD卡写入速度同时要求很高,更稳妥的是给屏幕和SD卡分配独立的SPI总线(ESP32通常有SPI2和SPI3两组),牺牲引脚换性能。凡是追求稳定性的产品级设计,我都建议“能分就分”,只有在引脚极度紧张时才考虑共享。
另外提一嘴,SD卡SPI模式有初始化时序陷阱:上电后需要至少74个时钟周期的延时,且片选应保持高电平;如果没满足这个条件,SD卡会拒绝进入SPI模式。很多共享总线项目里SD卡初始化失败,就是因为屏幕或Flash先干活占用了SCLK,导致SD卡上电信号没吃够足够的时钟脉冲。
6. 我踩过的坑与排查方法
SPI接口本身不难,难的是那些“看起来一切正常但就是不出数据”的玄学问题。我把自己和身边朋友踩过的高频问题整理成一个速查表,这些东西在数据手册里未必写得那么直白,但实践中太有用了。
6.1 典型问题和排查路径
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 读出来的数据全是0xFF | 从机没被正确选中、MISO线没接好、从机供电问题 | 检查CS是否正常拉低;万用表量MISO通断;确认从机电源电压 |
| 读出来的数据和预期不符,且规律性偏移 | SPI模式不匹配 | 逻辑分析仪抓时序,对照手册确认CPOL/CPHA |
| 有时候能通,有时候不通 | 时钟频率太高,从机跟不上 | 降低SPI预分频,比如从8MHz降到1MHz试一下 |
| 数据第一个字节丢失 | 从机在收到CS下降沿后准备时间不足 | 在CS拉低和SCLK启动之间插入小延时(如1~10us) |
| DMA模式数据只更新一半 | DMA传输被中断或配置成Circular模式 | 确认DMA工作在Normal模式,检查传输完成回调是否真正触发 |
| 屏幕花屏但初始化正常 | MISO/MOSI接反了,或者尝试了共享总线冲突 | 仔细核对硬件接线;断开SD卡再刷新屏幕验证 |
最让我印象深刻的是一次调试国产Flash芯片:初始化完全正常,读厂商ID也对,但连续读数据时前几百字节对、后续全是错的。排查到最后发现是SPI时钟频率6MHz时在布线上损耗太大,下降沿不够陡,从机采样出错。换成2MHz一切正常。这个案例说明SPI频率不是越高越好,高频状态下链路阻抗、走线长度、上下拉电阻都会影响信号完整性。PCB设计时SCLK走线尽量短而直,避免过孔和直角,必要时串33Ω电阻减小振铃。
6.2 几个随时能用的小技巧
- 调试SPI,别急着看协议,先准备逻辑分析仪或示波器。花几百块买一个8通道的24MHz逻辑分析仪,足以应对绝大多数SPI调试。抓完时序图,模式对不对、CS有没有拉低、字节边界清不清晰,一目了然。
- 用SPI直接驱动ST7789这类屏幕时,刷新率不是越高越好。SPI时钟频率再高,还要受屏幕控制器的扫描时间、像素时钟限制。很多情况下跑到40~60MHz已经是极限,继续超频只会出现花屏。用DMA加帧缓冲,比单纯超频靠谱得多。
- 自己写SPI驱动,初始化寄存器务必养成“先写配置,再读回校验”的习惯。很多外设都有状态寄存器或ID寄存器,上电后先读一遍,确认通信通路正常再继续初始化,能省下大量调试时间。
- 碰到SPI与I2C的区别问题,记住一句话:SPI靠CS选人、全双工、速度快、无应答;I2C靠地址选人、半双工、速度相对慢、有应答。选型时追求速率和全双工选SPI,追求引脚少、多从机总线简洁选I2C。
7. 关于SPI的FPGA和Verilog实现,再多说一嘴
SPI的热搜词里总有“SPI Verilog”“FPGA SPI”这类关键词。FPGA做SPI从机或主机,本质和单片机没有区别,核心就是按照时序图产生和采样信号。对FPGA工程师来说,最大的难点不是写代码,而是如何保证跨时钟域的稳定性。
单片机上有现成的SPI外设,而FPGA上所有时序逻辑都得手工设计。以SPI从机为例,核心模块无非是:检测SCLK边沿、在正确边沿锁存MOSI数据、在正确时刻更新MISO输出、处理CS信号复位内部逻辑。一个基础框架思路是:
- 状态机:检测CS下降沿进入“接收模式”,CS上升沿回到“空闲模式”。
- 移位寄存器:每次SCLK有效沿,把MOSI输入移入寄存器;如果需要回发数据,把移位寄存器内容在另一沿输出到MISO。
- 位计数:统计接收位数,通常是8位一个字节,凑满一个字节后产生中断或置位标志,通知上层逻辑取数据。
有一点务必注意:FPGA的时钟和外部SCLK往往是异步的,直接用外部SCLK驱动内部逻辑,会产生亚稳态风险。常见的做法是用系统时钟对SCLK打两拍同步,再做边沿检测,或者用内部高速时钟对数据进行oversampling采样。如果SCLK本身比系统时钟还快,那就得老老实实用SCLK域逻辑,配合双口RAM或异步FIFO做跨时钟域数据交换。网上有很多现成的SPI slave Verilog代码,但大多数只适合教学演示,真正用于工业环境,还是得结合CDC(跨时钟域)设计规范重写。
FPGA还有一个热点应用是“基于SPI接口的远程升级”和“SPI Flash存储”,本质上就是FPGA通过SPI访问外部Flash。这里FPGA一般扮演主机,主动读取Flash中的启动镜像。要注意的除了时序外,还有一个启动控制策略:FPGA上电后要先等待电源稳定,再操作Flash;如果Flash芯片支持Quad SPI,还可以通过配置寄存器切换成四线模式提高读取速度。但四线模式的IO方向控制和时序比标准SPI复杂不少,建议先从标准SPI跑通,再考虑优化。
最后分享一个个人习惯:所有涉及SPI的工程,我都会在硬件设计阶段预留测试点,把SCLK、MOSI、MISO、CS四根关键信号引出来。这样做的好处是后续不管用示波器还是逻辑分析仪,都能快速接入、快速定位。尤其是产品量产之后现场反馈通信故障时,测试点能省去大量拆机排查时间。SPI这个东西,只要你把原理吃透、时序摸清、工具配齐,基本就是一次通关的事。