4、SPI显示接口驱动:SPI协议基础、SPI模式配置、驱动SSD1306 OLED屏实战
SPI,全称 Serial Peripheral Interface,说白了就是串行外设接口。我刚开始接触嵌入式时,总觉得它比I2C复杂,后来才发现——其实SPI的逻辑非常直白,就是「主设备发时钟,从设备收数据」。今天咱们就把它彻底讲透,然后直接上手驱动一块SSD1306 OLED屏。
4.1 SPI协议基础:四根线,一个故事
SPI总线只需要四根线:
- SCLK(Serial Clock):时钟线,由主设备产生
- MOSI(Master Out Slave In):主设备输出,从设备输入
- MISO(Master In Slave Out):主设备输入,从设备输出
- CS(Chip Select):片选线,低电平有效
为什么说它简单?因为SPI是全双工的。主设备发一个bit的同时,从设备也能回一个bit。我在项目中遇到过一位同事,非要用SPI模拟UART,折腾了两天——其实选对协议比硬凑重要得多。
核心要点:SPI没有像I2C那样的设备地址,全靠CS片选来区分从设备。你拉低哪个CS,哪个设备就跟你通信。
4.2 SPI的四种模式:时钟极性(CPOL)和时钟相位(CPHA)
这是新手最容易踩坑的地方。SPI有四种模式,由两个参数决定:
| 模式 | CPOL | CPHA | 时钟空闲电平 | 数据采样边沿 |
|---|---|---|---|---|
| 模式0 | 0 | 0 | 低电平 | 上升沿 |
| 模式1 | 0 | 1 | 低电平 | 下降沿 |
| 模式2 | 1 | 0 | 高电平 | 上升沿 |
| 模式3 | 1 | 1 | 高电平 | 下降沿 |
你想想看,如果主设备配的是模式0,从设备配的是模式3,数据根本对不上。我曾经因为没仔细看SSD1306的数据手册,默认用了模式0,结果屏幕死活不亮——折腾了半小时才发现手册里写的是模式3。
我的习惯:拿到任何SPI器件,第一件事就是翻数据手册的「SPI Timing Diagram」章节。看清楚了再配寄存器,能省下大量调试时间。
4.3 SPI模式配置实战:以STM32为例
配置SPI其实就几步:
- 使能SPI和GPIO时钟
- 配置GPIO为复用功能
- 设置SPI参数(模式、波特率、数据位宽等)
- 使能SPI外设
下面是一个典型的初始化代码:
void SPI_Init(void)
{
// 1. 使能时钟
RCC->APB2ENR |= RCC_APB2ENR_SPI1EN;
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
// 2. 配置GPIO: PA5(SCK), PA7(MOSI), PA4(CS)
GPIOA->MODER |= GPIO_MODER_MODER5_1; // 复用功能
GPIOA->MODER |= GPIO_MODER_MODER7_1;
GPIOA->MODER |= GPIO_MODER_MODER4_0; // 推挽输出
GPIOA->AFR[0] |= 0x00500000; // PA5, PA7 复用为 SPI1
// 3. 配置SPI
SPI1->CR1 = SPI_CR1_SSM | SPI_CR1_SSI; // 软件片选
SPI1->CR1 |= SPI_CR1_BR_2 | SPI_CR1_BR_1; // 波特率分频
SPI1->CR1 |= SPI_CR1_MSTR; // 主模式
SPI1->CR1 |= SPI_CR1_CPOL | SPI_CR1_CPHA; // 模式3
// 4. 使能SPI
SPI1->CR1 |= SPI_CR1_SPE;
}
注意:配置完SPI后,记得先拉高CS引脚,避免在初始化过程中误触发从设备。我见过有人把CS初始化为低电平,结果一上电从设备就进入了错误状态。
4.4 驱动SSD1306 OLED屏实战
SSD1306是一款非常经典的OLED驱动芯片,支持SPI和I2C两种接口。咱们这里用SPI模式3来驱动它。
4.4.1 硬件连接
| SSD1306引脚 | MCU引脚 | 说明 |
|---|---|---|
| CS | PA4 | 片选,低电平有效 |
| DC | PA3 | 数据/命令选择:0=命令,1=数据 |
| RES | PA2 | 复位,低电平复位 |
| SCLK | PA5 | SPI时钟 |
| MOSI | PA7 | SPI数据 |
嗯,这里要注意:SSD1306的DC引脚不是SPI标准引脚,它是用来区分你发的是命令还是数据的。这是很多SPI器件都会有的额外控制线。
4.4.2 初始化序列
SSD1306上电后需要发送一串初始化命令。我习惯把这些命令放在一个数组里:
static const uint8_t init_cmds[] = {
0xAE, // 关闭显示
0xD5, 0x80, // 设置时钟分频
0xA8, 0x3F, // 设置多路复用比
0xD3, 0x00, // 设置显示偏移
0x40, // 设置显示起始行
0x8D, 0x14, // 启用电荷泵
0x20, 0x00, // 设置内存地址模式为水平
0xA1, // 段重映射(左右翻转)
0xC8, // COM扫描方向(上下翻转)
0xDA, 0x12, // 设置COM引脚配置
0x81, 0xCF, // 设置对比度
0xD9, 0xF1, // 设置预充电周期
0xDB, 0x40, // 设置VCOMH电压
0xA4, // 全局显示开启
0xA6, // 正常显示(非反色)
0xAF // 开启显示
};
发送命令的函数也很简单:
void SSD1306_WriteCmd(uint8_t cmd)
{
GPIO_ResetBits(GPIOA, GPIO_Pin_3); // DC = 0,表示命令
GPIO_ResetBits(GPIOA, GPIO_Pin_4); // CS = 0,选中设备
while (!(SPI1->SR & SPI_SR_TXE)); // 等待发送缓冲区空
SPI1->DR = cmd;
while (!(SPI1->SR & SPI_SR_RXNE)); // 等待接收完成(清标志)
GPIO_SetBits(GPIOA, GPIO_Pin_4); // CS = 1,释放设备
}
避坑指南:我曾经在发送命令时忘了拉低CS,结果数据根本没发出去。后来我养成了一个习惯:每次SPI传输前后都检查CS的电平状态。
4.4.3 显示一帧数据
SSD1306的分辨率是128×64,共1024个字节。每个字节控制8个像素点(垂直排列)。显示数据时,先设置页地址和列地址,然后连续发送数据:
void SSD1306_Display(void)
{
// 设置页地址范围(0~7)
SSD1306_WriteCmd(0x22);
SSD1306_WriteCmd(0x00); // 起始页
SSD1306_WriteCmd(0x07); // 结束页
// 设置列地址范围(0~127)
SSD1306_WriteCmd(0x21);
SSD1306_WriteCmd(0x00); // 起始列
SSD1306_WriteCmd(0x7F); // 结束列
// 发送帧缓冲区数据
GPIO_ResetBits(GPIOA, GPIO_Pin_3); // DC = 1,表示数据
GPIO_ResetBits(GPIOA, GPIO_Pin_4); // CS = 0
for (uint16_t i = 0; i < 1024; i++)
{
while (!(SPI1->SR & SPI_SR_TXE));
SPI1->DR = frame_buffer[i];
while (!(SPI1->SR & SPI_SR_RXNE));
}
GPIO_SetBits(GPIOA, GPIO_Pin_4); // CS = 1
}
4.5 知识体系总览
下面这张图把本章的核心逻辑串起来了:
4.6 常见问题与调试技巧
- 屏幕不亮:先检查复位时序。SSD1306上电后需要至少100μs的低电平复位脉冲。我习惯在初始化前先拉低RES,延时1ms后再拉高。
- 显示花屏:大概率是SPI模式不匹配。用逻辑分析仪抓一下SCLK的波形,看看空闲电平是低还是高,数据是在上升沿还是下降沿采样的。
- 显示偏移:检查列地址和页地址的设置。SSD1306的列地址范围是0~127,页地址范围是0~7,多写或少写都会导致显示错位。
重要提醒:SPI的时钟频率不是越高越好。SSD1306最高支持10MHz,但实际布线长了、干扰大了,跑5MHz都可能出错。我一般从1MHz开始调试,稳定后再慢慢往上提。
好了,SPI显示接口驱动的内容就到这里。你只要把协议基础、模式配置和SSD1306的初始化流程吃透,后面再遇到其他SPI屏(比如ST7735、ILI9341),思路是完全一样的。