
最近在做一个嵌入式项目时需要在小尺寸屏幕上显示实时数据0.96寸OLED屏凭借其高对比度、低功耗的特点成为首选。但在STM32上驱动SPI接口的OLED屏时发现网上资料虽然多但完整可用的工程源码较少配置细节也容易遗漏。本文将分享一套完整的STM32驱动0.96寸SPI OLED屏的实战方案从原理讲解到代码实现包含HAL库配置、SPI通信、显示驱动等核心内容适合有一定STM32基础的开发者快速上手。1. OLED显示屏与SPI通信基础1.1 OLED显示技术特点OLEDOrganic Light-Emitting Diode是有机发光二极管的简称与传统的LCD显示屏相比具有自发光、高对比度、响应速度快、视角广等优点。0.96寸OLED屏通常分辨率为128×64采用SSD1306驱动芯片通过SPI或I2C接口与主控芯片通信。在实际项目中选择OLED屏主要考虑以下几点首先是功耗OLED屏在显示深色内容时功耗极低适合电池供电设备其次是响应速度微秒级的响应时间适合显示动态数据最后是接口简单SPI接口只需少量引脚即可实现高速通信。1.2 SPI通信协议详解SPISerial Peripheral Interface是一种高速、全双工、同步的串行通信协议采用主从模式通信。SPI接口通常包含四根信号线SCLKSerial Clock时钟信号由主机产生MOSIMaster Output Slave Input主机输出从机输入MISOMaster Input Slave Output主机输入从机输出CSChip Select片选信号低电平有效对于OLED显示屏由于主要是主机向从机发送数据和命令MISO线通常可以省略这就是所谓的3线SPI模式。SPI通信的时序模式有4种由CPOL时钟极性和CPHA时钟相位组合决定SSD1306驱动芯片通常工作在模式0CPOL0CPHA0或模式3CPOL1CPHA1。2. 硬件准备与连接2.1 所需硬件组件本实验需要以下硬件组件STM32F103C8T6最小系统板蓝色pill板0.96寸SPI接口OLED显示屏6针接口ST-Link V2调试下载器杜邦线若干面包板或连接线STM32F103C8T6是意法半导体推出的基于Cortex-M3内核的32位微控制器具有64KB Flash、20KB RAM丰富的外设资源完全满足OLED驱动需求。0.96寸OLED屏的6针接口定义通常为GND、VCC、D0SCK、D1SDA、RES、DC、CS。有些模块可能将D0和D1标注为SCL和SDA但实际使用的是SPI协议而非I2C。2.2 硬件连接方案将OLED模块与STM32F103C8T6按照下表连接OLED引脚功能说明STM32引脚备注GND电源地GND共地VCC电源正极3.3VOLED工作电压D0/SCKSPI时钟线PA5SPI1_SCKD1/SDASPI数据线PA7SPI1_MOSIRES复位信号PA4自定义GPIODC数据/命令选择PA3高电平数据低电平命令CS片选信号PA2SPI片选RES复位信号用于硬件复位OLED模块DC信号用于区分发送的是数据还是命令CS片选信号在多个SPI设备时用于选择当前通信的设备。如果系统中只有一个SPI设备CS可以接固定低电平但为了代码规范性建议还是使用GPIO控制。3. 软件开发环境搭建3.1 工具链准备开发STM32项目需要以下软件工具Keil MDK-ARM或STM32CubeIDE本文以Keil为例STM32CubeMX图形化配置工具STM32F1xx HAL库ST-Link驱动软件首先安装Keil MDK-ARM确保安装了STM32F1系列设备支持包。STM32CubeMX是ST官方提供的图形化配置工具可以直观配置引脚、时钟、外设等参数并生成初始化代码。HAL库Hardware Abstraction Layer是ST提供的硬件抽象层库函数简化了外设操作。3.2 STM32CubeMX工程配置打开STM32CubeMX新建工程选择STM32F103C8Tx芯片。首先配置时钟系统外部高速时钟选择Crystal/Ceramic Resonator在Clock Configuration中设置系统时钟为72MHz。SPI1配置如下Mode: Full-Duplex MasterHardware NSS Signal: DisabledPrescaler: 256得到281.25kHz时钟CPOL: LowCPHA: 1 EdgeGPIO配置PA2CSGPIO_Output推挽输出高电平PA3DCGPIO_Output推挽输出高电平PA4RESGPIO_Output推挽输出高电平PA5SCKSPI1_SCKPA7MOSISPI1_MOSI在Project Manager中设置工程名称和路径Toolchain选择MDK-ARM生成代码时选择拷贝所有库文件生成独立的.c/.h文件。4. OLED驱动程序设计4.1 SSD1306驱动芯片指令集SSD1306驱动芯片提供了丰富的指令用于控制显示参数常用的指令包括0xAE/0xAF显示关闭/开启0x20设置内存地址模式0x21/0x22设置列地址/页地址0xB0-0xB7设置页起始地址0x40-0x7F设置显示起始行0x81设置对比度0xA8设置多路复用比率0xA4/A5正常显示/全亮显示0xA6/A7正常显示/反色显示理解这些指令的作用是编写驱动程序的基础。例如设置内存地址模式指令0x20后面需要跟一个参数选择模式0x00为水平地址模式0x01为垂直地址模式0x02为页地址模式。4.2 OLED初始化序列OLED模块上电后需要进行初始化配置以下是标准的初始化流程// OLED初始化函数 void OLED_Init(void) { OLED_RST_Set(); HAL_Delay(100); OLED_RST_Clr(); HAL_Delay(100); OLED_RST_Set(); HAL_Delay(100); OLED_Write_Cmd(0xAE); // 关闭显示 OLED_Write_Cmd(0x20); // 设置内存地址模式 OLED_Write_Cmd(0x10); // 页地址模式 OLED_Write_Cmd(0xB0); // 设置页起始地址 OLED_Write_Cmd(0xC8); // 设置COM扫描方向 OLED_Write_Cmd(0x00); // 设置低列地址 OLED_Write_Cmd(0x10); // 设置高列地址 OLED_Write_Cmd(0x40); // 设置显示起始行 OLED_Write_Cmd(0x81); // 设置对比度 OLED_Write_Cmd(0xFF); // 对比度值255 OLED_Write_Cmd(0xA1); // 设置段重映射 OLED_Write_Cmd(0xA6); // 正常显示 OLED_Write_Cmd(0xA8); // 设置多路复用比率 OLED_Write_Cmd(0x3F); // duty 1/64 OLED_Write_Cmd(0xD3); // 设置显示偏移 OLED_Write_Cmd(0x00); // 无偏移 OLED_Write_Cmd(0xD5); // 设置显示时钟分频 OLED_Write_Cmd(0x80); // 设置分频比 OLED_Write_Cmd(0xD9); // 设置预充电周期 OLED_Write_Cmd(0xF1); // OLED_Write_Cmd(0xDA); // 设置COM硬件配置 OLED_Write_Cmd(0x12); // OLED_Write_Cmd(0xDB); // 设置VCOMH电压倍率 OLED_Write_Cmd(0x40); // OLED_Write_Cmd(0x8D); // 设置充电泵 OLED_Write_Cmd(0x14); // 使能电荷泵 OLED_Write_Cmd(0xAF); // 开启显示 OLED_Clear(); // 清屏 }初始化过程中最重要的是复位序列和电荷泵设置电荷泵必须使能才能正常显示。延时时间要足够确保硬件稳定。4.3 SPI通信底层函数实现SPI数据发送的基础函数// 向OLED写入命令 void OLED_Write_Cmd(uint8_t cmd) { OLED_DC_Clr(); // DC线拉低表示写入命令 OLED_CS_Clr(); // 片选使能 HAL_SPI_Transmit(hspi1, cmd, 1, 100); OLED_CS_Set(); // 片选禁止 } // 向OLED写入数据 void OLED_Write_Data(uint8_t data) { OLED_DC_Set(); // DC线拉高表示写入数据 OLED_CS_Clr(); // 片选使能 HAL_SPI_Transmit(hspi1, data, 1, 100); OLED_CS_Set(); // 片选禁止 } // 连续写入数据 void OLED_Write_Data_Buffer(uint8_t *data, uint16_t size) { OLED_DC_Set(); OLED_CS_Clr(); HAL_SPI_Transmit(hspi1, data, size, 1000); OLED_CS_Set(); }HAL_SPI_Transmit函数是HAL库提供的SPI发送函数第一个参数是SPI句柄第二个参数是要发送的数据缓冲区第三个参数是数据长度第四个参数是超时时间毫秒。5. 显示功能实现5.1 基本显示操作实现清屏、设置光标等基本功能// 清屏函数 void OLED_Clear(void) { uint8_t i, j; for(j 0; j 8; j) { OLED_Set_Pos(0, j); for(i 0; i 128; i) { OLED_Write_Data(0x00); // 写入空数据 } } } // 设置显示位置 void OLED_Set_Pos(uint8_t x, uint8_t y) { OLED_Write_Cmd(0xB0 y); // 设置页地址 OLED_Write_Cmd(((x 0xF0) 4) | 0x10); // 设置列地址高4位 OLED_Write_Cmd((x 0x0F) | 0x01); // 设置列地址低4位 } // 开启/关闭显示 void OLED_Display_On(void) { OLED_Write_Cmd(0xAF); // 开启显示 } void OLED_Display_Off(void) { OLED_Write_Cmd(0xAE); // 关闭显示 }设置显示位置时需要注意OLED屏幕按页组织每页8行总共8页64像素。x坐标范围0-127y坐标范围0-7。5.2 字符显示实现显示字符需要字模数据以下是6×8点阵字符的字模和显示函数// 6×8点阵ASCII字符字模 const uint8_t F6x8[][6] { {0x00,0x00,0x00,0x00,0x00,0x00}, // 空格 {0x00,0x00,0x00,0x4F,0x00,0x00}, // ! {0x00,0x00,0x07,0x00,0x07,0x00}, // // ... 其他字符字模数据 }; // 显示一个字符 void OLED_Show_Char(uint8_t x, uint8_t y, uint8_t chr) { uint8_t c 0, i 0; if(chr 32 || chr 127) return; // 只显示可打印字符 c chr - 32; // 字模索引偏移 OLED_Set_Pos(x, y); for(i 0; i 6; i) { OLED_Write_Data(F6x8[c][i]); // 写入字模数据 } } // 显示字符串 void OLED_Show_String(uint8_t x, uint8_t y, uint8_t *str) { while(*str ! \0) { OLED_Show_Char(x, y, *str); x 6; // 字符宽度为6像素 if(x 122) // 换行判断 { x 0; y; } str; } }实际项目中可以使用更丰富的字库如8×16、16×16等点阵字库但需要更大的存储空间。对于中文显示需要额外的中文字库支持。5.3 图形显示功能实现基本的画点、画线等图形功能// 显示缓冲区 uint8_t OLED_GRAM[128][8]; // 刷新整个屏幕 void OLED_Refresh(void) { uint8_t i, j; for(j 0; j 8; j) { OLED_Set_Pos(0, j); for(i 0; i 128; i) { OLED_Write_Data(OLED_GRAM[i][j]); } } } // 画点函数 void OLED_Draw_Point(uint8_t x, uint8_t y, uint8_t mode) { uint8_t page y / 8; uint8_t bit y % 8; if(mode) OLED_GRAM[x][page] | (1 bit); // 画点 else OLED_GRAM[x][page] ~(1 bit); // 擦除点 } // 画线函数Bresenham算法 void OLED_Draw_Line(uint8_t x1, uint8_t y1, uint8_t x2, uint8_t y2) { int dx abs(x2 - x1); int dy abs(y2 - y1); int sx (x1 x2) ? 1 : -1; int sy (y1 y2) ? 1 : -1; int err dx - dy; while(1) { OLED_Draw_Point(x1, y1, 1); if(x1 x2 y1 y2) break; int e2 2 * err; if(e2 -dy) { err - dy; x1 sx; } if(e2 dx) { err dx; y1 sy; } } }使用显示缓冲区可以避免频繁操作SPI总线提高显示效率。先修改缓冲区内容然后一次性刷新到屏幕。6. 完整示例工程6.1 主程序框架#include main.h #include spi.h #include oled.h // 全局变量定义 SPI_HandleTypeDef hspi1; int main(void) { HAL_Init(); SystemClock_Config(); MX_SPI1_Init(); OLED_Init(); OLED_Show_String(0, 0, Hello CSDN!); OLED_Show_String(0, 2, STM32F103C8T6); OLED_Show_String(0, 4, OLED SPI Test); OLED_Show_String(0, 6, 128x64 Display); while(1) { // 可以添加动态显示内容 HAL_Delay(1000); } } // 系统时钟配置 void SystemClock_Config(void) { RCC_OscInitTypeDef RCC_OscInitStruct {0}; RCC_ClkInitTypeDef RCC_ClkInitStruct {0}; // 配置HSE振荡器 RCC_OscInitStruct.OscillatorType RCC_OSCILLATORTYPE_HSE; RCC_OscInitStruct.HSEState RCC_HSE_ON; RCC_OscInitStruct.PLL.PLLState RCC_PLL_ON; RCC_OscInitStruct.PLL.PLLSource RCC_PLLSOURCE_HSE; RCC_OscInitStruct.PLL.PLLMUL RCC_PLL_MUL9; HAL_RCC_OscConfig(RCC_OscInitStruct); // 配置系统时钟 RCC_ClkInitStruct.ClockType RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2; RCC_ClkInitStruct.SYSCLKSource RCC_SYSCLKSOURCE_PLLCLK; RCC_ClkInitStruct.AHBCLKDivider RCC_SYSCLK_DIV1; RCC_ClkInitStruct.APB1CLKDivider RCC_HCLK_DIV2; RCC_ClkInitStruct.APB2CLKDivider RCC_HCLK_DIV1; HAL_RCC_ClockConfig(RCC_ClkInitStruct, FLASH_LATENCY_2); }6.2 OLED头文件定义#ifndef __OLED_H #define __OLED_H #include main.h // GPIO操作宏定义 #define OLED_RST_Set() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET) #define OLED_RST_Clr() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET) #define OLED_DC_Set() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_3, GPIO_PIN_SET) #define OLED_DC_Clr() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_3, GPIO_PIN_RESET) #define OLED_CS_Set() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_SET) #define OLED_CS_Clr() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_RESET) // 函数声明 void OLED_Init(void); void OLED_Clear(void); void OLED_Refresh(void); void OLED_Set_Pos(uint8_t x, uint8_t y); void OLED_Write_Cmd(uint8_t cmd); void OLED_Write_Data(uint8_t data); void OLED_Show_Char(uint8_t x, uint8_t y, uint8_t chr); void OLED_Show_String(uint8_t x, uint8_t y, uint8_t *str); void OLED_Draw_Point(uint8_t x, uint8_t y, uint8_t mode); void OLED_Draw_Line(uint8_t x1, uint8_t y1, uint8_t x2, uint8_t y2); #endif7. 常见问题与解决方案7.1 硬件连接问题问题现象屏幕全白或全黑无任何显示排查步骤检查电源连接确保VCC接3.3VGND共地用万用表测量各引脚电压RESET、DC、CS信号是否正常检查SPI时钟线是否有波形可用示波器观察SCK信号确认OLED模块是否支持3.3V电平有些模块需要5V解决方案重新检查硬件连接确保所有信号线连接正确电源稳定。7.2 SPI通信问题问题现象屏幕有微弱显示但乱码或完全无反应排查步骤检查SPI模式设置SSD1306通常需要模式0或模式3确认时钟频率过高的频率可能导致通信失败检查片选信号时序确保在传输数据前拉低传输后拉高验证DC信号命令和数据传输时DC电平是否正确解决方案降低SPI时钟频率至100-400kHz进行测试确保时序符合要求。7.3 显示异常问题问题现象显示内容错位、重叠或部分显示排查步骤检查初始化序列是否正确特别是内存地址模式设置验证显示坐标计算确保不超出屏幕范围检查字模数据格式是否符合OLED的位映射方式确认刷新函数是否正确更新整个屏幕解决方案重新检查初始化代码确保每个配置命令的参数正确。8. 性能优化与进阶功能8.1 显示效率优化对于需要频繁更新的显示应用可以采取以下优化措施使用DMA传输减少CPU占用实现局部刷新而非全屏刷新采用双缓冲机制避免闪烁优化字模数据存储格式// DMA传输示例 void OLED_Refresh_DMA(void) { for(uint8_t page 0; page 8; page) { OLED_Set_Pos(0, page); OLED_DC_Set(); OLED_CS_Clr(); HAL_SPI_Transmit_DMA(hspi1, OLED_GRAM[page], 128); while(HAL_SPI_GetState(hspi1) ! HAL_SPI_STATE_READY); OLED_CS_Set(); } }8.2 高级显示功能实现更复杂的显示效果如图片显示、动画、菜单系统等// 显示BMP图片需提前取模 void OLED_Show_BMP(uint8_t x, uint8_t y, uint8_t width, uint8_t height, const uint8_t *bmp) { uint8_t page, col; for(page 0; page height/8; page) { OLED_Set_Pos(x, y page); for(col 0; col width; col) { OLED_Write_Data(bmp[page * width col]); } } } // 简单动画示例滚动显示 void OLED_Scroll_Display(void) { OLED_Write_Cmd(0x2E); // 关闭滚动 OLED_Write_Cmd(0x26); // 向右滚动 OLED_Write_Cmd(0x00); // 虚拟字节 OLED_Write_Cmd(0x00); // 起始页 OLED_Write_Cmd(0x07); // 滚动速度 OLED_Write_Cmd(0x07); // 终止页 OLED_Write_Cmd(0x00); // 虚拟字节 OLED_Write_Cmd(0xFF); // 虚拟字节 OLED_Write_Cmd(0x2F); // 开启滚动 }8.3 低功耗优化对于电池供电设备功耗优化尤为重要在不需要显示时关闭OLED0xAE命令降低SPI时钟频率使用睡眠模式时关闭SPI外设合理设计刷新频率避免不必要的更新本文提供的代码在STM32F103C8T6和0.96寸SPI OLED屏上实测可用涵盖了从基础驱动到高级功能的完整实现。在实际项目中可以根据具体需求调整显示内容和刷新策略结合RTOS或状态机实现更复杂的界面逻辑。