1. 先把SPI这四根线彻底搞明白
1.1 四线各司其职:SCLK、MOSI、MISO、CS分别干什么
SPI全称Serial Peripheral Interface,串行外设接口,由Motorola在二十世纪八十年代提出。名字听着正式,实际拆开看就是几根线的事儿。它有四根核心信号线:SCLK、MOSI、MISO、CS,这四根线决定了整个协议的骨架。
SCLK是时钟线,由主机产生,用来同步所有数据传输。MOSI全称Master Output Slave Input,主机输出、从机输入,数据方向是主机到从机。MISO全称Master Input Slave Output,从机输出、主机输入,数据方向是从机到主机。CS是片选线,低电平有效,主机把某个从机的CS拉低,就等于告诉这个从机“准备好,我要跟你通信了”。
这个结构很好理解,像一间办公室里只有一个领导在发号施令,SCLK就是领导敲桌子的节奏,MOSI是领导往下属递纸条的通道,MISO是下属往领导那儿递纸条的通道,CS则是领导点名叫哪位下属来汇报。SPI是典型的主从架构,整个总线上只有一个主机,多个从机之间不直接通信,所有数据都必须经过主机中转。
购买传感器、Flash芯片、屏幕模组时,看数据手册第一件事就是确认它支不支持SPI接口。支持的话,基本都能在这四根线上找到对应的引脚。当然,有些从机是半双工器件,比如单线SPI Flash或者部分温湿度传感器,它们的MOSI和MISO会合并成一根双向数据线,这是SPI的变体,后面遇到了再单独说。
1.2 为什么说SPI是全双工:移位寄存器的运转逻辑
很多人第一次接触SPI会被“全双工”这个概念搞晕。UART也是全双工,I2C是半双工,SPI也是全双工,那它们到底有什么区别?SPI的全双工,体现在“发送和接收同时进行、互不干扰”上,而这一切靠的是主从机内部各有一个移位寄存器。
数据传输时,主机和从机各自维护一个8位(或者16位、32位)的移位寄存器。以8位为例,主机把要发送的数据装入自己的移位寄存器,从机也把自己要回传的数据装入它的移位寄存器。时钟每来一个上升沿或下降沿,两个移位寄存器同时向左移动一位,主机最高位通过MOSI线移出去给从机,从机最高位通过MISO线移出去给主机。8个时钟脉冲之后,两个寄存器里的数据正好交换完毕。
所以SPI里有个经典说法:一次传输等于一次交换。主机写出去一个字节的同时,必然收回来一个字节。如果主机只关心发送、不关心从机回什么,那么读回来的那个字节通常会丢弃;如果主机只想读取从机的数据,那也必须先随便发一个字节,通常是0x00或0xFF,用来产生时钟脉冲,把从机的数据“踢”出来。这个特性在驱动Flash时特别明显,读数据的指令发完地址之后,后续每发一个0xFF,Flash就往MISO线上推一个字节,时钟停,数据就停。
我当年第一次调SPI Flash时,死活读不出数据,后来才意识到自己漏了“读操作也必须发送虚拟字节”这个关键点。如果你也卡在类似的读不出来、回的全是0xFF或0x00,先别急着怀疑硬件,想一想主机到底有没有把时钟持续给够。
2. 四种工作模式:时钟极性和相位的坑
2.1 CPOL和CPHA到底是什么
SPI不像UART那样有波特率误差协商,也不像I2C有地址仲裁,它的时序完全由主机定,但有一个参数必须跟从机对上,否则数据全乱。这个参数就是CPOL(时钟极性)和CPHA(时钟相位)。
CPOL决定SCLK在空闲状态时是高电平还是低电平。CPOL为0时,空闲状态SCLK是低电平;CPOL为1时,空闲状态SCLK是高电平。这个很好理解,就是“不干活的时候时钟线趴在哪边”。CPHA决定数据采样时刻,CPHA为0时,数据在时钟的第一个边沿被采样,也就是第一个跳变沿,此时通常是在边沿后锁存;CPHA为1时,数据在第二个边沿被采样。
打个比方,SCLK就像一扇推拉门,每开关一次就传递一个bit。CPOL决定门默认是关着还是开着,CPHA决定你是在门刚打开一条缝时递东西,还是等门完全打开后再递。门的状态和递东西的时点错一个,数据就会错位。
这两者组合起来就是四种模式,Mode 0到Mode 3。绝大多数SPI从机芯片都能支持其中一种或几种,但不同厂家默认推荐模式不一样,好多翻车现场就是主从模式没对上,现象还特别隐蔽。
2.2 四模式对照表与实际选型思路
这里把四种模式列出来,方便你对照芯片数据手册里的时序图查。
| 模式 | CPOL | CPHA | 空闲SCLK | 采样边沿 | 发送边沿 |
|---|---|---|---|---|---|
| Mode 0 | 0 | 0 | 低电平 | 上升沿 | 下降沿 |
| Mode 1 | 0 | 1 | 低电平 | 下降沿 | 上升沿 |
| Mode 2 | 1 | 0 | 高电平 | 下降沿 | 上升沿 |
| Mode 3 | 1 | 1 | 高电平 | 上升沿 | 下降沿 |
实际选型时有一个比较粗略的经验:如果是Flash芯片,比如W25Q系列、MX25L系列,厂家通常推荐Mode 0或Mode 3,多数情况下用Mode 0就能跑通。如果是LCD屏幕驱动芯片,比如ST7789、ST7735、ILI9341,同样Mode 0或Mode 3都能用,但有些屏幕厂商的例程默认Mode 0,有些默认Mode 3,移植的时候要仔细看初始化代码里对SPI控制寄存器的配置。
更可靠的方法永远是看从机数据手册里的时序图。手册会画SCLK空闲电平、数据在哪个边沿稳定、从机在哪个边沿采样。主机配置的原则就一条:保证从机采样数据的那一刻,数据线上已经是稳定状态。如果有示波器,把SCLK和MOSI同时抓出来观察会非常直观,模式对不对,眼图上一目了然。
2.3 时钟极性配错了会怎样:一次真实的翻车记录
我之前调一款国产加速度传感器,芯片手册写的是Mode 3。当时图省事,直接在原有工程模板上把SPI初始化参数Ctrl+C过来,跑了半天读出来的数据总是“拧巴”的,高字节对、低字节错,错得还很规律。
后来拿逻辑分析仪抓波形,才发现SCLK空闲电平被配成了低电平,主机用的是Mode 0。传感器的数据手册里明确写着“data is latched on rising edge of SCLK while SCLK idle high”,主机在上升沿采样,从机也在上升沿送出数据,两边动作都在同一个边沿上,这种“边沿打架”导致每一位数据的建立时间都不够,采回来的数自然是乱的。
排查时走了不少弯路,先是怀疑焊接问题,又怀疑供电不稳,最后看波形才锁定时序模式。从那以后我养成一个习惯:拿到一颗新的SPI芯片,第一件事就是去数据手册里搜“SPI Mode”或者“CPOL CPHA”这几个关键词,把推荐模式记在笔记本上,再开始写驱动。这个习惯帮我避开了后面很多莫名其妙的通信故障。
3. STM32环境下的SPI配置实操
3.1 CubeMX中的关键参数怎么选
如果你用STM32开发,大概率会借助STM32CubeMX生成初始化代码。打开芯片的SPI外设页面,会看到一堆下拉框和编辑框,很多人直接照着别人的配置填,填完也不知道每个参数是干嘛的。这里把最重要的几个参数过一遍。
第一个是Frame Format,也就是帧格式,通常选8位,对应一个字节。如果跟Flash做页编程或者跟某些音频芯片通信,可能需要16位甚至32位帧,这时候要注意发送缓冲区的数据类型也要跟着改。第二个是Data Size,8位还是16位,它和Frame Format是一回事,不同CubeMX版本叫法不一样。第三个是First Bit,多数情况下选MSB First,也就是最高位先发。少数芯片要求LSB First,比如某些RF模块和音频编解码器,这个也要看数据手册。
接下来是Prescaler,也就是预分频器,它决定SCLK的实际频率。STM32的APB外设时钟通常是几十到上百MHz,Prescaler把它分频到目标频率。SPI时钟频率能跑多高,不是主机单方面决定的,而是要看从机支持的最大SCLK频率。W25Q256手册里写着支持最高104MHz的读时钟,但实际PCB走线、信号完整性不到位时,跑这么高很容易出错。作为一个偏保守的调法,我先用4分频或者8分频把通信跑通,再逐步提高分频系数。这个思路我后面还会再提,很多SPI通信不稳定,罪魁祸首就是分频配得太激进。
CS引脚在CubeMX里也有讲究。如果使用硬件NSS,需要把NSS引脚配置成GPIO输出并拉高,或者启用硬件NSS输出功能。如果使用软件片选,那么NSS引脚只需要配置成普通GPIO输出,CubeMX里的SPI配置里有个NSS Signal Type,一般选Disable或者Software,然后把某个普通GPIO手动控制拉低拉高。我自己的习惯是绝大多数场景都用软件片选,原因后面单独说。
3.2 HAL库收发代码要点
CubeMX生成的HAL库代码里,收发函数主要有这么几个:HAL_SPI_Transmit负责只发送,HAL_SPI_Receive负责只接收,HAL_SPI_TransmitReceive负责同时收发。还有一个比较常用的是HAL_SPI_TransmitReceive_IT,这个是中断版本,配合回调函数使用。
只写不读的场景很常见,比如驱动ST7789屏幕时,初始化命令和像素数据都只往MISO方向走,从机不需要回数据,但SPI在硬件层面上依然会在MISO上收到东西。这些冗余收进来的字节,驱动里一般直接忽略。只读不写的场景前面提过,必须先发送虚拟字节。HAL_SPI_Receive函数内部也会处理这个问题,它会自动发送0xFF来产生时钟,所以不用自己额外再发虚拟字节。
下面给一段使用硬件SPI发送数据的参考代码,以STM32 + W25Q256为例,读取芯片ID的完整流程:
uint8_t spi_tx_buf[4]; uint8_t spi_rx_buf[4]; // 拉低片选,选中Flash HAL_GPIO_WritePin(FLASH_CS_GPIO_Port, FLASH_CS_Pin, GPIO_PIN_RESET); // 发送读ID命令 0x9F,后面跟3个虚拟字节,读回3字节ID spi_tx_buf[0] = 0x9F; spi_tx_buf[1] = 0x00; spi_tx_buf[2] = 0x00; spi_tx_buf[3] = 0x00; HAL_SPI_TransmitReceive(&hspi1, spi_tx_buf, spi_rx_buf, 4, 100); // 拉高片选,释放Flash HAL_GPIO_WritePin(FLASH_CS_GPIO_Port, FLASH_CS_Pin, GPIO_PIN_SET); // spi_rx_buf[1] 是厂商ID,spi_rx_buf[2] 是设备ID这段代码有个容易忽略的细节:片选拉低之后,发送和接收是同时进行的,数组长度必须一致,HAL库按同一长度管理收发,Tx和Rx的指针不能为空,否则会直接报错。很多人写代码时只调HAL_SPI_Transmit,发现没问题,换成TransmitReceive就开始HardFault,多是因为没处理好接收缓冲区。
DMA方式又是另一个话题。大批量读写Flash或者刷新LCD时,DMA能大幅减少CPU占用。用DMA时特别要注意缓冲区生命周期,HAL_SPI_TransmitReceive_DMA是异步的,函数返回时传输还没结束,如果此时局部缓冲区已经被释放,DMA就会踩到野指针上。正确做法是定义全局数组或者使用静态数组,并在传输完成回调里设置标志位。踩过几次坑之后,我现在只要看到SPI和DMA同时出现,第一反应就是检查缓冲区生命周期。
3.3 硬件片选与软件片选,我为什么更推荐后者
SPI的片选有硬件NSS和软件片选两种玩法。硬件NSS模式下,主机在每次传输前自动拉低NSS,传输结束后自动拉高,看起来省心,但实际用起来限制很多。
最直接的问题是多个从机挂在一条SPI总线上时,硬件NSS只管理一个引脚,无法自动切换多个从机。如果从机数量多,最常见的做法还是把NSS引脚配置成普通GPIO,自己写代码控制片选,芯片的NSS功能只用它的输入模式检测总线占用状态,其他时间IO口自己控制。
另外一个容易踩坑的点是,有些STM32型号在SPI从机模式下,NSS引脚的硬件管理会导致片选信号被内部逻辑占用,如果同时又想用这个引脚做别的功能,就会冲突。
软件片选的做法很简单:片选引脚就是普通GPIO,通信前拉低,通信结束后拉高。代码上多两行HAL_GPIO_WritePin,换来的是极大的灵活性。多从机切换、时序微调、调试时随时手动拉高拉低检查,全都方便。所以我现在的项目里,除非从机数量极少且引脚特别紧张,否则一律软件片选。
4. 一主多从与总线复用:屏幕、SD卡、Flash共存方案
4.1 共享SPI总线的基本盘:片选和MISO
实际项目里,一块主控同时接屏幕、SD卡、Flash的情况特别多。比如ESP32接一块ST7789屏幕,再挂一张TF卡,很多人会问屏幕和SD卡能不能共享同一条SPI总线。答案是可以,而且是很常见的做法,但有几个前提必须满足。
第一,所有从机的SCLK、MOSI可以共用,但CS必须各自独立接到主控的不同GPIO上。这是SPI一主多从最标准的接法。第二,MISO线能不能共用,取决于从机的输出驱动方式。像SD卡、SPI Flash这类器件,MISO输出是三态门控制,没有被片选选中时,MISO引脚处于高阻态,不会影响其他从机的通信。但有些简单器件或老式芯片的MISO不是三态,或者有些屏幕模块的MISO引脚压根没引出,甚至个别模组会把它接到别的地方去。这种情况下,直接把两条MISO并到一起就可能出问题,稳妥起见每个从机MISO线上串一个100欧姆到1千欧姆的电阻再并联。
屏幕和SD卡共享总线时还有一个细节,SD卡的SPI模式初始化比较挑剔,它要求主机速率不能太高,通常建议不超过400kHz,初始化成功之后再提高速率。但屏幕初始化时又需要高速推数据。如果一条总线上同时挂两个设备,一个要求慢速初始化,另一个需要高速刷屏,那就得用软件动态修改SPI分频系数,跑屏幕时高频,切到SD卡时降频。用标准库的话直接改SPI的CR1寄存器再重新使能,用HAL库可以直接调HAL_SPI_Init重新初始化。
4.2 屏幕和SD卡共享一条SPI的实测心得
我自己在ESP32上一个项目里同时挂了ST7789屏幕和TF卡模块,硬件设计上SCLK和MOSI直接并联,MISO线也并联,CS各自独立。实际跑下来发现,问题往往不在“能不能共用总线”,而在“切换的时候会不会互相干扰”。
最典型的现象是:单独刷屏正常,单独读写TF卡也正常,但刷完屏立刻读写TF卡,偶尔会读到错误数据。排查到最后,原因出在屏幕驱动里。有些LCD驱动在写完一帧数据之后,没有及时把CS拉高,或者拉高了之后SCLK还在继续跑,TF卡这边片选还没低呢,但SCLK线上的毛刺已经让SD卡内部状态机错乱了。
解决办法有两个方向。一个是在切换设备之前,确保上一个设备的CS已经稳定拉高,并且让SCLK空闲几个周期,最好再留一点延时,比如几个微秒。另一个是给CS信号加上拉电阻,因为CS线悬空时容易受干扰,上拉电阻能保证空闲时CS被稳定拉高,不会因为噪声被误选中。我后来在CS线上加了10k欧姆上拉,切换时的偶发错误明显少了很多。
还有一点要特别提醒,ESP32的SPI外设较特殊,它的GPSPI支持DMA和多设备绑定,但不能像STM32那样简单地把多个设备挂在同一条SPI总线上随便切换。ESP32的SPI主机驱动里,每个设备有独立的配置结构体,SPI bus上允许挂多个device,只要CS不同就行。但如果屏幕和SD卡对SPI模式、速率要求相差太大,ESP-IDF驱动里可以定义两个device结构体,各自设置自己的模式、时钟频率,在通信时分别取用,这个设计非常方便,避免了手动切分频的麻烦。
4.3 速率上不去的瓶颈在哪
SPI能跑多快,理论上由主从双方共同决定,但实际往往卡在信号完整性上。很多人上来就想把SCLK配到几十MHz,结果发现数据偶尔错、屏幕有雪花、Flash校验不过。
SCLK跑高之后,影响最大的几个因素:PCB走线长度、线间耦合、上下拉电阻阻值、从机端输入电容。杜邦线连接面包板时,几十MHz的SCLK基本是灾难,因为线间电容和电感让方波变成了圆波。老老实实降频,或者换用短而粗的连接线。PCB板上跑SPI,SCLK线要尽量短,尽量避免过孔,MOSI和MISO不要平行走太长距离,中间铺地隔开效果更好。
还有一个经常被忽视的点:从机的输入电容会跟主控输出阻抗组成低通滤波器,SCLK频率一高,上升沿变缓,采样点数据就不稳定。处理办法一是降频,二是在从机端的SCLK对地加一个小电容,比如10皮法到22皮法,可以滤掉一部分高频毛刺,但同时也会进一步减缓边沿,所以值不能加太大。
另外,如果从机是模块而非裸芯片,模块上往往已经带有电平转换芯片或限流电阻,这些器件本身也会增加信号延迟,模块的SPI最高速率往往比芯片手册低很多。这一点在淘宝常见的SPI屏幕模组和TF卡模块上尤其明显。
5. 硬件SPI不够用时的替代方案:IO口模拟SPI
5.1 什么时候需要用IO模拟
硬件SPI外设数量是有限的,有时候工程里同时接了Flash、屏幕、SD卡、传感器,占用了两三条SPI总线还不够,这时候IO口模拟SPI就成了常用兜底方案。有些MCU本身就低端,比如CH32V003、STM8系列或者某些8位单片机,硬件SPI根本没有或者只有一个,不够用就得用软件模拟。
IO口模拟SPI的另一个常见原因是管脚复用冲突。有些引脚既要用作PWM输出,又要接传感器中断,还要做SPI时钟,硬件SPI外设的引脚不能随便映射,冲突时就只能拿两个普通GPIO来模拟。
模拟SPI的适用场景包括低速传感器读取、LCD屏幕初始化、Flash的小批量读写、低速EEPROM读写等等。它的缺点是占用CPU时间、速度有限,通常工作在几百kHz到几MHz之间,不适合大批量数据传输。对很多应用来说这个速度足够用,而且代码轻量、便于移植,甚至在某些情况下比硬件SPI还好调试。
5.2 模拟SPI的代码模型与注意事项
模拟SPI的思路很简单:SCLK线用GPIO翻转,MOSI线按bit输出,MISO线按bit读取。以Mode 0为例,空闲时SCLK为低,数据在上升沿被从机采样,所以主机先把要发送的bit放到MOSI上,然后拉高SCLK,再拉低SCLK,这样完成一个bit的发送。读取时,主机拉高SCLK后延时一小段时间,再读取MISO引脚电平,然后拉低SCLK,这样完成一个bit的接收。
下面这段代码模拟的是Mode 0下的SPI主模式收发一个字节:
uint8_t soft_spi_read_write(uint8_t tx_data) { uint8_t rx_data = 0; for (uint8_t i = 0; i < 8; i++) { // 发送:先放数据,再拉高时钟 if (tx_data & 0x80) MOSI_GPIO_HIGH(); else MOSI_GPIO_LOW(); // 上升沿,从机在此采样 SCLK_GPIO_HIGH(); soft_spi_delay(); // 接收:从机在下降沿更新数据,所以我们读数据要等时钟稳定后 // 实际调试中发现,读操作放在拉高后比较稳妥,这里看器件要求 rx_data <<= 1; if (MISO_GPIO_READ()) rx_data |= 0x01; // 下降沿 SCLK_GPIO_LOW(); soft_spi_delay(); tx_data <<= 1; } return rx_data; }注意这段代码里读MISO的位置,我是放在SCLK拉高之后的,但不同器件要求不一样。有的器件在上升沿更新数据,有的在下降沿更新数据,有的要求在SCLK高电平中间读取。最可靠的方式是查看数据手册里发送端的时序图,看看从机到底在哪个边沿更新MISO,然后主机的采样点避开这个边沿就行。
模拟SPI的时序延时选择也很关键。最无脑的做法是在每次翻转后加一个delay,但delay太长了速度上不去,太短了信号不稳定。如果MCU跑在几十MHz,可以用几个空的for循环做几个机器周期的延时,能跑到1MHz左右。如果要更精确,可以用定时器或者硬件延时函数,精度更高。
模拟SPI跟硬件SPI混用时,还要注意总线空闲状态。模拟SPI的GPIO在空闲时如果配置成推挽输出且拉高,SCLK会被强行拉高,这会影响总线上其他硬件SPI设备。正确做法是空闲时把模拟SCLK配置成输入模式,或者把整根线拉高到空闲电平。更稳妥的方案是干脆把模拟SPI占用单独的引脚,不走共享总线,省得互相干扰。
6. 调试SPI的常用方法和问题排查
6.1 用示波器抓时序的几个观察点
SPI调试最重要的工具就是示波器或逻辑分析仪。没示波器时靠printf打印寄存器的值,也能排查出一部分问题,但遇到时序级别的故障,波形就是最直接的证据。
抓波形时,先看SCLK有没有输出,频率是否跟预期一致。很多SPI通信不工作的原因就是SCLK压根没波形,这时候问题在主机的时钟配置或GPIO初始化,跟从机无关。再看CS信号,通信期间CS有没有被正确拉低,有没有毛刺,有没有在传输中途意外拉高。CS如果中途抖动,从机就可能把一帧数据截成两段,收到的内容自然是乱的。
接着看MOSI上的数据是否稳定。理想状态下,MOSI数据应该在SCLK边沿前后保持稳定,建立时间和保持时间都要满足从机要求。如果MOSI在SCLK边沿附近变化,说明代码里发送数据的时机不对,或者是SPI模式不匹配。
最后看MISO,特别是从机回数据时。如果MISO线在CS拉低后一直是高电平,可能是从机没有响应,或者CS引脚没接对。如果MISO有波形但数据不对,要结合SCLK边沿来确认主机采样点是否正确,这多半是CPOL/CPHA配置问题。有逻辑分析仪的话,直接在软件里解出数据,跟寄存器里读到的值对比,排查会快很多。
6.2 常见故障速查与解决办法
我把这几年调SPI踩过的坑整理成一个速查表,遇到问题可以直接对着查。
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| SCLK无波形 | SPI外设未使能、GPIO复用错误、CubeMX配置没生成 | 检查初始化代码、分频系数、GPIO复用处 |
| CS无法拉低 | CS引脚被配置成复用功能、被其他外设占用 | 确保CS引脚配置为GPIO输出 |
| 读回全0xFF | 从机没有数据、MISO接线错误、CS没选通从机 | 检查接线、用示波器抓CS和MISO |
| 读回全0x00 | 从机被拉低占用、MISO被拉低、时钟频率过高 | 检查MISO线上拉、降低时钟频率 |
| 数据高低字节对调 | MSB/LSB设置反了 | 修改First Bit配置 |
| 数据错的很有规律 | CPOL/CPHA模式不匹配 | 查从机手册确认模式并修改 |
| 偶发数据错误 | 时钟频率过高、信号完整性差、CS毛刺 | 降低分频系数、优化走线、CS加上拉 |
| DMA传输HardFault | 缓冲区生命周期结束、DMA未关闭就访问数据 | 使用全局缓冲区,传输完成回调中处理数据 |
这里特别说一下“读回全0xFF”和“读回全0x00”的区分。MISO线上有上拉电阻时,如果从机没有被选中或者内部没数据返回,MISO保持高电平,读回的就是0xFF。如果MISO线上没有上拉电阻且内部无驱动,读回可能是0x00。如果从机明明选通了但回的还是0xFF或0x00,就要考虑从机供电、复位引脚、或者初始化时序是否正确。有些芯片上电后需要等待几毫秒稳定再通信,太早发指令就会被忽略。
6.3 进阶:自己写SPI从机需要注意的事
有些场景下需要把FPGA、MCU配置成SPI从机,去配合另一个主机工作。比如我最近在FPGA里写了一个SPI从机模块,配合ARM读取FPGA内部寄存器。自己写从机时,最容易踩的坑是边沿选择和时序约束。
从机模式下,SCLK是外部输入的,主机那边的极性一改,从机的采样点就得跟着改。FPGA做SPI从机时,建议用时钟上升沿采样、下降沿输出或反之,具体取决于主机的模式。如果是纯逻辑写从机,还要考虑外部SCLK的跨时钟域问题,需要用两级触发器同步,或者直接用PLL把SCLK引入,避免亚稳态。
另一个常见问题是CS信号的处理。从机在CS拉低时要同步初始化内部移位计数器,不能在MID-stream时期重置,否则数据就乱了。我见过一个同事写的从机模块,CS一拉低就清空移位寄存器,结果主机发送中途CS有点毛刺,整包数据就废了。正确的做法是,CS在整个传输过程中只作为帧同步信号,在CS下降沿准备接收,在CS上升沿检查字节是否完整。
FPGA做SPI从机的Verilog实现并不复杂,核心状态机就是IDLE、接收、发送、完成这四个状态,但细节决定了可靠性。跟主机联调时,不要把两边的代码都想当然,最好先写个简单的回环测试,让主机发0xA5、0x5A这些特定模式,从机原样返回,验证无误后再接正常业务逻辑。
写从机的还有一个容易被忽略的问题是MISO的释放时机。从机在CS未选中时,MISO必须处于高阻态,不能一直驱动。如果从机把MISO一直拉低或拉高,会直接影响其他挂在同一总线上的设备通信。FPGA里可以用三态缓冲器来实现,MCU从机模式则通常有硬件自动处理,但如果自己用GPIO模拟从机,就要手动控制MISO的输出使能。
我调SPI这些年最大的体会是:不要凭直觉猜问题,一定要先看波形,把SCLK、CS、MOSI、MISO四根线都看清了,再去改代码。很多时候硬件接线的问题被误判成软件Bug,就是因为懒得动手接示波器。SPI本身并不复杂,但“好用”和“用得对”之间,隔着一堆不起眼的小细节。把这些细节处理好,SPI其实是非常省心的通信方式。