OLED-SSD1306手册分析与驱动代码

Fenice Liu
Fenice Liu
发布于 2024-10-30 / 60 阅读
0
0

OLED-SSD1306手册分析与驱动代码

在日常进行电子设计/嵌入式开发的过程中,我们经常碰到这种状况:是的串口工作良好,但是我想要更加直观和更加方便的交互——这种时候我们就要用到UI界面了。当然,交互方式是多种多样的,例如触摸或者某种实体按钮,或许我们还会用一些UI框架例如LVGL或者更加重量级的Qt-Embedded等等……但是无论如何我们不可能绕开屏幕。并且很多时候我们并不想在这个“非核心”的功能上倾注太多资源,无论是软硬件开销还是开发时间……这时候一块1寸大小的单色点阵屏幕是不错的选择。

1.OLED点阵屏幕与SSD1306的硬件原理

OLED,Organic Light Emitted Diode,有机发光二极管,有机材料(如聚合物或小分子)作为发光层,当电流通过这些材料时,它们会发光。不做材料的朋友可以简单理解为一个LED就是发光灯珠——OLED屏幕的本质就是一大堆OLED的灯珠组成的点阵。而当前市面上相当常用的产品就是一款0.96寸或者1寸多大小的,128x64像素大小的SSD1306驱动的小屏幕模块——如果我们想要在嵌入式开发之中添加一个可以显示和交互的屏幕,核心只有一句话:

让一堆灯珠按照我们想要的方式亮起来

a.SSD1306简介与硬件指标和特性

SSD1306是由 Solomon Systech 公司在2006年首次推出,至今还在生产的一款单色OLED显示控制器芯片。虽然设计生产的年份较早,但是从发售至今在全球大量出货,随着近年来国内电子设计与DIY领域逐渐热闹起来,这款芯片驱动的OLED小屏幕在市场上可以说是大行其道,SSD1306本身也是经久不衰,并且SSD1306的作用机理和操作接口并不失一般性,因此分析透彻SSD1306是非常有价值的。其主要特性有:

  • 显示类型: 支持128x64或128x32像素的单色OLED显示。

  • 接口: 支持I2C和SPI通信接口,便于与微控制器连接。

  • 低功耗: SSD1306低功耗特性,适合便携设备。

  • 内置DC-DC转换器: SSD1306内置DC-DC转换器,能够提高电源效率。

SSD1306_features.png

一款单色OLED点阵屏幕驱动芯片简单的来讲只要包含三个部分即可:供电部分,逻辑数字电路部分,模拟点阵驱动部分。通过上图可以看到SSD1306对应的三个部分为:

  • 供电部分:可以通过外部直接供电也可以通过内部的DC-DC变换器进行主电源的供电,同时对电压和电流都有反馈控制

  • 数字逻辑部分:对外接口有SPI/IIC/68XX/80XX四种不同的接口,针对接口上的数据搭载了一个命令解释器,整合内置的GDDRAM显存可以生成逻辑上的显示数据传递给驱动层

  • 模拟驱动部分:LED驱动以及时钟电路可以将数字逻辑部分生成的逻辑显示数据转换为控制OLED点阵面板时对应的高低电平大小

上面是SSD1306的功能框图的简单总结,或许这有些令人疑惑,我们首先来思考一个简单的问题:假设我们自己要控制一个简单的3x3大小的LED矩阵我们该如何构建电路:

SSD1306_matrix.png

以上电路虽然十分简陋拥有一大堆隐患,但是具体思想仍然清晰,如果我们需要点亮D5二极管,那么将C2电平拉高而R2电平拉低即可实现效果。因此驱动电路简单的来讲就是通过操作二极管的阳极和阴极之间的电位差来精准调控对应的LED像素点。

在SSD1306的驱动之中,我们将所有二极管像素点的阳极称为Segment并且从物理上排列到屏幕的纵向方向上,因此Segment控制的是列电平,相当于上图的C1~C3,一共有128组编号为0~127。对应的,所有二极管像素点的阴极称为Common并且从物理上排列到屏幕的横向方向上(这里需要说明横向排列上并非像是纵向排列一样简单,后文详述),因此Common控制的都是行电平,一共有64组,编号为0~63。这里需要特殊说明的是从芯片封装角度来讲Segment部分是从上方统一出线的,而Common被分为了左侧和右侧两部分出线,也就是一侧0~31另一侧32~63。

下一个重要的概念是Page,我们不难想象GDDRAM也就是显存,其实就是128x64大小的一个寄存器阵列,由于屏幕是单色屏幕——就是像素点只有亮(具体多亮能不能实现灰度图另说)和不亮两种状态,那么每个寄存器存储的就是一个bit。而我们的通信协议传输的最小单位通常是一个字节(Byte)那么不难理解我们要将其中8个bit一起处理,所以每8个Common对应一个Page的单位,因此存储空间一共有8个Page,每个Page都有128个Segment对应的128个字节的信息,整体的GDDRAM一共有1024Byte=1kB空间。

SSD1306_page.png

SSD1306内部整合了时钟电路,具体来说就是晶振时钟源和对应的分频电路,结合GDDRAM之中的显存数据可以实现根据时钟自动刷新,也就是说SSD1306可以实现显示状态的自持即不需要外部通信线操作便可维持显示内容不变,而帧刷新也就是LED灯珠的显示过程如图所示:

SSD1306_charge.png

整个周期性的刷新过程可以大致分为三个阶段,由于第三个阶段过短并且比较固定,因此三阶段从逻辑上归入二阶段处理:

  • Phase 1:从LED两端电压正向偏置开始充电,到LED电流饱和压降稳定结束,称之为预充电(Pre-Charge)阶段

  • Phase 2:从LED电流饱和正常显示到放电准备给下个周期的显示预充电结束,这个阶段是我们观察到的正常显示效果的主要阶段,称之为发光/放电(Emissive/Discharge)阶段

两个阶段的长度可以通过通信协议软件配置,配置的时间单位是时钟电路分频后得到的时钟周期。两个阶段相加的时间加上50个周期的固定脉宽时间就是刷新一帧所用的完整时间,通过对于电路原理的分析我们能够得到这两个周期配置的策略:

  • 如果Phase 1配置过短将会导致LED未能进入饱和就会开始恒流显示的第二阶段,这回导致显示效果出现抖动、掉点、花屏等现象。如果本阶段配置过长基本只会导致功耗上升,因此不考虑低功耗的情况下这个阶段的时间应当尽量配置较长。

  • 如果Phase 2配置过长将会导致LED反向偏置过程时间加长,开关器件关断电流开销上升,在Phase 1已经较长的基础上使每帧更新的时间增加进而导致宏观上出现屏幕显示延迟的现象。而Phase 2配置过短则不会出现明显的缺陷,因此本过程应当尽量配置较短时间。

b.引脚定义与硬件实现中的注重点

SSD1306是一个FPC封装的驱动芯片,其引脚之多难以想象,光是Segment和Common加起来的192个引脚就让人感觉不佳了——但是我们要注意到我们从市面上买来直接使用的都是OLED屏幕厂商生产的继承了OLED发光面板和保护层以及驱动芯片的集成模块而非SSD1306本身。因此本文使用https://so.szlcsc.com/global.html?c=&k=C194313的0.96寸128x64白色OLED进行分析,其FPC焊盘共计引出30pin,列表如下所示:

PIN

引脚名称

引脚说明

供电引脚

2

C1P

如果使用内置的DC-DC变换器,将不需要用户自己提供用于驱动OLED面板的高电压电源,而是通过VDDB变换出直流高电压电源。由于FPC体积限制,该变换器的电荷泵部分的电容无法片上集成,需要片外接入。应当在C1P/C1N和C2P/C2N之间跨接两个电容,用作电荷泵充能使用的元器件,不可直接对地接入电容。

3

C1N

4

C2P

5

C2N

6

VDDB

内部DC-DC变换器供电引脚,如果不使用内部变换器应当直接与VDD短接;反之接入外部满足OLED屏幕电流需求的电源;该引脚需要提供一个稳压去耦电容。

8

VDD

数字逻辑电路电源,应当接入外部电源。

9

VSS

数字逻辑电路参考电平,应当接入外部地线。

28

VCC

模拟显示驱动电路电源,如果使用内部变换器那么VCC应当悬空;如果采用独立供电方案应当直接接入外部电源;无论采用何种供电方案都应该在VCC和地线之间接入一个电容用于稳压和去耦。

29

VLSS

模拟显示驱动电路参考电平与回流路径,应当接入外部地线。

驱动引脚

26

IREF

对比度调整和LED阳极引线的参考电流引脚,此引脚应当对地接入一个电阻用于反馈环路计算,具体机理与对地电阻阻值计算后文详述。

27

VCOMH

此引脚上的电平是Common信号的高电平值,应当对地接入一个电容用于稳压和去耦,此引脚机理后文详述。VCOMH上最好接入一个1uF到10uF之间的电容。

通信引脚

10

BS0

这三个引脚用于选择芯片对外通信的接口类型

BS0/1/2=[0/0/0] 4线全双工SPI串行总线模式

BS0/1/2=[1/0/0] 3线半双工SPI串行总线模式

BS0/1/2=[0/1/0] IIC串行总线模式

BS0/1/2=[0/0/1] 68XX并行总线模式

BS0/1/2=[0/1/1] 80XX并行总线模式

11

BS1

12

BS2

13

CS#

此引脚为片选信号,低电平有效能够使通信接口正常工作,拉高时芯片的对外通信逻辑电路不工作。

14

RES#

此引脚为重启引脚,低电平有效,拉低时芯片产生硬复位;芯片正常显示时应当保持高电平。

15

DC#

此引脚为命令/数据模式切换引脚

68XX/80XX并行总线模式下,此引脚拉高则D0-D7线上的数据将会被识别为GDDRAM数据;拉低时将会被识别为控制命令或者寄存器读写命令。

SPI模式下,无论全双工或者半双工,此引脚拉高将会将输入线D1上的数据识别为数据,反之识别为命令。

IIC模式下,命令/数据不依赖于此引脚切换,此引脚将会影响芯片的从机地址,具体对应值见下文。

16

R/W#

使用SPI或者IIC串行总线时应当直接接地。

68XX并行总线模式下,此引脚相当于RW线,拉高总线读取,拉低总线写入。

80XX并行总线模式下,此引脚相当于W线,拉低表示写入数据。

17

E/RD#

使用SPI或者IIC串行总线时应当直接接地。

68XX并行总线模式下,此引脚相当于EN线,此线拉高将会导致总线初始化,拉低正常工作。

80XX并行总线模式下,此引脚相当于R线,拉低表示读取数据。

18

D0

68XX/80XX 并行总线模式下此8个引脚为数据并行线。

SPI模式下,D0为SCLK时钟线,D1为写入即MOSI线,D2为MISO线。当SPI选择半双工三线模式时D2不接。

IIC模式下,D0为SCL时钟线,D1与D2应当接到一起作为SDA数据线使用。

当选择SPI或者IIC模式时D3~D7应当接地,而D2不使用时只能悬空,否则将会导致通信接口故障。

19

D1

20

D2

21

D3

22

D4

23

D5

24

D6

25

D7

保留引脚

1

NC

这三个引脚都是保留引脚,其中Pin 1和Pin 30名为保留实际上介入了内部ESD防护电路必须接地才能让内部的ESD电路正常工作;而Pin 7应当浮空不接,如果接地可能导致芯片的数字逻辑电路无法工作。

7

NC

30

NC

电气参数与阈值性能

  • VSS是所有逻辑部分电压阈值的参考电平

  • VDD供电电压极限阈值是-0.3V~4V 最佳供电电压为3.3V

  • 对于逻辑输出而言,高电平下限阈值为0.9VDD,低电平上限阈值为0.1VDD

  • 对于逻辑输入而言,高电平下限阈值为0.8VDD,低电平上限阈值为0.2VDD

  • 逻辑数字电路部分的灌电流为180~300uA

  • VCC独立供电时,有效值为0~11V,典型值为7.5~9.5V

  • 屏幕模块休眠时VDD/VCC的典型电流消耗为1/2uA,最大值为5/10uA

IREF机制详解与阻值计算

由于LED是流控元件,在SSD1306内部对于电流有较为精细的管理,在SSD1306的使用之中可以设置这样一个参数:Contrast代表屏幕显示的对比度,这个参数可以划分为256级,在软件通信协议中定义在一个字节内。从这里也可以看出这个参数并不是对于每一个像素点定义的而是针对于OLED整个显示面板定义的——这就说明了SSD1306不可能凭借纯粹的硬件功能实现灰度图的显示,只能实现二值图像的显示,但是并非不能够通过灰度抖动算法等邪道方法实现。

要了解参考电流和最大电流消耗的数值,就必须首先了解SSD1306的LED驱动方式。并非如同前文的简单举例的阳极阴极各自分离的开关驱动,SSD1306采取这样一种方式:

  • Segment引脚并不输出一个开关量,而是输出一个多值的电流离散量,并且始终输出,这个电流离散量根据全局设置的Contrast调解,一旦Segment-Common导通就能够通过控制电流量进而控制LED的发光强度实现对比度(全局亮度)调解。

  • Common引脚控制一个开关量,当Segment-Common电压小于LED导通电压乃至反向偏置的时候LED就会截止进而不发光;反之就会令Segment反馈控制的电流输出通过LED

那么我们通过SSD1306 DataSheet之中可以看到每一路Segment的输出电流:

I_{SEG}=\frac{Contrast}{256}\times I_{REF}\times 8

上式之中I_{SEG}代表每一路Segment的电流需求,I_{REF}代表当前IREF引脚上的流出电流,8代表该路Segment跨越的8个Page。由于SSD1306之中IREF引脚上最大流过的电流可以是12.5uA,我们可以算出在对比度(亮度)拉到最高时单路Segment电流需求为100uA,如果整个屏幕被点亮那么整个屏幕的电流消耗是12.8mA。

然而理论值总是与实际有所出入,因为不同的屏幕生产厂商在将SSD1306组合到屏幕模块中时根据工艺不同和使用的OLED面板体质不同或多或少会增加一些电流消耗。例如本文之中举例的OLED屏幕,DataSheet之中说明的面板功率消耗为VCC=9V/15mA——这与理论值的偏移并不严重。假设我们使用内置的DC-DC变换器,VDDB=3.3V那么整体电流消耗将会达到40.91mA。因此我们可以认为整体的电流消耗不超过50mA,功耗大约为150mW。

根据DataSheet,IREF上的电压为VCC-2.5V大小,然而它并未指出内部DC-DC变换器输出的VCC具体值或者典型值是多少,这里取最低的推荐值7.5V——那么IREF上的电压大约是5V。显然,IREF对地电阻越大IREF上的电流就越小,那么屏幕整体的功率和亮度就越小——这当然会降低实际的对比度从而影响显示质量,但是会有效的降低功耗。笔者这里假设不关心功耗,那么最佳策略就是达到12.5uA的峰值REF电流,计算出对地电阻为400kΩ,考虑到BOM配单的便捷性,可以选择390kΩ。

VCOMH机制详解

前文提到Common引脚提供了一个开关量控制Segment的电流源是否通过LED导通,那么显然这些引脚处于低电平会导通,处于高电平会截止——低电平好说,不需要主动驱动,那么高电平呢?事实上DataSheet之中语焉不详的VCOMH上的电平就是Common拉高时的电平,SSD1306提供了几种可用的选择:0.83/0.77/0.65VCC。

显然更高的电平可以带来更小的正向偏置电压,带来更小的漏电流和更小的漏光——这能够显著的提升显示的清晰度,但是维持一个更高的VCOMH同样也带来更大的功耗,又回到之前的逻辑:更好的显示效果和更大的功耗就是鱼与熊掌不可兼得。考虑到各个Common端上面的电平总是频繁和不确定的在VCOMH和VLSS之间波动,显然VCOMH需要接入一个对地电容用于稳压和去耦。

独立供电电源管理

SSD1306_power.png

如上图所示,SSD1306为了保护驱动电路实际上是需要有一定的电源管理的。简单的来说上电时必须首先让VDD上电让逻辑电路工作(很可能是因为定期刷新依赖GDDRAM)而后才能让VCC上电,VCC稳定后才能软件开启面板显示。反之掉电时也必须让显示面板先关闭,VCC开始掉电直到完全放电之后才能够让逻辑电路的VDD掉电。

VCC通过内部DC-DC变换器生成时SSD1306的驱动电路会自动处理电源管理的顺序问题,但是当用户直接提供VCC时就需要稍微注意一下电源上电和掉电的顺序问题。DataSheet指出,VDD和VCC上电和掉电的不稳定上升/下降期不超过100ms。并且这里需要补充的是:由于VCC低于VDD时芯片内置的ESD保护电路会保证VCC不会由于ESD冲击高于VDD从而引发问题,为了使ESD正常工作保证电源的稳定性,VCC/VDD/VDDB三个引脚不用时也要浮空和接对地电容而不能直接拉低。

IIC模式外围电路硬件实现

SSD1306_circuit.png

  • 由于使用了内部DC-DC变换器,必须在2/3,4/5上接入电荷泵电容,这里选择两个X5R 1uF耐压值为16V的陶瓷电容,耐压值十分重要,因为这里的电压值是升高了的VCC电压而不是VDD

  • C5和C8为VDD和VDDB引脚上的稳压和去耦电容,这里笔者仅仅为了BOM配单方便(绘图时暴躁而懒惰)选择了和C6/C12一样的电容,实际上并不需要这样的耐压值和容值

  • R7/R8是IIC的上拉电阻,由于IIC总线标准之中定义这两根IO线是开漏(Open Drain)模式,而笔者也没有测试过SSD1306是否支持推挽(Push-Pull)模式,因此为了保险起见还是加上了上拉电阻。如果用户想要更好的总线通信质量或者更高速度可以考虑将电阻重新选择,例如经典的4.7kΩ

  • VCOMH和VCC上分别接入了对应的稳压和去耦电容,这里容值选择可以比较随意

  • IREF上接入对地390kΩ电阻的原因上文IREF一段讲过,此处不再赘述

2.SSD1306操作接口与相关命令实现详解

如果仅从通信质量和通信速度上来说,选择68XX或者80XX并口肯定是最好的,但是考虑到硬件资源占用和大部分现代MCU都有SPI/IIC的软硬件开发集成,SPI和IIC就显得更加实用一些。笔者认为就这类OLED屏幕的使用场景和性能需求,使用时钟频率轻松过Mhz级别的,多一根线的SPI总线是在有些杀鸡用牛刀,使用IIC其实是最合适最省力的,一些小小的计算和如下作为论据:

  • IIC总线时钟频率有标速(100kHz),快速(400kHz),高速(1MHz),而SSD1306无论是原厂产品还是国内仿制产品都能够达到400KHz速率

  • 前文计算过GDDRAM一共具有1KB空间,传输每个Byte需要8个Bit数据和一个Bit的ACK,那么传递完整的一帧GDDRAM不做任何优化就需要23.04ms

  • 代入微不足道的命令字节和Slave地址字节,即使考虑到MCU图像处理能力带来的延迟,传输一帧的时间也不会超过30ms,也就是说完全能够保证人肉眼不可见延迟的30FPS帧率

a.IIC接口操作逻辑与基础命令

Bit 7 Bit 6 Bit 5 Bit 4 Bit 3 Bit 2 Bit 1 Bit 0
0 1 1 1 1 0 DC# R/W#

SSD1306的IIC从机地址如图所示,Bit[7:2]是固定的,Bit 0遵照IIC协议根据读写方向变化,Bit 1为DC#引脚上的电平逻辑值,那么当该引脚拉低从机地址为0x78,该引脚拉高从机地址为0x7A。每次IIC通信首个字节以从机地址开始,第二个字节用于判断后续指令是命令还是数据,我们可以认为第二个字节等效于“寄存器地址”,事实上这个字节如此定义:

Bit 7 Bit 6 Bit 5 Bit 4 Bit 3 Bit 2 Bit 1 Bit 0
Co D/C# 0 0 0 0 0 0

Bit 7的Co全称是Continuation Bit,如果此Bit为0代表后续所有字节都是本次传输的载荷(Payload),事实上这个Bit在使用过程之中一直是0,笔者推测这可能是为了兼容其他操作接口而不增加数字逻辑电路复杂度而进行的设计妥协。Bit 6确定了后续为命令或者数据,如果Bit 6=0代表后续载荷为命令,反之为写入GDDRAM的数据。

  • 第二个字节,即寄存器地址为0x00,表示后续写入命令

  • 第二个字节,即寄存器地址为0x40,表示后续写入数据

命令:开启/关闭显示面板

  • 该命令仅包含一个字节

  • 0xAE代表关闭显示面板,0xAF代表开启显示面板

  • RESET发生后需要发送0xAF屏幕才能工作

命令:设置二值色彩模式

  • 该命令仅包含一个字节

  • 0xA6代表正色(GDDRAM=1点亮LED),0xA7代表反色(GDDRAM=1熄灭LED)

  • RESET发生后默认显示模式为正色(0xA6)

命令:设置显示数据来源

  • 该命令仅包含一个字节

  • 0xA4代表根据GDDRAM中的数据显示,0xA5代表面板全部为逻辑1

  • RESET发生后默认数据来源为GDDRAM

命令:设置全局对比度

  • 该命令包含两个字节:0x81+Constrat Byte

  • Constrat Byte有效Bit为[7:0]代表设置全局对比度为[Byte 2+1]

  • RESET发生后默认Byte 2为0x7F,也就是对比度=0x7F+1=128

命令:设置Segment(横向)扫描顺序

  • 该命令仅包含一个字节

  • 0xA0代表GDDRAM中Column[0~127]顺序映射到Segment[0~127]

  • 0xA1代表GDDRAM中Column[0~127]逆序映射到Segment[127~0]

  • 设置0xA1之后图像显示左右镜像,RESET发生后默认为顺序映射

命令:设置Common(纵向)扫描顺序

  • 该命令仅包含一个字节

  • 0xC0代表GDDRAM中Row[0~63]顺序映射到Common[0~63]

  • 0xC8代表GDDRAM中Row[0~63]逆序映射到Common[63~0]

  • 设置0xC8之后图像显示上下镜像,RESET发生后默认为顺序映射

  • SSD1306可以设置纵向方向上的MUX值使GDDRAM局部生效,如果设置只有16~47行生效,那么反向映射时就是Row[16~47]逆序映射到引脚Common[31~0],具体的MUX命令解释详见下文

  • SSD1306的Common引脚分为左侧组和右侧组,他们的排列方式和分布方式在后文也会提到,但是这一点对于本指令不相关

b.寻址方式与相关操作方式

通过前文的叙述,我们可以了解到这样一件事:OLED屏幕的刷新我们只能控制速度而不能控制具体时机,也就是无法通过一个指令进行强制于某个时间点的刷新,而必须依赖于内部时钟电路的的自动刷新。那么我们实际上做的就是改变GDDRAM之中的显示数据,然后等待OLED屏幕自动将更改后的GDDRAM数据推送到面板上。当我们写入一个字节的数据时,实际上至少需要两个信息来完成这个动作:

  • 这个字节要写到那里去,也就是写入字节的具体地址

  • 这个字节究竟是什么,也就是写入字节的值是多少

显然,从最终的显示效果上来说字节地址是不表现在最终显示效果上的无效信息,我们甚至可以将其认为是某种不得不存在的噪声——那么假设我们每个字节的通信都是“地址+数据”的模式,那么无疑通信的信噪比将会相当低,最终导致刷新缓慢延迟令人痛苦。SSD1306给我们提供了一种解决方案:芯片内部有半自动化的寻址机制,在一大堆数据之中只有少数的几个点要求用户手动输入地址,剩余数据写入时依赖半自动化寻址。

命令:设置RAM自动寻址方式

  • 本命令包括两个字节,即0x20 + Addressing Byte

  • 第二个字节只有低两bit有效:00b代表水平(Horizontal)寻址,01b代表垂直(Vertical)寻址,10b代表分页(Page)寻址,11b为非法数据

  • RESET发生后默认寻址方式为10b也就是分页寻址,不使用的bit默认0

SSD1306_addrH.png

SSD1306_addrV.png

SSD1306_addrP.png

在SSD1306的寻址方式之中,纵向的单位就是Segment对应的COL,横向的单位是8个Common(ROW)组成的PAGE。每种寻址方式都会有一个主要自动增加的单位坐标和一个类似于“进位”的自增单位坐标,以及可能产生的循环方式。对于每个寻址方式来说都要设置寻址的起始点也就是一个起始COL坐标和一个起始PAGE坐标,这三种寻址方式的不同在于:

  • 水平(Horizontal,横向)寻址从字节(Start Page, Start Col)开始写入数据。每写入一个字节Col坐标自动+1,当Col坐标到达当前Page之中最后一个Col坐标时,下一次字节将会令Col重置为Start Col,而Page自动+1。等待Page坐标和Col坐标都到达面板之中最后一个Page和Col的坐标的时候,Col和Page都会重置为Start Col和Start Page形成循环。

  • 竖直(Vertical,纵向)寻址从字节(Start Page, Start Col)开始写入数据。每写入一个字节Page坐标自动+1,当Page坐标到达当前Col之中最后一个Page坐标时,下一次字节将会令Page重置为Start Page,而Col自动+1。等待Col坐标和Page坐标都到达面板之中最后一个Col和Page的坐标的时候,Page和Col都会重置为Start Page和Start Col形成循环。

  • 分页(Page)寻址从字节(Target Page, Start Col)开始写入数据,每写入一个字节Col坐标自动+1,当Col坐标到达Target Col之中最后一个Col坐标的时候,Col将会重置为Start Col而Page不会改变,将会直接形成页内循环。如果需要写入另一个Page之中的数据,必须用户手动更改Target Page坐标实现另一个页内的写入循环。

命令:设置COL寻址范围

  • 本命令含有三个字节 0x21+Start Col+End Col

  • 本命令仅仅在设置SSD1306在Vertical或者Horizontal寻址模式下有效

  • Start Col范围为[0x00,0x7F]代表开始寻址的列坐标

  • End Col范围为[Start Col,0x7F]代表结束寻址的列坐标

  • 注意End Col必须大于等于Start Col

  • RESET发生时Start Col重置为0x00,End Col重置为0x7F也就是全列

命令:设置PAGE寻址范围

  • 本命令含有三个字节0x22+Start Page+End Page

  • 本命令仅仅在设置SSD1306在Vertical或者Horizontal寻址模式下有效

  • Start Page范围为[0x00,0x07]代表开始寻址的页坐标

  • End Page范围为[Start Page,0x07]代表结束寻址的页坐标

  • 注意End Page必须大于等于Start Page

  • RESET发生时Start Page重置为0x00,End Page重置为0x07也就是全页

命令:设置页内COL循环点

  • 本命令有两种形态,都含有一个字节,仅仅在SSD1306处于Page寻址的模式下生效

  • Start Col坐标的传递分为高4bit和低4bit两种类型

  • 低4bit的指令为0x00~0x0F可以直接传递低4bit

  • 高4bit的指令为0x10~0x1F低4bit将会设置为Start Col高4bit,这里需要注意虽然可以传输4bit,但仅仅低3bit有效也就是0x10~0x17

  • Page寻址模式无法设置结束Col坐标,寻址Col范围是[Start Col,0x7F]

  • RESET发生时Start Col设置为0x00也就是页内全列

命令:设置循环页坐标

  • 本命令仅有一个字节,仅仅在SSD1306处于Page寻址的模式下生效

  • Target Page坐标传递通过0xB0~0xB7命令的低3bit传递

  • Page寻址模式除非手动通过此命令切换,Target Page坐标永不更改

  • RESET发生时Target Page设置为0x00也就是第一页

数据:开启GDDRAM数据写入流程

  • 认为其他命令已经设置完成,仅仅剩余寻址和数据部分

  • 首先需要发送寻址命令,对于几种方式的命令:

    • Vertical/Horizontal寻址:

      • Slave Address + 0x00 + 0x20 + Vertical(01b)/Horizontal(00b)

      • Slave Address + 0x00 + 0x21 + Start Col + End Col

      • Slave Address + 0x00 + 0x22 + Start Page + End Page

    • Page寻址:

      • Slave Address + 0x00 + 0x20 + Page(01b)

      • Slave Address + 0x00 + [0x00~0x0F](Start Col lower 4bits)

      • Slave Address + 0x00 + [0x10~0x17](End Col higher 3bits)

      • Slave Address + 0x00 + [B0~B7](Target Page)

  • 最后发送数据直到下次寻址方式改变:

    • Slave Address + 0x40 + [Sequence of Data Bytes]

c.SSD1306自带图像处理——滚动

OLED屏幕使用过程之中,SSD1306提供了一类类似于图像处理/加速的硬件指令:滚动。此处指的滚动并不是说使GDDRAM作为一个窗口在某个更大的内存空间上移动,而是使每次内置时钟电路将GDDRAM刷新到显示面板上时偏移整个GDDRAM的内容形成最终的一个滚动显示效果。SSD1306的滚动操作支持横向滚动、横向-纵向同时滚动,然而并不支持单独的纵向滚动,这算是一个遗憾。

命令:滚动效果启停

  • 本命令仅包含一个字节

  • 0x2E代表关闭所有滚动效果,0x2F代表开启已经配置好的滚动效果

  • 每次滚动全局开关从0x2F切换到0x2E之后需要完全重写GDDRAM以保证面板显示内容是GDDRAM的内容,而不是出现某种显示位移

  • RESET发生后默认滚动效果关闭,使用0x2F之前必须首先配置水平滚动或者水平-竖直滚动,SSD1306不能记忆滚动配置,如果有多个滚动配置,那么仅仅取最后一条滚动配置执行

命令:水平滚动配置

  • 本命令共包含七个字节,需要在0x2F开启滚动前配置完成

  • 结构:Direction + 0x00 + Start Page + Interval + End Page + 0x00 + 0xFF

  • Direction字节为命令字节,0x26代表向右滚动,0x27代表向左滚动,每次滚动会让所有的COL向对应的方向移动一列,在边界上出现循环

  • Start Page和End Page 字节都是低3bit有效,其他bit=0,代表滚动区域开始的Page和结束的Page;End Page必须大于等于Start Page,这样可以配置滚动区域为横向上覆盖全屏幕,纵向上覆盖指定的Page的一片显示区域。

  • Interval代表每次滚动之间间隔的内部时钟刷新帧的次数,低3bit有效,其他bit=0。8种不同的时间间隔为:

Bit[2:0] Frames Bit[2:0] Frames Bit[2:0] Frames Bit[2:0] Frames
000b 5 001b 64 010b 128 011b 256
100b 3 101b 4 110b 25 111b 2

命令:水平-竖直滚动配置

  • 本命令包含六个字节,需要在0x2F开启滚动前配置完成,不支持仅仅有纵向滚动而没有横向滚动的指令效果

  • 结构:Direction + 0x00 + Start Page + Interval + End Page + Offset

  • Direction字节为命令字节,0x29代表纵向+向左滚动,0x2A代表纵向+向右滚动;纵向滚动的方向并不通过本字节配置

  • Start Page,End Page,Interval三个字节与上一条指令配置完全相同

  • Offset字节低6bit有效,代表每次纵向滚动多少行。例如纵向滚动区域是0~63全屏滚动,那么如果Offset=1,每次横向滚动时整体就会纵向下移一行,而反向移动的效果就类似“补码”的结构,例如配置偏移63行,那么第0行将会移动到63行,第1行将会移动到第0行——这样就完成了反向向上移动一行的效果

  • 纵向滚动区域的范围并不被Start Page/End Page所控制而是在此命令执行前独立配置,后续指令将会涉及这部分内容

命令:竖直滚动区域范围配置

  • 本命令包含三个字节:0xA3 + Start Row + Row Count

  • Start Row字节低6bit有效,代表[0,Start Row)范围的行不参与滚动,规划出屏幕的上半部分的一个条带区域不参与滚动

  • Row Count字节低6bit有效,代表[Start Row, Start Row+Row Count)范围的行参与滚动,规划出一个条带区域参与滚动

  • Start Row+Row Count一定要小于等于整个有效显示区域的行数

  • 当有效显示区域整体覆盖全部64行时:

    • Start=0,Count=64 配置全屏幕参与纵向滚动

    • Start=0,Count<64配置屏幕上半部分的一块区域参与纵向滚动

    • Start>0,Start+Count=64配置下半部分区域参与纵向滚动

    • Start>0,Start+Count<64配置中心一段区域参与纵向滚动

  • RESET发生之后Row Count=64而Start Row=0也就是全屏参与滚动

d.Common偏移相关命令

Common偏移相关指令具有多个相似但是意义完全不同的指令,并且其作用顺序和作用场景需要结合实际图纸或者例子分析,这部分内容由于数据表多处叙述语焉不详非常容易造成歧义和理解错误,下文之中将会首先介绍各个指令的详细格式,然后结合实际图例分析指令作用。

命令:显示区域范围设置

  • 本指令包含两个字节,0xA8 + Multiplex

  • 虽然GDDRAM具有64个行,但是我们可以设置这64行之中究竟有多少具体显示在OLED面板上也就是有多少个GDDRAM会投射到Common引脚上面去。这样的话就可以适配不同尺寸的OLED面板,例如市面上使用SSD1306驱动的面板有128x64和128x32两种常见规格

  • Multiplex低6bit有效,假设这个值为N,那么具体显示的行数就是N+1行,例如32行屏幕这个值就应该设置为0x1F。0~14是非法值,也就是说SSD1306支持的OLED面板最少为16行。

  • 即使物理上的OLED面板有更多行也可以减少显示面积,例如64行的屏幕可以设置32的Multiplex值,只显示GDDRAM的32行。

  • RESET发生后MUX值自动设置为64,也就是Multiplex字节为0x3F

命令:GDDRAM行偏移

  • 本指令包含一个字节

  • 指令字节为0x40~0x7F,bit7=0,bit6=1为固定值,低6bit可以自行更改,代表从第几行开始将GDDRAM投射到OLED面板上去

  • GDDRAM偏移后形成环状映射,例如0~63行的显示设置从第8行开始投射,那么在OLED面板上的56行开始就是GDDRAM的第0行

  • RESET发生后行偏移值自动设置为0,也就是0x40

命令:OLED面板行偏移

  • 本指令包含两个字节,0xD3 + Offset

  • Offset低6bit有效,代表GDDRAM投射到Common引脚上之后在OLED面板上显示时进行多少行偏移

  • 这个映射同样也是环状映射,但是此命令与上一条命令的区别是作用时机的不同,它直接作用于OLED面板的Common驱动引脚而不是作用于GDDRAM存储空间

  • RESET发生后Offset值自动设置为0x00

命令:Common引脚硬件配置

  • 本指令包含两个字节,0xDA + Config

  • Config固定基础值为0xDA,其中bit 4和bit5可以更改

  • bit4=0代表左右两组Common顺序连接到OLED面板;bit4=1代表左右两组Common引脚以交错方式连接到OLED面板,这个设置可以让芯片适配不同屏幕厂商封装的OLED模块

  • bit5=0代表左右两组Common按照默认顺序使用,bit5=1代表左右两组Common按照相反的顺序使用

  • RESET发生后bit4=1,bit5=0也就是左右两组顺序调用,交叉连接

SSD1306_common.png

如同上图所示,展示了Common引脚硬件配置命令Bit [5:4]四种不同的配置情况:

  • 左上角:Bit4=0 顺序排列 Bit5=0 顺序调用。首先调用左侧的Common组,从Common[0]到Common[31]顺序连接到1~32行,随后调用右侧的Common组,从Common[32]到Common[63]顺序连接到33~64行。

  • 左下角:Bit4=0 顺序排列 Bit5=1 逆序调用。首先调用右侧的Common组,从Common[32]到Common[63]顺序连接到1~32行,随后调用左侧的Common组,从Common[0]到Common[31]顺序连接到33~64行。

  • 右上角:Bit4=1 交叉排列 Bit5=0 顺序调用。首先调用左侧的Common组,从Common[0]到Common[31]交叉连接到1~63行(奇数递增),随后调用右侧的Common组,从Common[32]到Common[63]交叉连接到2~64行(偶数递增)。

  • 右下角:Bit4=1 交叉排列 Bit5=1 逆序调用。首先调用右侧侧的Common组,从Common[32]到Common[63]交叉连接到1~63行(奇数递增),随后调用左侧的Common组,从Common[0]到Common[31]交叉连接到2~64行(偶数递增)。

对于Bit4=1情况下的交叉连接或许较为令人疑惑,事实上这是OLED模组生产厂商常用的封装方法,这能够显著提升出货良品率,并且有效的减少OLED面板上相邻行之间的串扰问题,从而避免花屏、闪烁、坏线等等影响使用体验和显示质量的问题。理解了Common方式之后接下来需要讨论的是MUX指令和偏移指令具体如何生效的问题,这里需要说明的是:这些指令互不影响各自的效果,但是他们的生效顺序是不变的。在讨论MUX指令和偏移指令时我们无需考虑Common接线方式的问题,通过下图展示这些指令的作用方式:

SSD1306_offset.png

可以观察到:

  • MUX命令(A8)会从行地址扫描序列0开始计算,超过MUX的部分全部不显示

  • GDDRAM行偏移(40~7F)直接作用于GDDRAM,在MUX命令之前生效,也就是首先GDDRAM先进行行偏移然后才进行后续投射到Common引脚上的MUX工作

  • OLED面板行偏移(D3)直接作用于已经投射到OLED面板上的数据,也就是在MUX命令之后生效,即D3命令并不会改变GDDRAM之中的数据,仅仅是在最终显示效果上偏移

  • 以上三个命令与前文提到的C0/C8是否反转扫描顺序这四个指令互相独立,但是生效顺序不同,再考虑到硬件映射问题总结为:

    • 首先根据DA命令确认Common引脚和GDDRAM之间的映射关系,此命令完全与其他步骤独立,用户无需在图像过程种考虑

    • 其次进行GDDRAM的偏置(0x40~0x7F),这一步真正改变显示的图像的内容本身的数据

    • 随后进行MUX参数的确认,被MUX影响到的Common引脚将不会投射到OLED面板上从而不显示

    • 再次进行面板偏移(D3)的执行,将面板已经确认好的内容进行偏移,包括被MUX遮罩的数据

    • 最后拉取数据时根据是否纵向偏转(C0/C8)进行顺序或者逆序的读写

e.时钟与其他命令

命令:NOP空指令

  • 本指令只有一个字节0XE3

命令:VCOMH电压配置

  • 本指令有两个字节,0xDB + Scale

  • Scale为VCC到VCOMH的缩放系数,0x00=0.65,0x20=0.77,0x30=0.83

  • RESET发生后Scale重设为0.77也就是0x20

命令:时钟频率设置

  • 本指令有两个字节,0xD5 + FreqConfig

  • FreqConfig高4bit控制震荡时钟频率,数值越高时钟频率越高

  • FreqConfig低4bit控制主时钟分频系数,系数=数值+1

  • RESET发生后FreqConfig重设为 1000b | 0000b

命令:充放电阶段时长设置

  • 本指令具有两个字节,0xD9 + PhaseConfig

  • PhaseConfig高4bit代表Phase2时钟周期数设置(0x1~0xF)

  • PahseConfig低4bit代表Phase1时钟周期数设置(0x1~0xF)

  • 两个Phase的时钟周期数设置为0都是不可取的值

  • RESET发生后FreqConfig的两个Phase长度都是2

命令:电荷泵开关配置

  • 本指令具有两个字节,0x8D + Status

  • Status字节固定基值为0x10,bit2=1代表开启电荷泵,0代表关闭

  • RESET发生后电荷泵默认关闭

  • 此处的电荷泵指的就是内部的DC-DC变换器

3.基于STM32的IIC代码实现与移植讨论

明确了SSD1306的作用机理和指令读写以及数据组织方式之后,接下来就是将这些概念和规定抽象化为可编译的代码等待编译后执行的问题了,嗯,终于到了造轮子的环节。一个良好的轮子应当符合高内聚低耦合的特性,那么显然对于控制屏幕的需求应当将相关指令和操作与实际的通信过程分离开,将高级的图形应用于相关指令过程分离开,因此我们的轮子至少需要包括:

  • 可以灵活移植和重写的通信接口代码,这部分代码应当不影响指令和数据的读写

  • 完全实现DataSheet之中描述的所有指令和数据的读写操作接口,不要进行过度封装,编写单位就是抽象DataSheet的指令部分

  • 基于指令抽象层实现高级图像功能,例如画线、画圈、写入文字等等

a.工程配置与基础定义

#ifndef OLED_DRIVER_H
#define OLED_DRIVER_H

/**
 * @note
 * If type bool is not available, define bool as:
 * #define false 0x00u
 * #define true  (!false)
 */

/// Address and register define
#define OLED_IIC_ADDR_BASE    0x78
#define OLED_CMD_REG          0x00
#define OLED_DAT_REG          0x40
/// Basic command
#define OLED_DISPLAY_ON       0xAF
#define OLED_DISPLAY_OFF      0xAE
#define OLED_COLOR_NORMAL     0xA6
#define OLED_COLOR_INVERSE    0xA7
#define OLED_DATA_RAM         0xA4
#define OLED_DATA_ALL         0xA5
#define OLED_CONTRAST         0x81
#define OLED_HORIZONTAL_LR    0xA0
#define OLED_HORIZONTAL_RL    0xA1
#define OLED_VERTICAL_UD      0xC0
#define OLED_VERTICAL_DU      0xC8
/// Addressing command
#define OLED_ADDRESS_MODE     0x20
#define OLED_ADDR_COL_RANGE   0x21
#define OLED_ADDR_PAGE_RANGE  0x22
#define OLED_ADDR_COL_BASE_L  0x00
#define OLED_ADDR_COL_BASE_H  0x10
#define OLED_ADDR_PAGE_BASE   0xB0
/// Scroll command
#define OLED_SCROLL_OFF       0x2E
#define OLED_SCROLL_ON        0x2F
#define OLED_SCROLL_RIGHT     0x26
#define OLED_SCROLL_LEFT      0x27
#define OLED_SCROLL_RIGHT_V   0x29
#define OLED_SCROLL_LEFT_V    0x2A
#define OLED_SCROLL_RANGE_V   0xA3
/// Other command
#define OLED_MUX_RATIO        0xA8
#define OLED_RAM_OFFSET_BASE  0x40
#define OLED_PANEL_OFFSET     0xD3
#define OLED_COMMON_CFG       0xDA
#define OLED_NOP              0xE3
#define OLED_VCOMH_SCALE      0xDB
#define OLED_CHARGE_PHASE     0xD9
#define OLED_CLOCK_DIV        0xD5
#define OLED_CHARGE_PUMP      0x8D
/// Fixed data or command payload
#define OLED_RESET_INTERVAL   100
#define OLED_WIDTH            128
#define OLED_HEIGHT           64  ///@attention This macro-param should determine to specific screen
#define OLED_PAGE             (OLED_HEIGHT/8)
#define OLED_ADDR_HORIZONTAL  0x00
#define OLED_ADDR_VERTICAL    0x01
#define OLED_ADDR_PAGE        0x02
#define OLED_MUX_MIN          0x0F
#define OLED_MUX_MAX          (OLED_WIDTH-1)
#define OLED_COMMON_BASE      0xCA
#define OLED_VCOMH_065        0x00
#define OLED_VCOMH_077        0x20
#define OLED_VCOMH_083        0x30
#define OLED_IIC_ADDR_ALT     0x00 ///@attention If pin DC# is pulled up, then this param should be 0x01
typedef enum{
  OLED_SCROLL_FRAME5 = 0,
  OLED_SCROLL_FRAME64,
  OLED_SCROLL_FRAME128,
  OLED_SCROLL_FRAME256,
  OLED_SCROLL_FRAME3,
  OLED_SCROLL_FRAME4,
  OLED_SCROLL_FRAME25,
  OLED_SCROLL_FRAME2,
  OLED_SCROLL_INVALID
} OLED_SCROLL_SPEED;

/// ASCII character font size
#define CHAR_SIZE_12          12
#define CHAR_SIZE_16          16
#define CHAR_SIZE_24          24

/**
 * @Note All functions below is the announcement of Command Abstract Layer.
 * These functions are all related to certain SSD1306 operation or instruction
 * and they should all implement in .c source file
 */
uint8_t OLED_Display(uint8_t on);
uint8_t OLED_ReverseColor(uint8_t reverse);
uint8_t OLED_EnableAllPanel(uint8_t all);
uint8_t OLED_Contrast(uint8_t contrast);
uint8_t OLED_ReverseHorizontal(uint8_t reverse);
uint8_t OLED_ReverseVertical(uint8_t reverse);
uint8_t OLED_AddressHorizontal(uint8_t startCol, uint8_t endCol, uint8_t startPage, uint8_t endPage);
uint8_t OLED_AddressVertical(uint8_t startCol, uint8_t endCol, uint8_t startPage, uint8_t endPage);
uint8_t OLED_AddressPage(uint8_t targetPage, uint8_t startCol);
uint8_t OLED_ScrollCancel();
uint8_t OLED_Scroll(uint8_t right, OLED_SCROLL_SPEED speed, uint8_t startPage, uint8_t endPage);
uint8_t OLED_ScrollWithVertical(uint8_t right, uint8_t down, OLED_SCROLL_SPEED hSpeed, uint8_t vSpeed, uint8_t startPage, uint8_t endPage, uint8_t startRow, uint8_t endRow);
uint8_t OLED_MuxRatio(uint8_t ratio);
uint8_t OLED_OffsetGRAM(uint8_t offset);
uint8_t OLED_OffsetPanel(uint8_t offset);
uint8_t OLED_ConfigCommon(uint8_t sequence,uint8_t reverse,uint8_t level);
uint8_t OLED_Idle();
uint8_t OLED_ChargePump(uint8_t enable);
uint8_t OLED_ChargePhase(uint8_t preCharge, uint8_t emit);
uint8_t OLED_ConfigClock(uint8_t osc,uint8_t divide);

/**
 * @Note These functions below is the announcement of Portable Hardware Layer.
 * These functions depends on sepecific platform or OLED screen, all of them is
 * implemented as "__weak" in source file, so that users can re-implement them
 * with no need to modify the source file
 */
uint8_t OLED_IIC_Write(uint8_t dc, uint8_t* payload, uint16_t size);
void OLED_Reset();
uint8_t OLED_Init();

/**
 * @Note These functions below is the graphic application method, which are not the
 * basic or core implementation for SSD1306 screen but quite important and convenient
 * during deleopment coding.
 */
uint8_t OLED_PushAll();
uint8_t OLED_Push(uint8_t page, uint8_t col, uint16_t length);
void OLED_FillAll(uint8_t data);
void OLED_Point(uint8_t x, uint8_t y, uint8_t on);
void OLED_Line(uint8_t x1, uint8_t y1, uint8_t x2, uint8_t y2, uint8_t on);
void OLED_Path(uint8_t* pathX, uint8_t* pathY, uint16_t nodes, uint8_t on, uint8_t close);
void OLED_Rectangle(uint8_t cx, uint8_t cy, float angle, uint8_t width, uint8_t height, uint8_t on);
void OLED_RegularPoly(uint8_t cx, uint8_t cy, uint8_t radius, uint8_t poly, uint8_t angle, uint8_t on);
void OLED_Arc(uint8_t cx, uint8_t cy, uint8_t radius, float startAngle, float endAngle, uint8_t on);
void OLED_Circle(uint8_t cx, uint8_t cy, uint8_t radius, uint8_t on);
void OLED_CharASCII(uint8_t x, uint8_t y, char c, uint8_t size, uint8_t on);
void OLED_StringASCII(uint8_t x,uint8_t y,const char* s,uint8_t size,uint8_t on);

#endif //OLED_DRIVER_H

b.关于代码的可移植性

/**
 * @Note Use STM32F103 as example portable platform
 * Using HAL middleware as driver library
 */
#include "stm32f1xx_hal.h"

/// Use STM32CubeMX to generate handler of I2C port
extern I2C_HandleTypeDef hi2c1;

/**
 * @brief This function is the mainly commnication method through IIC
 * @param dc Actually is a boolean param, when true it means transmit data to SSD1306, otherwise is command
 * @payload The pointer of data which waiting for transmission
 * @size The number of payload bytes
 * @return Whether transmission is successful
 * @attention Use __weak so that user can re-implement this function everywhere
 */
__weak uint8_t OLED_IIC_Write(uint8_t dc, uint8_t* payload, uint16_t size){
  return HAL_I2C_Mem_Write(&hi2c1,
                    (OLED_IIC_ADDR_BASE|(OLED_IIC_ADDR_ALT<<1)),
                    dc?OLED_DAT_REG:OLED_CMD_REG,
                    I2C_MEMADD_SIZE_8BIT,
                    payload,size,1000)!=HAL_OK;
}

/**
 * @brief This function can reset the OLED screen and waiting for VCC power switch
 * @attention Use __weak so that user can re-implement this function everywhere
 * @attention This funcion use HAL_Delay(SysTick) to delay, watching NVIC!
 */
__weak void OLED_Reset(){
  //Pull down DC# pin so that I2C slave address is 0x78
  HAL_GPIO_WritePin(OLED_DC_GPIO_Port, OLED_DC_Pin, GPIO_PIN_RESET);
  //Pull down RES# pin
  HAL_GPIO_WritePin(OLED_RE_GPIO_Port, OLED_RE_Pin, GPIO_PIN_RESET);
  //Waiting for VCC discharge
  HAL_Delay(OLED_RESET_INTERVAL);
  //Release RES# pin to launch OLED screen
  HAL_GPIO_WritePin(OLED_RE_GPIO_Port, OLED_RE_Pin, GPIO_PIN_SET);
  //Waiting for VCC charge
  HAL_Delay(OLED_RESET_INTERVAL);
}

/**
 * @brief This function should be called when initializing system
 * @attention Use __weak so that user can re-implement this function everywhere
 * @attention The initial steps and their params should depend on specific screen
 * @return Which step fails during initialization
 */
__weak uint8_t OLED_Init(){
  //Reset
  OLED_Reset();
  //Close OLED panel
  if(OLED_Display(false)) return 0x01;
  //Maximum clock frequency(highest OSC and lowest pre-scaler)
  if(OLED_ConfigClock(0x0F,0x00)) return 0x02;
  //Enable all 64-row OLED panel display
  if(OLED_MuxRatio(0x3F)) return 0x03;
  //No offset in both panel and GDDRAM
  if(OLED_OffsetPanel(0x00)) return 0x04;
  if(OLED_OffsetGRAM(0x00)) return 0x05;
  //Mirror display both horizontal and vertical
  if(OLED_ReverseHorizontal(true)) return 0x06;
  if(OLED_ReverseVertical(true)) return 0x07;
  //Alternative Common connections, not reverse LR sequence, highest VCOMH(0.83VCC)
  if(OLED_ConfigCommon(false,false,OLED_VCOMH_083)) return 0x08;
  //Maximum contrast and brightness
  if(OLED_Contrast(0xFF)) return 0x09;
  //Max pre-charge phase and Min emit/discharge phase
  if(OLED_ChargePhase(0x0F,0x01)) return 0x0A;
  //Open charge pump
  if(OLED_ChargePump(true)) return 0x0B;
  //Display from GDDRAM
  if(OLED_EnableAllPanel(false)) return 0x0C;
  //Normal display color strategy
  if(OLED_ReverseColor(false)) return 0x0D;
  //No scroll
  if(OLED_ScrollCancel()) return 0x0E;
  //Start display panel
  if(OLED_Display(true)) return 0x0F;
  return 0x00;
}

c.指令抽象层实现

#include "oled_driver.h"

/// Define a virtual graphic-RAM for local modification
static uint8_t RAM[OLED_PAGE][OLED_WIDTH] = {0x00};
/// Define a command sequence for avoid creating variables frequently
static uint8_t OLED_CMD[16] = {0x00};

/**
 * @brief 0xAE/0xAF command
 * @param on Boolean value, open or close panel display
 * @return Whether transmission succeeded
 */
uint8_t OLED_Display(uint8_t on){
  OLED_CMD[0] = on ? OLED_DISPLAY_ON : OLED_DISPLAY_OFF;
  return OLED_IIC_Write(false, OLED_CMD, 1);
}

/**
 * @brief 0xA6/0xA7 command
 * @param reverse Boolean value, inverse(0=Light) or normal(0=Dark) mode
 * @return Whether transmission succeeded
 */
uint8_t OLED_ReverseColor(uint8_t reverse){
  OLED_CMD[0] = reverse ? OLED_COLOR_INVERSE : OLED_COLOR_NORMAL;
  return OLED_IIC_Write(false, OLED_CMD, 1);
}

/**
 * @brief 0xA4/0xA5 command
 * @param all Boolean value, light entire panel or load from GDDRAM
 * @return Whether transmission succeeded
 */
uint8_t OLED_EnableAllPanel(uint8_t all){
  OLED_CMD[0] = all ? OLED_DATA_ALL : OLED_DATA_RAM;
  return OLED_IIC_Write(false, OLED_CMD, 1);
}

/**
 * @brief 0x81 command, set contrast(brightness)
 * @param contrast 0x00~0xFF value means constrast ratio as (contrast+1)/256
 * @return Whether transmission succeeded
 */
uint8_t OLED_Contrast(uint8_t contrast){
  OLED_CMD[0] = OLED_CONTRAST;
  OLED_CMD[1] = contrast;
  return OLED_IIC_Write(false, OLED_CMD, 2);
}

/**
 * @brief 0xA0/0xA1 command
 * @param reverse Boolean value, reverse=1: display from left to right(GRAM),
 * @return Whether transmission succeeded
 */
uint8_t OLED_ReverseHorizontal(uint8_t reverse){
  OLED_CMD[0] = reverse ? OLED_HORIZONTAL_RL : OLED_HORIZONTAL_LR;
  return OLED_IIC_Write(false, OLED_CMD, 1);
}

/**
 * @brief 0xC0/0xC8 command
 * @param reverse Boolean value, reverse=1: display from top to bottom(GRAM),
 * @return Whether transmission succeeded
 */
uint8_t OLED_ReverseVertical(uint8_t reverse){
  OLED_CMD[0] = reverse ? OLED_VERTICAL_DU : OLED_VERTICAL_UD;
  return OLED_IIC_Write(false, OLED_CMD, 1);
}

/**
 * @brief This function is a composition of horizontal/vertical addressing mode settings.
 * This function is not exposed, 2 more function will re-encapsulated based on this function.
 * @param horizontal Boolean value, horizontal addressing mode or vertical addresing mode
 * @param startCol Start addressing columns index
 * @param endCol End addressing columns index
 * @param startPage Start addressing page index
 * @param endPage End addressing page index
 * @return 0x00 means successful, 0xFF means col/page range out of index, other means failure step
 */
uint8_t OLED_AddressSequence(uint8_t horizontal,uint8_t startCol, uint8_t endCol, uint8_t startPage, uint8_t endPage){
  if(endCol>=OLED_WIDTH||endCol<startCol||endPage>=OLED_PAGE||endPage<startPage) return 0xFF;
  OLED_CMD[0] = OLED_ADDRESS_MODE;
  OLED_CMD[1] = horizontal ? OLED_ADDR_HORIZONTAL : OLED_ADDR_VERTICAL;
  OLED_CMD[2] = OLED_ADDR_COL_RANGE;
  OLED_CMD[3] = startCol;
  OLED_CMD[4] = endCol;
  OLED_CMD[5] = OLED_ADDR_PAGE_RANGE;
  OLED_CMD[6] = startPage;
  OLED_CMD[7] = endPage;
  //Set addressing mode
  if(OLED_IIC_Write(false, OLED_CMD, 2)) return 0x01;
  //Set columns range
  if(OLED_IIC_Write(false, OLED_CMD + 2, 3)) return 0x02;
  //Set page range
  if(OLED_IIC_Write(false, OLED_CMD + 5, 3)) return 0x03;
  return 0x00;
}

/**
 * @brief This function encapsulate OLED_AddressSequence, horizontal addressing
 * @return Result of OLED_AddressSequence
 */
uint8_t OLED_AddressHorizontal(uint8_t startCol, uint8_t endCol, uint8_t startPage, uint8_t endPage){
  return OLED_AddressSequence(true, startCol, endCol, startPage, endPage);
}

/**
 * @brief This function encapsulate OLED_AddressSequence, vertical addressing
 * @return Result of OLED_AddressSequence
 */
uint8_t OLED_AddressVertical(uint8_t startCol, uint8_t endCol, uint8_t startPage, uint8_t endPage){
  return OLED_AddressSequence(false, startCol, endCol, startPage, endPage);
}

/**
 * @brief This function is composition of page addressing mode instructions
 * @param targetPage Addressing page index
 * @param startCol Addresing start column index
 * @return 0xFF means col/page out of range, 0x00 means successful, other means failure step
 */
uint8_t OLED_AddressPage(uint8_t targetPage, uint8_t startCol){
  if(targetPage>=OLED_PAGE||startCol>=OLED_WIDTH) return 0xFF;
  OLED_CMD[0] = OLED_ADDRESS_MODE;
  OLED_CMD[1] = OLED_ADDR_PAGE;
  OLED_CMD[2] = OLED_ADDR_PAGE_BASE | targetPage;
  OLED_CMD[3] = OLED_ADDR_COL_BASE_L | (startCol & 0x0F);
  OLED_CMD[4] = OLED_ADDR_COL_BASE_H | (startCol >> 4);
  if(OLED_IIC_Write(false, OLED_CMD, 2)) return 0x01;
  if(OLED_IIC_Write(false, OLED_CMD + 2, 1)) return 0x02;
  if(OLED_IIC_Write(false, OLED_CMD + 3, 2)) return 0x03;
  return 0x00;
}

/**
 * @brief 0x2E command, close all scroll instruction
 * @return Whether action succeeded
 */
uint8_t OLED_ScrollCancel(){
  OLED_CMD[0] = OLED_SCROLL_OFF;
  return OLED_IIC_Write(false, OLED_CMD, 1);
}

/**
 * @brief This function apply horizontal scroll instruction to SSD1306
 * @param right Boolean value, Scroll from left to right or right to left
 * @param speed Enum value, how many frames interval between two scroll state
 * @param startPage Scroll area begin page index
 * @param endPage Scroll area end page index
 * @return 0xFF means param error, 0x00 means successful, other meanse failure step
 */
uint8_t OLED_Scroll(uint8_t right, OLED_SCROLL_SPEED speed, uint8_t startPage, uint8_t endPage){
  
if(endPage>=OLED_PAGE||endPage<startPage||speed>=OLED_SCROLL_INVALID) return 0xFF;
  if(OLED_ScrollCancel()) return 0x01;
  OLED_CMD[0] = right ? OLED_SCROLL_RIGHT : OLED_SCROLL_LEFT;
  OLED_CMD[1] = 0x00u;
  OLED_CMD[2] = startPage;
  OLED_CMD[3] = speed;
  OLED_CMD[4] = endPage;
  OLED_CMD[5] = 0x00u;
  OLED_CMD[6] = 0xFFu;
  OLED_CMD[7] = OLED_SCROLL_ON;
  //Set horizontal scroll parameters
  if(OLED_IIC_Write(false, OLED_CMD, 7)) return 0x02;
  //Start scroll process
  if(OLED_IIC_Write(false, OLED_CMD + 7, 1)) return 0x03;
  return 0x00;
}

/**
 * @brief This function apply horizontal&vertical instruction to SSD1306
 * @param right Boolean value, horizontal scroll direction, same as OLED_Scroll
 * @param down Boolean value, Scroll from up to down or down to up
 * @param hSpeed Enum value, same as OLED_Scroll funcion param speed
 * @param vSpeed 0x00~0x3F, how many rows scrolled when horizontal scroll frame switched
 * @param startPage same as OLED_Scroll
 * @param endPage same as OLED_Scroll
 * @param startRow vertical scroll area start row index
 * @param endRow vertical scroll area end row index
 * @return 0xFF means param error, 0x00 means successful, other meanse failure step
 */
uint8_t OLED_ScrollWithVertical(uint8_t right, uint8_t down, OLED_SCROLL_SPEED hSpeed, uint8_t vSpeed, uint8_t startPage, uint8_t endPage, uint8_t startRow, uint8_t endRow){
  if(endPage>=OLED_PAGE ||endPage<startPage ||hSpeed>=OLED_SCROLL_INVALID
     || endRow >= OLED_HEIGHT || endRow < startRow || vSpeed > 63 || vSpeed == 0) return 0xFF;
  if(OLED_ScrollCancel()) return 0x01;
  OLED_CMD[0] = OLED_SCROLL_RANGE_V;
  OLED_CMD[1] = startRow;
  //convert start-end value to offset-range value
  OLED_CMD[2] = endRow - startRow + 1;
  //convert vertical speed and direction to raw offset value
  OLED_CMD[3] = right ? OLED_SCROLL_RIGHT_V : OLED_SCROLL_LEFT_V;
  OLED_CMD[4] = 0x00u;
  OLED_CMD[5] = startPage;
  OLED_CMD[6] = hSpeed;
  OLED_CMD[7] = endPage;
  OLED_CMD[8] = down ? vSpeed : 64 - vSpeed;
  OLED_CMD[9] = OLED_SCROLL_ON;
  //Config vertical scroll area bound
  if(OLED_IIC_Write(false, OLED_CMD, 3)) return 0x02;
  //Config horizontal/vertical scroll command
  if(OLED_IIC_Write(false, OLED_CMD + 3, 6)) return 0x03;
  //Launch scroll process
  if(OLED_IIC_Write(false, OLED_CMD + 9, 1)) return 0x04;
  return 0x00;
}

/**
 * @brief 0xA8 command
 * @param ratio OLED panel modal rows 0x1F~0x3F
 * @return Whether configuration successful
 */
uint8_t OLED_MuxRatio(uint8_t ratio){
  if(ratio<OLED_MUX_MIN||ratio>OLED_MUX_MAX) return 0xFF;
  OLED_CMD[0] = OLED_MUX_RATIO;
  OLED_CMD[1] = ratio;
  return OLED_IIC_Write(false, OLED_CMD, 2);
}

/**
 * @brief 0x40~0x7F command
 * @param offset Offset row index, 0x00~0x3F
 * @return Whether configuration successful
 */
uint8_t OLED_OffsetGRAM(uint8_t offset){
  if(offset>=OLED_HEIGHT) return 0xFF;
  OLED_CMD[0] = OLED_RAM_OFFSET_BASE | offset;
  return OLED_IIC_Write(false, OLED_CMD, 1);
}

/**
 * @brief 0xD3 command
 * @param offset Offset row index, 0x00~0x3F
 * @return Whether configuration successful
 */
uint8_t OLED_OffsetPanel(uint8_t offset){
  if(offset>=OLED_HEIGHT) return 0xFF;
  OLED_CMD[0] = OLED_PANEL_OFFSET;
  OLED_CMD[1] = offset;
  return OLED_IIC_Write(false, OLED_CMD, 2);
}

/**
 * @brief This function is a compsition of common pins configuration
 * @param sequence Boolean value, sequence or alternative connection method
 * @param reverse Boolean value, whether reverse Left/Right common pins group
 * @param level Voltage on VCOMH, 0x00 0x20 0x30 is valid data
 * @return 0xFF means param error, 00 means successful, other means failure step
 */
uint8_t OLED_ConfigCommon(uint8_t sequence,uint8_t reverse,uint8_t level){
  if(level!=OLED_VCOMH_065&&level!=OLED_VCOMH_077&&level!=OLED_VCOMH_083) return 0xFF;
  OLED_CMD[0] = OLED_COMMON_CFG;
  OLED_CMD[1] = OLED_COMMON_BASE | ((sequence ? 0x00 : 0x01) << 4) | ((reverse ? 0x01 : 0x00) << 5);
  OLED_CMD[2] = OLED_VCOMH_SCALE;
  OLED_CMD[3] = level;
  if(OLED_IIC_Write(false, OLED_CMD, 2)) return 0x01;
  if(OLED_IIC_Write(false, OLED_CMD + 2, 2)) return 0x02;
  return 0x00;
}

/**
 * @brief 0xE3 command
 * @return Whether configuration successful
 */
uint8_t OLED_Idle(){
  OLED_CMD[0] = OLED_NOP;
  return OLED_IIC_Write(false, OLED_CMD, 1);
}

/**
 * @brief 0x8D command
 * @param enable Boolean value, open or close charge pump
 * @return Whether configuration successful
 */
uint8_t OLED_ChargePump(uint8_t enable){
  OLED_CMD[0] = OLED_CHARGE_PUMP;
  OLED_CMD[1] = enable ? 0x14 : 0x10;
  return OLED_IIC_Write(false, OLED_CMD, 2);
}

/**
 * @brief 0xD9 command
 * @param preCharge Phase 1 clock periods 0x01~0x0F
 * @param emit Phase 2 clock periods 0x01~0x0F
 * @return Whether configuration successful
 */
uint8_t OLED_ChargePhase(uint8_t preCharge, uint8_t emit){
  if(preCharge==0||preCharge>15||emit==0||emit>15) return 0xFF;
  OLED_CMD[0] = OLED_CHARGE_PHASE;
  OLED_CMD[1] = (emit << 4) | preCharge;
  return OLED_IIC_Write(false, OLED_CMD, 2);
}

/**
 * @brief 0xD5 command
 * @param osc Oscillator circuit frequency level 0x00~0x0F
 * @param divide Pre-scaler of clock circuit 0x00~0x0F
 * @return Whether configuration successful
 */
uint8_t OLED_ConfigClock(uint8_t osc,uint8_t divide){
  if(osc>15||divide>15) return 0xFF;
  OLED_CMD[0] = OLED_CLOCK_DIV;
  OLED_CMD[1] = (osc << 4) | divide;
  return OLED_IIC_Write(false, OLED_CMD, 2);
}

d.基础图形学工具

为了实现稳定的帧刷新和图像生成与编辑,我们通常在MCU的内存之中复刻一块空间进行操作而并不直接更新GDDRAM,等待修改完毕需要更新的时候将这块空间整体推送到GDDRAM之中。如果考虑到显示流畅度、总线传输本身的速度等问题,我们还可以尝试使用一个定时器定时触发更新任务并且通过DMA的方式使其在后台持续执行。这里这样处理除了DMA和中断之外的功能:

/**
 * @brief This function update all 1024Byte GRAM data to GDDRAM through single IIC transmission
 * Actually, user can switch to Vertical Addressing mode either, but page addressing is not recomended
 * cause to transmit all data page addressing mode will go for 8 transmission at least
 * @return Whether transmission succeeded
 */
uint8_t OLED_PushAll(){
  if(OLED_AddressHorizontal(0, OLED_WIDTH-1, 0, OLED_PAGE-1)) return 0x01;
  return OLED_IIC_Write(true, &RAM[0][0], OLED_PAGE*OLED_WIDTH);
}

/**
 * @brief This function only push part of the GRAM to GDDRAM and must config addressing params firstly
 * @param page Which page to start the push process 
 * @param col  Which column to start the push process
 * @param length How many bytes of data need to push
 * @return Whether the push process is successful
 */
uint8_t OLED_Push(uint8_t page, uint8_t col, uint16_t length){
  if(page>=OLED_PAGE||col>=OLED_WIDTH||length==0) return 0xFF;
  if(page*OLED_WIDTH+col+length>OLED_PAGE*OLED_WIDTH)
    length = OLED_PAGE*OLED_WIDTH-page*OLED_WIDTH+col;
  if(length==0) return 0xFF;
  return OLED_IIC_Write(true, &RAM[page][col], length);
}

/**
 * @brief Fill the whole scree with a single data for a (Seg,Page)
 * @param data 0x00 means clear screen, 0xFF means fill screen, other values are infrequently used
 * @attention All the function below is just operating GRAM but not update to OLED
 */
void OLED_FillAll(uint8_t data){
  memset(RAM, data, OLED_PAGE*OLED_WIDTH);
}

然而在屏幕的日常开发与使用过程之中,我们是不可能每次都在像素点级别的数据上进行操作的,因此封装几个常用的图形学工具。这些图形学工具当然可以进行算法调优和扩展,这里仅做出演示,大致包括:

  • 如何在GRAM上标记一个像素点

  • 如何在GRAM上绘制一条任意位置和方向的直线

  • 如何绘制多边形和最常用的矩形

  • 如何绘制圆弧和圆形

/**
 * @brief Set a single pixel in GRAM as on or off
 * @param x Column(Segment) index
 * @param y Row(Common, not page) index
 * @param on Boolean value, true means light up, false means turning off
 */
void OLED_Point(uint8_t x, uint8_t y, uint8_t on){
  if(x>=OLED_WIDTH||y>=OLED_HEIGHT) return;
  //operate single pixel through bit calculation
  if(on) RAM[y/8][x] |= 1<<(y%8);
  else RAM[y/8][x] &= ~(1<<(y%8));
}

/**
 * @brief Draw a arbitrarily defined with two endpoints to GRAM on or off
 * @param x1 Endpoint 1 column index
 * @param y1 Endpoint 1 row index
 * @param x2 Endpoint 2 column index
 * @param y2 Endpoint 2 row index
 * @param on Boolean value, true means light up, false means turning off
 */
void OLED_Line(uint8_t x1, uint8_t y1, uint8_t x2, uint8_t y2, uint8_t on){
 if(x1>=OLED_WIDTH||y1>=OLED_HEIGHT||x2>=OLED_WIDTH||y2>=OLED_HEIGHT) return;
  //Brensenham algorithm in integer version
  int16_t dx = (int16_t)(x2 - x1);
  int16_t dy = (int16_t)(y2 - y1);
  int8_t sx = (int8_t)((dx>0)?1:(dx<0)?-1:0);
  int8_t sy = (int8_t)((dy>0)?1:(dy<0)?-1:0);
  dx = (int16_t)abs(dx);
  dy = (int16_t)abs(dy);
  int16_t err = (int16_t)(dx - dy);
  int16_t err2;
  while(1){
    OLED_Point(x1,y1,on);
    if(x1==x2&&y1==y2) break;
    err2 = (int16_t)(err*2);
    if(err2 > -dy) { err=(int16_t)(err-dy); x1 += sx;}
    if(err2 < dx)  { err=(int16_t)(err-dx); y1 += sy;}
  }
}

/**
 * @brief Draw a path defined by a series of nodes to GRAM on or off
 * @param pathX Pointer(array) to the column index of nodes
 * @param pathY Pointer(array) to the row index of nodes
 * @param nodes Nodes count
 * @param on Boolean value, true means light up, false means turning off
 * @param close Boolean value, whether connect the tail to the head node
 */
void OLED_Path(uint8_t* pathX, uint8_t* pathY, uint16_t nodes, uint8_t on, uint8_t close){
  for(uint16_t i=0;i<nodes-1;i++){
    OLED_Line(pathX[i],pathY[i],pathX[i+1],pathY[i+1],on);
  }
  if(close&&nodes>2) OLED_Line(pathX[nodes-1],pathY[nodes-1],pathX[0],pathY[0],on);
}

/**
 * @brief Draw a rectangle defined by several necessary param to GRAM on or off
 * @param cx Central point column index
 * @param cy Central point row index
 * @param angle Rotate angle in degree, horizontal is 0 reference, cw/ccw depends on screen orientation
 * @param width Horizontal scale in pixel(reference angle)
 * @param height Vertical scale in pixel(reference angle)
 * @param on Boolean value, true means light up, false means turning off
 */
void OLED_Rectangle(uint8_t cx, uint8_t cy, float angle, uint8_t width, uint8_t height, uint8_t on){
  if(width==0||height==0) return;
  float sinA = sinf(angle*PI/180.0f);
  float cosA = cosf(angle*PI/180.0f);
  float dx = (float)width/2.0f;
  float dy = (float)height/2.0f;
  float x1 = (float)cx + dx*cosA - dy*sinA;
  float y1 = (float)cy + dx*sinA + dy*cosA;
  float x2 = (float)cx - dx*cosA - dy*sinA;
  float y2 = (float)cy - dx*sinA + dy*cosA;
  float x3 = (float)cx - dx*cosA + dy*sinA;
  float y3 = (float)cy - dx*sinA - dy*cosA;
  float x4 = (float)cx + dx*cosA + dy*sinA;
  float y4 = (float)cy + dx*sinA - dy*cosA;
  OLED_Line((uint8_t)x1,(uint8_t)y1,(uint8_t)x2,(uint8_t)y2,on);
  OLED_Line((uint8_t)x2,(uint8_t)y2,(uint8_t)x3,(uint8_t)y3,on);
  OLED_Line((uint8_t)x3,(uint8_t)y3,(uint8_t)x4,(uint8_t)y4,on);
  OLED_Line((uint8_t)x4,(uint8_t)y4,(uint8_t)x1,(uint8_t)y1,on);
}

/**
 * @brief Draw a regular polygon defined by several necessary param to GRAM on or off.
 * This function is a special case of OLED_Path, which is a regular polygon with all edges equal and
 * the first node will be located at the horizontal reference line.
 * @param cx Central point column index
 * @param cy Central point row index
 * @param radius Radius of external circle in pixel
 * @param poly How many edges the polygon has
 * @param angle Rotate angle in degree, horizontal is 0 reference, cw/ccw depends on screen orientation
 * @param on Boolean value, true means light up, false means turning off
 */
void OLED_RegularPoly(uint8_t cx, uint8_t cy, uint8_t radius, uint8_t poly, uint8_t angle, uint8_t on){
  if(poly<3||radius==0) return;
  uint8_t* xPath = (uint8_t*)malloc(poly*sizeof(uint8_t));
  uint8_t* yPath = (uint8_t*)malloc(poly*sizeof(uint8_t));
  for(uint8_t i=0;i<poly;i++){
    xPath[i] = cx+(uint8_t)((float)radius*cosf((float)i*2.0f*PI/(float)poly+(float)angle*PI/180.0f));
    yPath[i] = cy+(uint8_t)((float)radius*sinf((float)i*2.0f*PI/(float)poly+(float)angle*PI/180.0f));
  }
  OLED_Path(xPath,yPath,poly,on,true);
  free(xPath);
  free(yPath);
}

/**
 * @brief Draw a circular arc defined by several necessary param to GRAM on or off.
 * @param cx Central point column index
 * @param cy Central point row index
 * @param radius Radius of the arc
 * @param startAngle Arc start angle in degrees
 * @param endAngle  Arc end angle in degrees
 * @param on Boolean value, true means light up, false means turning off
 * @Attention Superior arc or inferior arc depends on start/end angle sequence, and if user
 * want to draw a full circle, the start angle angle end angle must be 0 for float calculation error.
 */
void OLED_Arc(uint8_t cx, uint8_t cy, uint8_t radius, float startAngle, float endAngle, uint8_t on){
  float startRad = startAngle*PI/180.0f;
  float endRad = endAngle*PI/180.0f;
  while(startRad>PI) startRad -= 2.0f*PI;
  while(startRad<-PI) startRad += 2.0f*PI;
  while(endRad>PI) endRad -= 2.0f*PI;
  while(endRad<-PI) endRad += 2.0f*PI;
  if(startRad>=endRad) endRad += 2.0f*PI;
  //Calculate maximum angle rad pace for no gap while drawing
  float pace = asinf(1.0f/(float)radius);
  for(float angle=startRad;angle<=endRad;) {
    OLED_Point(cx + (uint8_t) ((float) radius * cosf(angle)),
               cy + (uint8_t) ((float) radius * sinf(angle)), on);
    angle+=pace;
  }
}

/**
 * @brief Draw a circle defined by several necessary param to GRAM on or off.
 * This function is a special case of OLED_Arc, which is a full circle with start angle 0 and end angle 0.
 * @param cx Central point column index
 * @param cy Central point row index
 * @param radius Radius of the circle
 * @param on Boolean value, true means light up, false means turning off
 */
void OLED_Circle(uint8_t cx, uint8_t cy, uint8_t radius, uint8_t on){
  OLED_Arc(cx,cy,radius,0.0f,0.0f,on);
}

最后一个基本功能是文字的显示,不同于直觉性的认识,文字在计算机内部是对应到编码的,而在显示的级别上文字和一张小型图片并没有什么区别,数据结构上呈现出相当大的无规律性,因此我们需要“安装字体“——也就是将文字与编码对应起来。由于中文的显示较为复杂占据空间较多,这里先实现一个ASCII字符的显示工作。首先利用任意一个取模软件将我们所需要的字体字号进行取模转换为点阵对应的数据这里使用英文字母'A'展示一下数据的组织:

SSD1306_character.png

笔者习惯于采用列扫描方式,实际上扫描存储方式有很多,例如行扫描或者Z字扫描。汉字或者其他CJK字符的尺寸是一个正方形,然而ASCII字符以及大多数西文字符的尺寸都是一个长宽比2:1的矩形,我们习惯将字符的高度称为字号,一般来说这个值取8的整数效果最好,因为能够对齐到字节存储之中。将字符部分设置为1空白设置为0在取模过程中称为阴码,相反则称为阳码。如果字符的高度不足整数字节对应的bit,那么剩下的部分补0,这需要着重处理,否则OLED屏幕的字体显示就会出现过大的行间距因为额外的统计了不属于字体本身的补位0码。笔者这里对95个可见ASCII码(0x20~0x7E)进行了取模,共计三种字体库,定义在一个叫做font.h的头文件中:

  • fontASCII_1206[95][12]像素大小为12x6的字体库,由于小体积可以显示很多字符,但是难以查看

  • fontASCII_1608[95][16]像素大小为16x8的字体库,屏幕刚好显示四行,这是笔者最常用的字体大小

  • fontASCII_2412[95][36]像素大小为24x12的字体库,显示效果很棒,但是字号对于128x64屏幕确实有些过大

/**
 * @brief This function put visible ASCII character to GRAM
 * @param x Start pixel (left-top) column index
 * @param y Start pixel (left-top) row index
 * @param c Target ASCII character, must between 0x20(' ') and 0x7E('~')
 * @param size Font size, must be corresponded to the font library
 * @param on Boolean value, true means light up, false means turning off
 */
void OLED_CharASCII(uint8_t x, uint8_t y, char c, uint8_t size, uint8_t on){
  //Check font size
  if(size!=CHAR_SIZE_12&&size!=CHAR_SIZE_16&&size!=CHAR_SIZE_24) return;
  //Check character visibility
  if(c<0x20||c>0x7E) return;
  uint8_t cIndex = c - 0x20;
  //Calculate how many bytes the charater image costs
  uint8_t bytes = (size/8+((size%8)?1:0))*(size/2);
  uint8_t base = y;
  uint8_t tmp,i,j;
  for(i=0;i<bytes;i++){
	//pick up correspond bytes from font library to tmp
    switch(size){
      case CHAR_SIZE_12: tmp=fontASCII_1206[cIndex][i]; break;
      case CHAR_SIZE_16: tmp=fontASCII_1608[cIndex][i]; break;
      case CHAR_SIZE_24: tmp=fontASCII_2412[cIndex][i]; break;
      default: return;
    }
    //Decode the single bit from the byte(tmp) and write to GRAM
    for(j=0;j<8;j++){
      if(tmp&0x80) OLED_Point(x,y,on);
      else OLED_Point(x,y,!on);
	  //Offset and update coordinates
      tmp <<= 1;
      y++;
      if((y-base)==size){y=base;x++;break;}
    }
  }
}

/**
 * @brief This function encapsulate the OLED_CharASCII function and it can put ASCII string to GRAM
 * @param x Start pixel (left-top) column index
 * @param y Start pixel (left-top) row index
 * @param s String pointer or array name which need to put in GRAM
 * @param size Font size, should be corresponded to font library
 * @param Boolean value, true means light up, false means turning off
 */
void OLED_StringASCII(uint8_t x,uint8_t y,const char* s,uint8_t size,uint8_t on){
  char *p = (char*)s;
  while((*p<=0x7E)&&(*p>=0x20)){
    OLED_CharASCII(x,y,*p,size,on);
    x += size/2;
    if(x>=OLED_WIDTH) break;
    p++;
  }
}

参考文档两则与示例代码:


评论