STM32 SPI 驱动 OLED(SSD1306)屏幕:从原理到实战
👁 4 阅读 · 2026-08-18 · STM32 显示
本文深入讲解如何利用 STM32 的硬件 SPI 外设驱动 SSD1306 控制的 OLED 屏幕。内容涵盖 SPI 通信协议基础、SSD1306 初始化序列、显存管理策略以及完整的代码实现。通过本教程,你将掌握高效驱动 OLED 的方法,并理解底层时序与命令交互,为后续 GUI 开发打下坚实基础。
## 1. 为什么选择硬件 SPI 驱动 OLED?
SSD1306 是一款常用的单色 OLED 驱动芯片,支持 I2C、SPI 和并行接口。相比 I2C,SPI 传输速率更高(可达 10MHz 以上),刷新全屏(128x64)时优势明显。使用 STM32 硬件 SPI 而非软件模拟,可以大幅降低 CPU 占用,并利用 DMA 实现后台刷新。
- **硬件 SPI**:由外设自动产生时钟和数据,CPU 只需配置寄存器。
- **软件模拟**:占用 GPIO 和 CPU 时间,适合无硬件 SPI 的场合,但效率低。
## 2. SSD1306 基础原理
SSD1306 内部有 1KB 显存(128x64 位),分为 8 页(Page0-7),每页 128 字节。数据通过 SPI 发送,命令和数据的区分由 DC(数据/命令)引脚控制。
- **命令模式**:DC=0,发送控制命令(如设置页地址、对比度等)。
- **数据模式**:DC=1,发送显存数据。
SPI 通信时序:片选 CS 拉低,时钟 SCK 上升沿采样,数据 MOSI 按位发送。SSD1306 支持 4-wire SPI(CS、DC、SCK、MOSI),无需 MISO。
## 3. 硬件连接与配置
以 STM32F103C8T6 为例,使用 SPI1:
| OLED 引脚 | STM32 引脚 | 说明 |
|-----------|------------|------|
| CS | PA4 | 片选(软件控制) |
| DC | PA3 | 数据/命令选择 |
| SCK | PA5 | SPI1_SCK |
| MOSI | PA7 | SPI1_MOSI |
| RES | PA2 | 复位(软件控制) |
| VCC/GND | 3.3V/GND | 供电 |
注意:STM32 的 SPI 引脚需配置为复用推挽输出,速度设为 50MHz。
## 4. 初始化步骤
1. **使能时钟**:开启 GPIOA 和 SPI1 时钟。
2. **配置 GPIO**:SCK、MOSI 为复用推挽,CS、DC、RES 为通用推挽输出。
3. **配置 SPI1**:主机模式、8 位数据、极性 CPOL=0、相位 CPHA=0(模式 0),波特率预分频 16(约 4.5MHz)。
4. **复位 OLED**:RES 拉低 10ms,再拉高。
5. **发送初始化命令序列**:关闭显示、设置显示时钟、对比度、电荷泵等。
6. **清屏并开启显示**。
## 5. 完整代码实现
### 5.1 SPI 底层驱动
```c
// spi_oled.h
#ifndef __SPI_OLED_H
#define __SPI_OLED_H
#include "stm32f1xx_hal.h"
#define OLED_CS_LOW() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET)
#define OLED_CS_HIGH() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET)
#define OLED_DC_LOW() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_3, GPIO_PIN_RESET)
#define OLED_DC_HIGH() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_3, GPIO_PIN_SET)
#define OLED_RES_LOW() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_RESET)
#define OLED_RES_HIGH() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_SET)
void OLED_Init(void);
void OLED_Clear(void);
void OLED_ShowString(uint8_t line, uint8_t col, char *str);
void OLED_Update(void);
#endif
```
```c
// spi_oled.c
#include "spi_oled.h"
#include
SPI_HandleTypeDef hspi1;
uint8_t OLED_GRAM[128][8]; // 显存缓冲区
// 发送一个字节(命令或数据)
static void OLED_WriteByte(uint8_t data, uint8_t dc) {
OLED_DC_LOW();
if (dc) OLED_DC_HIGH();
OLED_CS_LOW();
HAL_SPI_Transmit(&hspi1, &data, 1, 100);
OLED_CS_HIGH();
}
// 发送命令
static void OLED_WriteCmd(uint8_t cmd) {
OLED_WriteByte(cmd, 0);
}
// 发送数据
static void OLED_WriteData(uint8_t data) {
OLED_WriteByte(data, 1);
}
// 初始化 SPI1
static void SPI1_Init(void) {
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_SPI1_CLK_ENABLE();
GPIO_InitTypeDef GPIO_InitStruct = {0};
// SCK: PA5, MOSI: PA7
GPIO_InitStruct.Pin = GPIO_PIN_5 | GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// CS: PA4, DC: PA3, RES: PA2
GPIO_InitStruct.Pin = GPIO_PIN_4 | GPIO_PIN_3 | GPIO_PIN_2;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
hspi1.Instance = SPI1;
hspi1.Init.Mode = SPI_MODE_MASTER;
hspi1.Init.Direction = SPI_DIRECTION_2LINES;
hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;
hspi1.Init.CLKPhase = SPI_PHASE_1EDGE;
hspi1.Init.NSS = SPI_NSS_SOFT;
hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_16;
hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
HAL_SPI_Init(&hspi1);
}
// OLED 初始化
void OLED_Init(void) {
SPI1_Init();
OLED_RES_LOW();
HAL_Delay(100);
OLED_RES_HIGH();
OLED_WriteCmd(0xAE); // 关闭显示
OLED_WriteCmd(0xD5); OLED_WriteCmd(0x80); // 设置时钟分频
OLED_WriteCmd(0xA8); OLED_WriteCmd(0x3F); // 设置 multiplex 比率
OLED_WriteCmd(0xD3); OLED_WriteCmd(0x00); // 显示偏移
OLED_WriteCmd(0x40); // 起始行
OLED_WriteCmd(0x8D); OLED_WriteCmd(0x14); // 使能电荷泵
OLED_WriteCmd(0x20); OLED_WriteCmd(0x00); // 水平寻址模式
OLED_WriteCmd(0xA1); // 段重映射
OLED_WriteCmd(0xC8); // COM 扫描方向
OLED_WriteCmd(0xDA); OLED_WriteCmd(0x12); // COM 引脚配置
OLED_WriteCmd(0x81); OLED_WriteCmd(0xCF); // 对比度
OLED_WriteCmd(0xD9); OLED_WriteCmd(0xF1); // 预充电周期
OLED_WriteCmd(0xDB); OLED_WriteCmd(0x40); // VCOMH 电压
OLED_WriteCmd(0xA4); // 全局显示开启
OLED_WriteCmd(0xA6); // 正常显示(非反色)
OLED_WriteCmd(0xAF); // 开启显示
OLED_Clear();
OLED_Update();
}
// 清屏(显存清零)
void OLED_Clear(void) {
memset(OLED_GRAM, 0, sizeof(OLED_GRAM));
}
// 将显存刷新到屏幕
void OLED_Update(void) {
for (uint8_t page = 0; page < 8; page++) {
OLED_WriteCmd(0xB0 + page); // 设置页地址
OLED_WriteCmd(0x00); // 列地址低 4 位
OLED_WriteCmd(0x10); // 列地址高 4 位
for (uint8_t col = 0; col < 128; col++) {
OLED_WriteData(OLED_GRAM[col][page]);
}
}
}
// 显示字符串(简化版,仅支持 ASCII 5x8 字体)
void OLED_ShowString(uint8_t line, uint8_t col, char *str) {
// 此处省略字体表,实际使用时需包含 ASCII 字模
// 示例:直接写入显存
while (*str) {
// 假设字体表为 font8x8[][8]
// for (uint8_t i=0; i<8; i++) OLED_GRAM[col][line] = font8x8[*str-32][i];
str++;
col++;
}
}
```
### 5.2 主函数调用示例
```c
int main(void) {
HAL_Init();
OLED_Init();
OLED_ShowString(0, 0, "Hello STM32!");
OLED_Update();
while (1) {}
}
```
## 6. 注意事项与优化技巧
- **时序要求**:SSD1306 的 SPI 最高频率通常为 10MHz,STM32 的 SPI 时钟需分频保证不超过此值。
- **显存管理**:使用全屏缓冲区(1KB)可简化绘图操作,但会增加内存占用。若内存紧张,可只更新局部区域。
- **DMA 加速**:将 `OLED_Update()` 中的数据传输改为 DMA,可显著降低 CPU 占用,适合动画显示。
- **电平匹配**:OLED 模块多为 3.3V 供电,STM32 的 GPIO 输出 3.3V 兼容,无需电平转换。
- **抗干扰**:SPI 线尽量短,必要时加 10kΩ 上拉电阻到 VCC。
- **字体与图形**:建议将字模存储在 Flash 中,避免占用 RAM。
## 7. 总结
通过硬件 SPI 驱动 SSD1306 OLED,不仅传输速度快,而且代码结构清晰。本文提供了完整的初始化流程和驱动代码,开发者可在此基础上扩展绘图函数、GUI 库(如 u8g2)或结合 RTOS 实现多任务显示。掌握这一技术,将为你的嵌入式项目增添直观的交互界面。