1. SPI 总线基础:协议概述与四线制详解

SPI,全称 Serial Peripheral Interface,是 Motorola 公司在上世纪 80 年代推出的一种同步串行通信接口。说实话,这玩意儿在嵌入式世界里太常见了。我做过的大大小小项目里,十块板子有八块都在用 SPI 接外设——从 SD 卡、Flash 存储器到各种传感器、显示屏,几乎无处不在。

为什么这么流行?因为它简单、快速、全双工。你想想看,一个时钟线、两根数据线、再加一根片选线,就能搞定高速通信。相比 I2C 那种需要地址帧、应答位的协议,SPI 的协议开销几乎为零。

1.1 四线制信号详解

标准的 SPI 接口有四根线,我习惯把它们分成两组:控制线和数据线。

信号名称 全称 方向 作用
SCK Serial Clock 主机 → 从机 由主机产生,决定通信速率
MOSI Master Out Slave In 主机 → 从机 主机发送数据给从机
MISO Master In Slave Out 从机 → 主机 从机发送数据给主机
CS Chip Select 主机 → 从机 片选信号,低电平有效

SCK(串行时钟):由主机产生,决定了 SPI 的通信速率。我见过有人把 SCK 拉到 40MHz 去驱动 Flash,结果信号反射得一塌糊涂。嗯,这里要注意——时钟频率不是越高越好,得看你的 PCB 走线和从机规格。

MOSI 和 MISO:这两根线实现了全双工通信。什么意思?就是主机发数据的同时,从机也能回数据。我记得刚入行时有个误区,以为 SPI 是「发完再收」,其实不是——每个时钟周期,MOSI 和 MISO 同时各传 1 位数据。

CS(片选):低电平有效。主机把 CS 拉低,表示「我要跟这个从机说话」。拉高就断开连接。多从机系统里,每个从机独占一根 CS 线。我曾经在项目里犯过一个低级错误——CS 拉低后忘了拉高,结果总线一直被占用,其他从机完全没法通信。

⚠ 避坑提醒:CS 信号必须干净。如果 CS 上有毛刺,从机会误判片选状态。我建议在 CS 引脚上加一个 10kΩ 上拉电阻,确保默认状态是高电平。

1.2 SPI 通信流程

一次典型的 SPI 传输过程是这样的:

  1. 主机将 CS 拉低,选中目标从机
  2. 主机在 SCK 上产生时钟脉冲
  3. 每个时钟周期,主机通过 MOSI 发送 1 位,从机通过 MISO 返回 1 位
  4. 传输完成后,主机将 CS 拉高

说白了,SPI 就是一个移位寄存器对。主机和从机内部各有一个 8 位(或更多位)的移位寄存器,时钟每跳一次,两边的寄存器就交换一位数据。

💡 核心要点:SPI 没有地址概念,也没有应答机制。主机发 8 位,从机就收 8 位;主机不发,从机也没法主动说话。这种「主从绑定」的设计让 SPI 非常高效,但也意味着主机必须全程参与每一次传输。

1.3 SPI 的四种模式(CPOL / CPHA)

这是 SPI 最让人头疼的地方,也是面试高频题。我当年第一次配置 SPI 时,对着数据手册看了半天,还是搞不清 CPOL 和 CPHA 到底怎么搭配。

简单来说:

  • CPOL(Clock Polarity):决定 SCK 空闲时的电平。CPOL=0 表示空闲低电平,CPOL=1 表示空闲高电平。
  • CPHA(Clock Phase):决定数据在 SCK 的哪个边沿采样。CPHA=0 表示第一个边沿采样,CPHA=1 表示第二个边沿采样。

组合起来就是四种模式:

模式 CPOL CPHA 空闲时钟电平 数据采样边沿
模式 0 0 0 低电平 上升沿(第一个边沿)
模式 1 0 1 低电平 下降沿(第二个边沿)
模式 2 1 0 高电平 下降沿(第一个边沿)
模式 3 1 1 高电平 上升沿(第二个边沿)

你可能会问:到底用哪种模式?答案是——看从机数据手册。大多数 SPI Flash 和 SD 卡支持模式 0 和模式 3。我个人习惯默认用模式 0,因为大部分 MCU 的 SPI 外设复位后默认就是模式 0。

🔧 实用技巧:如果你不确定从机支持哪种模式,先看数据手册里的时序图。图上会标出 SCK 波形和数据采样点。我曾经遇到过一款温湿度传感器,手册上写的是「模式 1」,但我用模式 0 也能通信——后来发现它其实两种模式都兼容,只是时序余量不同。

1.4 模式配置实战:以 STM32 为例

下面是一段 STM32 的 SPI 初始化代码,配置为模式 0:

// SPI1 初始化 —— 模式 0(CPOL=0, CPHA=0)
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 位数据
    hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;   // CPOL = 0
    hspi1.Init.CLKPhase = SPI_PHASE_1EDGE;       // CPHA = 0
    hspi1.Init.NSS = SPI_NSS_SOFT;               // 软件控制 CS
    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);
}

注意看 CLKPolarity 和 CLKPhase 这两个参数。CPOL=0 对应 SPI_POLARITY_LOW,CPHA=0 对应 SPI_PHASE_1EDGE。如果你要改成模式 3,就把 POLARITY_LOW 改成 POLARITY_HIGH,PHASE_1EDGE 改成 PHASE_2EDGE。

⚠ 常见错误:有些 MCU 的 SPI 外设,CPHA 的命名不是 1EDGE/2EDGE,而是 SAMPLE_FIRST/SAMPLE_SECOND。一定要仔细看芯片手册,别想当然。我曾经在 GD32 上踩过这个坑,配置写反了,数据全是乱的。

1.5 知识结构图:SPI 总线核心要素

下面这张图总结了 SPI 总线的核心知识结构,帮你快速建立整体认知:

SPI 总线核心知识结构 SPI 总线 四线制信号 通信模式 配置参数 SCK MOSI MISO CS CPOL CPHA 模式 0 / 1 / 2 / 3 时钟分频 数据位宽 MSB/LSB CS 控制 💡 核心要点 SPI 是全双工、主从同步串行总线,无地址、无应答,协议开销极低

1.6 避坑总结

最后,把我这些年积累的 SPI 经验浓缩成几条:

  • 先看手册,再写代码:从机的时序要求、支持的模式、最大时钟频率,手册里写得清清楚楚。别凭经验猜。
  • CS 控制要谨慎:多从机系统里,CS 切换时一定要留出足够的间隔时间。我见过有人连续发两帧数据,中间 CS 只拉高了 1μs,结果从机根本没反应过来。
  • 时钟频率不是越高越好:高速 SPI 对 PCB 走线要求很高。如果信号质量不好,降频往往比加滤波更有效。
  • 调试时先看波形:逻辑分析仪是 SPI 调试的利器。把 SCK、MOSI、MISO、CS 四根线都抓出来,一眼就能看出问题在哪。

🔧 我的习惯:每次新板子回来,我会先写一个 SPI 回环测试——把 MOSI 和 MISO 短接,发一串数据看能不能收回来。这个测试能快速验证 SPI 外设的配置是否正确,排除硬件连接问题。