SPI 驱动 OLED 液晶屏幕

罗大富 BigRich大约 17 分钟ESP32Arduino

这一节我们学习如何使用 ESP32 开发板,通过 SPI 控制 OLED 液晶屏。

实验原理

1. SPI

SPI(Serial Peripheral Interface) 协议是由摩托罗拉公司提出的通讯协议,即串行外围设备接口,是一种同步、全双工、主从式接口,但并不是所有的 SPI 都是全双工。来自主机或从机的数据在时钟上升沿或下降沿同步。主机和从机可以同时传输数据。SPI 接口可以是 1 线、2 线 3 线式或 4 线式,这节课,我们用到的就是 3 线 SPI。

产生时钟信号的器件称为 主机。主机和从机之间传输的数据与主机产生的时钟同步。同 I2C 接口相比,SPI 器件支持更高的时钟频率。用户应查阅产品数据手册以了解 SPI 接口的时钟频率规格。

标准 4 线 SPI 芯片的管脚上只占用四根线。

  • MOSI: 主器件数据输出,从器件数据输入。
  • MISO:主器件数据输入,从器件数据输出。
  • SCK: 时钟信号,由主设备控制发出。
  • CS(NSS): 从机设备选择信号,由主设备控制。当 CS 为低电平则选中从器件。

3 线 SPI 没有 MISO,或者 MISOMOSI 共线。

SPI 接口只能有一个主机,但可以有一个或多个从机。下图显示了主机和从机之间的 SPI 连接。来自主机的片选信号用于选择从机。这通常是一个低电平有效信号,拉高时从机与 SPI 总线断开连接。当使用多个从机时,主机需要为每个从机提供单独的片选信号。MOSI 和 MISO 是数据线。MOSI 将数据从主机发送到从机,MISO将数据从从机发送到主机。

ESP32 集成了 4 个 SPI 外设。

  • 其中两个在内部用于访问 ESP32 所连接的闪存。两个控制器共享相同的 SPI 总线信号,并且有一个仲裁器来确定哪个可以访问该总线。
  • 另外两个是通用 SPI 控制器,分别称为 HSPI 和 VSPI。它们向用户开放,具有独立的总线信号,分别具有相同的名称。每条总线具有 3 条 CS 线,最多能控制 6 个 SPI 从设备。

I2C 与 SPI 区别

I2C 只需两根信号线,而标准 SPI 至少四根信号,如果有多个从设备,信号需要更多。一些 SPI 变种虽然只使用三根线—— SCK、CS 和双向的 MISO/MOSI,但 CS 线还是要和从设备一对一根。另外,如果 SPI 要实现多主设备结构,总线系统需额外的逻辑和线路。用 I2C 构建系统总线唯一的问题是有限的 7 位地址空间,但这个问题新标准已经解决 --- 使用 10 位地址。

如果应用中必须使用高速数据传输,那么 SPI 是必然的选择。因为 SPI 是全双工,IIC 的不是。SPI 没有定义速度限制,一般的实现通常能达到甚至超过 10Mbps。IIC 最高的速度也就快速+模式(1Mbps)和高速模式(3.4Mbps),后面的模式还需要额外的 I/O 缓冲区,还并不是总是容易实现的。SPI 适合数据流应用,而 IIC 更适合“字节设备”的多主设备应用。

SPI 有一个非常大的缺陷,主要是没有标准的协议,SPI 比较混乱,主要是没有标准的协议,只有moto的事实标准。所以衍生出多个版本,但没有本质的差异。

2. OLED 屏幕

OLED,即有机发光二极管(Organic Light Emitting Diode)。OLED 由于同时具备自发光,不需背光源、对比度高、厚度薄、视角广、反应速度快、可用于挠曲性面板、使用温度范围广、构造及制程较简单等优异之特性,被称为是第三代显示技术。

LCD 都需要背光,而 OLED 不需要,因为它是自发光的。这样同样的显示 OLED 效果要来得好一些。以目前的技术,OLED 的尺寸还难以大型化,但是分辨率确可以做到很高。

我们今天用到的屏幕是 0.96 寸的 SSD1306 芯片驱动的 OLED 屏幕。他的分辨率是 128*64,意思就是横向有 128 个像素点,纵向有 64 个

OLED 显示屏模块接口定义:

  1. GND:电源地。
  2. VCC:电源正(3.3~5V)。
  3. D0:OLED 的 D0 脚,在 SPI 通信中为时钟管脚。
  4. D1:OLED 的 D1 脚,在 SPI 通信中为数据管脚。
  5. RES:OLED 的 RES 脚,用来复位(低电平复位)。
  6. DC:数据和命令控制管脚。
  7. CS:片选管脚。

硬件电路设计

物料清单(BOM 表):

材料名称数量
0.96 寸 OLED 屏幕1
按键2
杜邦线(跳线)若干
面包板1

软件程序设计

如果想要使用 Arduino 控制 SSD1306 驱动的 OLED 屏幕,有以下两种第三方库可以使用:

  1. Adafruit_SSD1306 库:专门针对 SSD1306 驱动 OLED 屏幕的显示图形库;
  2. U8G2 库:目前 Arduino 平台上使用最广泛的 OLED 库。

1. Adafruit_SSD1306 控制 OLED 屏幕

想要使用 Adafruit_SSD1306,还需要安装 Adafruit_GFX 第三方库。Arduino 的 Adafruit_GFX 库为我们所有的 LCD 和 OLED 显示器提供了通用语法和图形功能集,也就是说这是一个通用图形库,并不针对特定的显示器型号。

  • Adafruit_GFX 定义了一系列的绘画方法(线,矩形,圆等等),属于基础类,并且最重要的一点,drawPixel 方法由子类来实现;
  • Adafruit_SSD1306 定义了一系列跟 SSD1306 有关的方法,并且重写了 drawPixel 方法,属于扩展类。

首先,我们就需要先下载这两个第三方库,PlatformIO 已经为我们提供了方便的下载途径,我们可以直接在 PlatformIO 的 PIO HOME 页面中选择 Libraries 中分别搜索 Adafruit GFX LibraryAdafruit_SSD1306,然后添加到项目中即可。

下载完以上两个第三方库之后,打开 platformio.ini 文件,可以看到 lib_deps 中出现了 SSD1306Adafruit GFX Library 两个依赖,

在学习 Adafruit_SSD1306 之前,你需要明白无论什么 OLED 屏幕,最终都可以抽象为像素点阵,想显示什么内容就把具体位置的像素点亮起来。比如 SSD1306-12864 就是一个 128X64 像素点阵,这个点阵拥有自己的一套坐标系,在坐标系中,左上角是原点,向右是X轴,向下是Y轴。

接下来,我们就可以深入学习该库了。

SSD1306 包括 IIC 和 SPI 总线版本,所以针对不同版本又有对应的构造器方法,因为我们的 OLED 是 SPI 版本的,因此,我们只讲 SPI 总线的构造方法。以下代码是

/*! 
    @brief  Constructor for SPI SSD1306 displays, using software (bitbang)
            SPI.(软件SPI总线)
    @param  w
            Display width in pixels
    @param  h
            Display height in pixels
    @param  mosi_pin
            MOSI (master out, slave in) pin (using Arduino pin numbering).
            This transfers serial data from microcontroller to display.
    @param  sclk_pin
            SCLK (serial clock) pin (using Arduino pin numbering).
            This clocks each bit from MOSI.
    @param  dc_pin
            Data/command pin (using Arduino pin numbering), selects whether
            display is receiving commands (low) or data (high).
    @param  rst_pin
            Reset pin (using Arduino pin numbering), or -1 if not used
            (some displays might be wired to share the microcontroller's
            reset pin).
    @param  cs_pin
            Chip-select pin (using Arduino pin numbering) for sharing the
            bus with other devices. Active low.
    @return Adafruit_SSD1306 object.
    @note   Call the object's begin() function before use -- buffer
            allocation is performed there!
*/
Adafruit_SSD1306(uint8_t w, uint8_t h, int8_t mosi_pin, int8_t sclk_pin,
    int8_t dc_pin, int8_t rst_pin, int8_t cs_pin);
    
/*!
    @brief  Constructor for SPI SSD1306 displays, using native hardware SPI.(硬件SPI总线)
    @param  w
            Display width in pixels
    @param  h
            Display height in pixels
    @param  spi
            Pointer to an existing SPIClass instance (e.g. &SPI, the
            microcontroller's primary SPI bus).
    @param  dc_pin
            Data/command pin (using Arduino pin numbering), selects whether
            display is receiving commands (low) or data (high).
    @param  rst_pin
            Reset pin (using Arduino pin numbering), or -1 if not used
            (some displays might be wired to share the microcontroller's
            reset pin).
    @param  cs_pin
            Chip-select pin (using Arduino pin numbering) for sharing the
            bus with other devices. Active low.
    @param  bitrate
            SPI clock rate for transfers to this display. Default if
            unspecified is 8000000UL (8 MHz).
    @return Adafruit_SSD1306 object.
    @note   Call the object's begin() function before use -- buffer
            allocation is performed there!
*/    
Adafruit_SSD1306(uint8_t w, uint8_t h, SPIClass *spi,
    int8_t dc_pin, int8_t rst_pin, int8_t cs_pin, uint32_t bitrate=8000000UL);

软件 SPI 总线用法,代码如下:

#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
// 软件SPI总线
// Declaration for SSD1306 display connected using software SPI (default case):
#define OLED_MOSI   13
#define OLED_CLK   18
#define OLED_DC    2
#define OLED_CS    4
#define OLED_RESET 15
Adafruit_SSD1306 oled(SCREEN_WIDTH, SCREEN_HEIGHT,
  OLED_MOSI, OLED_CLK, OLED_DC, OLED_RESET, OLED_CS);

使用硬件 SPI 总线,代码如下:

#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
#define OLED_DC     2
#define OLED_CS     4
#define OLED_RESET  15
Adafruit_SSD1306 oled(SCREEN_WIDTH, SCREEN_HEIGHT,
  &SPI, OLED_DC, OLED_RESET, OLED_CS);

接下来的方法(函数)无论是 I2C 还是 SPI 总线构建的,用法都是一致的:

  • clearDisplay:清除显示,该方法仅清除 Arduino 缓存,不会立即显示在屏幕上,可以通过调用 display 来立即清除;
  • display:显示内容,这个方法才是真正把绘制内容画在 OLED 屏幕上(非常重要);
  • drawCircle:绘制空心圆;
  • fillCircle:绘制实心圆;
  • drawTriangle:绘制空心三角形;
  • fillTriangle:绘制实心三角形;
  • drawRoundRect:绘制空心圆角方形;
  • fillRoundRect:绘制实心圆角方形;
  • drawBitmap:绘制 Bitmap 图形;
  • drawXBitmap:绘制 XBitmap 图形;
  • drawChar:绘制单个字符;
  • getTextBounds:计算字符串在当前字体大小下的像素大小,返回左上角坐标以及宽度高度像素值;
  • setTextSize:设置字体大小;
  • setFont:设置字体;
  • setCursor:设置光标位置;
  • setTextColor:设置字体颜色;
  • setTextWrap:设置是否自动换行;
  • drawPixel:绘制像素点;
  • drawFastHLine:绘制水平线;
  • drawFastVLine:绘制垂直线;
  • startscrollright:滚动到右边;
  • startscrollleft:滚动到左边;
  • startscrolldiagright:沿着对角线滚动到右边;
  • startscrolldiagleft:沿着对角线滚动到左边;
  • stopscroll:停止滚动:

使用 Adafruit_SSD1306 库分为三个步骤:

  1. 初始化 OLED,调用构造函数,调用 begin 方法;
  2. 初始化成功后,调用绘制类函数,当然可以设置颜色、字体等
  3. 绘制完毕,调用显示类函数 display。

了解完基本原理之后,我们就可以写一个简单的程序了,比如我们可以在屏幕上显示一些图形和字符,代码如下:

#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128 // OLED 显示屏宽度
#define SCREEN_HEIGHT 64 // OLED 显示屏高度

// 软件SPI总线
#define OLED_MOSI 13
#define OLED_CLK 18
#define OLED_DC 2
#define OLED_CS 4
#define OLED_RESET 15
Adafruit_SSD1306 oled(SCREEN_WIDTH, SCREEN_HEIGHT,
                      OLED_MOSI, OLED_CLK, OLED_DC, OLED_RESET, OLED_CS);

void setup()
{
  oled.begin();
  oled.clearDisplay();                          // 清除显示
  oled.drawFastHLine(32, 5, 48, SSD1306_WHITE); // 绘制水平线
  oled.drawLine(32, 5, 48, 30, SSD1306_WHITE);  // 绘制线
  oled.drawRect(5, 5, 10, 25, SSD1306_WHITE);   // 绘制矩形
  oled.fillRect(75, 5, 10, 30, SSD1306_WHITE);  // 绘制实心矩形
  oled.setCursor(5, 50);                        // 设置光标位置
  oled.setTextSize(2);                          // 设置字体大小
  oled.setTextColor(WHITE);                     // 设置文本颜色
  oled.println("Hello, world!");                // 显示文字
  oled.display();                               // 显示内容
}

void loop()
{
}

2. 在 OLED 上显示进度条

我们也可以在 OLED 屏幕中实现一个进度条加载的动画效果,代码如下:

#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128 // OLED 显示屏宽度
#define SCREEN_HEIGHT 64 // OLED 显示屏高度

// 软件SPI总线
#define OLED_MOSI 13
#define OLED_CLK 18
#define OLED_DC 2
#define OLED_CS 4
#define OLED_RESET 15
Adafruit_SSD1306 oled(SCREEN_WIDTH, SCREEN_HEIGHT,
                      OLED_MOSI, OLED_CLK, OLED_DC, OLED_RESET, OLED_CS);

// 初始化进度条变量
int progress = 0;

void setup()
{
  oled.begin();
  oled.setTextSize(2);              // 设置字体大小
  oled.setTextColor(SSD1306_WHITE); // 设置文本颜色
  oled.display();                   // 显示内容
}

void loop()
{
  // 清空屏幕
  oled.clearDisplay();

  // 设置光标位置
  oled.setCursor(25, 40);
  // 显示文字
  oled.println("Process");

  // 显示进度条边框
  oled.drawRoundRect(0, 10, 128, 20, 5, SSD1306_WHITE);
  // 显示进度
  oled.fillRoundRect(5, 15, progress, 10, 2, SSD1306_WHITE);

  // 进度递增
  if (progress < 118)
  {
    progress++;
  }
  else
  {
    progress = 0;
  }

  // 刷新屏幕
  oled.display();

  delay(50); // 延迟一段时间后更新显示
}

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 库):

  1. 基本函数
    • begin():初始化方法;
    • initDisplay():初始化显示控制器,这个方法不需要我们单独调用,会在 begin 函数主动调用一次,我们主要理解即可,会在里面针对具体的 OLED 进行配置;;
    • clearDisplay():清除屏幕内容,这个方法不需要我们单独调用,会在 begin 函数主动调用一次,我们主要理解即可,并且不要在 firstPage 和 nextPage 函数之间调用该方法;
    • clear():清除操作;
    • clearBuffer():清除缓冲区;
    • enableUTF8Print():开启 Arduino 平台下支持输出 UTF8 字符集,我们的中文字符就是UTF8;
    • home():重置显示光标的位置,回到原点(0,0);
  2. 绘制相关函数
    • drawPixel():绘制像素点;
    • drawHLine():绘制水平线;
    • drawLine():两点之间绘制线
    • drawBox():画实心方形;
    • drawFrame():画空心方形
    • drawCircle():画空心圆;
    • drawDisc():画实心圆;
    • drawStr():绘制字符串,需要先设置字体,调用 setFont 方法;
    • drawXBM()/drawXBMP():绘制图像;
    • firstPage()/nextPage():绘制命令,firstPage 方法会把当前页码位置变成 0,修改内容处于 firstPage 和 nextPage 之间,每次都是重新渲染所有内容;
    • print():绘制内容;
  3. 显示配置相关函数
    • getDisplayHeight():获取显示器的高度;
    • getDisplayWidth():获取显示器的宽度;
    • setCursor():设置绘制光标位置;
    • setDisplayRotation():设置显示器的旋转角度;
    • setFont():设置字体集(字体集用于字符串绘制方法或者glyph绘制方法);
  4. 缓存相关函数
    • getBufferPtr():获取缓存空间的地址;
    • getBufferTileHeight():获取缓冲区的Tile高度,一个tile等于8个像素点;
    • getBufferTileWidth():获取缓冲区的Tile宽度;
    • getBufferCurrTileRow():获取缓冲区的当前Tile row;
    • clearBuffer():清除内部缓存区;
    • sendBuffer():发送缓冲区的内容到显示器。

U8g2 支持以下两种绘制模式:

  1. Full screen buffer mode,全屏缓存模式;
  2. Page mode,分页模式;

全屏缓存模式使用步骤:

  1. 构造对象,根据 OLED 的型号选择对应的构造器,构造器必须带 F,因此,需要使用 U8G2_SSD1306_128X64_NONAME_F_4W_SW_SPI
  2. 初始化对象,使用 begin() 方法,清除缓冲区内容,使用 u8g2.clearBuffer();
  3. 绘制内容,使用绘制函数或者设置字体等;
  4. 发送缓冲区的内容到显示器 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 库的分页模式实现进度条效果

分页模式的使用步骤:

  1. 构造对象,根据 OLED 的型号选择对应的构造器;
  2. 初始化对象,使用 begin() 方法,调用 firstPage() 进入第一页
  3. 开始一个 do while 循环,循环条件是 nextPage(),作用是进入下一页,如果还有下一页则返回 true;
  4. 在循环内部 操作一些绘制方法。

注意

请注意,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 大概就是下面这个样子

按键控制菜单的原理其实很简单 -,当我检测到按键按下的时候,就切换屏幕状态,因为只有部分区域发生了改变,让你产生立箭头移动的错觉,代码如下:

#include <Arduino.h>
#include <U8g2lib.h>

// PlatformIO 中 自己编写的函数如果处于末尾,需要在文件顶部显式声明
void display_menu(unsigned int index);

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;

void setup()
{
  // 初始化 OLED 对象
  u8g2.begin();
  u8g2.setFont(u8g2_font_6x12_tr);
  
  // 配置输入按键
  pinMode(BUTTON_UP, INPUT_PULLUP);
  pinMode(BUTTON_DOWN, INPUT_PULLUP);
}

void loop()
{
  // 判断按键是否按下,并记录当前箭头位置
  if(!digitalRead(BUTTON_UP)) 
  {
    order = (order - 1) % 4;
  }else if (!digitalRead(BUTTON_DOWN))
  {
    order = (order + 1) % 4;
  }
  
  // 显示菜单
  display_menu(order);

  // 延时
  delay(100);
}

void display_menu(unsigned int index)
{
  // 进入第一页
  u8g2.firstPage();
  do
  {
    // 绘制页面内容
    u8g2.drawStr(0, 12, "Menu");
    u8g2.drawHLine(0, 14, 128);
    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()); // 进入下一页,如果还有下一页则返回 True.
}

在写这个程序的时候,我们用到了以下几个新的知识点,

  1. 使用 unsigned int 可以声明小于 0 的整数;
  2. do ... while 是先执行一次循环体,再判断的循环;
  3. 自己写的函数需要在文件顶部声明;
  4. 指针数组是一个数组,其中的每个元素都是指针类型的变量。换句话说,指针数组存储了多个指针,每个指针可以指向内存中的不同位置。
上次编辑于:
贡献者: Luo