SPI 通信协议基础:四线制与模式配置
SPI,全称 Serial Peripheral Interface,串行外设接口。说白了,就是让主控芯片和外围设备(比如咱们的 OLED 驱动芯片)之间快速交换数据的一种方式。我最早接触 SPI 是在做一款温湿度传感器驱动的时候,当时觉得这东西比 I2C 简单多了——没有地址、没有应答,就是纯粹的时钟和数据交换。
但简单归简单,坑也不少。咱们这节课就把 SPI 的四根线、四种模式、以及时序图彻底讲透。你把这些搞明白了,后面驱动 OLED 就是水到渠成的事。
SPI 四线制详解
标准的 SPI 通信需要四根信号线。我习惯把它们分成两组:控制线和数据线。
| 信号线 | 全称 | 方向 | 作用 |
|---|---|---|---|
| SCLK | Serial Clock | 主→从 | 提供同步时钟,决定数据传输速率 |
| MOSI | Master Out Slave In | 主→从 | 主设备发送数据给从设备 |
| MISO | Master In Slave Out | 从→主 | 从设备返回数据给主设备 |
| CS | Chip Select | 主→从 | 片选信号,低电平有效,选中目标设备 |
关键点:SPI 是全双工通信。什么意思?就是主设备在发送数据的同时,也能接收从设备返回的数据。一个时钟脉冲,MOSI 上送出去一位,MISO 上收回来一位,同时进行。
咱们重点说说 CS 片选信号。SPI 总线上可以挂多个从设备,但同一时刻只能有一个被选中。CS 拉低,表示「嘿,轮到你了」;CS 拉高,表示「没你事了,歇着吧」。我曾经在一个项目里犯过低级错误——CS 没有拉高就切换了从设备,结果两个设备同时响应,数据全乱了。嗯,这个坑你们别踩。
对于 OLED 驱动芯片(比如常见的 SSD1306),我们通常只需要用到 SCLK、MOSI、CS 三根线,外加一根 DC(数据/命令选择) 线。MISO 在 OLED 场景下基本不用,因为 OLED 是纯输出设备,不需要回传数据。但 SPI 协议本身是支持 MISO 的,这个要清楚。
SPI 模式:CPOL 和 CPHA
SPI 有四种工作模式,由两个参数决定:CPOL(时钟极性) 和 CPHA(时钟相位)。
- CPOL:决定时钟空闲时的电平。
- CPOL = 0:空闲时 SCLK 为低电平
- CPOL = 1:空闲时 SCLK 为高电平
- CPHA:决定数据在哪个时钟沿被采样。
- CPHA = 0:在第一个时钟沿采样(上升沿或下降沿,取决于 CPOL)
- CPHA = 1:在第二个时钟沿采样
组合起来就是四种模式:
| 模式 | CPOL | CPHA | 空闲时钟电平 | 数据采样沿 |
|---|---|---|---|---|
| 模式 0 | 0 | 0 | 低电平 | 上升沿(第一个沿) |
| 模式 1 | 0 | 1 | 低电平 | 下降沿(第二个沿) |
| 模式 2 | 1 | 0 | 高电平 | 下降沿(第一个沿) |
| 模式 3 | 1 | 1 | 高电平 | 上升沿(第二个沿) |
个人经验:绝大多数 OLED 驱动芯片(包括 SSD1306、SH1106)都使用 SPI 模式 0(CPOL=0, CPHA=0)。我习惯在初始化代码里先把这个配好,省得后面调半天发现是模式不对。
为什么会这样?因为模式 0 的时序最直观:空闲时时钟是低,第一个时钟沿(上升沿)采样数据,第二个时钟沿(下降沿)切换数据。你想想看,这跟大多数人的直觉是一致的——时钟来了,读数据;时钟走了,换数据。
SPI 时序图分析
光说理论不够,咱们直接看时序图。下面这张图展示了 SPI 模式 0 下一个字节(0x55,即二进制 01010101)的传输过程。
从时序图里你能看到几个关键点:
- CS 先拉低,告诉从设备「准备接收」。
- SCLK 开始产生脉冲,每个脉冲传输一位数据。
- MOSI 上的数据在上升沿被采样(模式 0 的特点)。
- 数据是 MSB first,即最高位先传。0x55 的二进制是 01010101,先传最高位的 0,最后传最低位的 1。
避坑指南:我曾经在调试一块 LCD 屏幕时,发现显示全是乱的。查了半天,最后发现是 SPI 模式配错了——芯片手册写的是模式 0,但我代码里配成了模式 3。模式 0 和模式 3 的时钟极性正好相反,数据采样沿也差了一个相位。所以,拿到任何新芯片,第一件事就是看数据手册里的 SPI 时序图,确认 CPOL 和 CPHA。
SPI 通信的代码骨架
下面是一个典型的 SPI 发送单字节的代码片段,基于 STM32 HAL 库。你可以在任何 MCU 上找到类似的实现。
// SPI 发送一个字节(模式 0)
void SPI_WriteByte(uint8_t data) {
for (int i = 0; i < 8; i++) {
// 先拉低时钟(CPOL=0,空闲低电平)
SCLK_LOW();
// 设置 MOSI 电平:从最高位开始
if (data & 0x80) {
MOSI_HIGH();
} else {
MOSI_LOW();
}
// 延时,保证数据建立时间
delay_us(1);
// 拉高时钟,上升沿采样
SCLK_HIGH();
delay_us(1);
// 数据左移,准备下一位
data <<= 1;
}
// 传输结束,时钟恢复空闲低电平
SCLK_LOW();
}
个人习惯:实际项目中我很少用这种「手动翻转 GPIO」的方式,太慢了。一般会用 MCU 的硬件 SPI 外设,配置好寄存器后直接往数据寄存器里写字节就行。但手动模拟 SPI 有个好处——你可以精确控制时序,调试时特别有用。我记得有一次硬件 SPI 的时钟频率配错了,就是靠手动模拟 SPI 才定位到问题的。
SPI 通信流程总结
咱们把整个 SPI 通信流程串起来,看看数据是怎么从主设备走到 OLED 驱动芯片的。
这个流程看起来简单,但实际项目中要注意的细节不少。比如 CS 拉低后要留一点建立时间再开始发时钟;传输结束后也要留一点保持时间再拉高 CS。这些时序参数在芯片数据手册里都有,写驱动的时候一定要查。
核心要点回顾:
- SPI 四线制:SCLK(时钟)、MOSI(主发从收)、MISO(主收从发)、CS(片选)
- OLED 驱动通常只用 SCLK、MOSI、CS,外加 DC 线区分命令/数据
- SPI 模式由 CPOL(时钟极性)和 CPHA(时钟相位)决定,共 4 种
- OLED 常用模式 0(CPOL=0, CPHA=0):空闲低电平,上升沿采样
- 数据以 MSB first 方式传输,每 8 位为一个字节
好了,SPI 通信协议的基础就讲到这里。你把这些概念吃透了,后面写 OLED 驱动的时候就会发现——原来那些初始化命令、像素数据,本质上就是在 SPI 总线上按特定时序发送一串 0 和 1 而已。没什么神秘的。