ESP32 学习笔记(09)· SPI 驱动 OLED 液晶屏(下):U8G2 库与按键菜单

注:本系列为个人学习 ESP32 的记录,主要通过再记录的方式学习,参考和搬运转载极客侠 GeeksMan 的文章。
著作权归极客侠 GeeksMan 所有,基于 GPL 3.0 协议。原文链接:https://docs.geeksman.com/esp32/Arduino/01.esp32-arduino-intro.html
接上文:本篇承接《08. SPI 驱动 OLED 液晶屏(上):Adafruit_SSD1306 库》,硬件接线与 SPI、OLED 基础概念请参见上一篇;下文小节序号沿用原文。
3. U8G2 库控制 OLED
学会使用 Adafruit_SSD1306 库之后,我们再学习另一个并且是 Arduino 平台上使用最广泛的 OLED 库 - U8G2 库open in new window。U8g2 是嵌入式设备的单色图形库,一句话简单明了。主要应用于嵌入式设备,包括我们常见的单片机。
安装方法与 Adafruit_SSD1306 一致,只需要在 PlatformIO 中的 libraries 中搜索对应的库,添加到项目中即可。

为什么要运用 U8g2 库?也就是说 U8g2 库能带给我们什么样的开发便利,主要考虑几个方面:
- 平台支持性好,兼容多款开发板如 ESP32、ESP8266、Arduino Uno 等;
- 显示控制器支持性好,基本上市面上的 OLED 都完美支持;
- API 众多,特别支持了中文,支持了不同字体,这是一个对于开发者来说不小的福利。
因为 U8G2 库兼容很多版本的驱动以及不同尺寸的 OLED,所以 U8G2 构造方法有很多,但是我们需要根据我们自己的 OLED 的型号,选择适合我们的构造方法。打开 U8g2lib.h 文件,找到构造器的位置:

我们可以看到这些构造方法的名字有一定的规律:U8G2_驱动芯片_屏幕尺寸_缓存大小_总线,而我们的 OLED 尺寸是 128x64,SPI 总线,SSD1306 驱动,因此,我们可以搜索 U8G2_SSD1306_128X64,

HW 表示硬件(hardware),SW 表示软件(software),4W 表示 4 线,因此,我们就找到了最适合我们的构造器
U8G2_SSD1306_128X64_NONAME_1_4W_SW_SPI
U8G2_SSD1306_128X64_NONAME_2_4W_SW_SPI
U8G2_SSD1306_128X64_NONAME_F_4W_SW_SPI
这里的 1、2、F 表示不同的缓存大小:
1;只有一页的缓冲区,需要使用 firstPage/nextPage 方法来循环更新屏幕,使用 128 字节的内存;2:保持两页的缓冲区,使用 256 字节的内存;F:保存有完整的显示的缓存,可以使用所有的函数,但是 ram 消耗大,一般用在 ram 空间比较大的开发板;
所有的软件模拟总线构造函数的第一个参数都是 rotation, 这个参数表示显示内容是否旋转,U8G2 提供了以下几个选项:
U8G2_R0:不旋转;U8G2_R1:顺时针转 90°;U8G2_R2:顺时针转 180°;U8G2_R3:顺时针转 270°;U8G2_MIRROR:镜像翻转;
构造完对象之后,我们就可以学习 U8G2 的方法了,方法可以分为四大类(这里我们只列举了部分,详细内容可以查阅 u8g2 库):
- 基本函数
begin():初始化方法;initDisplay():初始化显示控制器,这个方法不需要我们单独调用,会在 begin 函数主动调用一次,我们主要理解即可,会在里面针对具体的 OLED 进行配置;;clearDisplay():清除屏幕内容,这个方法不需要我们单独调用,会在 begin 函数主动调用一次,我们主要理解即可,并且不要在 firstPage 和 nextPage 函数之间调用该方法;clear():清除操作;clearBuffer():清除缓冲区;enableUTF8Print():开启 Arduino 平台下支持输出 UTF8 字符集,我们的中文字符就是UTF8;home():重置显示光标的位置,回到原点(0,0);
- 绘制相关函数
drawPixel():绘制像素点;drawHLine():绘制水平线;drawLine():两点之间绘制线drawBox():画实心方形;drawFrame():画空心方形drawCircle():画空心圆;drawDisc():画实心圆;drawStr():绘制字符串,需要先设置字体,调用 setFont 方法;drawXBM()/drawXBMP():绘制图像;firstPage()/nextPage():绘制命令,firstPage 方法会把当前页码位置变成 0,修改内容处于 firstPage 和 nextPage 之间,每次都是重新渲染所有内容;print():绘制内容;
- 显示配置相关函数
getDisplayHeight():获取显示器的高度;getDisplayWidth():获取显示器的宽度;setCursor():设置绘制光标位置;setDisplayRotation():设置显示器的旋转角度;setFont():设置字体集(字体集用于字符串绘制方法或者glyph绘制方法);
- 缓存相关函数
getBufferPtr():获取缓存空间的地址;getBufferTileHeight():获取缓冲区的Tile高度,一个tile等于8个像素点;getBufferTileWidth():获取缓冲区的Tile宽度;getBufferCurrTileRow():获取缓冲区的当前Tile row;clearBuffer():清除内部缓存区;sendBuffer():发送缓冲区的内容到显示器。
U8g2 支持以下两种绘制模式:
Full screen buffer mode,全屏缓存模式;Page mode,分页模式;
全屏缓存模式使用步骤:
- 构造对象,根据 OLED 的型号选择对应的构造器,构造器必须带
F,因此,需要使用U8G2_SSD1306_128X64_NONAME_F_4W_SW_SPI; - 初始化对象,使用 begin() 方法,清除缓冲区内容,使用 u8g2.clearBuffer();
- 绘制内容,使用绘制函数或者设置字体等;
- 发送缓冲区的内容到显示器 u8g2.sendBuffer()。
了解完构造方法与使用方法之后,我们就可以来在程序中使用 U8G2 库了,代码如下:
#include <Arduino.h>
#include <U8g2lib.h>
// 构造对象
U8G2_SSD1306_128X64_NONAME_F_4W_SW_SPI u8g2(U8G2_R0, /* clock=*/18, /* data=*/13,
/* cs=*/4, /* dc=*/2, /* reset=*/15);
void setup(void)
{
// 初始化 oled 对象
u8g2.begin();
// 开启中文字符集支持
u8g2.enableUTF8Print();
}
void loop(void)
{
// 设置字体
u8g2.setFont(u8g2_font_unifont_t_chinese2);
// 设置字体方向
u8g2.setFontDirection(0);
//
u8g2.clearBuffer();
u8g2.setCursor(0, 15);
u8g2.print("Hello GeeksMan!");
u8g2.setCursor(0, 40);
u8g2.print("你好, ESP32!");
u8g2.sendBuffer();
delay(1000);
}
4. U8G2 库的分页模式实现进度条效果
分页模式的使用步骤:
- 构造对象,根据 OLED 的型号选择对应的构造器;
- 初始化对象,使用 begin() 方法,调用 firstPage() 进入第一页
- 开始一个 do while 循环,循环条件是 nextPage(),作用是进入下一页,如果还有下一页则返回 true;
- 在循环内部 操作一些绘制方法。
注意
请注意,firstPage() 和 nextPage() 必须配合使用,并在循环中正确调用。另外,确保在每次循环开始时使用 u8g2.clearBuffer() 清除缓冲区,以防止前一页的内容残留在当前页上。
#include <Arduino.h>
#include <U8g2lib.h>
U8G2_SSD1306_128X64_NONAME_2_4W_SW_SPI u8g2(U8G2_R0, /* clock=*/18, /* data=*/13,
/* cs=*/4, /* dc=*/2, /* reset=*/15);
int progress = 0;
void setup()
{
// 初始化 OLED 对象
u8g2.begin();
}
void loop()
{
// 进入第一页
u8g2.firstPage();
do
{
// 显示进度条边框
u8g2.drawFrame(0, 10, 128, 20);
// 显示进度
u8g2.drawBox(5, 15, progress, 10);
} while (u8g2.nextPage()); // 进入下一页,如果还有下一页则返回true
// 进度递增
if (progress < 118)
{
progress++;
}
else
{
progress = 0;
}
}
5. 按键控制菜单
在搞清楚 U8G2 库的使用方法之后,我们就可以设计一个按键控制菜单了,UI 大概就是下面这个样子

按键控制菜单的原理其实很简单 -,当我检测到按键按下的时候,就切换屏幕状态,因为只有部分区域发生了改变,让你产生立箭头移动的错觉,代码如下:
/*
* OLED菜单显示程序
* 使用U8g2库控制SSD1306 OLED显示屏
* 通过两个按钮在菜单项之间导航
*/
#include <Arduino.h> // 包含Arduino核心功能库
#include <U8g2lib.h> // 包含U8g2图形库,用于控制OLED显示屏
// 函数声明 - 在PlatformIO中,如果函数定义在调用之后,需要提前声明
void display_menu(unsigned int index);
/*
* 初始化U8g2库实例
* 参数说明:
* U8G2_R0 - 显示旋转方向(0度旋转)
* 18 - 时钟引脚(SCK)
* 13 - 数据引脚(MOSI)
* 4 - 片选引脚(CS)
* 2 - 数据/命令选择引脚(DC)
* 15 - 复位引脚(RESET)
* 使用软件SPI方式驱动OLED显示屏
*/
U8G2_SSD1306_128X64_NONAME_2_4W_SW_SPI u8g2(U8G2_R0, /* clock=*/18, /* data=*/13,
/* cs=*/4, /* dc=*/2, /* reset=*/15);
// 定义菜单项数量和内容
#define MENU_SIZE 4 // 菜单项总数
char *menu[MENU_SIZE] = {"Item 1", "Item 2", "Item 3", "Item 4"}; // 菜单项文本
// 定义按钮引脚
#define BUTTON_UP 12 // 向上导航按钮连接的引脚
#define BUTTON_DOWN 14 // 向下导航按钮连接的引脚
// 定义当前选中的菜单项索引
unsigned int order = 0; // 初始选中第一个菜单项(索引0)
/*
* 初始化设置函数
* Arduino启动时自动调用一次,用于初始化硬件和变量
*/
void setup()
{
// 初始化OLED显示屏
u8g2.begin(); // 启动U8g2库
u8g2.setFont(u8g2_font_6x12_tr); // 设置显示字体为6x12像素的字体
// 配置按钮引脚为输入模式,并启用内部上拉电阻
pinMode(BUTTON_UP, INPUT_PULLUP); // 上按钮,默认高电平,按下时变为低电平
pinMode(BUTTON_DOWN, INPUT_PULLUP); // 下按钮,默认高电平,按下时变为低电平
}
/*
* 主循环函数
* 在setup()执行完成后重复执行,实现程序的主要功能
*/
void loop()
{
// 检测上按钮是否按下(低电平表示按下)
if(!digitalRead(BUTTON_UP))
{
delay(10); // 简单延时消抖(实际应用中可能需要更完善的消抖处理)
// 减少当前选项索引,实现向上导航
// 使用模运算确保索引在0到3之间循环
order = (order - 1) % 4;
}
// 检测下按钮是否按下(低电平表示按下)
else if (!digitalRead(BUTTON_DOWN))
{
// 增加当前选项索引,实现向下导航
// 使用模运算确保索引在0到3之间循环
order = (order + 1) % 4;
}
// 显示菜单,传入当前选中的菜单项索引
display_menu(order);
// 延时100毫秒,降低循环执行频率,减少CPU占用
delay(100);
}
/*
* 显示菜单函数
* 在OLED屏幕上绘制菜单界面
* 参数:index - 当前选中的菜单项索引
*/
void display_menu(unsigned int index)
{
// 开始绘制第一页内容
u8g2.firstPage();
do
{
// 绘制菜单标题
u8g2.drawStr(0, 12, "Menu"); // 在坐标(0,12)处绘制"Menu"文本
// 绘制标题下方的水平分隔线
u8g2.drawHLine(0, 14, 128); // 从(0,14)到(128,14)绘制水平线
// 遍历所有菜单项
for (int i = 0; i < MENU_SIZE; i++)
{
// 检查当前项是否为选中的项
if (i == index)
{
// 绘制选中指示符(箭头)和菜单文本
u8g2.drawStr(5, (i + 2) * 12 + 2, ">"); // 在左侧绘制箭头
u8g2.drawStr(20, (i + 2) * 12 + 2, menu[i]); // 绘制菜单文本
}
else
{
// 绘制普通菜单文本(无箭头)
u8g2.drawStr(5, (i + 2) * 12 + 2, menu[i]); // 绘制菜单文本
}
}
} while (u8g2.nextPage()); // 继续处理下一页(如果有),直到所有内容绘制完成
}
在写这个程序的时候,我们用到了以下几个新的知识点,
- 使用
unsigned int可以声明小于 0 的整数; do ... while是先执行一次循环体,再判断的循环;- 自己写的函数需要在文件顶部声明;
- 指针数组是一个数组,其中的每个元素都是指针类型的变量。换句话说,指针数组存储了多个指针,每个指针可以指向内存中的不同位置。
系列导航:返回目录索引 | 上一篇:08. SPI 驱动 OLED 液晶屏(上):Adafruit_SSD1306 库 | 下一篇:10. 中断:外部中断与定时器中断