OrangePi ZERO 2 外设应用程序开发之 I²C 配置及驱动 OLED 屏幕

如果需要把 OrangePi ZERO 2 的一些系统信息显示出来,在没有远程终端和显示器的情况下,可以用一个 OLED 屏幕呈现这些基本信息。OLED 屏幕非常轻薄,理论上很灵活,可以产生更明亮、更清晰的图像。

本篇使用的 OLED 屏幕尺寸大小为 0.96 寸,分辨率为 128×64,核心控制 IC 为 SSD1306。

一、0.96 寸 OLED 屏幕硬件描述

OLED 屏幕有多种分辨率(如 128×64、128×32)和颜色(如白色、蓝色和双色 OLED)。OLED 屏幕的接口分别有 IIC 接口和 SPI 接口两种,它们都有一个共同点,它们的核心是一个强大的单芯片 CMOS OLED 驱动控制器——SSD1306,该芯片可以处理所有 RAM 缓冲。

1. 电源

与 LCD 不同,OLED 不需要背光,因为它可以产生自己的光。所以无论是高对比度、极宽的视角以及显示深黑色水平的能力,OLED 都要强于 LCD。背光的不存在显著地降低了功耗。OLED 平均使用约为 20mA,但这取决于 OLED 的照明程度。

SSD1306 的工作电压为 1.65V – 3.3V,而 OLED 面板需要 7V – 15V 的电源电压。所有这些不同的功率要求都通过内部电荷泵电路来满足。这使得可以在不需要逻辑电平转换器的情况下将显示器连接到任何其它 5V 逻辑微控制器。

2. OLED 内存映射

为了控制显示器,理解 OLED 屏幕的存储器映射至关重要。

无论 OLED 屏幕的大小如何,SSD1306 都包括 1KB 图形显示数据 RAM(GDDRAM),该 RAM 存储要在屏幕上显示的位模式。这个 1KB 的内存区域分为 8 页(从 0 到 7),每个页面具有 128 列/段(块 0 到 127),并且每列可以存储 8 位数据(从 0 到 7)。这些数据刚好为 1KB,如下证明:

8 pages × 128 segments × 8 bits of data = 8192 bits = 1024 bytes = 1KB memory

每个位代表屏幕上的单个OLED像素,可以通过编程方式打开或关闭。

3. OLED 屏幕的技术规格

显示技术 Display TechnologyOLED (Organic LED)
单片机接口 MCU InterfaceI2C / SPI
屏幕尺寸 Screen Size0.96 Inch Across
分辨率 Resolution128×64 pixels
工作电压 Operating Voltage3.3V – 5V
工作电流 Operating Current20mA max
可视角度 Viewing Angle160°
每行字符数 Characters Per Row21
字符行数 Number of Character Rows7

4. OLED 屏幕引脚定义

其中SCL是 IIC 接口的串行时钟引脚,SDA是 IIC 接口的串行数据引脚。

二、OrangePi ZERO 2 IIC 配置及测试

1.OrangePi ZERO 2 的 IIC 3 通道配置

OrangePi ZERO 2 的 26 pin 引脚中,有一对 IIC 接口,为 IIC 3通道(后称 i2c3),分别是 3 号引脚(SDA)和 5 号引脚(SCK)组成。

如果系统内核是 4.9 的版本,i2c3 通道默认是开启状态,可以直接使用。而内核版本为 5.16 的系统,i2c3 通道默认是关闭的,需要手动打开才能使用。如果不确定自己用的是什么内核版本的系统,可以在终端上输入uname -r来查看内核版本(下图所示为 5.16 的内核版本)。

在“/boot/orangepiEnv.txt”中加入 i2c3 的配置,然后重启香橙派就可以打开 i2c3 了,具体操作如下:

sudo vim /boot/orangepiEnv.txt

然后在任意位置插入下面的配置信息:

overlays=i2c3

如果之前文本已经有了overlays这一行的信息,说明之前配置过其他的接口,那就需要在该配置后面补上 i2c3。如图所示,之前测试了 PWM 通道 1 和 2,所以留下了之前的配置信息,如果这些配置还有作用,就把其他的配置写在这一行的后,每项配置用空格隔开。当然,如果之前的配置信息不需要,可以删除,只留下有用的。

重启香橙派后,终端输入ls /dev/i2c-*,只要有出现/dev/i2c-3,就说明已经打开了 i2c3 的通道了。

2. IIC 测试

测试 IIC 总线之前,首先要安装 i2c-tools,输入下面的命令进行安装:

sudo apt updatesudo apt install i2c-tools -y

接着接入一个 IIC 总线的设备,就以本篇主要的设备 OLED 屏幕为例。接线图如下:

连接好设备后,在命令行输入下面的命令:

sudo i2cdetect -y 3

如果能检测到连接的 IIC 设备的地址,就说明香橙派的 IIC 能正常使用。如下图所示,该设备的 IIC 地址为 0x3C。(每个 IIC 设备的地址不一定相同,只要有出现地址即可。)

三、wiringOP 库的 OLED 测试 demo

第一次用香橙派驱动 OLED 屏幕可以使用 wiringOP 库的 demo 代码测试一下,具体步骤如下:

先找到 wiringOP 库的安装路径,并进入文件夹找到example的文件夹,在这个文件夹里面就有驱动 OLED 的 demo。

直接用cp命令复制该文件到测试的文件夹中。

编译后,输入下面的命令运行(需要指定 IIC 通道):

sudo ./oled_demo /dev/i2c-3

最终的效果如下图:

四、wiringOP 库的 OLED 相关代码浅析

以下是 wiringOP 库自带的 OLED 屏幕测试 demo 的源代码:

/* * Copyright (c) 2015, Vladimir Komendantskiy * MIT License * * SSD1306 demo of block and font drawing. *///// fixed for OrangePiZero by HypHop//#include #include #include #include #include #include #include "oled.h"#include "font.h"int oled_demo(struct display_info *disp) {int i;char buf[100];//putstrto(disp, 0, 0, "Spnd spd2468 rpm");//oled_putstrto(disp, 0, 9+1, "Spnd cur0.46 A");oled_putstrto(disp, 0, 9+1, "Welcome to");disp->font = font1;//oled_putstrto(disp, 0, 18+2, "Spnd tmp53 C");oled_putstrto(disp, 0, 18+2, "----OrangePi----");disp->font = font2;//oled_putstrto(disp, 0, 27+3, "DrvX tmp64 C");oled_putstrto(disp, 0, 27+3, "This is 0.96OLED");oled_putstrto(disp, 0, 36+4, "");oled_putstrto(disp, 0, 45+5, "");disp->font = font1;//oled_putstrto(disp, 0, 54, "Total cur2.36 A");oled_putstrto(disp, 0, 54, "*****************");oled_send_buffer(disp);disp->font = font3;for (i=0; i<100; i++) {sprintf(buf, "Spnd spd%d rpm", i);oled_putstrto(disp, 0, 0, buf);oled_putstrto(disp, 135-i, 36+4, "===");oled_putstrto(disp, 100, 0+i/2, ".");oled_send_buffer(disp);}//oled_putpixel(disp, 60, 45);//oled_putstr(disp, 1, "hello");return 0;}void show_error(int err, int add) {//const gchar* errmsg;//errmsg = g_strerror(errno);printf("\nERROR: %i, %i\n\n", err, add);//printf("\nERROR\n");}void show_usage(char *progname) {printf("\nUsage:\n%s \n", progname);}int main(int argc, char **argv) {int e;char filename[32];struct display_info disp;if (argc < 2) {show_usage(argv[0]);return -1;}memset(&disp, 0, sizeof(disp));sprintf(filename, "%s", argv[1]);disp.address = OLED_I2C_ADDR;disp.font = font2;e = oled_open(&disp, filename);if (e < 0) {show_error(1, e);} else {e = oled_init(&disp);if (e < 0) {show_error(2, e);} else {printf("---------start--------\n");if (oled_demo(&disp) < 0)show_error(3, 777);printf("----------end---------\n");}}return 0;}

整个代码实际不到 100 行,先从main函数的变量开始看。整型变量e是一个标志位(这个变量命名有些随意,这个不要学),为 0 时就是相关函数执行成功了。字符型数组filename用于存放 IIC 设备通道的编号。结构体变量disp是香橙派定义的一个结构体的变量,结构体内部变量在头文件oled.h中声明,具体如下:

struct display_info {int address;int file;struct font_info font;uint8_t buffer[8][128];};

struct display_info这个结构体的四个成员分别是:IIC 设备的地址、打开 IIC 设备通道时的文件描述符、字体相关的结构体、IIC 屏幕的缓存区。其中字体相关的结构体在头文件font.h中声明,具体如下:

struct font_info {uint8_t width;uint8_t height;uint8_t spacing;uint8_t offset;uint8_t *data;};

struct font_info这个结构体的五个成员分别是:字符宽度、字符高度、字符间距、字符偏移和常用的 ASCII 表的点阵数据。

为了调库者使用方便,wiringOP 库还提供了三种预设:

static struct font_info font1 = { 5, 7, 1, 0, (uint8_t *)font1_data };static struct font_info font2 = { 6, 8, 0, 32, (uint8_t *)font2_data };static struct font_info font3 = { 5, 8, 0, 31, (uint8_t *)font3_data };

接着就是代码的业务流程了,先判断传入的参数个数是否正确,其实就是在确定是否写明使用哪个 IIC 通道,如果没写明,就在调用show_usage函数结束后直接退出程序。如果有指定 IIC 通道,则在初始化disp结构体变量后,把指定的 IIC 通道、IIC 设备地址和指定的字体信息分别写入disp结构体变量。

if (argc < 2) {show_usage(argv[0]);return -1;}memset(&disp, 0, sizeof(disp));sprintf(filename, "%s", argv[1]);disp.address = OLED_I2C_ADDR;disp.font = font2;

接着就是调用oled_open函数,如果顺利再执行 OLED 的初始化函数,也就是调用oled_init函数。如果初始化成功,就可以执行显示的工作了,这里是调用了oled_demo函数。

e = oled_open(&disp, filename);if (e < 0) {show_error(1, e);} else {e = oled_init(&disp);if (e < 0) {show_error(2, e);} else {printf("---------start--------\n");if (oled_demo(&disp) < 0)show_error(3, 777);printf("----------end---------\n");}}

如果后面要用 OLED 屏幕去显示其他的信息或图案,其实并不需要去深究oled_open函数和oled_init函数具体做了什么,按照 demo 的调用顺序照抄即可。

除了以上两个 API 之外,wiringOP 库还有关闭 IIC 通道文件的oled_close和清屏函数oled_clear,以上函数的声明如下:

/* oled.h */extern int oled_open(struct display_info* disp, char* filename);extern int oled_init(struct display_info* disp);extern int oled_close(struct display_info* disp);extern void oled_clear(struct display_info *disp);

对于以上 API,我们只需知道调用的场合即可,不需要深究函数原型。

oled_demo函数里面调用了两个用于显示的 API,其中oled_putstrto函数的作用是在屏幕上的指定位置显示指定的内容,oled_send_buffer函数则是把需要显示的内容刷新到屏幕上。

int oled_demo(struct display_info *disp){int i;char buf[100];oled_putstrto(disp, 0, 9+1, "Welcome to");disp->font = font1;oled_putstrto(disp, 0, 18+2, "----OrangePi----");disp->font = font2;oled_putstrto(disp, 0, 27+3, "This is 0.96OLED");oled_putstrto(disp, 0, 36+4, "");oled_putstrto(disp, 0, 45+5, "");disp->font = font1;oled_putstrto(disp, 0, 54, "*****************");oled_send_buffer(disp);disp->font = font3;for (i=0; i<100; i++) {sprintf(buf, "Spnd spd%d rpm", i);oled_putstrto(disp, 0, 0, buf);oled_putstrto(disp, 135-i, 36+4, "===");oled_putstrto(disp, 100, 0+i/2, ".");oled_send_buffer(disp);}return 0;}

oled_putstrto函数的原型如下所示,其中第二个参数表示横坐标,取值范围为 0 – 125。第三个参数表示纵坐标,取值范围为 0 – 63。第四个参数是需要显示的字符串。

// put string to the buffer at xyvoid oled_putstrto(struct display_info *disp, uint8_t x, uint8_t y, char *str) {uint8_t a;int slen = strlen(str);uint8_t fwidth = disp->font.width;uint8_t fheight = disp->font.height;uint8_t foffset = disp->font.offset;uint8_t fspacing = disp->font.spacing;int i=0;int j=0;int k=0;for (k=0; k<slen; k++) {a=(uint8_t)str[k];for (i=0; i<fwidth; i++) {for (j=0; j<fheight; j++) {if (((disp->font.data[(a-foffset)*fwidth + i] >> j) & 0x01))oled_putpixel(disp, x+i, y+j, 1);elseoled_putpixel(disp, x+i, y+j, 0);}}x+=fwidth+fspacing;}}

除了oled_putstrto函数之外,还有oled_putstroled_putpixel两个函数也可以在指定位置显示需要的内容。oled_putstr函数是以页选址的模式确定纵坐标,取值范围为 0 – 7。oled_putpixel函数则是指定屏幕上某个点的亮灭状态。以上函数具体的声明如下:

/* oled.h */extern int oled_send_buffer (struct display_info* disp);extern void oled_putstr(struct display_info *disp, uint8_t line, uint8_t *str);extern void oled_putpixel(struct display_info *disp, uint8_t x, uint8_t y, uint8_t on);extern void oled_putstrto(struct display_info *disp, uint8_t x, uint8_t y, char *str);

关于以上函数的具体使用,将在下一篇博文里详谈。

亮灭状态。以上函数具体的声明如下:

/* oled.h */extern int oled_send_buffer (struct display_info* disp);extern void oled_putstr(struct display_info *disp, uint8_t line, uint8_t *str);extern void oled_putpixel(struct display_info *disp, uint8_t x, uint8_t y, uint8_t on);extern void oled_putstrto(struct display_info *disp, uint8_t x, uint8_t y, char *str);

关于以上函数的具体使用,请看下篇《OrangePi ZERO 2 外设应用程序开发之基于 wiringOP 库的 OLED 二次开发》。