跳到主要内容

第 07 节 驱动 RGB565 屏幕

关于各种 LCD 显示屏的开发,ESP32-S3 目前可以支持的接口类型有 SPI (QSPI)、I80、和 RGB,而在 ESP32-S3-Touch-LCD-7 上我们集成的是一个 RGB565 接口的 7inch 屏幕。相比于其他接口的屏幕,采用 RGB 接口可以驱动更大分辨率的 LCD,并且能够达到较高的刷新率,占用 CPU 极低,可以完美适配 LVGL,适用于各种智能开发项目。

ESP32-S3-Touch-LCD-7 RGB LCD demo 1

在本节当中将会教大家如何驱动板载的 RGB 屏,提供了一个驱动屏幕显示色彩条纹的例程。

引脚连接

大多数 RGB LCD 采用 SPI + RGB 接口,而本产品当中仅采用了 RGB 接口的方式,使得驱动方法更加简单直接。

ESP32-S3 可支持 16-bit RGB565 和 8-bit RGB888 两种色彩格式,在本产品当中,我们用到的是 16-bit RGB565,ESP32-S3 与 RGB 屏幕的连接方式如下,需要连接 HSYNC、VSYNC、PCLK、DE、D0 ~ D15 (5 bit R + 6 bit G + 5 bit B)。

ESP32-SLCD简介
GPIO0G3绿色数据第三位
GPIO1R3红色数据第三位
GPIO2R4红色数据第四位
GPIO3VSYNC竖向同步信号
GPIO5DE数据使能信号
GPIO7PCLK时钟信号
GPIO10B7蓝色第七位
GPIO14B3蓝色第三位
GPIO17B6蓝色第六位
GPIO18B5蓝色第五位
GPIO21G7绿色第七位
GPIO38B4蓝色第四位
GPIO39G2绿色第二位
GPIO40R7红色第七位
GPIO41R6红色第六位
GPIO42R5红色第五位
GPIO45G4绿色第四位
GPIO46HSYNC横向同步信号
GPIO47G6绿色第六位
GPIO48G5绿色第五位
CH422GLCD简介
EXIO2DISP背光使能引脚

硬件连接

为了将对应的代码上传到 ESP32-S3,需要用 Type-C 转 Type-A 的线将 ESP32-S3-Touch-LCD-7 的 USB 口接到电脑的 USB 口:

ESP32-S3-Touch-LCD-7 I2C demo 2

示例代码

请先从以下地址下载例程包:ESP32-S3-Touch-LCD-7 示例程序 (如果先前已经下载,直接打开对应文件夹即可) 将例程压缩包解压之后,打开对应的 08_DrawColorBar 文件夹

ESP32-S3-Touch-LCD-7 RGB LCD demo 2

双击打开 08_DrawColorBar.ino:

ESP32-S3-Touch-LCD-7 RGB LCD demo 3

选择开发板型号 Waveshare ESP32-S3-Touch-LCD-7 与端口

ESP32-S3-Touch-LCD-7 RGB LCD demo 4

接着并设置开发板参数:

ESP32-S3-Touch-LCD-7 RGB LCD demo 5

然后点击箭头 Upload 按钮进行程序上传,等待上传完成即可:

ESP32-S3-Touch-LCD-7 RGB LCD demo 6

运行结果

程序上传完成之后,重新给 ESP32-S3-Touch-LCD-7 上电,可以看到屏幕点亮并且显示三种颜色条纹渐变的效果。

ESP32-S3-Touch-LCD-7 RGB LCD demo 7

打开 Arduino IDE 当中的串口助手,还可以看到程序运行的打印结果如下:

ESP32-S3-Touch-LCD-7 RGB LCD demo 8

代码回顾

屏幕内置 ST7262 驱动 IC,在例程当中我们使用 ESP_Display_Panel 库对屏幕进行了驱动,屏幕的引脚定义和初始化函数定义都在 waveshare_lcd_port.h 和 waveshare_lcd_port.cpp 当中。

知识点补充:
  • ESP_Display_Panel 是 ESP-IDF 官方提供的 统一屏幕驱动库,用于驱动各种 LCD、RGB 屏和电子纸,它把“总线 → 面板 → 触摸”都抽象成标准接口,让你不需要深入硬件细节,也能快速驱动不同型号的显示屏。
  • 使用这个库步骤基本是 3 步:创建 Bus 对象(负责底层数据传输)、创建 Panel 对象(负责具体 LCD/EPD 芯片驱动)、开始绘图。
  • 如果需要用到 LVGL,需要创建一个 Panel 与 LVGL display driver 的绑定才可以正常使用。

1. 在 waveshare_lcd_port.h 当中我们进行了 RGB 屏幕的各项参数的定义

    #include <Arduino.h>
#include <esp_display_panel.hpp>

引入 esp_display_panel.hpp 核心库头,包含 Bus/Panel/Touch 等定义,我们后面要使用库的 API 就来自它。

    #define EXAMPLE_LCD_NAME                        ST7262 // LCD IC 名字,用于 EXAMPLE_LCD_CLASS 宏构建类名
#define EXAMPLE_LCD_WIDTH (800) // LCD 宽度像素,与面板实际分辨率一致
#define EXAMPLE_LCD_HEIGHT (480) // LCD 高度像素,与面板实际分辨率一致
#define EXAMPLE_LCD_COLOR_BITS (24) // LCD 色深,24 表示每像素 24bpp
#define EXAMPLE_LCD_RGB_DATA_WIDTH (16) // RGB 数据位宽,这里是 16-bit 并口
#define EXAMPLE_LCD_RGB_COLOR_BITS (16) // RGB 色深也是 16-bit

这一部分是 LCD 面板参数的宏定义

    #define EXAMPLE_LCD_RGB_TIMING_FREQ_HZ          (16 * 1000 * 1000) // RGB timing frequency
#define EXAMPLE_LCD_RGB_TIMING_HPW (4) // Horizontal pulse width
#define EXAMPLE_LCD_RGB_TIMING_HBP (8) // Horizontal back porch
#define EXAMPLE_LCD_RGB_TIMING_HFP (8) // Horizontal front porch
#define EXAMPLE_LCD_RGB_TIMING_VPW (4) // Vertical pulse width
#define EXAMPLE_LCD_RGB_TIMING_VBP (8) // Vertical back porch
#define EXAMPLE_LCD_RGB_TIMING_VFP (8) // Vertical front porch
#define EXAMPLE_LCD_RGB_BOUNCE_BUFFER_SIZE (EXAMPLE_LCD_WIDTH * 10)

这一部分是定义 RGB 时序参数,直接对应 LCD datasheet 的 HSYNC/VSYNC、前沿/后沿(porch)、脉冲宽度(PW)和像素时钟频率(PCLK)。EXAMPLE_LCD_RGB_BOUNCE_BUFFER_SIZE 是驱动内部为了平稳输出分配的“临时缓冲区”大小,这里设置的是 width * 10。

    #define EXAMPLE_LCD_RGB_IO_DISP            (-1)  // RGB display pin number
#define EXAMPLE_LCD_RGB_IO_VSYNC (3) // VSYNC pin number
#define EXAMPLE_LCD_RGB_IO_HSYNC (46) // HSYNC pin number
#define EXAMPLE_LCD_RGB_IO_DE (5) // Data enable pin number
#define EXAMPLE_LCD_RGB_IO_PCLK (7) // Pixel clock pin number
#define EXAMPLE_LCD_RGB_IO_DATA0 (14) // RGB data pin 0
...
#if EXAMPLE_LCD_RGB_DATA_WIDTH > 8
#define EXAMPLE_LCD_RGB_IO_DATA8 (48) // RGB data pin 8
...
#endif
#define EXAMPLE_LCD_RST_IO (-1) // Reset pin number
#define EXAMPLE_LCD_BL_IO (-1) // Backlight pin number
#define EXAMPLE_LCD_BL_ON_LEVEL (1) // Backlight ON level
#define EXAMPLE_LCD_BL_OFF_LEVEL !EXAMPLE_LCD_BL_ON_LEVEL // Backlight OFF level

这一部分则是引脚映射,按照 ESP32-S3 与 RGB 屏幕之间的硬件连接进行填写。

IO_DISP、RST_IO、BL_IO 这三个是接到了扩展 IO 芯片上面,因此这里不做定义,填 -1 在代码中检测会跳过,这三个引脚其实都已经被硬件拉高。

    #define EXAMPLE_LCD_ENABLE_CREATE_WITH_CONFIG   (0)
#define EXAMPLE_LCD_ENABLE_PRINT_FPS (1)
#define EXAMPLE_LCD_ENABLE_DRAW_FINISH_CALLBACK (1)

#if EXAMPLE_LCD_ENABLE_PRINT_FPS
#define EXAMPLE_LCD_PRINT_FPS_PERIOD_MS (1000)
#define EXAMPLE_LCD_PRINT_FPS_COUNT_MAX (50)
#endif
  • EXAMPLE_LCD_ENABLE_CREATE_WITH_CONFIG:如果为 1,表示通过“配置结构体”方式创建 LCD 对象,为 0 则使用内置的 create 函数,这里我们设置为 0。
  • EXAMPLE_LCD_ENABLE_PRINT_FPS:开启后库会周期性打印帧率(用于 debug 性能)。PRINT_FPS_PERIOD_MS 是打印间隔,COUNT_MAX 表示统计时最大帧数等。
  • EXAMPLE_LCD_ENABLE_DRAW_FINISH_CALLBACK:当一帧绘制完成后是否触发回调(常用于通知上层释放缓冲或做下一步操作)

    #define _EXAMPLE_LCD_CLASS(name, ...) LCD_##name(__VA_ARGS__)
#define EXAMPLE_LCD_CLASS(name, ...) _EXAMPLE_LCD_CLASS(name, ##__VA_ARGS__)

这两行是宏技巧,把 EXAMPLE_LCD_CLASS(ST7262, args...) 展开为 LCD_ST7262(args...),目的是把 EXAMPLE_LCD_NAME 与实际的类名/构造函数连接起来,便于写通用的 create 函数。

    void waveshare_lcd_init();

初始化函数的声明,具体定义会在 cpp 函数当中列出。

2. 总线和面板等各部分的构造和屏幕初始化的步骤主要集中在 waveshare_lcd_port.cpp 当中

首先是关于创建 BusRGB 和具体的 LCD(Panel)对象,这里是给出了 create_lcd_without_config()create_lcd_with_config() 两种方式,分别是“宏快捷构造”与“显式配置结构体”。

    static LCD *create_lcd_without_config(void)
{
BusRGB *bus = new BusRGB(
#if EXAMPLE_LCD_RGB_DATA_WIDTH == 8
/* 8-bit RGB IOs */
EXAMPLE_LCD_RGB_IO_DATA0, EXAMPLE_LCD_RGB_IO_DATA1, EXAMPLE_LCD_RGB_IO_DATA2, EXAMPLE_LCD_RGB_IO_DATA3,
EXAMPLE_LCD_RGB_IO_DATA4, EXAMPLE_LCD_RGB_IO_DATA5, EXAMPLE_LCD_RGB_IO_DATA6, EXAMPLE_LCD_RGB_IO_DATA7,
EXAMPLE_LCD_RGB_IO_HSYNC, EXAMPLE_LCD_RGB_IO_VSYNC, EXAMPLE_LCD_RGB_IO_PCLK, EXAMPLE_LCD_RGB_IO_DE,
EXAMPLE_LCD_RGB_IO_DISP,
/* RGB timings */
EXAMPLE_LCD_RGB_TIMING_FREQ_HZ, EXAMPLE_LCD_WIDTH, EXAMPLE_LCD_HEIGHT,
EXAMPLE_LCD_RGB_TIMING_HPW, EXAMPLE_LCD_RGB_TIMING_HBP, EXAMPLE_LCD_RGB_TIMING_HFP,
EXAMPLE_LCD_RGB_TIMING_VPW, EXAMPLE_LCD_RGB_TIMING_VBP, EXAMPLE_LCD_RGB_TIMING_VFP
#elif EXAMPLE_LCD_RGB_DATA_WIDTH == 16
/* 16-bit RGB IOs */
EXAMPLE_LCD_RGB_IO_DATA0, EXAMPLE_LCD_RGB_IO_DATA1, EXAMPLE_LCD_RGB_IO_DATA2, EXAMPLE_LCD_RGB_IO_DATA3,
EXAMPLE_LCD_RGB_IO_DATA4, EXAMPLE_LCD_RGB_IO_DATA5, EXAMPLE_LCD_RGB_IO_DATA6, EXAMPLE_LCD_RGB_IO_DATA7,
EXAMPLE_LCD_RGB_IO_DATA8, EXAMPLE_LCD_RGB_IO_DATA9, EXAMPLE_LCD_RGB_IO_DATA10, EXAMPLE_LCD_RGB_IO_DATA11,
EXAMPLE_LCD_RGB_IO_DATA12, EXAMPLE_LCD_RGB_IO_DATA13, EXAMPLE_LCD_RGB_IO_DATA14, EXAMPLE_LCD_RGB_IO_DATA15,
EXAMPLE_LCD_RGB_IO_HSYNC, EXAMPLE_LCD_RGB_IO_VSYNC, EXAMPLE_LCD_RGB_IO_PCLK, EXAMPLE_LCD_RGB_IO_DE,
EXAMPLE_LCD_RGB_IO_DISP,
/* RGB timings */
EXAMPLE_LCD_RGB_TIMING_FREQ_HZ, EXAMPLE_LCD_WIDTH, EXAMPLE_LCD_HEIGHT,
EXAMPLE_LCD_RGB_TIMING_HPW, EXAMPLE_LCD_RGB_TIMING_HBP, EXAMPLE_LCD_RGB_TIMING_HFP,
EXAMPLE_LCD_RGB_TIMING_VPW, EXAMPLE_LCD_RGB_TIMING_VBP, EXAMPLE_LCD_RGB_TIMING_VFP
#endif
);
return new EXAMPLE_LCD_CLASS(
EXAMPLE_LCD_NAME, bus, EXAMPLE_LCD_WIDTH, EXAMPLE_LCD_HEIGHT, EXAMPLE_LCD_COLOR_BITS, EXAMPLE_LCD_RST_IO
);
}
  • 这里根据 EXAMPLE_LCD_RGB_DATA_WIDTH(8 或 16)选择不同的构造参数列表,我们设置的是 16,所以这里会根据 16 位的数据宽度依次将等数据引脚(DATA0~DATA15)、HSYNC、VSYNC、PCLK、DE、DISP、RGB 时序等原始参数传给构造函数,用于构建 **BusRGB**对象。
  • 接下来会调用 EXAMPLE_LCD_CLASS(EXAMPLE_LCD_NAME, ...),也就是把 EXAMPLE_LCD_NAME 拼成类名 LCD_ST7262 ,最后是 LCD_ST7262(bus, EXAMPLE_LCD_WIDTH, EXAMPLE_LCD_HEIGHT, EXAMPLE_LCD_COLOR_BITS, EXAMPLE_LCD_RST_IO), 传入 bus 和一些基础参数(分辨率、色深、复位引脚),这一步是为了创建具体 Panel(LCD)对象以及与 bus 的绑定。

    static LCD *create_lcd_with_config(void)

static LCD *create_lcd_with_config(void) 则是用显式配置结构体 (BusRGB::Config、LCD::Config 创建 Bus 和 Panel,配置更清晰/可扩展。构造一个 BusRGB::Config bus_config 用于包含所有显示参数,构造一个 LCD::Config lcd_config 用于指定复位引脚、分辨率等。

    DRAM_ATTR int frame_count = 0;
DRAM_ATTR int fps = 0;
DRAM_ATTR long start_time = 0;

IRAM_ATTR bool onLCD_RefreshFinishCallback(void *user_data)
{
if (start_time == 0) {
start_time = millis();

return false;
}

frame_count++;
if (frame_count >= EXAMPLE_LCD_PRINT_FPS_COUNT_MAX) {
fps = EXAMPLE_LCD_PRINT_FPS_COUNT_MAX * 1000 / (millis() - start_time);
esp_rom_printf("LCD FPS: %d\n", fps);
frame_count = 0;
start_time = millis();
}

return false;
}
#endif // EXAMPLE_LCD_ENABLE_PRINT_FPS

#if EXAMPLE_LCD_ENABLE_DRAW_FINISH_CALLBACK
IRAM_ATTR bool onLCD_DrawFinishCallback(void *user_data)
{
esp_rom_printf("LCD draw finish callback\n");

return false;
}
#endif
  • 这段代码是 在 LCD 刷新完成的中断回调中统计真实帧率(FPS),使用了 IRAM / DRAM 安全写法。onLCD_RefreshFinishCallback() 这个是 LCD 刷新完成回调函数,会在 LCD 刷新完成一帧后被驱动调用,每刷新一帧将会进行计数,当帧数达到 EXAMPLE_LCD_PRINT_FPS_COUNT_MAX 的设定值(.h 文件当中设置成了 50)时,则会计算 FPS 的值并打印。
  • onLCD_DrawFinishCallback() 这个是完成一次绘制的回调函数,会打印一句话。

    void waveshare_lcd_init(void)

这是最重要的入口——把各部分串起来实际启动屏幕,我们分别来看看每一步函数都执行了什么。

    #if EXAMPLE_LCD_ENABLE_CREATE_WITH_CONFIG
Serial.println("Initializing \"RGB\" LCD with config");
auto lcd = create_lcd_with_config();
#else
Serial.println("Initializing \"RGB\" LCD without config");
auto lcd = create_lcd_without_config();
#endif

根据宏选择“有 config”或“无 config”方式创建 LCD*,在头文件当中我们已经将 EXAMPLE_LCD_ENABLE_CREATE_WITH_CONFIG 设置为 0,因此我们将会采用 create_lcd_without_config() 的方式去创建对象。

    auto bus = static_cast<BusRGB *>(lcd->getBus());
bus->configRGB_BounceBufferSize(EXAMPLE_LCD_RGB_BOUNCE_BUFFER_SIZE); // Set bounce buffer to avoid screen drift
  • 从 lcd 取得 Bus 指针并转换成 BusRGB* ,然后调用配置函数设置“bounce buffer”大小。
  • Bounce buffer (回弹缓冲):在并口 RGB 驱动中经常使用一个中间缓冲区来处理数据节拍和避免显示漂移(当驱动无法持续提供像素数据时用缓冲补齐)。这里设置一个像素级的缓冲大小以平衡内存占用和稳定性。

    lcd->init();

这是关键的初始化调用,实际将会进入到 LCD_ST7262 的初始化函数,在这个初始化函数当中会进行各项配置和创建 RGB Panel(esp_lcd_new_rgb_panel) ,完成了 构建 RGB LCD 驱动的全部底层准备工作。

    #if EXAMPLE_LCD_ENABLE_PRINT_FPS
// Attach a callback function which will be called when the Vsync signal is detected
lcd->attachRefreshFinishCallback(onLCD_RefreshFinishCallback);
#endif
#if EXAMPLE_LCD_ENABLE_DRAW_FINISH_CALLBACK
// Attach a callback function which will be called when every bitmap drawing is completed
lcd->attachDrawBitmapFinishCallback(onLCD_DrawFinishCallback);
#endif

绑定两个回调函数,分别会在一帧刷新完成和一次绘制完成后调用。

    assert(lcd->begin());

init() 的准备工作最后启动起来并且执行面板初始化命令,assert() 用来确保 begin() 返回成功(布尔真),失败的话会触发断言,帮助快速定位初始化失败问题。

    if (lcd->getBasicAttributes().basic_bus_spec.isFunctionValid(LCD::BasicBusSpecification::FUNC_DISPLAY_ON_OFF)) {
lcd->setDisplayOnOff(true);
}

检查并打开显示。

    Serial.println("Draw color bar from top left to bottom right, the order is B - G - R");
lcd->colorBarTest();

colorBarTest() 是一个内置的测试函数,用来绘制一组色条(蓝-绿-红)以便快速可视验证显示效果。

3. 主程序 DrawColorBar.ino

    Serial.begin(115200); // Initialize serial communication at 115200 baud rate
Serial.println("RGB LCD example start"); // Print start message for RGB LCD example
waveshare_lcd_init(); // Initialize the RGB LCD
Serial.println("RGB LCD example end"); // Print end message for RGB LCD example

void setup() 函数当中进行串口初始化和执行 waveshare_lcd_init() 函数