本教程详细介绍如何使用 STM32F103C8T6 微控制器(常见为 “蓝色飞线板” Blue Pill)通过 I2C 接口驱动 0.96 寸 OLED 显示屏(SSD1306 控制器),使用 STM32 HAL 库进行开发。内容从基础知识开始,一步步带领初学者完成 OLED 显示从初始化到显示文字、图形和图片的完整过程。
1. 基础知识
OLED 显示屏工作原理
OLED(Organic Light-Emitting Diode,有机发光二极管)显示屏通过有机材料在通电时自发光来显示图像。每个像素点本身就是一个微型发光二极管,因此无需背光,具有高对比度和宽视角等优点。0.96 寸 OLED 通常为单色显示(如白色、蓝色等),分辨率常见为 128×64 像素。这意味着屏幕上共有 128×64 = 8192 个像素点,每个像素可以单独点亮或熄灭。由于像素数量多,OLED 模块内部集成了专门的驱动控制器来管理这些像素的显示数据。
当需要显示图像时,主控(比如 STM32)将像素数据发送给 OLED 的驱动芯片,驱动芯片将数据存入显示内存,然后控制 OLED 面板上对应的位置发光或熄灭,从而形成图像或文字。因为OLED像素自发光,所以对比度极高,可以在黑暗环境下清晰显示,同时功耗相对低(点亮像素才耗电)。
SSD1306 控制器介绍
SSD1306 是一款常用的单色OLED显示屏驱动控制器。许多 0.96 寸 128×64 OLED 模块都使用 SSD1306 芯片。该芯片内置了显示所需的RAM(Graphic Display Data RAM, GDDRAM),大小为 128×64位(即 8192 位,大约 1KB),用于存储要显示的图案数据 (STM32入门HAL库-硬件I2C与0.96寸OLED_stm32 hal i2c oled-优快云博客) (STM32入门HAL库-硬件I2C与0.96寸OLED_stm32 hal i2c oled-优快云博客)。主控可以通过接口(I2C 或 SPI)读写这个显存,从而控制OLED上每个像素的明暗。SSD1306 支持三种地址模式:页地址模式、水平地址模式和垂直地址模式,可以灵活地控制数据在显存中的写入方式。但对于128×64的整屏更新,通常使用页模式或水平模式。页模式下显存被分成8个“页”(page,每页8行像素),每页有128列;水平模式下显存按行连续。 (STM32入门HAL库-硬件I2C与0.96寸OLED_stm32 hal i2c oled-优快云博客)
SSD1306 支持 I2C、SPI 等通信方式。本教程的OLED模块使用 I2C 接口(4针:VCC、GND、SCL、SDA)。SSD1306 芯片本身有一个 I2C 从机地址,通常根据模块的连线默认地址为 0x3C 或 0x3D(7位地址)。在7位地址为0x3C的情况下,对应的8位读写地址为0x78/0x79 (How to interface OLED with STM32? - OLED/LCD Supplier) (How to interface OLED with STM32? - OLED/LCD Supplier)(0x78 用于写,0x79 用于读)。大部分OLED模块将地址脚(俗称 SA0 或 D/C# 引脚)接地,使地址为0x3C。本教程假定OLED地址为0x3C(写操作地址0x78)。需要注意的是,如果你的OLED模块地址脚接高电平,则地址将变为0x3D(写地址0x7A)。
SSD1306 没有独立的命令引脚,但通过 I2C 总线发送数据时可以使用一个控制字节(control byte)来区分命令和数据。根据SSD1306数据手册:当控制字节的 D/C# 位为0时,表示后续字节为命令;为1时表示后续字节是显示数据。而控制字节的 Co 位用于指示是否还有后续控制字节,一般我们将 Co 设为0即可表示单次发送。 (How to interface OLED with STM32? - OLED/LCD Supplier) (How to interface OLED with STM32? - OLED/LCD Supplier)因此,通过I2C发送字节时:
- 若需发送命令,控制字节应为
0x00
(二进制 0b00000000,Co=0,D/C#=0) (How to interface OLED with STM32? - OLED/LCD Supplier)。 - 若需发送数据,控制字节应为
0x40
(二进制 0b01000000,Co=0,D/C#=1) (How to interface OLED with STM32? - OLED/LCD Supplier)。
I2C 通信协议基础
I2C(Inter-Integrated Circuit)是一种常用的串行通信总线,是双线半双工同步通信协议。有一条数据线 SDA 和一条时钟线 SCL,通过主从架构传输数据。STM32F103 作为主机(master),OLED 模块上的 SSD1306 控制器作为从机(slave)。I2C 通信基本要点:
- 寻址:主机通过发送从设备地址来选择通信对象。地址通常为7位(还有1位表示读/写操作)。对于我们的OLED,从机地址7位是0x3C,发送时组合读写位形成8位地址0x78(写)或0x79(读)。
- 起始与停止:当总线空闲时,SCL和SDA均为高电平。主机产生起始条件(Start)时,会在 SCL 高电平时拉低 SDA (STM32入门HAL库-硬件I2C与0.96寸OLED_stm32 hal i2c oled-优快云博客)。传输结束时,主机产生停止条件(Stop),即在 SCL高电平时拉高SDA (STM32入门HAL库-硬件I2C与0.96寸OLED_stm32 hal i2c oled-优快云博客)。
- 数据有效性:在SCL为高期间,SDA上的电平若保持稳定即表示一个有效的比特位 (STM32入门HAL库-硬件I2C与0.96寸OLED_stm32 hal i2c oled-优快云博客)。逻辑1通常表示 SDA高电平,逻辑0表示 SDA低电平(在SCL为高时采样判定)。
- 读写:紧跟在起始之后,主机发送7位地址+1位读/写位。读位(1)表示主机想从从机读数据,写位(0)表示主机想写数据到从机 (STM32入门HAL库-硬件I2C与0.96寸OLED_stm32 hal i2c oled-优快云博客)。发送完地址+读/写后,从设备需要回应一个 ACK(应答)位(拉低 SDA 表示应答)。如果没有应答,表示通信失败或无设备响应。
- 数据传输:地址后就是数据传输阶段。在写操作中,主机持续发送数据字节,每字节后从机返回ACK。 (STM32入门HAL库-硬件I2C与0.96寸OLED_stm32 hal i2c oled-优快云博客)在读操作中,由从机发送数据,主机在每字节后发送ACK表示继续读取,或发送NACK表示最后一个字节读取完成。
- 控制字节与寄存器地址:某些I2C设备有寄存器地址概念,主机在地址后可能需要发送寄存器地址再发送数据。但对于SSD1306,使用控制字节区分命令/数据,可以把它理解为一种“寄存器地址”。HAL库提供的
HAL_I2C_Mem_Write
接口正是利用这一点,将控制字节作为“存储器地址”发送,然后再发送数据。
总的来说,与SSD1306通信的流程通常是:Start -> 设备地址(0x78) -> 控制字节(0x00或0x40) -> 数据 ... -> Stop。我们会利用HAL库简化这个过程,用户只需调用相应的函数,不必手动控制总线时序。
2. 硬件连接
OLED 模块与 STM32F103C8T6 的连线方式
0.96寸 OLED 模块(SSD1306驱动)通常有 4 根引脚,分别标注为:GND、VCC、SCL、SDA。如下面图片所示,它们的功能分别是:电源地(GND)、电源正极(VCC)、时钟线(SCL)、数据线(SDA)。 (SSD1306 OLED with STM32 Blue Pill using STM32CubeIDE)其中VCC工作电压范围通常为 3.3V~5V (SSD1306 OLED with STM32 Blue Pill using STM32CubeIDE)。使用 STM32F103 时推荐直接接 3.3V(与STM32逻辑电平匹配)。SCL和SDA将用于I2C通信,需要连接到STM32的 I2C 引脚。
(SSD1306 OLED with STM32 Blue Pill using STM32CubeIDE)上图显示了 OLED 模块与 STM32F103C8T6(蓝色开发板)的连接示意。其中:
- 将 OLED的 GND 接 STM32 的 GND(地线公用)。
- 将 OLED的 VCC 接 STM32 的 3.3V 电源输出(为OLED供电) (SSD1306 OLED with STM32 Blue Pill using STM32CubeIDE)。
- 将 OLED的 SCL 引脚连接到 STM32 的 PB6 引脚(STM32F103C8T6 的 I2C1_SCL 引脚) (SSD1306 OLED with STM32 Blue Pill using STM32CubeIDE)。
- 将 OLED的 SDA 引脚连接到 STM32 的 PB7 引脚(STM32F103C8T6 的 I2C1_SDA 引脚) (SSD1306 OLED with STM32 Blue Pill using STM32CubeIDE)。
上述连接中,PB6/PB7 对应 STM32 的硬件 I2C1 通道。这与我们稍后在 CubeMX 中配置I2C1接口相对应。请确保所有地线共地,即OLED模块的GND和STM32的GND相连,否则通信可能不稳定甚至无法工作。
硬件电路分析:I2C 总线是集电极开路输出,需要上拉电阻将线路拉至高电平。很多 OLED 模块板上已经内置了适当阻值(如4.7kΩ)的上拉电阻到VCC,如果模块没有内置上拉,则需要在SCL和SDA线上加上拉。STM32F103的硬件I2C引脚本身不提供内部上拉(即便可以配置,一般也使用外部上拉保证总线电气性能),因此务必确认线路有上拉电阻。在实际电路中,如果SCL或SDA线上没有上拉电阻,会导致总线始终读为低电平或通信失败。另外,由于OLED模块工作电压3.3~5V都可,用3.3V供电时I2C信号也是3.3V电平,完全兼容STM32F103的3.3V IO,不需要电平转换器。如果使用5V供电OLED模块,也通常接受3.3V的逻辑电平输入(因为很多模块的控制引脚通过FET或电阻兼容3.3V),但最好查阅具体模块资料以确保。
3. 开发环境配置
STM32CubeIDE 项目创建
使用 STM32CubeIDE 可以方便地创建 STM32F103C8T6 工程并自动生成 HAL 库初始化代码。基本步骤:
- 创建新项目:打开 STM32CubeIDE,选择 File -> New -> STM32 Project。在器件选型页面中,可以直接搜索 STM32F103C8T6 并选择它对应的 LQFP48 封装芯片(如果有 Blue Pill 开发板选项也可直接选板子)。然后点击 “Next”,填写项目名称,比如 “OLED_SSD1306_Demo”,选择 “STM32Cube”初始化方式,完成项目创建。
- CubeMX 引脚配置:新项目打开后,会出现 STM32CubeMX 配置界面。在Pinout & Configuration视图中展开左侧的 Connectivity 菜单,找到 I2C1 并启用它(Mode 选 I2C)。CubeMX 会自动将 I2C1 的引脚分配为 PB6(SCL) 和 PB7(SDA)。确认这两个引脚被标记为绿色的 I2C功能引脚 (SSD1306 OLED with STM32 Blue Pill using STM32CubeIDE)。如果需要使用其他 I2C 通道或引脚,也可以在图形界面中选择不同引脚,但本教程以默认的 I2C1 PB6/PB7 为例。
- I2C 参数设置:点击 I2C1 外设,在下方 Parameter Settings 中设置 I2C 的速度模式。将 “I2C Speed Mode” 改为 Fast Mode,并将时钟频率设置为 400kHz (SSD1306 OLED with STM32 Blue Pill using STM32CubeIDE)。Fast Mode (400kHz) 比标准100kHz更快,能提高屏幕刷新的速度,减少显示延迟。其他I2C参数(如地址模式7-bit、关闭时钟拉伸等)可以保持默认。
- 时钟配置:由于I2C时序受主频影响,我们确保 STM32 工作在最高频率 72MHz。切换到上方的 Clock Configuration 选项卡。在这里将 HCLK 设置为 72MHz(如果使用外部8MHz晶振,则 PLL倍频9得到72MHz)。如果你的 Blue Pill 板有8MHz外部晶振,在 RCC 配置中将 High Speed Clock (HSE) 源设置为 “Crystal/Ceramic Resonator” (SSD1306 OLED with STM32 Blue Pill using STM32CubeIDE)。CubeMX 会自动计算 PLL 参数使系统时钟 72MHz。如果没有外部晶振,也可以使用内部8MHz RC振荡器并配置PLL至72MHz。确认 I2C1 所在的 APB1 总线时钟为 36MHz(默认APB1预分频2),这样I2C模块才能正确产生400kHz时序。
- 生成代码:配置完成后,按 Ctrl+S 保存,会提示生成代码。点击 “Yes” 生成初始化代码并切换到开发视图。CubeIDE 会生成包含 HAL 库初始化的 C 源文件,如
main.c
、stm32f1xx_hal_msp.c
、i2c.c
等。其中i2c.c
中的MX_I2C1_Init()
函数已经根据我们设置的参数初始化好了 I2C1 外设,包括时钟频率等。我们在编写驱动时会用到hi2c1
这个 I2C 句柄。
CubeMX 生成 I2C 初始化代码
CubeMX 自动生成的 I2C 初始化代码大致如下(位于 i2c.c
):
I2C_HandleTypeDef hi2c1;
void MX_I2C1_Init(void) {
hi2c1.Instance = I2C1;
hi2c1.Init.ClockSpeed = 400000; // 时钟速度 400kHz
hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2; // 快速模式占空比
hi2c1.Init.OwnAddress1 = 0;
hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
hi2c1.Init.OwnAddress2 = 0;
hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;
HAL_I2C_Init(&hi2c1);
}
可以看到,I2C1被配置为7位地址模式,时钟400kHz等。如果你的CubeMX版本有所不同,代码格式可能稍有差异,但核心参数需要与上述类似。注意:HAL 库的 I2C 初始化会配置GPIO为开漏模式,并在 HAL_I2C_Init
中使能I2C外设时钟等。确保在 main()
函数中正确调用 MX_I2C1_Init();
和 HAL_Init();
、SystemClock_Config();
等初始化代码。
完成以上工程配置后,我们就可以开始编写 SSD1306 OLED 的驱动代码了。
4. 驱动编写
下面我们将编写控制 SSD1306 OLED 的驱动程序,包括 I2C 通信读写操作封装、屏幕初始化以及清屏和填充功能。为了结构清晰,我们可以新建一个专门的源文件和头文件(例如 ssd1306.c
和 ssd1306.h
)来存放OLED驱动代码。在其中我们会用到 CubeMX 生成的 hi2c1
句柄来进行 I2C 通信。
I2C 读写 SSD1306 寄存器的方法
由于 SSD1306 通过 I2C 接口接收命令和数据,我们先封装两个基础函数:发送命令和发送数据到 OLED。利用 HAL 库的 HAL_I2C_Mem_Write
可以轻松地发送控制字节+数据。其函数原型:
HAL_StatusTypeDef HAL_I2C_Mem_Write(I2C_HandleTypeDef *hi2c,
uint16_t DevAddress,
uint16_t MemAddress,
uint16_t MemAddSize,
uint8_t *pData,
uint16_t Size, uint32_t Timeout);
DevAddress
是从设备地址(注意:HAL 要求是左移1位后的8位地址,对于0x3C的7位地址,这里填0x78)。MemAddress
是要写入的从设备内部寄存器地址,我们将其用于发送 控制字节 (0x00 或 0x40)。MemAddSize
设为I2C_MEMADD_SIZE_8BIT
,表示寄存器地址大小为1字节。pData
和Size
则是实际要发送的数据缓冲区和长度。
基于此,我们实现两个辅助函数:一个发送单字节命令,一个发送单字节数据:
#include "i2c.h" // 包含hi2c1的声明
#define OLED_I2C_ADDR 0x78 // OLED I2C写地址 (0x3C<<1)
void OLED_WriteCommand(uint8_t cmd) {
// 发送一个命令字节 (控制字节=0x00)
HAL_I2C_Mem_Write(&hi2c1, OLED_I2C_ADDR, 0x00,
I2C_MEMADD_SIZE_8BIT, &cmd, 1, HAL_MAX_DELAY);
}
void OLED_WriteData(uint8_t data) {
// 发送一个数据字节 (控制字节=0x40)
HAL_I2C_Mem_Write(&hi2c1, OLED_I2C_ADDR, 0x40,
I2C_MEMADD_SIZE_8BIT, &data, 1, HAL_MAX_DELAY);
}
上述 OLED_WriteCommand
会先发送设备地址0x78,然后发送控制字节0x00,接着发送 cmd
数据。例如,如果 cmd = 0xAE
(关闭显示命令),总线上序列为:[0x78] [0x00] [0xAE]
。OLED_WriteData
类似,只是控制字节为0x40。我们以后也可以扩展这两个函数以发送多个字节的数据,比如连续显示数据。本教程后续代码中,我们也可能直接使用 HAL_I2C_Mem_Write
来一次发送多字节数据。
初始化 SSD1306 显示屏
OLED 屏上电后默认为关闭状态,我们需要按照SSD1306的数据手册发送一系列初始化命令来配置显示参数并点亮屏幕。典型的 SSD1306 初始化步骤如下:
- 关闭显示:发送命令
0xAE
,让OLED进入休眠(关闭面板输出,防止初始化过程中出现杂乱像素)。 - 设置时钟分频和振荡频率:命令对
0xD5
后跟参数0x80
。0x80
是默认设置,快速模式下可根据需要调整。 - 设置多路复用比:命令对
0xA8
后跟参数0x3F
。对于128×64 OLED,高度64像素,对应MUX比为0x3F(即63,表示驱动0-63行)。 - 设置显示偏移:命令对
0xD3
后跟参数0x00
。将显示起始行偏移设为0。 - 设置显示开始行:命令
0x40
,将起始行号设置为0(0x40 | 0)。 - 启用电荷泵:命令对
0x8D
后跟0x14
。开启内部电荷泵以提供OLED所需电压(0x14表示在显示开启时启用)。如果使用外部供电OLED则此步骤不同,一般0x10关闭电荷泵。 - 设置内存地址模式:命令对
0x20
后跟0x02
。选择 页地址模式(Page Addressing Mode)。 (STM32入门HAL库-硬件I2C与0.96寸OLED_stm32 hal i2c oled-优快云博客)页地址模式便于逐页更新显示。也可以选0x00(horizontal)模式一次性写满屏数据,这里选页模式便于理解控制。 - 列地址重映射:命令
0xA1
,将列地址0映射到SEG127,引脚翻转左右镜像。大多数模块需要设置这个保证显示内容左右正确,否则可能水平翻转。 - 行扫描方向翻转:命令
0xC8
,将行扫描从COM[N-1]到COM0(N为行数)翻转,从而上下颠倒扫描。与硬件连接有关,多数模块需要这样设置以使图像正向显示。 - 设置 COM 引脚硬件配置:命令对
0xDA
后跟0x12
。0x12
适用于64行的OLED屏 (将COM配置为替代模式,开启COM左/right remap)。对于32行高的OLED,这个值应为0x02。 - 设置对比度:命令对
0x81
后跟0x7F
。0x7F是中等亮度,范围0x00~0xFF。可以根据需要设亮一些(0xCF)或暗一些。 - 设置预充电周期:命令对
0xD9
后跟0xF1
。在使用内部电荷泵时,0xF1是推荐值;若外部供电典型值为0x22。 - 设置 VCOMH 除数:命令对
0xDB
后跟0x40
。将 VCOMH 电平设置为0.77×Vcc,默认即可。 - 整个显示开启/关闭:命令
0xA4
,取消整个显示开启(A5是Entire Display ON,会点亮所有像素,用于测试,A4则恢复根据RAM内容显示)。 - 设置正常/反相显示:命令
0xA6
,选择正常显示模式(A7则是反相,像素bit=1显示灭,0显示亮)。 - 开启显示:命令
0xAF
,点亮OLED面板,开始显示。
根据以上步骤,我们在代码中依次调用 OLED_WriteCommand()
:
void OLED_Init(void) {
HAL_Delay(100); // 上电延时,确保OLED电源稳定
OLED_WriteCommand(0xAE); // 1. 显示关闭
OLED_WriteCommand(0xD5); // 2. 设置显示时钟分频/振荡频率
OLED_WriteCommand(0x80); // 分频因子&振荡频率设置,0x80默认
OLED_WriteCommand(0xA8); // 3. 设置多路复用比
OLED_WriteCommand(0x3F); // 64行 (0x3F)
OLED_WriteCommand(0xD3); // 4. 设置显示偏移
OLED_WriteCommand(0x00); // 无偏移
OLED_WriteCommand(0x40); // 5. 设置显示开始行 (0)
OLED_WriteCommand(0x8D); // 6. 电荷泵设置
OLED_WriteCommand(0x14); // 开启电荷泵
OLED_WriteCommand(0x20); // 7. 内存地址模式
OLED_WriteCommand(0x02); // 页地址模式 (Page Mode)
OLED_WriteCommand(0xA1); // 8. 列地址重映射 (A0->A1 左右翻转)
OLED_WriteCommand(0xC8); // 9. 行扫描方向翻转 (上下翻转)
OLED_WriteCommand(0xDA); // 10. COM 引脚配置
OLED_WriteCommand(0x12); // COM配置 (0x12 for 64行)
OLED_WriteCommand(0x81); // 11. 对比度设置
OLED_WriteCommand(0x7F); // 对比度值 (0x7F)
OLED_WriteCommand(0xD9); // 12. 预充电周期
OLED_WriteCommand(0xF1); // 设置为 0xF1
OLED_WriteCommand(0xDB); // 13. 设置 VCOMH 电平
OLED_WriteCommand(0x40); // 0x40 默认
OLED_WriteCommand(0xA4); // 14. 取消全显示,按照RAM内容显示
OLED_WriteCommand(0xA6); // 15. 正常显示 (非反相)
OLED_WriteCommand(0xAF); // 16. 开启显示
}
以上就是初始化所需的命令序列。调用 OLED_Init()
后,OLED屏应当被点亮(此时RAM内容不确定,可能会随机点亮一些像素)。接下来通常会清屏以熄灭残留像素。
提示:有些SSD1306模块的初始化命令略有不同,或者对比度等需要调整。上面的序列是常用的配置,能适用于大多数128×64 OLED。如果你的屏幕高度是32,则需要将多路复用比(0xA8)改为0x1F,COM引脚配置(0xDA)改为0x02等。
显存缓冲区、清屏与填充函数
为了方便地对OLED进行绘图和显示,我们通常维护一个与OLED显存对应的**帧缓冲(Framebuffer)**在STM32内存中。对于128×64的屏幕,显存大小为128×64/8 = 1024 字节。我们可以定义一个全局缓冲数组:
#define OLED_WIDTH 128
#define OLED_HEIGHT 64
static uint8_t OLED_Buffer[OLED_WIDTH * OLED_HEIGHT / 8];
这里每个字节对应OLED屏幕上的8个垂直像素(一页中的一列)。缓冲区的索引关系如下:假设像素坐标 (x, y),其中 x范围0127,y范围063。可以计算对应缓冲区索引:
- 页号
page = y / 8
(取整除,每页8行像素)。 - 在该页内的bit位
bit = y % 8
(y在该页中的第几行)。 - 缓冲区索引
index = page * OLED_WIDTH + x
。 - OLED_Buffer[index] 的某一位对应那个像素。例如将该字节的第
bit
位设1,则 (x,y) 像素亮,设0则灭。
有了软件缓冲区,我们的绘图操作都可以先在这个缓冲中完成,再统一刷新到OLED,减少闪烁和I2C通信次数。
清屏函数:将缓冲区全部清零,并更新到显示,使屏幕像素全灭。
void OLED_Clear(void) {
memset(OLED_Buffer, 0x00, sizeof(OLED_Buffer)); // 缓冲设0
// 刷新OLED显示
for (uint8_t page = 0; page < 8; page++) {
OLED_WriteCommand(0xB0 + page); // 设置页地址(0xB0 = page0)
OLED_WriteCommand(0x00); // 设置列低4位为0
OLED_WriteCommand(0x10); // 设置列高4位为0 (列地址0)
// 按当前页,将该页的缓冲区数据写出128字节
HAL_I2C_Mem_Write(&hi2c1, OLED_I2C_ADDR, 0x40, I2C_MEMADD_SIZE_8BIT,
&OLED_Buffer[page * OLED_WIDTH], OLED_WIDTH, HAL_MAX_DELAY);
}
}
OLED_Clear()
利用一个循环逐页发送数据。每一页我们先发送3个命令:设置当前页地址,以及将列地址设为0(低4位和高4位命令分别是0x00和0x10)。然后通过 HAL_I2C_Mem_Write
发送整页的 128 字节显示数据(控制字节0x40自动加在前面)。这样8页数据依次发送完毕,就完成了全屏刷新。由于我们清屏时缓冲都为0x00,所以OLED上也全黑了。
填充屏幕函数:与清屏类似,可以填充亮点满屏。例如将缓冲区全置为0xFF并刷新,则屏幕所有像素点亮:
void OLED_Fill(uint8_t color) {
// color=1 -> 全亮; color=0 -> 全黑
uint8_t fill = (color ? 0xFF : 0x00);
memset(OLED_Buffer, fill, sizeof(OLED_Buffer));
// 刷新显示(与OLED_Clear类似)
for (uint8_t page = 0; page < 8; page++) {
OLED_WriteCommand(0xB0 + page);
OLED_WriteCommand(0x00);
OLED_WriteCommand(0x10);
HAL_I2C_Mem_Write(&hi2c1, OLED_I2C_ADDR, 0x40, I2C_MEMADD_SIZE_8BIT,
&OLED_Buffer[page * OLED_WIDTH], OLED_WIDTH, HAL_MAX_DELAY);
}
}
这样调用 OLED_Fill(1)
就能点亮全屏,OLED_Fill(0)
则清空全屏。实践中我们更常用的是清屏,将其用于初始化后的屏幕清空。
说明:上述清屏/填充函数中,我们每次刷新都逐页发送数据,采用的是页地址模式写入。如果在初始化时选择的是水平地址模式(0x20,0x00),也可以一次性连续发送1024字节刷新全屏,但代码略复杂一些。逐页更新虽然多发送了些命令,但逻辑清晰,适合初学者理解。如果追求极致性能,可考虑水平模式下一次性发送整屏数据甚至使用DMA,但初期不必纠结。
现在我们已经能够初始化OLED、清空和填充屏幕了。接下来实现文本和图形的绘制函数。
5. 字符显示
OLED 常用于显示字符和字符串。由于 SSD1306 是图形点阵屏,并不像字库液晶那样自带字模,我们需要在程序中存储字体点阵库。常见做法是预先准备好字模数据,例如ASCII字符的 5x7、6x8、8x16 等大小的点阵,并以数组形式存储。这里以常用的 6×8 和 8×16 英文字体为例说明。
字符库存储方案 (6×8、8×16 字体)
6×8 字体通常指每个字符占用 6 列、8 行点阵。其中宽度6包含了字符实际5列像素和1列间隔(或边框)空白。高度8正好在一页内(8像素高),适合在单页显示。我们可以设计这样一个字模数组:Font6x8[字符数][6]
,每个字符对应6个字节,每个字节的8位对应该字符的一列像素(从上到下8像素)。例如字符 'A'
的6字节可能表示从左到右每列的像素。ASCII字符从空格' ' (0x20)开始,我们可以令 Font6x8[0]
对应空格,Font6x8[1]
对应 '!',等等。这样字符的数组索引 = 字符ASCII码 - 0x20。
举例来说,假设字模中 'A'
7×8点阵(这里为说明简化),字模如下图形:
##
# #
# #
####
# #
# #
# #
(下面这一行空白)
将其列划分可以得到每列的8位二进制值,存入数组。例如 'A'
的字模数组可能定义为 {0x7E, 0x09, 0x09, 0x7E, 0x00, 0x00}
(这里只是举例,并非实际值)。具体字模数据可通过字体生成工具或者手工定义。通常我们会准备好标准ASCII可见字符(0x20~0x7E,共95个)的数组。由于数据量较大,在此不一一列出,可在实际代码中包含字库头文件或自行生成。
8×16 字体指宽度约8像素、高度16像素的字符。高度16像素跨越OLED的两页(每页8像素),需要在垂直方向分成高8和低8位两部分来存储。常见做法是每个字符用16字节表示:前8字节对应字符上半部分每列点阵,下8字节对应下半部分。或者使用两个独立数组分别存储字符上半部分和下半部分字模。在显示时,需要在相邻的两页上叠加绘制同一个字符的上下部分。8×16字体适合在屏幕上显示大一点的字,在64像素高的屏幕上可以显示4行(16*4=64)。
本教程中我们以较简单的6×8字体演示,因为它易于实现且能显示较多字符。8×16字体实现原理类似,只是绘制函数需要处理两页。
显示字符串方法
要在OLED上显示字符,基本思路是:读取字符的点阵数据 -> 将对应像素写入显存缓冲 -> 最后刷新显示。我们可以实现以下函数:
OLED_DrawPixel(x, y, color)
: 绘制单个像素到缓冲区。OLED_DrawChar(x, y, char, font, color)
: 在指定坐标绘制一个字符(利用某种字体点阵)。OLED_ShowString(x, y, *str, font, color)
: 从指定位置开始显示字符串。
其中 color
可以用1表示点亮(白色),0表示熄灭(黑色),以便支持反色显示等效果。
先实现画点函数:
void OLED_DrawPixel(uint8_t x, uint8_t y, uint8_t color) {
if (x >= OLED_WIDTH || y >= OLED_HEIGHT)
return; // 越界保护
uint16_t index = (y / 8) * OLED_WIDTH + x;
uint8_t bit = y % 8;
if (color)
OLED_Buffer[index] |= (1 << bit); // 置位
else
OLED_Buffer[index] &= ~(1 << bit); // 清位
}
OLED_DrawPixel
根据上面讲的映射计算出缓冲区索引,并设置相应位。这样我们可以单独控制缓冲里的每个像素位。注意这里只修改缓冲区,并不立即发送到屏幕,需等调用刷新函数才实际显示。
接着,实现绘制6×8字体字符的函数。假设我们有字体数组 Font6x8
:
extern const uint8_t Font6x8[][6]; // 声明外部字体表 (需自行定义并初始化)
void OLED_DrawChar(uint8_t x, uint8_t y, char chr, uint8_t color) {
if (chr < 0x20 || chr > 0x7E)
chr = '?'; // 非可显示字符用 '?' 代替
uint8_t index = chr - 0x20;
// 每个字符宽6
for (uint8_t col = 0; col < 6; col++) {
uint8_t lineBits = Font6x8[index][col];
// lineBits的每个位对应该列的像素,从字模取出
for (uint8_t bit = 0; bit < 8; bit++) {
uint8_t pixelColor = (lineBits >> bit) & 0x1; // 取该位
if (!color) pixelColor = !pixelColor; // 如果color=0黑底白字,可取反
OLED_DrawPixel(x + col, y + bit, pixelColor);
}
}
}
上述函数假设 y
是 8 的倍数(即从页开始的位置,如0, 8, 16,...),这样字符不会跨页,简化了处理。Font6x8[index][col]
返回字符的第col列的8个像素bit,最底下bit为第0行,顶部bit为第7行(具体取决于字模定义的位顺序,这里假设最低位对应字体上方像素)。然后我们逐位将对应位置的像素写入缓冲。我们加入了 if (!color) pixelColor = !pixelColor
,是为了支持 color=0
表示反色显示(黑底白字或白底黑字的切换)。如果只需要单一白字黑底,可以省略这个判定,直接使用字模位作为像素。
最后,实现显示字符串:
void OLED_ShowString(uint8_t x, uint8_t y, const char *str, uint8_t color) {
while (*str) {
OLED_DrawChar(x, y, *str, color);
x += 6; // 移动光标6列(字体宽度)
if (x + 6 > OLED_WIDTH) {
x = 0; // 换行到头
y += 8; // 下移一页(8像素高)
}
if (y >= OLED_HEIGHT) {
break; // 超出屏幕则退出
}
str++;
}
}
OLED_ShowString
会按照6像素宽递增 X 坐标。若一行快到边界,则换行到下一行(Y加8像素)。这个简单实现假定字符串不超过屏幕容纳范围。调用该函数后,同样需要调用刷新函数(如 OLED_UpdateScreen()
类似于我们前面的清屏函数中的刷新过程)才能将缓冲内容更新到OLED上。实际上,我们可以将刷新过程封装成 OLED_UpdateScreen()
函数,这里直接使用刚才清屏时写的逐页刷新的逻辑即可。
提示:字库数据建议存放在 Flash 常量区,例如使用 const uint8_t Font6x8[][6] PROGMEM = {...}
等(PROGMEM用于AVR,这里STM32直接const即可)。将字模放在程序存储器可以节省RAM。对于中文字符(16×16或更大)显示原理类似,但需要更大字库,可以根据需要拓展。
6. 图形绘制
除了文字,我们还可能需要在OLED上绘制基本图形,如点、线、矩形、圆形,甚至位图图片。利用我们已有的 OLED_DrawPixel
可以构建出更复杂的图形绘制函数。
画点、画线
画点已经由 OLED_DrawPixel
实现,可以直接使用。画线则需要在两个坐标间插入多个点。通用的画任意两点之间直线可以使用 Bresenham 算法,它能高效地计算一条近似直线的所有像素坐标。下面是 Bresenham 画线的一个实现:
void OLED_DrawLine(int x0, int y0, int x1, int y1, uint8_t color) {
if (x0 == x1 && y0 == y1) {
OLED_DrawPixel(x0, y0, color);
return;
}
int dx = (x1 > x0 ? x1 - x0 : x0 - x1);
int dy = (y1 > y0 ? y0 - y1 : y1 - y0);
int sx = x0 < x1 ? 1 : -1;
int sy = y0 < y1 ? 1 : -1;
int err = dx + dy; // 注意dy已取负
while (1) {
OLED_DrawPixel(x0, y0, color);
if (x0 == x1 && y0 == y1) break;
int e2 = 2 * err;
if (e2 >= dy) {
err += dy;
x0 += sx;
}
if (e2 <= dx) {
err += dx;
y0 += sy;
}
}
}
这个函数能绘制任意倾斜的线段,包括水平和垂直线。对于水平或垂直的特殊情况,其实可以用更简单的方法(例如循环画点),但上面算法已经涵盖所有情况。调用如 OLED_DrawLine(0,0, 127,63, 1);
将画一条从屏幕左上角到右下角的对白线(需要刷新后可见)。
画矩形、画圆
矩形可以看作是绘制四条边的组合。如果要绘制空心矩形框,只需画上边、下边两条水平线和左边、右边两条垂直线;如果要填充矩形,可以在Y方向逐行绘制水平线填满区域。示例空心矩形代码:
void OLED_DrawRectangle(int x, int y, int width, int height, uint8_t color) {
// 绘制矩形的四条边
OLED_DrawLine(x, y, x + width - 1, y, color); // 上边
OLED_DrawLine(x, y + height - 1, x + width - 1, y + height - 1, color); // 下边
OLED_DrawLine(x, y, x, y + height - 1, color); // 左边
OLED_DrawLine(x + width - 1, y, x + width - 1, y + height - 1, color); // 右边
}
需要注意坐标不要超出屏幕范围。填充矩形可以在上边和下边之间用一个嵌套循环调用 OLED_DrawPixel
或多次 OLED_DrawLine
实现,此处不再冗长展开。
圆形绘制可以使用 Bresenham 圆算法。画圆原理是利用圆的对称性,每计算出一个象限的像素,即可绘制出八个对称点。算法如下(绘制空心圆周):
void OLED_DrawCircle(int x0, int y0, int r, uint8_t color) {
int a = 0;
int b = r;
int d = 1 - r; // 判定参数
while (a <= b) {
// 八个对称点
OLED_DrawPixel(x0 + a, y0 + b, color);
OLED_DrawPixel(x0 - a, y0 + b, color);
OLED_DrawPixel(x0 + a, y0 - b, color);
OLED_DrawPixel(x0 - a, y0 - b, color);
OLED_DrawPixel(x0 + b, y0 + a, color);
OLED_DrawPixel(x0 - b, y0 + a, color);
OLED_DrawPixel(x0 + b, y0 - a, color);
OLED_DrawPixel(x0 - b, y0 - a, color);
a++;
if (d < 0) {
d += 2 * a + 1;
} else {
b--;
d += 2 * (a - b) + 1;
}
}
}
该算法从 (0, r)
开始逐步向圆周中间计算,当 a <= b
时绘制八个对称点。当 a > b 时完成。调用例如 OLED_DrawCircle(64, 32, 10, 1);
会以 (64,32) 为圆心绘制半径10的圆。若要实心填充圆,可以在每个y上绘制水平线连接左右两边的像素。
显示 BMP 图片方法
OLED 128×64 常用于显示简单的图标或图片。由于是单色显示,我们需要将图像转换为单色位图数据数组,然后通过程序将其发送给OLED。步骤如下:
-
图片转换:准备好所需显示的图片(例如 logo,大小不要超过128×64像素)。使用PC上的工具将图片转换为C语言数组。例如可以使用在线工具如 LCD Assistant 或 Image2cpp,选择输出格式为单色位图,水平字节排列(对应SSD1306页模式)或直接选择SSD1306格式。一张128×64全屏图片会转换成 1024 字节数组。如果是部分区域的小图片,则字节数 = 宽 * 高 / 8(高度需是8的倍数或处理对齐)。工具通常会输出形如
{0xFF, 0x00, ...}
的十六进制初始器。我们可将此数组复制到程序中,例如:const uint8_t IMAGE_Logo[1024] = { /* 128x64位图数据,例如0x00,0xFF,... 共1024字节 */ };
-
显示图片:有几种方法将图像数据绘制出来:
- 方法一:如果图像正好是全屏大小(128×64),可以直接将此数组的数据复制到OLED缓冲区然后刷新:
其中memcpy(OLED_Buffer, IMAGE_Logo, 1024); OLED_UpdateScreen(); // 刷新OLED显示缓冲区内容
OLED_UpdateScreen()
的实现和前面清屏时的刷新过程类似(逐页发送缓冲数据)。这一方法简单粗暴,将整个屏幕内容替换为图片。 - 方法二:如果只想在屏幕某个区域显示图片(例如一个小图标),可以编写函数将图像数组绘制到缓冲中的指定位置。该函数需要按照字节逐行或逐页拷贝数据,并正确地对齐目标缓冲。例如:
这样可以将一个宽w、高h(8的整数倍)的位图绘制到(x,y)起始的位置。但需要注意避免越界,以及如果y不是8的整数倍,高级处理需要将字节拆分,这里简化要求y对齐页。void OLED_DrawBitmap(uint8_t x, uint8_t y, const uint8_t *bitmap, uint8_t w, uint8_t h) { // 假设h是8的倍数 for (uint8_t j = 0; j < h/8; j++) { // 对于每页 for (uint8_t i = 0; i < w; i++) { // 对于每列 OLED_Buffer[(y/8 + j) * OLED_WIDTH + x + i] = bitmap[j * w + i]; } } }
- 方法三:不用缓冲,直接一次性发送位图数据到OLED。这需要先发送设置地址范围的命令,然后发送数据数组。对于全屏图片,可以直接用与清屏类似的方法发送数组。对局部图片,则需设置起始页、结束页、起始列、结束列,再发送数据。因为较复杂,这里不细述。一般推荐使用缓冲方法方便统一管理屏幕内容。
- 方法一:如果图像正好是全屏大小(128×64),可以直接将此数组的数据复制到OLED缓冲区然后刷新:
总之,显示BMP图片的核心在于准备好正确格式的数组,然后将其内容送入OLED显示缓冲。在本教程提供的示例代码中,我们会演示加载全屏图片的方法。
7. 示例代码
下面通过几个示例来展示如何使用以上驱动函数实现实际效果。这些示例假设已经完成前面的初始化和函数实现。
首先,在 main.c
或合适的位置调用初始化函数点亮屏幕,并清屏:
OLED_Init(); // 初始化OLED屏幕
OLED_Clear(); // 清屏,所有像素熄灭
示例1:显示字符串 "Hello, OLED!"
利用我们编写的文本显示函数,在屏幕指定位置显示一行字符串:
OLED_Clear(); // 先清屏
OLED_ShowString(0, 0, "Hello, OLED!", 1); // 在顶行(页0)显示字符串
OLED_UpdateScreen(); // 将缓冲内容更新到屏幕
调用 OLED_ShowString(0,0,...)
会把 "Hello, OLED!" 从左上角开始显示。由于使用6×8字体,字符串长度不会超过一行(128/6 ≈ 21字符,本例13字符)。OLED_UpdateScreen()
则执行实际的数据传输,将整个屏幕的显存数据发送给OLED。执行后应能看到 OLED 屏幕左上方显示出 Hello, OLED!。如果希望在不同位置显示,可以调整 x, y
参数(y需是8的倍数)。例如 OLED_ShowString(0, 8, "STM32 I2C OLED", 1)
会在第二行显示另一串字。别忘了每次修改缓冲后都需要更新屏幕才能反映变化。
示例2: 显示简单几何图形
在屏幕上绘制一些基本形状,如线条、矩形、圆:
OLED_Clear(); // 清屏
// 绘制对白线:从左上到右下
OLED_DrawLine(0, 0, OLED_WIDTH-1, OLED_HEIGHT-1, 1);
// 绘制矩形边框:位于(10,10),宽50高30
OLED_DrawRectangle(10, 10, 50, 30, 1);
// 绘制圆:圆心(64,32),半径20
OLED_DrawCircle(64, 32, 20, 1);
OLED_UpdateScreen(); // 刷新显示,呈现图形
执行后,屏幕上应出现一条对角线、一矩形和一个圆形轮廓。我们也可以尝试画实心矩形或改变参数观测效果。绘制图形的函数调用顺序可以随意,最终的 OLED_UpdateScreen()
会一次性把所有图形输出。由于我们在缓冲中绘制,它们可以重叠组合。如果画线和画圆有部分重叠,缓冲处理(这里全部用点设置为1)会使重叠区域保持亮点(也可能需要根据需要XOR等操作实现不同效果,这里均为简单绘制)。
示例3: 显示 BMP 图片
假设我们已经将一张 128×64 的单色图片转换为 C 数组 IMAGE_Logo[1024]
(替换为你的图片数据)。我们将其全屏显示:
// 假设 IMAGE_Logo 已包含128x64图片的数据
extern const uint8_t IMAGE_Logo[1024];
// 将图片数据复制到缓冲区
memcpy(OLED_Buffer, IMAGE_Logo, 1024);
OLED_UpdateScreen(); // 刷新屏幕显示图片
HAL_Delay(5000); // 延时5秒,让图片保持显示
这段代码会直接把图片数组拷贝到 OLED_Buffer,然后刷新屏幕。延时5秒以便肉眼看清(可选)。实际应用中,可以根据需要停留或在循环中显示不同画面。若想在不清屏的情况下叠加图片(比如logo和文字共存),就需要按位操作缓冲,这里演示的是替换整个屏幕内容。
以上三个示例展示了文本、图形和图像的显示方法。用户可以将它们结合,比如先显示文字,再显示形状,或者显示形状后在空白处显示文字等,关键在于正确更新缓冲区并刷新。
完整的代码应包含初始化、刷新和各种绘制函数以及字模数据。由于篇幅原因,这里没有给出所有内容的连续代码。不过,通过本节的示例,读者可以把各部分拼合到一起,做出一个功能完整的 OLED 驱动程序。
8. 调试与优化
在实际开发中,可能会遇到各种问题。下面总结一些常见问题的原因和解决思路,并提供优化性能的建议。
常见问题分析及解决方案
-
OLED 无显示/不工作:
- 电路连接问题:首先检查电源和地是否正确连接,3.3V 是否供到 OLED 模块;SCL/SDA 连线是否正确接到 PB6/PB7,有没有松动或接反。确认模块的 GND 和 STM32 的 GND 必须共地。
- I2C 地址错误:确保代码里使用的 I2C 地址与实际模块一致。大部分 0.96寸 OLED 模块地址为0x3C(0x78),但也有可能是0x3D(0x7A)。如果初始化后完全黑屏,没有任何随机点亮,怀疑是不是地址不对,可尝试修改
OLED_I2C_ADDR
为0x7A 或使用I2C扫描程序探测地址。 - 初始化序列问题:如果电路和地址都正确,但仍无显示,可能是初始化命令有误。检查是否执行了
OLED_Init()
且包含了0xAF
开启显示命令。一些情况下如果省略了电荷泵开启(0x8D/0x14)或对比度设置不当,屏幕也可能一直不亮。可以在初始化后尝试调用OLED_Fill(1)
全亮屏来测试OLED是否点亮,以区分是初始化问题还是后续刷新问题。 - I2C 总线问题:如果程序卡在发送函数(例如
HAL_I2C_Mem_Write
阻塞)不往下执行,可能是I2C通信没有成功完成。检查CubeMX里是否启用了对应GPIO的复用以及 RCC 时钟。也可用示波器/逻辑分析仪观察 SCL/SDA 波形。必要时降低 I2C 时钟速率(如改回100kHz)以排除高速下信号完整性问题。STM32F1的I2C有时存在启动失败或总线繁忙问题,若总线一直处于忙状态,可尝试在调试时手动释放SCL/SDA或复位设备。 - 代码逻辑错误:确认缓冲区更新和
OLED_UpdateScreen()
刷新调用顺序正确。例如调用OLED_ShowString
后一定要刷屏才会看到结果。如果只调用了一次OLED_Init()
而从未调用清屏或更新,屏幕可能仍旧停留在上电时的随机状态或上次显示的数据。
-
显示异常/杂乱:
- 数据写入错位:如果屏幕上出现错位的像素列或行,可能是地址模式和刷新方法不匹配。例如我们用的是页模式刷新,但初始化时地址模式设置错了(比如仍是水平模式0x00),则写满一页后SSD1306会自动换行到下一行而不是下一页,导致混乱。解决方法:确保初始化选择的地址模式与刷新代码一致。我们在初始化用了页模式0x02,对应逐页写入是正确的。或者在刷新前发送
0x21
(设置列地址范围)和0x22
(设置页地址范围)精确指定写入区域也可以避免错位。 - 像素镜像翻转:如果发现显示出来的内容左右反了或上下颠倒了,可能是初始化的
0xA0/A1
或0xC0/C8
设置与屏幕实际连接不符。可以尝试交换这些命令(A0<->A1,C0<->C8)来纠正。每款模块行列引脚可能连线不同,通过这些命令可以适配。 - 部分残留或闪烁:如果清屏后仍有少量像素残留,可能是刷新不彻底或没有覆盖所有显存页面。确保刷新循环覆盖07页,列0127全部写入。如有遗漏会导致某些区域保持之前的数据。闪烁通常是因为反复清屏-全刷新的过程被肉眼捕捉到,可以考虑优化刷新方式减少不必要的全屏重绘(见下文优化)。
- 数据写入错位:如果屏幕上出现错位的像素列或行,可能是地址模式和刷新方法不匹配。例如我们用的是页模式刷新,但初始化时地址模式设置错了(比如仍是水平模式0x00),则写满一页后SSD1306会自动换行到下一行而不是下一页,导致混乱。解决方法:确保初始化选择的地址模式与刷新代码一致。我们在初始化用了页模式0x02,对应逐页写入是正确的。或者在刷新前发送
-
I2C通信错误:
- NACK错误:发送命令返回 HAL_ERROR 可能是没有收到ACK,常由地址不对或设备未初始化好引起。可在上电后延时一段时间再初始化OLED,或检查线是否接触不良。
- 总线死锁:调试过程中如果意外中止程序,可能导致OLED正忙时退出,造成SDA或SCL拉低,导致下次运行时总线忙。可在初始化I2C前手工GPIO模拟产生几个时钟释放总线,或者简单粗暴地断电重启设备。
代码优化建议
- 减少I2C刷新次数:尽量先在内存缓冲区完成所有绘制操作,然后调用一次
OLED_UpdateScreen()
刷新。不要在每画完一点或一条线后立刻刷屏,那样会产生大量I2C通信,降低效率并可能引起闪烁。如果需要实时刷新,也应在需要的区域更新,不必每次都全屏刷新。 - 批量发送数据:我们在刷新时用了每页一次的 128字节发送。实际上可以在水平地址模式下用一条 I2C Write 发送全屏1024字节数据 (STM32入门HAL库-硬件I2C与0.96寸OLED_stm32 hal i2c oled-优快云博客)。这只需发送一次设备地址和控制字节,速度更快。如果使用 HAL,可以调用
HAL_I2C_Mem_Write(&hi2c1, 0x78, 0x40, ..., buffer, 1024, ...)
前提是先发送相应的设置列、页起始地址的命令并使用水平模式(或者HAL_I2C_Master_Transmit
拼一个0x40在数据前面发送也可)。总之批量传输能显著提高刷新效率。 - 使用DMA:STM32 的 I2C 外设支持 DMA 传输。我们可以配置 I2C1 的TX DMA通道,这样在发送大块数据(如屏幕缓冲)时,不用CPU逐字节等待传输完成。HAL库提供
HAL_I2C_Mem_Write_DMA
等函数,可以在更新屏幕时调用,让DMA控制器自动发送1024字节,同时CPU可以去处理其他任务,从而提高并行效率。需要注意DMA传输完成后通常通过回调函数通知,可以在回调里处理后续逻辑。 - 优化绘图算法:基础绘图算法如 Bresenham 线算法已经较高效。但特殊情况下可以简化,例如水平线和垂直线可直接用
memset
或简单循环优化。填充矩形和圆形也可按Scanline填充以减少函数调用开销。 - 字体和图像存储:将不常修改的字库或位图使用
const
限定放Flash,可以节省RAM。如果有大量图片,可以存储在外部Flash或SD卡,根据需要读取显示。 - 分区域更新:如果屏幕大部分内容不变,仅小区域更新,可以只更新对应的页和列范围,而不必每次都刷新全屏1024字节。 (STM32入门HAL库-硬件I2C与0.96寸OLED_stm32 hal i2c oled-优快云博客)例如制作一个简单跑马灯,只刷新文字所在的页范围。这样减少I2C传输字节数,提升帧率。可以封装
OLED_UpdateArea(x, y, w, h)
按区域刷新。 - 调低对比度节省功耗:OLED功耗与点亮像素和对比度设置有关。在不需要高亮时,可以用命令
0x81, contrast
动态调整亮度,或者在空闲时发送0xAE
关闭显示以省电,适用于电池供电场景。
当掌握了基本方法后,读者可以尝试封装更高级的接口,例如打印数字、绘制进度条,甚至制作简单动画,不断提高开发技能。 (SSD1306 OLED with STM32 Blue Pill using STM32CubeIDE)