1. 项目概述:从点亮一块OLED屏说起
如果你手头有一块STM32开发板和一块小小的OLED显示屏,想把“Hello World”或者传感器数据漂亮地显示出来,那么你大概率绕不开今天要聊的这个话题。OLED显示实验,几乎是每个STM32学习者都会经历的“里程碑”式项目。它不像点个LED灯那么简单,也不像驱动TFT彩屏那么复杂,正好卡在一个既能让你理解嵌入式显示的基本原理,又不会一开始就被吓退的甜点位置。
这个实验的核心目标很明确:让STM32单片机通过I2C或SPI通信协议,控制一块单色(通常是128x64像素)的OLED屏幕,在上面显示你想要的文字、图形或数据。听起来简单,但当你打开一份“OLED显示实验”的代码时,可能会被里面各种初始化序列、显存操作、字库函数搞得有点懵。网上的代码很多,但往往只给个库文件,告诉你“调用这个函数就能显示”,至于为什么这么写、底层发生了什么、有哪些坑,却很少讲透。
今天,我就以一个做过不下几十次类似移植和调试的老鸟视角,带你把这套代码从外到里、从原理到实操,彻底拆解清楚。我们不止看代码怎么写,更要弄懂每一个步骤背后的“所以然”。无论是你用的是SSD1306、SH1106这类常见驱动芯片的OLED,还是正在为如何显示中文、如何做菜单而头疼,这篇文章里的思路和避坑经验,都能给你直接的参考。
2. 核心思路与方案选型:为什么是“软件模拟I2C”?
在动手写代码之前,我们先得把方案定下来。OLED模块与STM32的连接,主流就两种方式:I2C和SPI。SPI速度快,适合刷新率要求高的场景;I2C接线少(只需两根线:SCL和SDA),节省IO口,在显示静态信息或低速更新时完全够用,因此更常见。
2.1 硬件I2C vs 软件模拟I2C
这里就会遇到第一个关键选择:用STM32自带的硬件I2C外设,还是用普通GPIO口来模拟I2C时序(即软件模拟I2C)?
很多新手会想,既然单片机有硬件I2C,干嘛不用?效率不是更高吗?道理没错,但现实很骨感。STM32F1系列等较早型号的硬件I2C外设,在早期固件库中存在一些已知的稳定性和兼容性问题,比如在某些情况下容易卡死在BUSY状态,调试起来比较麻烦。虽然后续的HAL库和LL库已经改善了很多,但“软件模拟I2C”因其极高的可控性和移植性,依然是大量教程和实际项目中的首选。
注意:选择软件模拟I2C,并非因为它技术更先进,恰恰是因为它“简单粗暴好控制”。你可以完全掌控时序的每一个微秒,出错了也容易定位。对于OLED这种对速率不敏感的从设备,软件模拟完全能满足需求,且一份代码稍作修改就能在不同型号的STM32甚至其他单片机间移植,这是它的巨大优势。
因此,我们接下来的代码详解,将基于软件模拟I2C驱动SSD1306这款最普遍的OLED驱动芯片来展开。理解了这一种,硬件I2C或其他驱动芯片的移植都是触类旁通。
2.2 代码架构设计
一份易于理解和复用的OLED显示代码,通常会分成几个层次清晰的模块:
- 底层驱动层(OLED.c/.h):负责最基础的GPIO初始化、I2C时序模拟、向OLED发送命令和数据。这是和硬件直接打交道的部分。
- 核心功能层:负责OLED驱动芯片(如SSD1306)的初始化序列、显存(GRAM)操作、基础画点函数。这是显示功能的基础。
- 应用层:基于画点函数封装出的高级功能,如画线、画矩形、显示字符、显示字符串、显示数字、显示图像等。我们用户主要调用的就是这一层的函数。
这样的分层使得底层更换通信方式(比如换SPI)时,上层应用代码几乎不用改动。
3. 底层驱动详解:模拟I2C的“脉搏”
让我们从最底层开始,看看如何用两根普通的IO口“模仿”出I2C的通信波形。I2C协议有严格的时序要求,包括起始信号、停止信号、应答信号以及数据位的读写。
3.1 GPIO引脚初始化
首先,你需要定义两个GPIO引脚分别作为I2C的SCL(时钟线)和SDA(数据线)。注意,SDA线需要配置为开漏输出(Open-Drain Output),并同时开启内部上拉电阻。这是因为I2C总线是“线与”结构,多个设备可以挂在同一总线上,开漏输出配合上拉电阻才能实现正确的电平冲突检测和双向通信(虽然我们这里模拟写为主,但也要遵循规范)。
// OLED.h 中定义引脚 #define OLED_I2C_SCL_PIN GPIO_Pin_6 #define OLED_I2C_SCL_PORT GPIOB #define OLED_I2C_SDA_PIN GPIO_Pin_7 #define OLED_I2C_SDA_PORT GPIOB // OLED.c 中的初始化函数片段 void OLED_I2C_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // 使能GPIOB时钟 // 配置SCL为推挽输出,低速即可 GPIO_InitStructure.GPIO_Pin = OLED_I2C_SCL_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz; GPIO_Init(OLED_I2C_SCL_PORT, &GPIO_InitStructure); // 配置SDA为开漏输出,并启用上拉 GPIO_InitStructure.GPIO_Pin = OLED_I2C_SDA_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; // 开漏输出 GPIO_Init(OLED_I2C_SDA_PORT, &GPIO_InitStructure); GPIO_SetBits(OLED_I2C_SDA_PORT, OLED_I2C_SDA_PIN); // 先拉高 GPIO_SetBits(OLED_I2C_SCL_PORT, OLED_I2C_SCL_PIN); }3.2 关键时序模拟
I2C的通信速率(标准模式100kbps,快速模式400kbps)由SCL时钟线的高低电平持续时间决定。在软件模拟中,我们通过插入Delay_us()微秒级延时来实现。下面是最关键的几个信号函数:
// 产生I2C起始信号:SCL高电平期间,SDA一个下降沿 void OLED_I2C_Start(void) { OLED_SDA_OUT(); // 设置SDA为输出模式 OLED_I2C_SDA(1); OLED_I2C_SCL(1); Delay_us(5); // 保持时间,根据速率调整,这里约合100kHz OLED_I2C_SDA(0); Delay_us(5); OLED_I2C_SCL(0); // 钳住总线,准备发送数据 } // 产生I2C停止信号:SCL高电平期间,SDA一个上升沿 void OLED_I2C_Stop(void) { OLED_SDA_OUT(); OLED_I2C_SDA(0); OLED_I2C_SCL(1); Delay_us(5); OLED_I2C_SDA(1); Delay_us(5); } // 等待应答信号:发送完一个字节后,主机释放SDA(设为输入),并检测SDA是否被从机拉低 uint8_t OLED_I2C_WaitAck(void) { uint8_t ack = 0; OLED_SDA_IN(); // 设置SDA为输入模式,读取外部电平 OLED_I2C_SCL(1); Delay_us(2); if (GPIO_ReadInputDataBit(OLED_I2C_SDA_PORT, OLED_I2C_SDA_PIN) == 0) { ack = 1; // 收到应答 } else { ack = 0; // 无应答 } OLED_I2C_SCL(0); OLED_SDA_OUT(); // 恢复输出模式 return ack; }3.3 发送一个字节
这是最核心的函数,它按照I2C协议,将8位数据从高位(MSB)到低位(LSB)依次放到SDA线上,并在每个bit后产生一个SCL脉冲。
void OLED_I2C_SendByte(uint8_t byte) { uint8_t i; OLED_SDA_OUT(); for (i = 0; i < 8; i++) { OLED_I2C_SCL(0); // 拉低时钟线,准备改变数据 Delay_us(2); if (byte & 0x80) { // 判断最高位 OLED_I2C_SDA(1); } else { OLED_I2C_SDA(0); } Delay_us(2); OLED_I2C_SCL(1); // 拉高时钟线,从机在此刻采样数据 Delay_us(5); byte <<= 1; // 左移,准备发送下一位 } OLED_I2C_SCL(0); // 发送完8位后,时钟线拉低 // 紧接着,通常会调用 WaitAck() 等待从机应答 }实操心得:这里的延时
Delay_us(5)不是绝对的,它决定了I2C的通信速率。如果你用的单片机主频很高,或者对显示刷新有更高要求,可以适当减小这个延时。但要注意,必须满足OLED驱动芯片数据手册里对SCL低电平/高电平最小时间的要求。一个稳妥的做法是,先用保守的延时让屏幕跑起来,再逐步优化。另外,OLED_SDA_OUT()和OLED_SDA_IN()这两个宏,用于快速切换SDA引脚的方向,是模拟I2C读操作(虽然本例中写OLED不需要读)和等待应答的关键,务必正确实现。
4. 驱动芯片初始化与显存操作:点亮屏幕的第一步
底层通信打通后,我们就要和SSD1306芯片“对话”了。对话的第一步,就是发送一系列初始化命令,配置它的工作模式。这就像给一个新设备上电后,要设置它的分辨率、扫描方式、对比度等参数。
4.1 初始化命令序列
SSD1306的初始化通常通过一个命令数组来完成。这些命令可以在其数据手册中找到。以下是一个典型的初始化序列:
// SSD1306 初始化命令表 const uint8_t OLED_InitCmd[] = { 0xAE, // 关闭显示(Display OFF) 0xD5, 0x80, // 设置显示时钟分频比和振荡器频率 0xA8, 0x3F, // 设置多路复用率 (MUX Ratio) 为64 (0x3F for 128x64) 0xD3, 0x00, // 设置显示偏移 (Display Offset) 为0 0x40, // 设置显示起始行 (Start Line) 为0 0xA1, // 设置段重映射 (Segment Re-map) 为0xA1(左右翻转,视屏幕物理连接而定) 0xC8, // 设置COM扫描方向 (COM Output Scan Direction) 为0xC8(上下翻转) 0xDA, 0x12, // 设置COM硬件引脚配置 (COM Pins Hardware Configuration) 0x81, 0xCF, // 设置对比度控制 (Contrast Control) 为0xCF 0xD9, 0xF1, // 设置预充电周期 (Pre-charge Period) 0xDB, 0x30, // 设置VCOMH电压倍率 (VCOMH Deselect Level) 0xA4, // 禁止整体显示开启 (Disable Entire Display On) 0xA6, // 设置正常显示 (Normal Display, 非反色) 0x8D, 0x14, // 启用电荷泵稳压器 (Enable Charge Pump) 0xAF // 开启显示 (Display ON) };发送这些命令的函数,需要先发送一个控制字节(0x00代表命令,0x40代表数据),然后是命令本身。
void OLED_Write_Cmd(uint8_t cmd) { OLED_I2C_Start(); OLED_I2C_SendByte(0x78); // 从机地址,写模式 (0x3C << 1) OLED_I2C_WaitAck(); OLED_I2C_SendByte(0x00); // 控制字节,表示后续是命令 OLED_I2C_WaitAck(); OLED_I2C_SendByte(cmd); // 命令字节 OLED_I2C_WaitAck(); OLED_I2C_Stop(); }4.2 显存(GRAM)与页地址模式
这是理解OLED显示的核心。SSD1306的显存是一个位图(Bitmap),大小为128 x 64 bits。但它被组织成8页(Page),每页8行(Row),每页有128列(Column)。也就是说,一页对应屏幕上的一个“横条”,高度为8个像素点。
| 内存结构 | 对应屏幕区域 | 数据位含义 |
|---|---|---|
| Page 0 (0xB0~0xB7) | 屏幕顶部8行 (Y=0~7) | 每列一个字节,低位对应上方像素 |
| Page 1 | 接下来8行 (Y=8~15) | 同上 |
| ... | ... | ... |
| Page 7 | 屏幕底部8行 (Y=56~63) | 同上 |
当我们向显存的某个位置写入一个字节(8位)数据时,实际上是在设置当前页、当前列对应的8个垂直像素点的亮灭(1亮,0灭)。这种“页地址模式”是SSD1306的默认模式,也是我们最常用的模式。
因此,在显示字符或图像前,我们需要设置当前操作的“页”和“列”地址。
void OLED_Set_Pos(uint8_t page, uint8_t col) { OLED_Write_Cmd(0xB0 + page); // 设置页地址 (0xB0 ~ 0xB7) OLED_Write_Cmd(col & 0x0F); // 设置列地址低4位 OLED_Write_Cmd(0x10 | (col >> 4)); // 设置列地址高4位 }4.3 清屏与更新显示
有了以上基础,清屏函数就很好理解了:遍历所有页(0~7)和所有列(0~127),向每个位置写入0x00(全灭)。
void OLED_Clear(void) { uint8_t page, col; for (page = 0; page < 8; page++) { OLED_Set_Pos(page, 0); for (col = 0; col < 128; col++) { OLED_Write_Data(0x00); // 向显存写入0 } } }OLED_Write_Data函数与写命令函数类似,只是控制字节发送0x40。
注意事项:很多新手会发现清屏后,屏幕并非全黑,可能边缘有亮线或残留。这通常是因为初始化命令不完整或电荷泵未正确启用导致的。请务必检查初始化序列中
0x8D, 0x14(开启电荷泵)和0xAF(开启显示)这两条命令是否成功发送。电荷泵是为OLED像素点提供驱动电压的内部电路,不开启它屏幕就无法正常显示。
5. 应用层功能实现:从画点到显示字符串
现在,我们有了操作显存的能力。接下来要构建上层建筑,实现我们最终需要的显示功能。一切的基础,是一个“画点”函数。
5.1 基础画点函数
画点函数需要根据给定的坐标(x, y),计算出该点位于哪一页(Page)、哪一列(Column),以及在该列字节中的哪一位(Bit)。
void OLED_DrawPoint(uint8_t x, uint8_t y, uint8_t mode) { // 参数检查 if (x >= OLED_WIDTH || y >= OLED_HEIGHT) return; uint8_t page = y / 8; // 确定在哪一页 uint8_t bit_pos = y % 8; // 确定在字节中的位 uint8_t current_data; // 1. 先读取当前显存中该位置的数据(避免影响其他像素) // 注意:SSD1306在页地址模式下,连续写入会自动列地址递增,但读取操作较复杂。 // 简易做法:在MCU端维护一个全尺寸的显存缓冲区(OLED_GRAM[8][128])。 // 这里假设我们使用了缓冲区。 current_data = OLED_GRAM[page][x]; // 2. 修改指定位 if (mode) { // 画亮 current_data |= (1 << bit_pos); } else { // 画暗 current_data &= ~(1 << bit_pos); } // 3. 将修改后的数据写回缓冲区,并更新到物理OLED OLED_GRAM[page][x] = current_data; OLED_Set_Pos(page, x); OLED_Write_Data(current_data); }这里引出了一个非常重要的优化策略:使用显存缓冲区(Frame Buffer)。即我们在STM32的内存中开辟一个二维数组OLED_GRAM[8][128],完全映射OLED的物理显存。任何画点、画线操作都先修改这个缓冲区,然后通过一个OLED_Refresh()函数将整个缓冲区一次性更新到OLED。这样做的好处是:
- 避免了频繁的、零碎的I2C通信,大幅提高效率。
- 方便实现局部刷新、动画等效果。
- 简化了“读-改-写”操作(如画点函数中需要先读取当前值)。
5.2 显示字符与字符串
显示字符的本质,就是根据字符的编码(通常是ASCII码),从一个预先做好的“字库”数组中,找到这个字符对应的点阵数据,然后将其绘制到缓冲区的指定位置。字库通常是8x16像素(16字节)或6x8像素(6字节)等规格。
// 假设有8x16 ASCII字库数组 const uint8_t ASCII_8x16[][16] void OLED_ShowChar(uint8_t x, uint8_t y, char chr, uint8_t size, uint8_t mode) { uint8_t i, j, temp; uint8_t x_pos = x, y_pos = y; if (size == 16) { // 显示8x16字符 chr = chr - ' '; // ASCII码偏移,字库从空格(0x20)开始存储 for (i = 0; i < 16; i++) { // 遍历16字节的字模数据 temp = ASCII_8x16[chr][i]; for (j = 0; j < 8; j++) { // 每个字节的8个bit if (temp & 0x80) { OLED_DrawPoint(x_pos + j, y_pos + i, mode); } else { OLED_DrawPoint(x_pos + j, y_pos + i, !mode); } temp <<= 1; } } } // 可以添加其他尺寸字体的支持 } // 显示字符串:循环调用显示字符函数即可 void OLED_ShowString(uint8_t x, uint8_t y, char *str, uint8_t size, uint8_t mode) { while (*str != '\0') { OLED_ShowChar(x, y, *str, size, mode); x += 8; // 字符宽度为8像素,显示完一个后x坐标增加 str++; if (x > OLED_WIDTH - 8) { // 简单的换行处理 x = 0; y += 16; // 行高为16像素 } } }5.3 显示数字与图像
显示数字(尤其是浮点数)和图像,思路是相通的:将其转换为字符串或提取图像的点阵数组,然后进行绘制。
显示数字:使用
sprintf将数字格式化为字符串,再调用OLED_ShowString。char buffer[20]; float temperature = 25.6; sprintf(buffer, "Temp: %.1fC", temperature); OLED_ShowString(0, 0, buffer, 16, 1);注意:
sprintf会占用较多资源,在资源紧张的单片机上慎用,可以考虑用更轻量的itoa、ftoa函数或自己实现转换。显示图像:首先需要将图片(如Logo)通过取模软件(如PCtoLCD2002)转换为C语言数组。取模时需注意扫描方式(水平/垂直)、字节顺序(高位在上/在下)是否与你的画点函数逻辑匹配。显示时,就是将该数组数据按坐标写入缓冲区。
6. 常见问题排查与调试技巧实录
即使代码逻辑正确,在实际焊接和调试中,你依然会遇到各种各样的问题。下面是我总结的几个最常见的问题及其排查思路。
6.1 屏幕完全不亮或全亮
| 现象 | 可能原因 | 排查步骤 |
|---|---|---|
| 屏幕完全不亮,无任何反应 | 1. 电源接错或电压不对(OLED通常是3.3V或5V)。 2. I2C地址错误。 3. 初始化序列未成功执行,特别是 0xAE(关显示)后未执行0xAF(开显示)。4. 电荷泵未启用( 0x8D, 0x14)。 | 1. 用万用表测量VCC和GND引脚电压。 2. 用逻辑分析仪或示波器抓取I2C总线波形,看起始信号、地址字节(0x78或0x7A)是否有应答。 3. 单步调试,确认初始化命令数组被完整发送。 4. 检查初始化序列,确保包含电荷泵使能命令。 |
| 屏幕全亮(所有像素点都亮) | 1. 对比度设置异常(命令0x81后的参数通常为0xCF左右,设为0xFF可能全亮)。2. 显存被全部写入了0xFF。 3. 硬件连接问题,如RESET引脚未正确初始化。 | 1. 检查初始化序列中的对比度命令值。 2. 检查清屏或刷新函数,是否错误地写入了0xFF。 3. 检查OLED模块的RESET引脚,通常需要先拉低再拉高以完成复位。 |
6.2 显示乱码、错位或镜像
| 现象 | 可能原因 | 排查步骤 |
|---|---|---|
| 字符显示为乱码 | 1. 字库数组与显示函数不匹配(如用了8x16字库但按6x8解析)。 2. 取模软件设置与代码扫描方式不匹配(如取模是逐列,代码是逐行)。 3. 字符编码问题(非ASCII字符)。 | 1. 确认OLED_ShowChar函数中的size参数与字库实际尺寸一致。2. 用取模软件生成一个简单的图形(如一个实心矩形),显示出来看是否正确,以验证取模设置。 3. 显示纯英文和数字测试。 |
| 显示内容上下或左右颠倒 | 1. SSD1306初始化命令中段重映射(0xA0/A1)和COM扫描方向(0xC0/C8)设置与屏幕物理连接不符。 | 1. 尝试修改初始化命令:将0xA1改为0xA0,或将0xC8改为0xC0,观察显示变化。这两个命令控制显示内容的镜像。 |
| 字符显示位置不对 | 1. 坐标计算错误,特别是换行逻辑。 2. 页地址和列地址设置函数 OLED_Set_Pos有误。 | 1. 在OLED_ShowString函数中,在每次更新x坐标后添加延时并观察,看是否超出边界。2. 单独测试 OLED_Set_Pos函数,看是否能正确将光标定位到指定页和列。 |
6.3 I2C通信失败
这是软件模拟I2C最常见的问题。没有逻辑分析仪的情况下,可以用“数字IO模拟法”初步判断:
- 检查引脚配置:确认SDA是否配置为开漏输出(GPIO_Mode_Out_OD)并开启了内部上拉(或外部接了上拉电阻)。
- 检查延时:将
Delay_us(5)等延时加大到Delay_us(10)或Delay_us(20),排除因主频过快导致时序不满足的问题。 - 检查应答:在
OLED_I2C_WaitAck()函数中,如果始终收不到应答(返回0),可以尝试在发送从机地址后,不检查应答直接继续发送。有些OLED模块对地址应答不严格。但这只是调试手段,稳定后应恢复应答检查。 - 用示波器或逻辑分析仪:这是最直接有效的方法。抓取SCL和SDA的波形,看起始信号、停止信号、数据位电平、ACK信号是否符合I2C协议规范。重点看SDA电平变化是否发生在SCL低电平期间。
6.4 显示闪烁或刷新慢
如果使用了OLED_Refresh()函数一次性刷新整个缓冲区,但依然感觉刷新慢或有闪烁:
- 优化
OLED_Refresh():这个函数需要循环发送128*8=1024字节的数据。确保在发送数据时,只发送了控制字节(0x40)一次,然后连续发送所有数据,而不是每发送一个数据字节就重复起始、地址、控制字节过程。这能极大提升速度。 - 局部刷新:如果只更新了屏幕一小部分区域,可以只刷新对应的页和列,而不是全屏刷新。这需要你记录哪些区域的显存被修改了。
- 提高I2C速率:在确保稳定的前提下,逐步减小
Delay_us()的延时值,提高通信频率。 - 检查主循环:确保主循环中没有不必要的阻塞延时。显示刷新应放在定时器中断或主循环快速执行。
7. 项目进阶与优化思路
当你成功点亮屏幕并显示基本信息后,可以考虑以下方向让项目更上一层楼:
7.1 实现多级菜单系统
这是嵌入式GUI的入门课。一个简单的菜单系统可以这样设计:
- 定义菜单结构体:包含菜单项ID、显示文本、上级菜单ID、下级菜单ID、对应的功能函数指针等。
- 维护当前状态:记录当前所在的菜单层级和选中的项。
- 与按键交互:通过按键(上、下、确认、返回)来切换选中项、进入子菜单、执行功能或返回上级。
- 刷新显示:根据当前状态,在
OLED_Refresh()中绘制菜单边框、选中项高亮(反白显示)和所有菜单项文本。
关键在于状态机的清晰设计,将用户输入、菜单状态和显示内容解耦。
7.2 移植轻量级图形库(如u8g2、LVGL)
如果你需要显示更复杂的图形、动画或控件,手动造轮子会很累。可以考虑移植成熟的图形库。
- u8g2:非常轻量,单色屏驱动支持极其丰富,API简单,适合资源紧张的STM32F1系列。你需要为其提供画点、初始化等几个底层回调函数。
- LVGL:功能强大,支持抗锯齿、动画、多种控件,但资源消耗也大,更适合STM32F4、H7等带有足够RAM和Flash的型号。移植LVGL需要提供显示缓冲区、输入设备接口和心跳时钟。
7.3 使用硬件I2C或SPI加速
当显示内容复杂、刷新率要求高时,软件模拟I2C可能成为瓶颈。此时可以切换到硬件I2C或SPI。
- 硬件I2C:使用STM32CubeMX配置I2C外设,生成代码。重点处理中断或DMA传输,并注意处理可能的错误状态(BUSY、AF等)。
- SPI:SPI是四线制(SCK, MOSI, MISO, CS),速度远快于I2C。很多OLED模块也支持SPI接口。切换后,只需重写底层的
OLED_Write_Cmd和OLED_Write_Data函数,上层应用代码几乎不用改动,这正是分层架构的好处。
7.4 低功耗优化
对于电池供电的设备,OLED的功耗需要关注。
- 利用睡眠命令:SSD1306提供了
0xAE(关闭显示)命令。在不需要显示时,发送此命令可以关闭像素点驱动,显著降低功耗。需要显示时再发送0xAF开启。 - 降低刷新率:如果不是必须实时更新,可以降低全屏刷新的频率,比如每500ms刷新一次。
- 关闭电荷泵:在进入深度睡眠前,可以发送命令
0x8D, 0x10关闭电荷泵。但唤醒后必须重新初始化电荷泵和显示。
最后,我个人最深刻的一个体会是:把显存缓冲区(Frame Buffer)用起来。早期我为了省那1KB的RAM,直接操作物理OLED,结果做任何动态效果都卡顿,调试也困难。自从用了缓冲区,不仅流畅度提升,实现滚动、局部更新、双缓冲消影等功能都变得轻而易举。这1KB的RAM投资,绝对是OLED显示项目中最划算的一笔。