SPI基础:四线制同步通信、主从模式、时钟极性与相位详解

从一次半夜的SPI调试说起

凌晨两点,示波器探头夹在板子上,屏幕上的波形像心电图一样跳。我盯着MISO线上那串本该是0xA5的数据,结果读回来全是0xFF。主控是STM32F4,从设备是一颗AD7793 ADC芯片。代码写了三遍,寄存器配了五遍,SPI时钟频率从4MHz降到125kHz,问题依旧。

后来发现,不是硬件坏了,不是焊接虚了,是CPOL和CPHA配反了。从设备手册里写着“数据在SCLK的上升沿被采样”,我配成了下降沿。就这么一个比特位的差异,折腾了四个小时。

从那以后,我养成了一个习惯:写SPI驱动之前,先把从设备手册里关于时序的那几页打印出来,用红笔圈出采样沿和空闲电平。这不是小题大做,SPI的坑,十有八九都出在时钟极性和相位上。

四线制,每一根线都有脾气

SPI的全称是Serial Peripheral Interface,串行外设接口。名字里带“串行”,但它的物理层是四条独立的信号线,各司其职,不像I2C那样靠一根线既传时钟又传地址。

  • SCLK(Serial Clock):时钟线,由主设备产生。这根线的频率决定了通信速度,也决定了从设备能不能跟上节奏。有些从设备手册会写“最大SCLK频率10MHz”,你非要用12MHz,偶尔能通,偶尔丢数据,查都查不出来。
  • MOSI(Master Out Slave In):主设备输出,从设备输入。数据从主设备流向从设备。注意方向,别接反了。我见过有人把MOSI和MISO焊反,结果主设备发0x55,从设备收到0xAA,还以为是芯片坏了。
  • MISO(Master In Slave Out):主设备输入,从设备输出。数据从从设备流回主设备。如果从设备没被片选选中,MISO通常处于高阻态,这时候主设备读到的是不确定值。
  • CS/SS(Chip Select / Slave Select):片选线,低电平有效。这根线决定了哪个从设备在跟主设备说话。一个主设备可以挂多个从设备,每个从设备独占一根CS线。CS拉低,从设备开始监听SCLK;CS拉高,从设备闭嘴,MISO释放。

四根线,缺一不可。有人问能不能省掉CS线,用软件控制从设备?可以,但前提是你只有一个从设备,而且那个从设备允许CS一直拉低。大多数SPI从设备不这么干,它们靠CS的下降沿复位内部状态机。

主从模式,谁说了算

SPI是典型的主从架构。主设备负责产生时钟,决定什么时候开始通信、什么时候结束。从设备被动响应,没有时钟就什么也干不了。

主设备可以是MCU,可以是FPGA,也可以是带SPI主控功能的SoC。从设备五花八门:ADC、DAC、传感器、存储器、显示屏、无线模块……只要支持SPI接口,都能挂上去。

通信过程很简单:主设备把CS拉低,然后开始产生SCLK脉冲。每个时钟周期,主设备通过MOSI发一个比特,同时从设备通过MISO回一个比特。所以SPI是全双工的,发和收同时进行。你发一个字节,必然同时收到一个字节。如果只发不收,收到的数据可以扔掉;如果只收不发,主设备也得发点东西,通常是0x00或0xFF,用来产生时钟。

这个特性很重要。很多新手写SPI接收函数,只调用接收API,不发送数据,结果读回来的全是0。因为主设备没产生时钟,从设备根本没机会把数据送出来。

时钟极性和相位,SPI的灵魂

时钟极性(CPOL)和时钟相位(CPHA)是SPI最让人头疼的部分,也是面试必考题。但别被名字吓住,理解起来其实很简单。

CPOL决定SCLK空闲时的电平

  • CPOL=0:空闲时SCLK为低电平
  • CPOL=1:空闲时SCLK为高电平

CPHA决定数据在哪个边沿被采样

  • CPHA=0:在SCLK的第一个边沿采样(如果是CPOL=0,第一个边沿是上升沿;CPOL=1,第一个边沿是下降沿)
  • CPHA=1:在SCLK的第二个边沿采样

组合起来就是四种模式,通常称为Mode 0、Mode 1、Mode 2、Mode 3:

  • Mode 0:CPOL=0,CPHA=0。空闲低电平,上升沿采样,下降沿切换数据。这是最常见的模式,很多传感器和存储器默认用这个。
  • Mode 1:CPOL=0,CPHA=1。空闲低电平,下降沿采样,上升沿切换数据。
  • Mode 2:CPOL=1,CPHA=0。空闲高电平,下降沿采样,上升沿切换数据。
  • Mode 3:CPOL=1,CPHA=1。空闲高电平,上升沿采样,下降沿切换数据。

怎么选?看从设备手册。手册里通常会有一张时序图,标出SCLK、MOSI、MISO的波形关系。找到“Data is sampled on the rising edge of SCLK”这句话,就知道CPHA=0且CPOL=0(Mode 0)或者CPHA=1且CPOL=1(Mode 3)。具体哪个,看空闲电平。

这里踩过坑:有些从设备手册的时序图画得不规范,采样沿标得不清楚。这时候别猜,直接看数据手册里“SPI Timing Characteristics”表格,里面会明确写“t_su”和“t_h”对应哪个边沿。或者用示波器抓一下从设备在已知模式下的输出,反推它的时序要求。

实际工程中的SPI配置

以STM32为例,配置SPI的代码大概长这样:

SPI_HandleTypeDef hspi1;

void MX_SPI1_Init(void)
{
    hspi1.Instance = SPI1;
    hspi1.Init.Mode = SPI_MODE_MASTER;          // 主模式
    hspi1.Init.Direction = SPI_DIRECTION_2LINES; // 全双工
    hspi1.Init.DataSize = SPI_DATASIZE_8BIT;     // 8位数据,别用16位除非从设备明确支持
    hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;   // CPOL=0
    hspi1.Init.CLKPhase = SPI_PHASE_1EDGE;       // CPHA=0,第一个边沿采样
    hspi1.Init.NSS = SPI_NSS_SOFT;               // 软件控制CS,别用硬件NSS,坑多
    hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_16; // 分频系数,根据主频算
    hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;      // MSB先行,大部分从设备都是
    hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
    hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
    hspi1.Init.CRCPolynomial = 10;
    HAL_SPI_Init(&hspi1);
}

别这样写:把NSS配成硬件模式,然后指望HAL库自动管理CS。硬件NSS在单从设备场景下勉强能用,但多从设备时根本没法用。老老实实把CS引脚配成GPIO,手动拉低拉高,可控性高得多。

发送接收的代码:

uint8_t tx_data = 0xA5;
uint8_t rx_data = 0;

// 拉低CS
HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET);

// 发送一个字节,同时接收一个字节
HAL_SPI_TransmitReceive(&hspi1, &tx_data, &rx_data, 1, HAL_MAX_DELAY);

// 拉高CS
HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET);

这里踩过坑:HAL_SPI_TransmitReceive的最后一个参数是超时时间。别设成0,否则函数可能立即返回,数据还没发完。也别设成HAL_MAX_DELAY,万一SPI出问题,程序就卡死了。根据数据长度和波特率算一个合理值,比如1ms。

时钟频率的取舍

SPI的时钟频率不是越高越好。频率高了,信号完整性下降,走线长了会反射,从设备也可能跟不上。

经验法则:

  • 板内通信(同一块PCB,走线<10cm),频率可以跑到几十MHz,前提是PCB layout注意阻抗匹配和回流路径。
  • 板间通信(通过排线或线缆连接),频率降到1MHz以下比较安全。排线长了就是天线,高频信号串扰严重。
  • 如果从设备是ADC或传感器,注意它们的转换时间。有些ADC在转换期间不能读数据,否则读到的可能是旧值。

别这样写:上来就把SPI时钟配到芯片支持的最高频率。先按从设备手册推荐的最大频率的一半开始,调通了再往上提。出问题的时候,降频是第一排查手段。

多从设备的CS管理

一个SPI总线上挂多个从设备时,CS的管理就变得关键。

  • 每个从设备独占一根CS引脚,不能共用。
  • 通信前,把目标从设备的CS拉低,其他从设备的CS保持高电平。
  • 通信结束后,把CS拉高。注意,有些从设备要求CS拉高后保持一段时间(比如100ns),才能进行下一次通信。手册里叫“CS deselect time”。
  • 切换从设备时,先拉高当前设备的CS,延时一小段时间,再拉低下一个设备的CS。别连续操作,给从设备一点反应时间。

这里踩过坑:两个从设备共用一根CS,以为靠地址区分。SPI没有地址概念,CS就是地址。共用CS的结果是两个从设备同时响应,MISO线上数据打架,读回来的数据全是乱的。

个人经验性建议

  1. 调试SPI,示波器是必须的。逻辑分析仪也行,但示波器能看到信号质量。抓波形的时候,同时抓SCLK、MOSI、MISO、CS四根线,对照手册时序图,一眼就能看出问题。

  2. 从设备手册的时序图,打印出来贴在工位上。别嫌麻烦,SPI的坑十有八九出在时序上。CPOL/CPHA配错、CS时序不满足、数据建立时间不够,这些都能在时序图上找到答案。

  3. 写驱动的时候,把CS控制放在最外层。不要在SPI发送函数内部拉低拉高CS,那样不方便调试。把CS控制放在调用层,出了问题可以单独测试CS的时序。

  4. SPI通信失败,先降频。把时钟频率降到1MHz以下,如果通了,说明是信号完整性问题。如果还不通,查接线、查配置、查从设备供电。

  5. 不要迷信HAL库。HAL库封装了SPI的底层操作,但出了问题,HAL库不会帮你排查。学会直接操作寄存器,至少要知道SPI_CR1、SPI_CR2、SPI_DR这几个寄存器是干什么的。关键时刻,寄存器级调试比HAL库快十倍。

  6. SPI的MISO线,上拉还是下拉? 看从设备。如果从设备的MISO在CS未选中时是高阻态,建议在MISO上加一个上拉电阻(10kΩ),防止浮空输入导致MCU误判。如果从设备的MISO在未选中时是推挽输出,就别加上拉了,会拉低电平。

  7. 最后一条,也是最实用的一条:如果项目时间紧,SPI调不通,别死磕。换一个支持SPI的从设备型号,或者换一个MCU的SPI外设,有时候硬件bug就是存在,换个方案比修bug快得多。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐