## 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 实现多任务显示。掌握这一技术,将为你的嵌入式项目增添直观的交互界面。