文章目录
- 前言
- 一、控制原理
-
- [1.1 像素底层逻辑](#1.1 像素底层逻辑)
- [1.2 驱动OLED屏幕](#1.2 驱动OLED屏幕)
- 二、单片机控制OLED
-
- [2.1 通信原理](#2.1 通信原理)
- [2.2 代码实现](#2.2 代码实现)
-
- [2.2.1 写命令](#2.2.1 写命令)
- [2.2.2 OLED初始化代码](#2.2.2 OLED初始化代码)
- [2.2.3 测试程序](#2.2.3 测试程序)
- [2.2.4 画点函数](#2.2.4 画点函数)
- 三、移植Keysking的代码显示字模
-
- [3.1 代码移植](#3.1 代码移植)
- [3.2 注意事项](#3.2 注意事项)
- 总结
前言
单片机开发中我们经常会用到OLED显示屏,这篇文章对OLED的控制原理、如何与单片机通信,以及如何移植别人的代码,来实现自己想要的显示效果做出介绍,主要是为了复习查看,参考 B站Keysking 的 OLED教学视频 所写。
一、控制原理
1.1 像素底层逻辑
OLED,即有机发光二极管(Organic Light Emitting Diode)。我们以中景园电子的OLED为例进行说明。
OLED的分辨率是 128 × 64 ,也就是说屏幕上总共有 8192 个会发光的小点(像素)。如果你每次只控制一个点,不仅写代码累死,屏幕刷新也会非常慢,我们的单片机也没有这么多的控制引脚!
因此,就需要用到屏幕驱动芯片,下面以SSD1306屏幕驱动芯片 为例,进行说明。

为了高效地控制这8192个像素点,SSD1306芯片在硬件设计和软件驱动上,采用了一种 "降维打击"和"分层抽象" 的策略。
1.2 驱动OLED屏幕
SSD1306芯片并不支持你"随心所欲地控制任意一个单独的点"。
在最常用的工作模式下,它是按 "页(Page)" 来管理的。
- 分块 : 屏幕宽128,高64。它把高度切成了 8个横条,每个横条称为一"页"(Page 0 到 Page 7)。
- 1个字节控制一列: 每一"页"的高度刚好是 8个像素。
- 关键点 : 当STM32通过I2C给OLED发送 1个字节(8位二进制数,比如 0x01,即 0000 0001) 时,OLED会在当前位置竖着点亮对应的8个像素。
硬件层面上,你每次操作的最小单位不是1个点,而是竖排的8个点(1个字节)。
设置完一字节的8个像素后,列地址会自动+1.
二、单片机控制OLED
2.1 通信原理
SSD1306 是一个非常强大的多面手,它支持多达 5种 不同的通信接口,具体用哪种接口,是由芯片上的三个硬件引脚(BS0, BS1, BS2)的电平(接高电平还是低电平)来决定的。
我买的模块已经固定死了,也就是大家常见的 4针脚 I2C 总线通信的那款,需要两根线:SCL(时钟线,控制节奏) 和 SDA(数据线,传输内容)。

如上图所示,I2C通信的流程和它的数据帧格式是相对应的由起始位,寻址位,数据传输和停止位构成。
(1)第一句先喊"名字"(设备地址) :OLED模块通常有一个固定的I2C地址 ,大部分是 0x78(也有部分是0x7A,看模块背面的电阻怎么焊的)。每次通信,STM32必须先发 0x78,OLED听到叫自己,才会搭理你。
(2)第二句说明"意图"(控制字) :OLED芯片有两个截然不同的"大脑":一个是设置寄存器 (管亮度、对比度、在哪一页画图),另一个是显存GRAM(管哪个灯亮)。
- 如果STM32接着发 0x00 ,OLED就知道:"哦,接下来你要发的是命令(Command),比如让我清屏或者调节亮度。"
- 如果STM32接着发 0x40 ,OLED就知道:"哦,接下来你要发的是数据(Data),让我按顺序点亮屏幕上的灯。"
(3)第三句发送"内容"(具体的字节) :根据第二步的意图,发送具体的命令代码或像素数据。

2.2 代码实现
用HAL库实现,采用CubeMX + Keil 的方式,这里选择硬件I2C,使用单片机的I2C1外设,对引脚进行初始化。

在现代STM32开发中,我们不需要自己手动去拉高拉低引脚电平来模拟I2C时序,ST官方提供的 HAL库 中的 HAL_I2C_Master_Transmit 函数已经帮我们搞定了最底层的时序。
2.2.1 写命令
我们只需要一层一层往上写代码,先写一个函数用于向OLED发送指令
c
// OLED器件地址
#define OLED_ADDRESS 0x78
/**
* @brief 向OLED发送指令
*/
void OLED_SendCmd(uint8_t cmd){
uint8_t sendBuffer[2];
sendBuffer[0] = 0x00;
sendBuffer[1] = cmd;
HAL_I2C_Master_Transmit(&hi2c1,OLED_ADDRESS,sendBuffer,2,HAL_MAX_DELAY);
}
2.2.2 OLED初始化代码
SSD1306刚上电时是懵的,你必须发一长串特定的命令给它,配置它的时钟、电荷泵、扫描方向等。这串代码在所有的OLED驱动里都是固定的,直接抄就行(这就是查数据手册得来的) 。

参考数据手册中的命令,对OLED进行初始化操作
c
// ========================== OLED驱动函数 ==========================
/**
* @brief 初始化OLED (SSD1306)
* @note 此函数是移植本驱动时的重要函数 将本驱动库移植到其他驱动芯片时应根据实际情况修改此函数
*/
void OLED_Init(void){
OLED_SendCmd(0xAE); /*关闭显示 display off*/
OLED_SendCmd(0x20);
OLED_SendCmd(0x10);
OLED_SendCmd(0xB0);
OLED_SendCmd(0xC8);
OLED_SendCmd(0x00);
OLED_SendCmd(0x10);
OLED_SendCmd(0x40);
OLED_SendCmd(0x81);
OLED_SendCmd(0xDF);
OLED_SendCmd(0xA1);
OLED_SendCmd(0xA6);
OLED_SendCmd(0xA8);
OLED_SendCmd(0x3F);
OLED_SendCmd(0xA4);
OLED_SendCmd(0xD3);
OLED_SendCmd(0x00);
OLED_SendCmd(0xD5);
OLED_SendCmd(0xF0);
OLED_SendCmd(0xD9);
OLED_SendCmd(0x22);
OLED_SendCmd(0xDA);
OLED_SendCmd(0x12);
OLED_SendCmd(0xDB);
OLED_SendCmd(0x20);
OLED_SendCmd(0x8D);
OLED_SendCmd(0x14);
OLED_SendCmd(0xAF); /*开启显示 display ON*/
}
2.2.3 测试程序
我们对OLED进行了初始化配置,那么接下来写一段测试程序,测试它的显示效果。
这里我们从第0页,第0列开始绘图,观察实验现象,之前说过驱动芯片设置完一字节的像素后,列地址会自动+1。
这里,我们将要发送的数据,填入一个数组,方便滚屏操作。
类似这样,将页地址递增8次,就可以完成整个屏幕的设置。
c
void OLED_Test(void){
OLED_SendCmd(0xB0);//页地址,第0页
OLED_SendCmd(0x00);//列地址(0x00),第0列的低四位
OLED_SendCmd(0x10);//第0列高四位
uint8_t sendBuffer[] = {0x40,0xAA,0xAA,0xAA,0xAA,0xAA,0xAA,0xAA,0xAA,0xAA,0xAA,0xAA};//
HAL_I2C_Master_Transmit(&hi2c1,OLED_ADDRESS,sendBuffer,sizeof(sendBuffer),HAL_MAX_DELAY);
}
在main函数中,调用初始化函数,然后在 while 中调用 OLED_Test() 函数,你将看到下面左图的显示效果。

得到这样一个花的屏幕,是因为刚启动的SSD1306中的数据是随机的,这样数据为1亮,数据为0灭,就是这样的效果。
而我们实际想要的是右图的效果,因此,我们需要对OLED显示屏清屏。
具体做法是在单片机内存里建一个数组,模拟屏幕,将128*64个像素全部设置为0;然后把内存中的数组一次性"倒"入OLED芯片里.

OLED_NewFrame 函数实现清屏操作,OLED_ShowFrame的作用是将显存显示到屏幕。
c
// OLED参数
#define OLED_PAGE 8 // OLED页数
#define OLED_ROW 8 * OLED_PAGE // OLED行数
#define OLED_COLUMN 128 // OLED列数
// 显存
uint8_t OLED_GRAM[OLED_PAGE][OLED_COLUMN];
/**
* @brief 清空显存 绘制新的一帧
*/
void OLED_NewFrame(void){
for(int i=0;i<8;i++){
for(int j=0;i<128;j++){
OLED_GRAM[i][j] = 0;
}
}
}
/**
* @brief 将当前显存显示到屏幕上
* @note 此函数是移植本驱动时的重要函数 将本驱动库移植到其他驱动芯片时应根据实际情况修改此函数
*/
void OLED_ShowFrame(void){
static uint8_t sendBuffer[129];
sendBuffer[0] = 0x40;
for(uint8_t i=0;i<8;i++){
for(uint8_t j=0;j<128;j++){
sendBuffer[j+1] = OLED_GRAM[i][j];
}
OLED_SendCmd(0xB0+i);//设置页地址
OLED_SendCmd(0x00);//设置列地址低4位
OLED_SendCmd(0x10);//设置列地址高四位
HAL_I2C_Master_Transmit(&hi2c1,OLED_ADDRESS,sendBuffer,sizeof(sendBuffer),HAL_MAX_DELAY);
}
}
2.2.4 画点函数
我们想要绘制一个像素点该怎么处理,下面编写画点函数。

把OLED想象成一块画布,假设你要在坐标 (x, y) 画一个点:
- 算它在哪一页(Y坐标除以8):page = y / 8;
- 算它在这一页的第几位(Y坐标对8取余):bit = y % 8;
- 修改STM32里的数组(把这一位置1,其他位不动):OLED_GRAM[page][x] |= (1 << bit);
所有的字符、汉字显示,底层都在调用类似这样的改写内存的逻辑。
c
/**
* @brief 设置一个像素点
* @param x 横坐标
* @param y 纵坐标
* @param color 颜色
*/
void OLED_SetPixel(uint8_t x,uint8_t y){
//判断X或Y是否越界
if(x>=128 || y>=64) return;
OLED_GRAM[y/8][x] |= 0x01 << (y % 8);
}
刷新屏幕 :当你把想画的点、想写的字都在STM32的 OLED_GRAM 数组里修改好之后,调用一个 OLED_ShowFrame() 函数。这个函数会以极快的速度,把这1024个字节一次性打包扔给OLED屏幕。屏幕瞬间显示出你画的内容!
至此,OLED的基础配置完成。
当有一天,想换一个SPI接口的屏幕,或者想把屏幕移植到另一款单片机(比如ESP32、51单片机)上时,你只需要修改最底层的 OLED_SendCmd 函数。只要搞定了发命令和发数据的方式,上面几层的画点、画字、刷新代码,完全不用改,直接照搬! 这就是理解底层驱动逻辑的最大意义。
三、移植Keysking的代码显示字模
3.1 代码移植
B站 UP Keysking 做了详细的OLED显示屏教学,并且在他的开源网站 波特律动文档站 给了丰富的驱动文件,因此我们就无需自己再去实现字模和图像的函数了,直接从网站下载移植即可。

并且他制作的取模软件,不仅可以取字模,还可以实现图片的效果!

3.2 注意事项

移植后的代码,一般包含4个文件,OLED.c 和 OLED.h 是OLED显示屏的驱动文件;而 font.c 和 font.h 是字体库,我们取模后要将字模或者图模存放在这里面,如果是图片则需要在front.h文件中进行声明!
看看自己的驱动芯片通信方式和引脚是什么,然后修改引脚和通信方式。
如果使用Keil,编码方式要改为UTF-8,否则无法显示中文 。

图片显示效果如下

总结
以上为所有内容,有了这个驱动文件之后,我们的OLED可以有更加丰富的显示效果!