SPI 同步总线
SPI 同步总线
一、概述
SPI(Serial Peripheral Interface,串行外设接口)是 Motorola 提出的同步、串行、全双工总线,用于 MCU 与外设(Flash、SD 卡、LCD、传感器、ADC 等)之间的高速通信。
一句话核心:主设备用时钟线 SCK 驱动收发节奏(同步),用 CS 片选线选择从设备,通过 MOSI/MISO 双向同时收发数据——一个移位寄存器在时钟边沿"边发边收"。
二、四线连接
| 信号 | 全称 | 方向 | 作用 |
|---|---|---|---|
| MOSI | Master Out Slave In | 主→从 | 主设备发送数据给从设备 |
| MISO | Master In Slave Out | 从→主 | 从设备发送数据给主设备 |
| SCK | Serial Clock | 主→从 | 时钟信号,驱动同步 |
| CS | Chip Select | 主→从 | 片选,低电平选中从设备 |
连接示意(单主单从):
┌──────────┐ ┌──────────┐
│ │──SCK──► │ │
│ 主机 │──MOSI─► │ 从机 │
│ (Master) │◄─MISO── │ (Slave) │
│ │──CS───► │ │
└──────────┘ └──────────┘关键理解:CS 低电平有效——主设备把某个从机的 CS 拉低,才与它通信;一个主机可接多个从机(每个从机一根独立 CS)。SPI 是主从模式,从机不能主动发起通信,一切由主机驱动时钟。
三、为什么用 SPI
| 优点 | 说明 |
|---|---|
| 高速 | 可达几十 MHz(比 I2C 快得多) |
| 全双工 | MOSI/MISO 同时收发 |
| 简单高效 | 无地址、无 ACK,协议开销小 |
| 灵活 | 4 种模式适配不同外设 |
| 缺点 | 说明 |
|---|---|
| 线多 | 4 根线,多从机时每从机要加一根 CS |
| 无寻址/应答 | 硬件无 ACK,可靠性靠协议层保证 |
| 单主为主 | 多主机需额外仲裁(少见) |
四、SPI 工作原理(移位寄存器)
SPI 的核心是移位寄存器:主机和从机各有一个 8 位移位寄存器,在时钟边沿同时左移一位——主机发出的位从 MOSI 进从机寄存器,从机的位从 MISO 进主机寄存器。一个时钟周期完成"发 1 位 + 收 1 位"。
主机移位寄存器 从机移位寄存器
┌──────────────┐ ┌──────────────┐
│ b7 b6 ... b0 │──MOSI─►│ b7 b6 ... b0 │
│ │◄─MISO──│ │
└──────────────┘ └──────────────┘
每时钟边沿:主从寄存器同时左移,最高位互相交换例子:主机发 0xA5、从机发 0x3C,8 个时钟周期的完整交换
假设 MSB 先发(SPI 常用约定,通常默认 MSB first)。主机发送 0xA5 = 1010_0101、从机发送 0x3C = 0011_1100,每个时钟周期主/从同时把各自的最高位移给对方、再左移:
| 时钟周期 | 主机经 MOSI 发出 | 主机经 MISO 收到 | 说明 |
|---|---|---|---|
| 1 | b7 = 1 | 从机 b7 = 0 | 发 1、收 0 |
| 2 | b6 = 0 | 从机 b6 = 0 | 发 0、收 0 |
| 3 | b5 = 1 | 从机 b5 = 1 | 发 1、收 1 |
| 4 | b4 = 0 | 从机 b4 = 1 | 发 0、收 1 |
| 5 | b3 = 0 | 从机 b3 = 1 | 发 0、收 1 |
| 6 | b2 = 1 | 从机 b2 = 1 | 发 1、收 1 |
| 7 | b1 = 0 | 从机 b1 = 0 | 发 0、收 0 |
| 8 | b0 = 1 | 从机 b0 = 0 | 发 1、收 0 |
8 个时钟周期后:主机发完 1 字节,同时收到从机 1 字节(全双工)——主机发出的 0xA5 经 MOSI 全部移入从机寄存器,从机发出的 0x3C 经 MISO 全部移入主机寄存器,双方寄存器内容互换(主机"读"到从机的 0x3C,从机"读"到主机的 0xA5)。
关键理解:SPI 发送和接收是绑定的——发 1 字节必然收 1 字节(即使从机数据无效,也会移位进来)。读外设时主机发的字节其实只是"时钟产生器":发什么无所谓(常发
0xFF或0x00),目的只是产生 8 个时钟,把从机要回的数据"顶"进主机寄存器(如读 Flash 时发0xFF把 ID 顶出来)。
五、SPI 4 种工作模式(CPOL 与 CPHA)
SPI 有 4 种模式,由 CPOL(时钟极性) 和 CPHA(时钟相位) 决定:
| 模式 | CPOL | CPHA | 空闲 SCK | 数据采样边沿 |
|---|---|---|---|---|
| Mode 0 | 0 | 0 | 低 | 上升沿(第1边沿) |
| Mode 1 | 0 | 1 | 低 | 下降沿(第2边沿) |
| Mode 2 | 1 | 0 | 高 | 下降沿(第1边沿) |
| Mode 3 | 1 | 1 | 高 | 上升沿(第2边沿) |
- CPOL(Clock Polarity):SCK 空闲时的电平。
0=空闲低、1=空闲高。 - CPHA(Clock Phase):数据在哪个边沿采样。
0=第1个边沿(前沿)、1=第2个边沿(后沿)。
数值演算(Mode 0 时序,发送 0xA5,MSB 先发):
Mode 0 = CPOL 0(空闲低)+ CPHA 0(第 1 个边沿,即上升沿采样)。发送 0xA5 = 1010_0101 的 8 个时钟周期:
时钟周期: T1 T2 T3 T4 T5 T6 T7 T8
SCK: ┌┐ ┌┐ ┌┐ ┌┐ ┌┐ ┌┐ ┌┐ ┌┐
─┘└────┘└────┘└────┘└────┘└────┘└────┘└────┘└───
MOSI: 1 0 1 0 0 1 0 1
采样: ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑
位: b7 b6 b5 b4 b3 b2 b1 b0读图:SCK 空闲时是低电平;每个时钟周期 SCK 先升后降,上升沿(
┌,即 ↑ 处)采样。每个上升沿接收方读一次 MOSI:T1 采到1、T2 采到0、……、T8 采到1,得到1010_0101 = 0xA5。空闲低 + 上升沿采 = Mode 0。
四种模式怎么记:
| 模式 | 空闲电平 | 采样边沿 | 一句话 |
|---|---|---|---|
| Mode 0 | 低 | 第 1 边沿(上升) | 空闲低,前沿采 |
| Mode 1 | 低 | 第 2 边沿(下降) | 空闲低,后沿采 |
| Mode 2 | 高 | 第 1 边沿(下降) | 空闲高,前沿采 |
| Mode 3 | 高 | 第 2 边沿(上升) | 空闲高,后沿采 |
如何确定外设用哪种模式:查该外设数据手册的时序图,看两点——①SCK 空闲时是高还是低(决定 CPOL);②数据位在 SCK 的哪个边沿保持稳定、接收方应在此边沿采样(决定 CPHA)。多数 Flash、SD 卡、LCD 用 Mode 0 或 Mode 3。
关键理解:主从设备的 SPI 模式必须一致(CPOL/CPHA 相同),否则数据错位——一方上升沿采、另一方下降沿采,会整体错半拍。通信前先用读 ID 之类的只读命令试通,确认模式后再正常读写。
六、SPI 通信流程
SPI 通信由主机全程主导,整个过程分 4 步(主从分工如下):
| 步骤 | 主机动作 | 从机动作 | 涉及信号 |
|---|---|---|---|
| ① 片选 | CS 拉低(选中从机) | 被选中,准备就绪 | CS(低有效) |
| ② 时钟 | 产生 SCK 时钟脉冲 | 跟随 SCK 边沿 | SCK |
| ③ 数据 | MOSI 发 1 位 + MISO 收 1 位(每时钟 1 位) | MOSI 收位 + MISO 发位 | MOSI/MISO |
| ④ 结束 | CS 拉高 | 释放,本次事务结束 | CS |
时序上强调两点:①CS 低电平覆盖整个事务(命令 + 全部数据期间都要保持低,CS 拉高即结束);②SCK 只在 CS 有效期间产生,且"发 1 位必收 1 位"(全双工,见第四节)。
把四线随时间的变化画在一起(以发 1 字节 = 8 个时钟为例):
┌─────────────── 整个事务 CS 保持低电平 ───────────────┐
CS: ────┘ └────
SCK: ┌┐ ┌┐ ┌┐ ┌┐ ┌┐ ┌┐ ┌┐ ┌┐
MOSI: b7 b6 b5 b4 b3 b2 b1 b0 (主机发,每时钟 1 位)
MISO: x7 x6 x5 x4 x3 x2 x1 x0 (从机发,每时钟 1 位)读图:
CS低电平框住整个事务;SCK在CS有效期间产生 8 个脉冲;每个时钟同时推进MOSI发 1 位、MISO收 1 位(b7~b0是主机发出的位,x7~x0是收到从机的位)。8 个时钟后主机发完 1 字节、收到从机 1 字节。
例:读 SPI Flash 的厂商/设备 ID
/* 发送 0x9F(读ID命令) + 3 个空字节,读回 3 字节 ID */
uint8_t cmd[4] = {0x9F, 0x00, 0x00, 0x00};
SPI_ChipSelect_LOW(); // 1. 拉低 CS 选中
SPI_Send(cmd, 4); // 2. 发命令 + 收 3 字节(全双工)
SPI_ChipSelect_HIGH(); // 3. 拉高 CS 释放
// 读回的 buf[1]~buf[3] 即 JEDEC ID(厂商/型号/容量)为什么这样就能读出 ID:关键在于外设(Flash)内置了"命令 → 应答数据"的状态机:
- 命令触发:Flash 收到
0x9F这个命令后,内部状态机进入"输出 JEDEC ID"模式,把预先存好的 3 字节 ID(厂商/型号/容量)装进它的移位寄存器; - 时钟推动输出:SPI 是"发 1 位必收 1 位",主机每发一个字节(产生 8 个时钟),Flash 就沿 MISO 移出 ID 的一个字节;
- 空字节 = 时钟发生器:所以后 3 个字节发
0x00(内容无所谓),纯粹为了产生8 × 3个时钟,把 Flash 预置的 3 字节 ID 逐位"顶"出来; - CS 全程低:命令和后续时钟必须在一个 CS 低电平周期内完成,Flash 才会持续回数据;CS 拉高即结束本次事务。
一句话:发
0x9F是"点单",后面发 3 个空字节是"付了 3 组时钟钱",Flash 就把 3 字节 ID 从 MISO 逐位移出端上来。所以buf[1]~buf[3]读到的就是那 3 字节 ID。
收发数据演算(读回 JEDEC ID,以 Winbond W25Q 为例):一次"读 ID"要连续收发 4 个字节——第 1 字节发命令,后 3 字节发空数据(只为了产生时钟把 ID 顶出来):
| 字节 | MOSI 发出 | MISO 收回 | 含义 |
|---|---|---|---|
| 1 | 0x9F | (忽略,多为 0xFF) | 发"读 JEDEC ID"命令 |
| 2 | 0x00(空) | 0xEF | 顶出厂商 ID(Winbond) |
| 3 | 0x00(空) | 0x40 | 顶出型号字节 |
| 4 | 0x00(空) | 0x17 | 顶出容量字节 |
读 ID 全程时序(CS / SCK / MOSI / MISO 四线):整个读 ID 期间 CS 保持低电平(选中),4 个字节各占 8 个 SCK 时钟:
CS: ────┐ └────
└────── 读 ID 期间 CS 全程低电平(选中)──────┘
字节: 第1字节 第2字节 第3字节 第4字节
MOSI: 9F(命令) 00(空) 00(空) 00(空)
MISO: (忽略) EF(厂商) 40(型号) 17(容量)
作用: 发读ID命令 顶出厂商ID 顶出型号 顶出容量
└───── 每个字节 = 8 个 SCK 时钟周期 ─────┘逐字节深化演算(以第 2 字节为例:发 0x00,收厂商 ID 0xEF = 1110_1111,MSB 先发)
第 2 字节的 8 个时钟周期里,主机 MOSI 发的 8 位都是 0(空数据),同时从 MISO 逐位收进 0xEF:
| 时钟周期 | MOSI 发出 | MISO 收到 | 收到位 |
|---|---|---|---|
| 1 | 0 | 1 | b7 |
| 2 | 0 | 1 | b6 |
| 3 | 0 | 1 | b5 |
| 4 | 0 | 0 | b4 |
| 5 | 0 | 1 | b3 |
| 6 | 0 | 1 | b2 |
| 7 | 0 | 1 | b1 |
| 8 | 0 | 1 | b0 |
8 周期后,主机寄存器收到 1 1 1 0 1 1 1 1 = 0xEF,即 Winbond 厂商 ID。主机发的 8 个 0 并不携带数据,纯粹是产生 8 个时钟,把从机的 ID"顶"进主机寄存器。
每个读回字节都发生在"主机发一个空字节产生的 8 个时钟"期间——这正是第四节讲的"发字节 = 产生时钟,把从机数据顶出来"。所以读外设时,MOSI 发什么不重要,重要的是产生足够多的时钟。
七、SPI vs I2C 对比
| 维度 | SPI | I2C |
|---|---|---|
| 同步/异步 | 同步 | 同步 |
| 线数 | 4(MOSI/MISO/SCK/CS) | 2(SDA/SCL) |
| 全双工 | 全双工 | 半双工 |
| 寻址 | CS 片选(无地址) | 7/10 位地址 |
| 速率 | 高(几~几十 MHz) | 低(100k/400k/1M) |
| 从机选择 | 每从机一根 CS | 共总线,地址区分 |
| 应用 | Flash/屏/高速外设 | 传感器/EEPROM/低速外设 |
八、常见坑与排查
| 问题 | 可能原因 | 排查 |
|---|---|---|
| 数据全错/乱 | 主从 SPI 模式(CPOL/CPHA)不一致 | 核对时序图,统一模式 |
| 无响应 | CS 未选中、接线错 | 确认 CS 低电平有效,查 MOSI/MISO 是否接反 |
| 读到 0xFF | 外设未就绪/未选中 | 发 0xFF 顶数据前先确认 CS 时序 |
| 高位/低位错 | 数据位序(MSB/LSB first)不一致 | 统一 MSB first 或 LSB first |
| 速度过快丢数据 | 外设最大频率不足 | 降低 SCK 频率 |
| 多字节粘连/CS 没释放 | 多从机时 CS 未在字节间拉高 | 每发完一组就拉高 CS,再选下一从机 |