Skip to content

0.96寸OLED显示屏详解 ​

我们之前讲了 LCD1602,今天我们将它的进阶模块——OLED。它接线更少,性能更强,也能显示中文和图像了。

大家在学习单片机的时候是否会遇到调试的问题呢?例如 “这串代码我到底运行成功了没有” ,我相信很多刚开始学习单片机的小白一般都遇到过这样的问题。但是单片机不像电脑,是没有屏幕也没有输出结果窗口的。那么我们该如何得知调试的结果呢?有一个方法是通过串口打印在运行成功代码会向串口发送特定的字符到电脑上,但是这个方法的弊端是如果单片机被放置到一个实际的项目当中,就无法通过串口来得知结果了。还有一个方法就是将结果打印到屏幕上,当然我们的单片机是没有屏幕的,所以我们需要为单片机增添一个外置的屏幕模块。我们今天就来学习一下如何使用 OLED 屏幕模块。

OLED(Organic Light-Emitting Diode)具有高亮度、高对比度和快速响应的特点,能够提供生动逼真的图像和文字显示效果。由于不需要背光源,OLED 屏更薄、更轻、更省电。它在智能手机、可穿戴设备、汽车仪表盘等领域得到广泛应用。本文将通过介绍 OLED 的工作原理、特性和应用案例,带大家深入了解 OLED。

1. 源码下载及前置阅读 ​

  • STM32F103C8T6模板工程

链接:https://pan.baidu.com/s/1gJigWypYfwLo2cn2nP2DGg?pwd=yqya 提取码:yqya

  • 本文的源码(显示字符、汉字、图像)

链接:https://pan.baidu.com/s/1L5jptEjo-IocwNmM_GBXWA?pwd=wla6 提取码:wla6

  • 本文的源码(播放视频)

链接:https://pan.baidu.com/s/1zhDFfwZGPHxgorCWY2mAMA?pwd=gu2w 提取码:gu2w

如果你是嵌入式开发小白,那么建议你先读读下面几篇文章。

往期教程,有兴趣的小伙伴可以看看。

2. IIC通信 ​

OLED 屏幕模块是通过 IIC 进行通信的,什么是 IIC 通信呢?

2.1 IIC通信介绍 ​

IIC 通信通常也叫 I2C,I2C( Inter-Integrated Circuit ),中文应该叫集成电路总线,它是一种串行通信总线,使用多主从架构,是由飞利浦( Philips )公司在 1980 年代初设计的一种串行、半双工的通信,主要用于近距离、低速的芯片之间的通信。I2C 总线有两根双向的信号线,一根数据线 SDA 用于收发数据,一根时钟线 SCL 用于通信双方时钟的同步。I2C 总线硬件结构简单,简化了 PCB 布线,降低了系统成本,提高了系统可靠性,由于它的简单性,它被广泛用于微控制器与传感器阵列,显示器,IoT 设备,EEPROM 等之间的通信。

I2C 通信协议的特点:

  • 同步、半双工;

  • 只需要两条总线;

  • 所有组件之间都存在简单的主/从关系,连接到总线的每个设备均可通过唯一地址进行软件寻址;

  • I2C 是真正的多主设备总线,可提供仲裁和冲突检测;

  • 没有严格的波特率要求,例如使用 RS232 ,主设备生成总线时钟;

I2C 总线是一种多主机总线,连接在 I2C 总线上的器件分为主机和从机。主机每一次发起和结束一次通信,从机只能被动呼叫;当总线上有多个主机同时启用总线时,I2C 也具备冲突检测和仲裁的功能来防止错误产生。总线具有极低的电流消耗,抗噪声干扰能力强,增加总线驱动器可以使总线电容扩大10倍,传输距离达到 15m ;兼容不同电压等级的器件,工作温度范围宽。

I2C 总线可以通过外部连线进行在线检测,便于系统故障诊断和调试,故障可以立即被寻址,软件也有利于标准化和模块化,缩短开发时间。

每个连接到 I2C 总线上的器件都有一个唯一的地址(7bit),且每个器件都可以作为主机也可以作为从机(但是同一时间主机只能有一个),总线上的器件增加和删除不影响其他器件正常工作。I2C 总线在通信时总线上发送数据的器件为发送器,接收数据的器件为接收器。

I2C 总线上可挂接的设备数量受总线的最大电容 400pF 限制。串行的8位双向数据传输速率在标准模式下可达 100Kbit/s ,快速模式下可达 400Kbit/s ,高速模式下可达 3.4Mbit/s 。

2.2 IIC通信协议 ​

  • 那么 I2C 是如何实现通信的呢?

    1. 主机发送起始信号然后启用总线;
    2. 主机发送一个字节数据指明从机地址和后续字节的传送方向;
    3. 被寻址的从机发送应答信号去回应主机;
    4. 发送器发送一个字节数据;
    5. 接收器发送应答信号回应发送器;
    6. 接下来会一直重复 4 和 5 的步骤直到通信完成后主机发送停止信号并释放总线。
  • 起始信号与停止信号

    当 SCL 为高电平时,SDA 产生下降沿,就是电平由高变低表示起始信号;

    当 SCL 为高电平时,SDA 产生上升沿,就是电平由低变高表示停止信号;

    当 SCL 与 SDA 同时为高电平时,表示当前为空闲状态。

    起始信号和停止信号都是由主机发出,起始信号产生后总线处于占用状态,停止信号产生后总线被释放,处于空闲状态。

  • 应答信号与非应答信号

    I2C 总线上的所有数据都是以 8bit 字节传输的,发送器每发送一个字节,就在第 9 个时钟开始时释放数据线,由接收器反馈一个应答信号。应答信号为低电平时,规定为有效应答位(ACK),表示接收器已经成功地接收了该字节;应答信号为高电平时,规定为非应答位(NACK),一般表示接收器接收该字节没有成功。对于反馈有效应答位 ACK 的要求是,接收器在第 9 个时钟脉冲之前的低电平期间将 SDA 线拉低,并且确保在该时钟的高电平期间为稳定的低电平。 如果接收器是主控器,则在它收到最后一个字节后,发送一个 NACK 信号,以通知被控发送器结束数据发送,并释放 SDA 线,以便主控接收器发送一个停止信号。

2.3 软件模拟IIC通信 ​

STM32 的硬件 I2C 存在着一些 BUG ,有时候莫名会出现问题,所以大多数情况下都会使用软件模拟 I2C 。使用软件模拟 I2C 主要是方便程序的移植,在使用时只需要修改一下相应的 GPIO 口即可。而 oled 屏幕模块就是使用软件模拟 I2C 通信的,我们将会在下文学习软件模拟 I2C 通信。

3. OLED屏幕 ​

OLED 屏幕模块型号多种多样,屏幕大小、引脚(4P 支持 I2C,7P 支持 I2C 和 SPI)、字体颜色不同。本次我们以 0.96 寸,4P,白色为例来讲解,一通百通。

3.1 0.96OLED屏幕介绍 ​

0.96 寸 4P OLED 屏幕模块是一种显示屏模块,它包括一个 0.96 英寸的 OLED 显示屏和四个引脚。这种 OLED 屏幕模块通常用于嵌入式系统和小型电子设备中,可以显示文本、图像和其他类型的信息。由于其小尺寸和低功耗,也常用于智能手表、健康追踪器和其他便携式设备中。

下面这个是一款常见的 0.96 寸四针 OLED 屏幕模块,某宝价格在 7~15 左右。

0.96OLED 参数:

  • 驱动芯片:SSD1306
  • 分辨率:128 × 64
  • 显示尺寸:0.96英寸
  • 外形尺寸:27.5 × 27.8(mm)
  • 玻璃尺寸:26.7 × 19.26 × 1.4(mm)
  • 显示区域:21.74(W)× 10.864(mm)
  • 视角:全视角
  • 工作电压:3.3V ~ 5V
  • 工作温度:-20℃ ~ 70℃
  • 存储温度:-30℃ ~ 80℃
  • 使用寿命:>=16000 小时
  • 支持接口:I2C

参考接线如下:

OLEDSTM32备注
GNDG电源负极
VCC3.3/5V电源正极
SCLB8(SCL1)/B10(SCL2)时钟线
SDAB9(SDA1)/B11(SDA2)数据线

0.96OLED 屏幕模块的优点有:

  1. 高对比度和高亮度:OLED 屏幕模块可以提供深黑色背景和明亮的字色,因此可以实现高对比度和高亮度的显示效果;
  2. 自发光:OLED 屏幕模块不需要背光灯,因此可以实现更薄、更轻的设计;
  3. 响应速度快:OLED 屏幕模块的响应速度非常快,可以实现流畅的动画和视频效果;
  4. 视角宽:OLED 屏幕模块的视角非常宽,即使从不同角度观察,图像和文字也能保持清晰。

0.96OLED 屏幕模块的缺点有:

  1. 显示寿命短:与 LCD 显示屏相比,OLED 的显示寿命相对较短,因为它的有机材料会随着时间的推移而逐渐失效;
  2. 显示面积小:0.96 寸 OLED 屏幕模块的显示面积相对较小,因此不适合用于需要大屏幕的应用。

3.2 SSD1306介绍 ​

接下来我们介绍一下 OLED 屏模块的芯片——SSD1306。

SSD1306 是一款带控制器的用于 OLED 点阵图形显示系统的单片 CMOS OLED/PLED 驱动器。它由 128 个 SEG(列输出)和 64 个 COM(行输出)组成。该芯片专为共阴极 OLED 面板设计。 SSD1306 内置对比度控制器、显示 RAM(GDDRAM)和振荡器,以此减少了外部元件的数量和功耗。该芯片有 256 级亮度控制。数据或命令由通用微控制器通过硬件选择的 6800/8000 系通用并行接口、I2C 接口或串行外围接口发送。该芯片适用于许多小型便携式应用,如手机副显示屏、MP3 播放器和计算器等。

SSD1306 芯片结构图:

SSD1306 芯片引脚图:

SSD1306 芯片引脚说明:

  • NC:空引脚,不要短接;

  • VCC:显示面板驱动电源引脚;

  • VCOMH:用于 COM 的高电平电压输出的引脚,与 VSS 之间应该连接一个电容;

  • IREF:段输出电流参考脚,VSS 间应连接一个电阻,以将 IREF 电流保持在 12.5uA ;

  • D[7~0]:连接到 MCU 的 8 位双向数据总线,不同 MCU 总线接口有不同用法;

  • E (RD#):与 6800/8080 通用并行总线接口相关,使用串行接口时作拉高处理;

  • R/W#(WR#):与 6800/8080 通用并行总线接口相关,使用串行接口时作拉高处理;

  • D/C#:数据/命令控制脚,不同 MCU 总线接口有不同用法;

  • RES#:复位信号引脚,低电平有效;

  • CS#:芯片片选引脚,低电平有效;

  • B[2:0]:MCU 总线接口选择脚。通过配置 B0~B2 来选择不同的 MCU 总线接口;

  • VDD:芯片逻辑器件供电引脚;

  • VSS:接地引脚;

  • CL:外部时钟输入引脚。用外部时钟时,此引脚输入外部时钟信号。用内部时钟时应连接到 VSS ;

  • CLS:内部时钟启用引脚。拉高时内部时钟启用,拉低时内部时钟禁用;

  • FR:用于输出 RAM 写同步信号,不使用应当 NC pin 处理;

  • TR0~TR6:测试保留引脚,当做 NC pin 处理;

  • VLSS:模拟接地引脚,在外部连接到 VSS ;

  • SEG0~SEG127:列输出脚,OLED 关闭时,处于 VSS 状态;

  • COM0~COM63:行输出脚,OLED 关闭时,处于高阻抗状态。

SSD1306 的初始化过程:

当 RES# 输入低电平时

  1. 首先关闭显示(AEH)
  2. 然后进入 128x64 显示模式
  3. 接着恢复到默认的 SEG 和 COM 映射关系(A0H,D3H-00H)
  4. 清除串行接口中移位寄存器内的数据
  5. GDDRAM 显示开始行设为0(40H)
  6. 列地址计数器重置为 0
  7. 恢复到默认的 COM 扫描方向(C0H)
  8. 对比度寄存器初始化为 7FH(81H-7FH)
  9. 正常显示模式(A4H)

基础配置: IIC 总线包含从机地址位 SA0,数据信号线 SDA(SDAOUT/D2 输出和 SDAIN/D1 输入)和时钟信号线 SCL组成。SDA 和 SCL 线都必须接上拉电阻,RES#用来初始化芯片。 IIC设备在数据传输之前都必须识别从机地址。SSD1306 的从机地址有 0111100b 和 0111101b 两种,通过将 SA0(D/C#)脚上拉到高电平可以设置从机地址第七位为 1,将SA0(D/C#)脚下拉到低电平可以设置从机地址第七位为 0。通过SA0(D/C#)脚的上拉和下拉来设置从机地址,从而令总线上可以存在最多 2 个 SSD1306 驱动器。

B7B6B5B4B3B2B1B0
0111100R/W#

​ SDAOUT/D2和SDAIN/D1连接到一起作为SDA。SDAIN引脚必须连接到SDA,SDAOUT引脚可以不连接。当SDAOUT引脚不连接,应答信号将会被12C总线忽略。

写入时序:

  1. 主机先发起开始(START)信号,然后发送 1byte 首字节,包括从机地址(7位)和读写数据位(1 位,最低位,0 为写模式),驱动器识别从机地址为本机地址之后,将会发出应答信号(ACK)。

  2. 主机收到从机(驱动器)的应答信号之后,随后传输 1byte 控制字节。一个控制字节主要由CO 和 D/C# 位后面再加上六个 0 组成的。如果 Co 为 0 ,后面传输的信息就只包含数据字节。D/C# 位决定了下个数据字节是作为命令还是数据。D/C# 为 0 时,下一个数据被视为命令;DC# 为 1 时,下一个数据被视为显示数据,存储到 GDDRAM 中。

  3. 收到控制字节 ACK 信号之后,传输要写入的数据字节。

  4. 传输完毕之后主机发出结束(STOP)信号。

3.3 GDDRAM介绍 ​

SSD1306 的 GDDRAM 用于存储将显示在 128×64 单色点阵屏上的图像,其中每个位对应屏幕上的一个像素。此 RAM 大小为 128×64 位,被分成八个页面,如下表格所示

Page 0
Page 1
Page 2
Page 3
Page 4
Page 5
Page 6
Page 7
  • 每个页面的分配情况为:

    PAGE0(COM0-COM7)

    PAGE1 -> COM8-COM15

    PAGE2 -> COM16-COM23

    PAGE3 -> COM24-COM31

    PAGE4 -> COM32-COM39

    PAGE5 -> COM40-COM47

    PAGE6 -> COM48-COM55

    PAGE7 -> COM56-COM63

  • 若行重映射(Row re-mapping),则为:

    PAGE0 -> COM56-COM63

    PAGE1 -> COM48-COM55

    PAGE2 -> COM40-COM47

    PAGE3 -> COM32-COM39

    PAGE4 -> COM24-COM31

    PAGE5 -> COM16-COM23

    PAGE6 -> COM8-COM15

    PAGE7 -> COM0-COM7

  • 段的写入顺序:

    SEG0 至 SEG127

  • 若列重映射(column re-mapping),则为:

    SEG127 至 SEG0

    当向 GDDRAM 写入一个字节时, 此字节将写入当前页的当前段(当前列, 即列地址指针(column address pointer)指向的列), 字节的 LSB 写入当前最低列, MSB 写入当前最高列。

3.4 GDDRAM寻址模式 ​

GDDRAM 有三种寻址模式:

  1. 页寻址

    在页寻址模式下,读写显示 RAM 后,列地址指针自动增加 1。如果列地址指针到达列结束地址,则列地址指针将被重置为列起始地址,而页面地址指针将不会被更改。用户必须设置新的页面和列地址,才能访问下一页 RAM 内容。页面寻址模式的 page 和列地址点的移动顺序如图:

  2. 垂直寻址

    在水平寻址模式下,读写显示 RAM 后,列地址指针自动增加 1。如果列地址指针到达列结束地址,则将列地址指针重置为列起始地址,页地址指针增加 1。水平寻址模式下的页面和列地址点的移动顺序如图所示(起始页为 0,终止页为 7。起始列为 0,终止列为127)。当列和页面地址指针都到达结束地址时,指针将重置为列起始地址和页面起始地址(图中的虚线)。

  3. 水平寻址

    在垂直寻址模式下,读写显示 RAM 后,页面地址指针自动增加 1。如果页面地址指针到达页面结束地址,则页面地址指针重置为页面起始地址,列地址指针增加 1。垂直寻址模式的页面和列地址点的移动顺序如图所示(起始页为 0,终止页为 7。起始列为 0,终止列为127)。当列和页面地址指针都到达结束地址时,指针将重置为列起始地址和页面起始地址(图中的虚线)。

3.5 指令详解 ​

3.5.1 基础指令 ​

1.设置对比度 (指令为 “81H+A[7:0]“ )   这是一条双字节的指令,由第二条指令指定要设置的对比度级数。这条指令用于设置屏幕的对比度。   A[7:0] 从 00H~FFH 分别指定对比度为 1~256 级。SEG(段)输出的电流大小随对比度级数的增加而增加。

2.设置全屏全亮 (指令为 ”A4H / A5H“ )   这是一条单字节的指令,用于开关屏幕全亮模式。   A4H 将屏幕显示模式设置为正常模式,这时屏幕将会显示 GDDRAM 中输出的数据。   A5H 将屏幕显示模式设置为全亮模式,这时屏幕将会无视 GDDRAM 中的数据,并点亮全屏。   通过 A5H 设置全屏点亮之后可以通过 A4H 来回复正常显示。

3.设置正常/反转显示 (指令为 ”A6H / A7H“ )   这是一条单字节的指令,用于设置屏幕的显示模式。   A6H 设置显示模式为 1 亮 0 灭,而 A7H 设置显示模式为 0 亮 1 灭。

4.开关显示屏 (指令为 ”AEH / AFH” )   这是一条单字节的指令。   AEH 关闭屏幕,而 AFH 开启屏幕。   屏幕关闭时,所有 SEG 和 COM 的输出被分别置为 Vss 和高阻态。

3.5.2 硬件指令 ​

1.设置GDDRAM起始行 (指令为 ”40H~7FH“ )   这是一条单字节的指令。   高 2 位规定为 01b,由低 6 位的取值来决定起始行。整体指令从 40H~7FH 分别设置起始行为 0~63。

2.设置SEG映射关系 (指令为 ”A0H / A1H“ )   这是一条单字节的指令。   A0H 设置 GDDRAM 的 COL0 映射到驱动器输出 SEG0 。   A1H 设置 COL127 映射到 SEG0 。

3.设置COM 扫描方向 (指令为 ”C0H / C8H“ )   这是一条单字节的指令。   C0H 设置 从 COM0 扫描到 COM[N-1] ,N 为复用率。   C1H 设置 从 COM[N-1] 扫描到 COM0 。

4.设置复用率 (指令为 ”A8H+A[5:0]“ )   这是一条双字节的指令,由 A[5:0] 指定要设置的复用率。   复用率(MUX ratio)即选通的 COM 行数,不能低于 16 ,通过 A[5:0] 来指定。   A[5:0] 高两位无规定视为 0 ,所以第二条指令从 0FH~3FH 的取值设置复用率为 1~64(即A[5:0]+1)。A[5:0] 从 0 到 14 的取值都是无效的。

5.设置垂直显示偏移 (指令为 ”D3H+A[5:0]“ )   这是一条双字节的指令,由 A[5:0] 指定偏移量。   垂直显示偏移即整个屏幕向上移动的行数,最顶部的行会移到最底行。   A[5:0] 高两位无规定视为 0 ,所以第二条指令从 0FH~3FH 的取值设置垂直偏移为 0~63 。

6.设置COM硬件配置 (指令为 ”DAH+A[5:4]“ )   这是一条双字节指令,由 A[5:4] 进行设置。   A[5] 位设置 COM 左右反置,A[4] 用来设置序列/备选引脚配置,其他位有规定,规定如下所示。

SSD1306 的 COMn 引脚一共有左边 COM32~COM63 和右边 COM0~COM31 共 64 个(金手指面朝上方)。通过设置 A[5] 可以让左右 COM 引脚的输出互换。A[5]=0 时禁止左右反置,A[5]=1 时启用左右反置。   COM 引脚的排列有序列和奇偶间隔(备选)两种,通过 A[4] 进行设置。A[4]=0 时使用序列 COM 引脚配置,A[5]=1 时使用奇偶间隔(备选)COM 引脚配置。

3.5.3 地址指令 ​

1.设置GDDRAM寻址模式 (指令为 “20H+A[1:0]” )  这是一条双字节的指令,由 A[1:0] 指定要设置的地址模式。   A[1:0]=00b 时为水平地址模式;

​ A[1:0]=01b 时为垂直地址模式;

​ A[1:0]=10b 时为页地址模式;

​ A[1:0]=11b 时为无效指令;  由于第二条指令前 6 位值无规定,所以直接用 0 替代,可以得到:00H- 水平;01H- 垂直;02H 页。

2.设置起始/终止列地址 (指令为 ”21H+A[6:0]+B[6:0]“ )   这是一条三字节的指令,由 A[6:0] 指定起始列地址,B[6:0] 指定终止列地址。   同样,由于前 1 位值无规定,所以:A[6:0] 和 B[6:0] 从 00H~7FH 的取值指定起始/终止列地址为 0~127。   这条指仅在水平/垂直模式下有效,用来设置水平/垂直模式的初始列和结束列

3.设置起始/终止页地址 (指令为 “22H+A[2:0]+B[2:0]“ )   这是一条三字节的指令,由 A[2:0] 指定起始也地址,B[2:0] 指定终止页地址。   由于前 5 位值无规定,所以:A[2:0]和B[2:0]从 00H~07H 的取值指定起始/终止页地址为 0~7。   这条指仅在水平/垂直模式下有效,用来设置水平/垂直模式的初始页和结束页

4.设置起始列地址低位 (指令为 ”00H~0FH“ )   这是一条单字节的指令。   高 4 位恒定为 0H,低4位为要设置的起始列地址的低 4 位。这条指令仅用于页寻址模式。

5.设置起始列地址高位 (指令为 ”10H~1FH“ )   这是一条单字节的指令   高 4 位恒定为 1H ,低 4 位为要设置的起始列地址的高 4 位。这条指令仅用于页寻址模式。

6.设置页地址 (指令为 ”B0H~B7H“ )   这是一条单字节的指令   高 4 位恒定为 BH,第 5 位规定为 0 ,低 3位用于设置页地址,从 B0H~B7H 分别设置起始页为 0~7。这条指令仅用于页寻址模式下。

3.5.4 时序指令 ​

1.设置显示时钟分频数和fosc (指令为 ”D5H+A[7:0]“ )

​ 这是一条双字节的指令,它用于设置显示时钟分频数和振荡器频率的。

​ A[3:0]用于指定分频数,分频数 = A[3:0]+1 。

​ A[7:4]用于指定振荡频率,振荡频率 = A[7:4] 。

2.设置预充电周期 (指令为 ”D9H+A[7:0]“ )

​ 这是一条双字节的指令,用于设置预充电周期的。

​ 在预充电期间的持续时间是以 DCLK 的数量计算的。复位值为 2 DCLK 。

3.设置VCOMH输出的高电平 (指令为 ”DBH+A[6:4]“ )

​ 这是一条双字节的指令,用于设置 Vcomh 输出的。

4.空操作 (指令为 ”E3H“ )

​ 这是一条单字节的指令,它没有任何操作。

3.5.5 滚动指令 ​

1.禁止水平滚动(指令为 ”2EH“ )

这是一条单字节的指令,它用于禁止水平滚动。

停止由指令 26H/27H/29H/2AH 配置的滚动。(通过这条命令的停止滚动后需要重写 GDDRAM 中的内容)

2.启用水平滚动(指令为 ”2FH“ )

这是一条单字节的指令,它用于启用水平滚动。

启用由指令 26H/27H/29H/2AH 配置的滚动。(后一个配置会覆盖前面的配置。正确的命令序列应该是:配置命令 -> 启动命令,例如:2AH,2FH )。

3.连续水平滚动设置(指令为 ”26H/27H+A[7:0]+B[2:0]+C[2:0]+D[2:0]+E[7:0]+F[7:0]“ )

这是一条七字节的指令,它用于设置连续水平滚动。

26H 为水平右滚动,27H 为水平左滚动。(每次水平滚动的时候只滚动 1 列)。

A[7:0]:为空比特,要设为 00H 。

B[2:0]:设置滚动起始页地址 (起始页必须要小于结束页)。

输入指令 ( 00H~07H ):

指令00H01H02H03H04H05H06H07H
表示PAGE0PAGE1PAGE2PAGE3PAGE4PAGE5PAGE6PAGE7

C[2:0]:设置滚动速度(多少帧滚动一次)。

输入指令( 00H~07H ):

指令00H01H02H03H04H05H06H07H
表示5帧64帧128帧256帧3帧4帧25帧2帧

D[2:0]:设置滚动结束页地址(结束页必须要大于起始页)

输入指令 ( 00H~07H ):

指令00H01H02H03H04H05H06H07H
表示PAGE0PAGE1PAGE2PAGE3PAGE4PAGE5PAGE6PAGE7

E[7:0]:为空比特,要设为 00H 。

F[7:0]:为空比特,要设为 FFH 。

4.连续水平和垂直滚动设置(指令为 ”29H/2AH+A[7:0]+B[2:0]+C[2:0]+D[2:0]+E[5:0]“ )

这是一条六字节的指令,它用于设置连续水平和垂直滚动。

​ A[7:0]:为空比特,要设为 00H 。

B[2:0]:设置滚动起始页地址 (起始页必须要小于结束页)。

输入指令 ( 00H~07H ):

指令00H01H02H03H04H05H06H07H
表示PAGE0PAGE1PAGE2PAGE3PAGE4PAGE5PAGE6PAGE7

C[2:0]:设置滚动速度(多少帧滚动一次)。

输入指令( 00H~07H ):

指令00H01H02H03H04H05H06H07H
表示5帧64帧128帧256帧3帧4帧25帧2帧

D[2:0]:设置滚动结束页地址(结束页必须要大于起始页)

输入指令 ( 00H~07H ):

指令00H01H02H03H04H05H06H07H
表示PAGE0PAGE1PAGE2PAGE3PAGE4PAGE5PAGE6PAGE7

E[5:0]:设置垂直偏移量,00H~3FH 表示垂直偏移量为 0~63 行(为 0 时则无垂直滚动)。

5.设置垂直滚动区域 (指令为 ”A3H“ )

这是一条单字节的指令,它用于设置垂直滚动的区域。

A[5:0]:设置顶部固定区域行数,A[5:0]+B[6:0] < MUX ratio /复用率。

B[6:0]:设置滚动区域行数,垂直滚动偏移必须要小于滚动区域行数。

4. 项目实战 ​

4.1 硬件准备 ​

本教程使用的硬件如下:

  • 单片机:STM32F103C8T6
  • 屏幕:0.96 OLED 模块
  • 烧录器:ST-Link V2

接线如下:

OLEDSTM32F103C8T6
GNDGND
VCC3.3
SCLB8
SDAB9

烧录的时候接线如下表,如果不会烧录的话可以看我之前的文章【STM32下载程序的五种方法】。

ST-Link V2STM32
SWCLKSWCLK
SWDIOSWDIO
GNDGND
3.3V3V3

接好如下图。开发板使用的是我们自绘的板子。大家也可以用自己的板子,只要是 STM32F103C8T6 主控芯片就行。

4.2 初始化OLED ​

首先我们需要为 OLED 屏幕模块做初始化才能够使用。0.96 寸 OLED 屏幕模块的驱动网上一大堆,随便下载一个移植即可,或者使用本文提供的源码 oled.c 。

首先需要创建一个 oled.c 和 oled.h 的文件。

打开 oled.h ,我们需要先配置引脚。本文将使用 B8 和 B9,oled.h 的代码如下:

c
#ifndef __OLED_H_
#define __OLED_H_

#include "stm32f1xx_hal.h"

#define OLED_ADDRESS	0x78     //oled地址(0x78/0x7A)

#define OLED_I2C_SCL_CLK()		__HAL_RCC_GPIOB_CLK_ENABLE()
#define OLED_I2C_SCL_GPIO  		GPIOB
#define OLED_I2C_SCL_PIN   		GPIO_PIN_8

#define OLED_I2C_SDA_CLK()   	__HAL_RCC_GPIOB_CLK_ENABLE()
#define OLED_I2C_SDA_GPIO  		GPIOB
#define OLED_I2C_SDA_PIN   		GPIO_PIN_9

#define OLED_SCL_RESET() 		HAL_GPIO_WritePin(OLED_I2C_SCL_GPIO,OLED_I2C_SCL_PIN,GPIO_PIN_RESET)  //SCL
#define OLED_SCL_SET() 			HAL_GPIO_WritePin(OLED_I2C_SCL_GPIO,OLED_I2C_SCL_PIN,GPIO_PIN_SET)

#define OLED_SDA_RESET() 		HAL_GPIO_WritePin(OLED_I2C_SDA_GPIO,OLED_I2C_SDA_PIN,GPIO_PIN_RESET)  //SDA
#define OLED_SDA_SET() 			HAL_GPIO_WritePin(OLED_I2C_SDA_GPIO,OLED_I2C_SDA_PIN,GPIO_PIN_SET)

void OLED_Display_On(void);														//开启OLED显示
void OLED_Display_Off(void);													//关闭OLED显示
void OLED_Init(void);															//OLED初始化
void OLED_Fill(uint8_t fill_Data);												//填充全屏(清除屏幕)
void OLED_Show_Char(uint8_t x, uint8_t y, uint8_t num, uint8_t size);			//显示一个字符
void OLED_Show_ChineseFont(uint8_t x, uint8_t y, uint8_t N, uint8_t size);		//显示汉字
void OLED_DrawBMP(uint8_t x0,uint8_t y0,uint8_t x1,uint8_t y1,uint8_t *bmp);	//显示图片

#endif

在 oled.c 中,我们需要初始化一下 GPIO 引脚,如下:

c
void OLED_IO_Init(void)         //初始化引脚
{
    GPIO_InitTypeDef GPIO_Initure;

    OLED_I2C_SCL_CLK();                                     //开启SCL时钟
    GPIO_Initure.Pin=OLED_I2C_SCL_PIN;                      //配置SCL引脚
    GPIO_Initure.Mode=GPIO_MODE_OUTPUT_PP;                  //推挽输出
    GPIO_Initure.Pull=GPIO_PULLUP;                          //上拉
    GPIO_Initure.Speed=GPIO_SPEED_FREQ_HIGH;                //高速
    HAL_GPIO_Init(OLED_I2C_SCL_GPIO, &GPIO_Initure);
    
    OLED_I2C_SDA_CLK();                                     //开启SDA时钟
    GPIO_Initure.Pin=OLED_I2C_SDA_PIN;                      //配置SDA引脚
    HAL_GPIO_Init(OLED_I2C_SDA_GPIO, &GPIO_Initure);
}

初始化完成之后我们需要配置一个 I2C 通信协议用于为 OLED 屏幕模块的通信。

首先需要配置时序,如下:

c
void OLED_I2C_Start(void)       //发送起始信号
{
    OLED_SCL_SET();
    OLED_SDA_SET();
    OLED_SDA_RESET();
    OLED_SCL_RESET();
}

void OLED_I2C_Stop(void)        //发送停止信号
{
    OLED_SCL_SET() ;
    OLED_SDA_RESET();
    OLED_SDA_SET();
}

void OLED_I2C_Ack(void)         //应答信号
{
    OLED_SCL_SET();
    OLED_SCL_RESET();
}

void OLED_I2C_WriteByte(uint8_t data)        //写入一个字节
{
    uint8_t i;
    uint8_t m,da;
    da = data;
    OLED_SCL_RESET();
    for(i = 0; i < 8; i++)
    {
        m = da;
        m = m&0x80;
        if(m == 0x80)
          OLED_SDA_SET();
        else 
            OLED_SDA_RESET();
            da = da << 1;
          OLED_SCL_SET();
          OLED_SCL_RESET();
    }
}

void OLED_WriteCmd(uint8_t Cmd)			//写命令
{
    OLED_I2C_Start();
    OLED_I2C_WriteByte(0x78);
    OLED_I2C_Ack();
    OLED_I2C_WriteByte(0x00);
    OLED_I2C_Ack();
    OLED_I2C_WriteByte(Cmd); 
    OLED_I2C_Ack();
    OLED_I2C_Stop();
}

void OLED_WriteDat(uint8_t Data)		//写数据
{ 
    OLED_I2C_Start();
    OLED_I2C_WriteByte(0x78);
    OLED_I2C_Ack();
    OLED_I2C_WriteByte(0x40);
    OLED_I2C_Ack();
    OLED_I2C_WriteByte(Data); 
    OLED_I2C_Ack();
    OLED_I2C_Stop();
}

这样,OLED 屏幕模块的通信协议就配置好了。

接着我们需要配置 OLED 屏幕模块的初始化,从上文的 OLED 模块学习中我们得知,OLED 屏幕模块需要给 SSD1306 驱动模块配置完才能使用,配置代码如下:

c
void OLED_Display_On(void)//开启OLED显示
{
	OLED_WriteCmd(0x8D);  //设置电荷泵
	OLED_WriteCmd(0x14);  //开启电荷泵
	OLED_WriteCmd(0xAF);  //开启显示,唤醒
}
  
void OLED_Display_Off(void)//关闭OLED显示
{
	OLED_WriteCmd(0x8D);  //设置电荷泵
	OLED_WriteCmd(0x10);  //关闭电荷泵
	OLED_WriteCmd(0xAE);  //关闭显示,休眠
}

void OLED_Init(void)		  //OLED初始化
{
	OLED_IO_Init();           //初始化IIC接口
	
	delay_ms(100); 			  //延时,很重要,保证oled准备就绪
	
	OLED_WriteCmd(0xAE);      //关闭显示
	OLED_WriteCmd(0x20);	  //设置内存地址模式	
	OLED_WriteCmd(0x02);	  //[1:0],00,列地址模式;01,行地址模式;10,页地址模式;默认10;
	OLED_WriteCmd(0xB0);	  //Set Page Start Address for Page Addressing Mode,0-7
	OLED_WriteCmd(0xC8);	  //设置COM扫描方向;bit3:0,普通模式;1,重定义模式 COM[N-1]->COM0;N:驱动路数
	OLED_WriteCmd(0x00);      //设置列低地址
	OLED_WriteCmd(0x10);      //设置列高地址
	OLED_WriteCmd(0x40);      //设定开始行地址
	OLED_WriteCmd(0x81);      //对比度设置
	OLED_WriteCmd(0xFF);      //亮度调节 0x00~0xff,越大越亮
	OLED_WriteCmd(0xA1);      //段重定义设置,bit0:0,0->0;1,0->127
	OLED_WriteCmd(0xA6);      //设置显示方式;bit0:1,反相显示;0,正常显示	
	OLED_WriteCmd(0xA8);      //--set multiplex ratio(1 to 64)
	OLED_WriteCmd(0x3F);      //默认0X3F(1/64) 
	OLED_WriteCmd(0xA4);      //0xa4,Output follows RAM content;0xa5,Output ignores RAM content
	OLED_WriteCmd(0xD3);      //设置显示偏移
	OLED_WriteCmd(0x00);      //-not offset
	OLED_WriteCmd(0xD5);      //设置显示时钟分频因子,震荡频率
	OLED_WriteCmd(0xF0);      //--set divide ratio
	OLED_WriteCmd(0xD9);      //设置预充电周期
	OLED_WriteCmd(0x22);
	OLED_WriteCmd(0xDA);      //设置COM硬件引脚配置
	OLED_WriteCmd(0x12);
	OLED_WriteCmd(0xDB);      //设置VCOMH 电压倍率
	OLED_WriteCmd(0x20);      //[6:4] 000,0.65*vcc;001,0.77*vcc;011,0.83*vcc
	OLED_WriteCmd(0x8D);      //--set DC-DC enable
	OLED_WriteCmd(0x14);
	OLED_WriteCmd(0xAF);      //开启显示
}

这样,OLED 屏幕模块的初始化配置就完成了。

4.3 让OLED亮起来 ​

接下来我们该如何使用 OLED 屏幕模块亮起来?

从上文 OLED 屏幕模块的学习中,我们可以知道 OLED 屏幕模块的显示原理,通过通信协议来操控指定的某些像素点来实现成像,那么我们代码可以这么写:

c
void OLED_Fill(uint8_t fill_Data)			//全屏填充(清除屏幕)
{
	uint8_t m,n;
	
	for(m = 0; m < 8; m++)
	{
		OLED_WriteCmd(0xB0+m);		//设置页地址(0~7)
		OLED_WriteCmd(0x00);		//设置显示位置—列低地址
		OLED_WriteCmd(0x10);		//设置显示位置—列高地址   
		
		for(n = 0; n < 128; n++)
		{
			OLED_WriteDat(fill_Data);
		}
	}
}

这样就可以了。

在 main.c 中代码如下:

c
int main(void)
{
    HAL_Init();                         /* 初始化HAL库 */
    sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
    delay_init(72);                     /* 延时初始化 */

		OLED_Init();   //oled初始化
		OLED_Fill(0xFF);//全屏点亮
		delay_ms(2000);
		OLED_Fill(0x00);//全屏熄灭
		delay_ms(2000);
	
		
    while(1)
    { 
		
	}		
}

编译下载。可以看到我们的 OLED 屏幕直接亮了起来,然后 2s 之后又灭掉了,说明我们的初始化很成功。

4.4 显示字符 ​

那么我们该如何显示一个字符呢?这里我们需要用到字库,可以在网上找适配的字库用,也可以直接使用本文的字库文件(codetab.h)。

在 oled.c 中,我们需要定义一个函数用来显示字符。通过循环遍历待写入的字节,得到一个我们想要显示的数,这个数需要事先被定义在字库当中。

OLED_Show_Char 函数中我们需要填入 x(0~127),y(0~7),显示的字符(需要用 ‘ ’ 包起来。在字库中定义),字符的大小(显示的字符大小也需要提前在字库中定义)。

c
void OLED_Show_Char(uint8_t x, uint8_t y, uint8_t num, uint8_t size)		//显示一个字符
{
	uint8_t i,j,page;
	
    num = num-' ';                  //得到偏移后的值
	page = size / 8;
	
	if(size % 8) page++;
	
	for(j = 0; j < page; j++)
	{
		OLED_SetPos(x, y+j);
		for(i = size / 2 * j; i < size / 2*(j+1); i++)
		{
			if(size == 12)
			{
				OLED_WriteDat(Ascii_F6X12[num][i]);
			}
			else if(size == 16)
			{
				OLED_WriteDat(Ascii_F8X16[num][i]);
			}
			else if(size == 24)
			{
				OLED_WriteDat(Ascii_F24X24[num][i]);
			}
		}
	}
}

这里我们可以使用一下上面的函数打印一个字符 ”A” ,main.c 代码如下:

c
int main(void)
{
    HAL_Init();                         /* 初始化HAL库 */
    sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
    delay_init(72);                     /* 延时初始化 */

	OLED_Init();    //oled初始化
	OLED_Fill(0x00);//清屏
				
    while(1)
    { 
			OLED_Show_Char(64,4,'A',16);
    }
}

编译下载。我们可以在 OLED 屏幕上看到一个字符 ‘A’ ,这样我们就成功的在 OLED 屏幕上显示一个字符了。

4.5 显示汉字 ​

我们该如何在 OLED 屏幕上显示汉字?首先我们需要一个取模软件,用于提取汉字。

本文使用的取模软件是 PCtoLCD2002 ,打开软件是这个样子的。

我们需要先配置一下软件,打开上面一排图标中的那个小齿轮

这样设置,如下图:

设置完成之后将模式改为字符模式并将字体大小改为16*16,如下图:

接着在输入框中输入汉字,然后点击生成字模就可以了。生成之后我们只需要复制下框中的代码即可,如图:

我们将复制好的代码粘贴到字库当中,在 Font_16x16 中,如图:

要在每个字的前面添加该字作为引索。

我们在 oled.c 中添加一个函数用于显示汉字,代码如下:

c
void OLED_Show_ChineseFont(uint8_t x, uint8_t y, uint8_t N, uint8_t size)		//显示汉字
{
	uint8_t i,j;
	for(j = 0; j < size/8; j++)
	{
		OLED_SetPos(x , y+j);
		for(i = size * j; i < size*(j+1); i++)
		{
			if(size == 16)
			{
				OLED_WriteDat(Font_16x16[N][i]);
			}
			else if(size == 24)
			{
				OLED_WriteDat(Font_24x24[N][i]);
			}
		}
	}
}

在 main.c 函数中,代码如下:

c
int main(void)
{
    HAL_Init();                         /* 初始化HAL库 */
    sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
    delay_init(72);                     /* 延时初始化 */

		OLED_Init();   //oled初始化
		
		OLED_Fill(0x00);//全屏熄灭
		
    while(1)
    { 		
		OLED_Show_ChineseFont(32,3,1,16);  //显示Font_16x16字库中的第一个汉字
		OLED_Show_ChineseFont(64,3,3,16);  //显示Font_16x16字库中的第二个汉字		
	}
}

编译下载之后,可以看到 OLED 屏幕上显示出了汉字。这样我们就完成了 OLED 屏幕显示汉字了。

需要注意,在编译下载之前需要打开右上角的小扳手,将编码格式改为 Chinese GB2312 (Simplified),这是汉字的编码格式,有时候,你在移植别人代码的时候,会发现别人代码的注释都是乱码,可能就是你没有改这个。

4.6 显示图像 ​

如何使用 OLED 屏幕来显示一张图片呢?

首先在 oled.c 中添加一个用于显示图片的函数,代码如下:

c
void OLED_DrawBMP(uint8_t x0,uint8_t y0,uint8_t x1,uint8_t y1,uint8_t *bmp)		//显示图片
{
	uint16_t j=0;
	uint8_t x,y;

	for(y = y0; y < y1; y++)
	{
		OLED_SetPos(x0,y);
		
		for(x = x0; x < x1; x++)
		{
			OLED_WriteDat(bmp[j++]);
		}
	}
}

OLED_DrawBMP 中 x0 ,y0 代表着 x 与 y 显示的起始位置,而 x1,y1 代表着 x 与 y 显示的终止位置。一般填图片的大小。而 bmp 则是填入图片的名称。

我们还是需要用到取模软件,打开取模软件 PCtoLCD2002 ,将模式调为图形模式,如下图:

这个取模软件目前只支持 BMP 的文件格式,我们可以通过 Photoshop 等软件,对图片进行处理,最后保存为 BMP 格式。

这里我们可以用 Photoshop 随便打开一张你想显示到 OLED 屏幕上的图片,打开图像/调整/阈值,对图片进行二极化(选择一个合适的阈值),然后再裁剪一下图片大小,选择像素,根据 OLED 屏幕的尺寸进行调整。我这里调的是 56*56 ,最后保存为 BMP 格式。如下图:

再通过取模软件打开,打开之后点击生成字模就可以将 BMP 格式的图片转换成字模。

图片可以使用本文提供的处理过的 BMP 图片。将图片放入取模软件,点击生成字模,如下图:

将代码粘贴到字库中,如下图:

在 main.c 中代码如下:

c
int main(void)
{
    HAL_Init();                         /* 初始化HAL库 */
    sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
    delay_init(72);                     /* 延时初始化 */

		OLED_Init();    //oled初始化
		OLED_Fill(0x00);//清屏	
	
    while(1)
    { 			
		OLED_DrawBMP(0,0,56,7,(unsigned char *)BMP_Data);//显示BMP图片		
    }
}

编译下载,可以看到图片成功的显示在了 OLED 屏幕上。这样我们就完成了 OLED 屏幕显示图片了。

4.7 播放视频 ​

播放视频的原理很简单,我们已经学会了显示图像,而视频就是一帧一帧的图像,我们不断显示一张接一张的图像就成了视频。由于 STM32F103C8T6 只有 64K 的FLASH,所以直接存储图像数据既浪费资源又无法播放长视频。那么我们怎么办呢?我在这里教大家利用 Python 和串口,将图片数据源源不断的传给 OLED。

  1. 安装 Python 和 PyCharm

首先我们需要安装 Python 和 PyCharm,网上资料很多,这里不多讲了。

  1. 配置国内源

安装好后,我们需要下载依赖包。当然,因为默认 pip 下载是基于国外源,很慢,甚至会超时,所以我们要配置成国内源,一劳永逸。打开运行工具(win+r),输入 cmd ,打开命令提示符,输入以下命令。

python
# 永久设置阿里云的国内源
pip config set global.trusted-host mirrors.aliyun.com
pip config set global.index-url https://mirrors.aliyun.com/simple

# 永久设置清华大学的国内源
pip config set global.trusted-host pypi.tuna.tsinghua.edu.cn
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple/

效果如下:

  1. 安装依赖包

然后我们就很以超快的速度安装依赖包啦,同样是在命令提示符内,输入下面三个命令。

python
pip install numpy
pip install opencv-python
pip install pyserial

效果如下:

  1. 编写程序

代码我只讲重点,更多细节请看文头的源码,里面注释超全。

oled.c 内的显存数组更新代码,在 while 循环里不断调用该函数,即可实现不断显示一张接一张的图像:

c
void OLED_Update(void)
{
    uint8_t j;

    /*遍历每一页*/
    for (j = 0; j < 8; j++)
    {
        /*设置光标位置为每一页的第一列*/
        OLED_SetCursor(j, 0);
        /*连续写入128个数据,将显存数组的数据写入到OLED硬件*/
        OLED_WriteData(OLED_DisplayBuf[j], 128);
    }
}

uart1.c 内的中断处理函数,每次被中断就接收一个字节的数据并存放到 OLED 显存数组。

c
void USART1_IRQHandler(void)
{
    static uint8_t p0 = 0, p1 = 0;

    if (__HAL_UART_GET_FLAG(&uart1_handle, UART_FLAG_RXNE) == SET)
    {
        /*接收一个字节数据*/
        uint8_t RxData;
        HAL_UART_Receive(&uart1_handle, &RxData, 1, 1000);

        /*将一个字节放到OLED显存数组的指定位置*/
        OLED_DisplayBuf[p0][p1] = RxData;

        /*位置自增*/
        p1++;
        if (p1 >= 128)
        {
            p1 = 0;
            p0++;
            if (p0 >= 8)
            {
            p0 = 0;
            }
        }

    __HAL_UART_CLEAR_FLAG(&uart1_handle, UART_FLAG_RXNE);
    }
}
  1. 硬件连接

在之前接线的基础上需要增加一个 USB 转 TTL,接线如下:

USB 转 TTLSTM32F103C8T6
GNDGND
VCCVCC
RXDPA9
TXDPA10

接好如下图,这也是最终效果图,先给大家预告下哈哈。

  1. 烧录代码,运行 Python

将工程源码烧录到单片机上,运行。再打开 Python 文件并运行。注意 Python 文件内的串口号要和你们自己的对应,不然会在打开串口那报错。

效果如下:

5. 总结 ​

0.96 寸 OLED 屏幕模块具有高亮度、快速响应、低功耗等优点,而且价格也不贵,同时它还适用于多种智能设备。我们不仅可以用它来打印一些调试的信息,也可以拿来显示图片播放视频什么的。玩好它不仅可以为我们的单片机调试带来极大的方便,同时它的能实现许多有趣的项目。让我们一起玩转 OLED 屏幕模块吧!