当前位置: 首页 > news >正文

《DNESP32P4开发指南_V1.0》第二十一章 RGBLCD实验

第二十一章 RGBLCD实验

本章将继续学习ESP_IDF的LCD外设驱动,主要学习RGB接口屏幕的驱动方法。本章用到的是ESP32-P4的 Camera-LCD控制器驱动RGBLCD屏幕,实现和RGBLCD屏之间的通信,实现ASCII字符、图形和彩色的显示。
本章分为如下几个小节:
21.1 RGBLCD和ESP32-P4的LCD模块介绍
21.2 硬件设计
21.3 程序设计
21.4 下载验证

21.1 RGBLCD和ESP32-P4的LCD模块介绍
21.1.1 RGBLCD简介
在第20章,我们已经介绍过TFTLCD液晶屏了,实际上RGBLCD也是TFTLCD,只是接口不同而已。接下来我们简单介绍一下RGBLCD的驱动。
(1)RGBLCD的信号线
RGBLCD的信号线如表21.1.1.1所示:

QQ截图20260508110211

表21.1.1.1 RGBLCD信号线
一般RGB屏都有如表21.1.1.1所示的信号线,有24根颜色数据线(RGB各8根,即RGB888格式),这样可以表示最多1600W色,DE、VS、HS和CLK,用于控制数据传输。
像素同步时钟信号线LCD_CLK:液晶屏与外部使用同步通讯方式,以CLK信号作为同步时钟,在同步时钟的驱动下,每个时钟传输一个像素点数据。
水平同步信号线LCD_HSYNC:有时也被称为行同步信号,用于表示液晶屏一行像素数据的传输结束,每传输完成液晶屏的一行像素数据时,LCD_HSYNC发生电平跳变,如分辨率为800x480的显示屏(800列,480行),传输一帧的图像LCD_HSYNC的电平会跳变480次。
垂直同步信号线LCD_VSYNC:有时也被称为场同步信号,用于表示液晶屏一帧像素数据的传输结束,每传输完成一帧像素时,LCD_VSYNC会发生电平跳变。其中“帧”是图像的单位,一幅图像称为一帧,在液晶屏中,一帧指一个完整屏液晶像素点。
数据使能信号线DE:用于表示数据的有效性,当DE信号线为高电平时,RGB信号线表示的数据有效。
RGB数据线:用来传输颜色数据。
(2)RGBLCD的驱动模式
RGB屏一般有2种驱动模式:DE模式和HV模式。DE模式使用DE信号来确定有效数据(DE为高/低时,数据有效),而HV模式,则需要行同步信号HSYNC和场同步信号VSYNC,来表示扫描的行和列。
DE模式和HV模式的行扫描时序图(以800*480的LCD面板为例),如图21.1.1.1所示:

image001

图21.1.1.1 DE/HV模式行扫描时序图
从图中可以看出,DE和HV模式,时序基本一样,DE模式需要提供DE信号(DEN),而HV模式,则无需DE信号。图中的HSD即HSYNC信号,用于行同步,注意:在DE模式下面,是可以不用HSYNC信号和VSYNC信号,即可以不接,液晶照样可以正常工作。在引脚不是太充足的情况下,可以选择使用DE模式。
图中的thpw为水平同步有效信号脉宽,用于表示一行数据的开始;thb为水平后廊,表示从水平有效信号开始,到有效数据输出之间的像素时钟个数;thfp为水平前廊,表示一行数据结束后,到下一个水平同步信号开始之前的像素时钟个数。
图21.1.1.1仅是一行数据的扫描,输出800个像素点数据,而液晶面板总共有480行,这就还需要一个垂直扫描时序图,如图21.1.1.2所示:

image003

图21.1.1.2 垂直扫描时序图
图中的VSD就是垂直同步信号LCD_VSYNC,HSD就是水平同步信号LCD_HSYNC,DE为数据使能信号。如图可知,一个垂直扫描,刚好就是480个有效的DE脉冲信号,每一个DE时钟周期,扫描一行,总共扫描480行,完成一帧数据的显示。这就是800480的LCD面板扫描时序,其他分辨率的LCD面板,时序类似。
图中的tvpw为垂直同步有效信号脉宽,用于表示一帧数据的开始;tvb为垂直后廊,表示垂直同步信号以后的无效行数,tvfp为垂直前廊,表示一帧数据输出结束后,到下一个垂直同步信号开始之前的无效行数;这几个时间在配置RGBLCD设备时序时,需要进行设置。
(3)正点原子 RGBLCD模块
正点原子目前提供五款 RGBLCD 模块:ATK-MD0430R-480272(4.3寸,480
272)、ATK-MD0430R-800480(4.3寸,800480) ATK-MD0700R-800480(7寸,800480)、 ATK-MD0700R-1024600(7 寸,1024600)和ATK-MD1010R(10.1 寸,1280800),这里我们以 ATK-MD0430R-800480为例,给大家介绍。该模块的接口原理图如图21.1.1.3 所示:

image005

图21.1.1.3 RGBLCD模块对外接口原理图
图中 J1就是对外接口,是一个 40PIN 的 FPC 座( 0.5mm 间距),通过 FPC 线,可以连接到ESP32-P4开发板底板的RGB接口上面,从而实现和ESP32-P4的连接。该模块的接口十分完善,采用 RGB888 格式,并支持 DE&HV 模式,还支持触摸屏(电阻/电容)和背光控制。右侧的几个电阻,并不是都焊接的,而是可以用户自己选择。LCD_R7/G7/B7是用来设置RGBLCD的ID的, 由于 RGBLCD 没有读写寄存器,也就没有所谓的 ID, 所以在这里我们通过在模块上面,控制 R7/G7/B7 的上/下拉,来自定义 RGBLCD 模块的 ID,帮助主控芯片去判断当前 LCD 面板的分辨率和相关参数,以提高程序兼容性。这几个位的设置关系如表21.1.1.2 所示:

QQ截图20260508110229

表21.1.1.2正点原子 RGBLCD模块ID对应关系
这样我们在程序里面,读取 LCD_R7/G7/B7,得到 M0:M2 的值,从而判断 RGBLCD 模块的型号,并执行不同的配置,即可实现不同 LCD模块的兼容。
更详细的RGBLCD相关内容,可以参考正点原子提供的《ATK-MD0430R模块用户手册_V1.0》和《ATK-MD0430R模块使用说明_V1.0》,在这两个手册中,会有LCD相关参数信息。
这里还要说明一下,市面上有一些RGBLCD屏幕,是带有三线SPI接口,需要通过SPI接口对其进行配置操作,才能让屏幕正常显示。而正点原子的RGBLCD屏幕是不需要通过SPI接口配置的,使用起来更加方便。

21.1.2 ESP32-P4的LCD模块简介
ESP32-P4的LCD_CAM控制器包括独立的LCD控制模块和Camera(摄像头)控制模块。其中LCD模块用于发送并行视频数据信号,其总线支持RGB、MOTO6800和I8080等接口时序。Camera模块用于接收并行视频数据信号,其总线支持DVP 8/16位并行输入模式。
本小节主要介绍LCD_CAM控制器的LCD模块-RGB接口部分内容。
首先我们通过分析LCD_CAM控制器的结构框图,了解一下其工作过程。结构框图如下图所示。

image007

图21.1.2.1 LCD_CAM控制器结构框图
LCD控制模块(红色虚线以下)包含发送控制单元(LCD_Ctrl)、发送异步FIFO(Async TX FIFO)、LCD时钟生成模块(LCD_Clock Generator)和格式转换模块(RGB/YUV Converter)。
发送控制单元:用于控制LCD数据的发送。LCD数据从上图可知,可由GDMA取自内部或外部存储器。
发送异步FIFO:用于与外部设备进行交互。
LCD时钟生成模块:用于生成LCD_PCLK时钟。时钟源经过时钟生成模块处理后,生成LCD模块所需要的时钟LCD_PCLK,这一过程如下图所示。

image009

图21.1.2.2 LCD模块时钟生成过程
LCD模块时钟由三个时钟源提供,分别是XTAL_CLK、PLL_F160M_CLK和APLL_CLK。可由HP_SYS_CLKRST_PERI_CLK_CTRL19_REG的HP_SYS_CLKRST_LCD_CLK_SRC_SEL位决定,0选择使用XTAL_CLK,1选择使用PLL_F160M_CLK,2选择使用APLL_CLK。
选择好时钟源后(即得到LCD_CLK_SRC),经过降频得到LCD_CLK,后面再经过分频最终得到LCD_PCLK,这三者的关系如下:

image011

Note:
① 第一条公式中的N、b和a都是通过HP_SYS_CLKRST_PERI_CLK_CTRL110_REG寄存器进行配置的。N的取值范围为2≤N≤256,通过HP_SYS_CLKRST_LCD_CLK_DIV_NUM位进行配置。b是通过HP_SYS_CLKRST_LCD_CLK_DIV_NUMERATOR位进行配置,而a是通过HP_SYS_CLKRST_LCD_CLK_DIV_DENONMINATOR位进行配置。
② 第二条公式中的MO是通过LCD_CAM_LCD_CLOCK_REG寄存器进行配置的。MO的数值主要是由LCD_CAM_LCD_CLK_EQU_SYSCLK和LCD_CAM_LCD_CLKCNT_N去决定,当LCD_CAM_LCD_CLK_EQU_SYSCLK为1,MO为1;当LCD_CAM_LCD_CLK_EQU_SYSCLK为0时,MO为LCD_CAM_LCD_CLKCNT_N+1。
格式转换模块:用于各种格式的视频数据互相转换。在上图中,该部分是虚线框,也就是可不使用该模块。
Camera-LCD控制器的CAM和LCD接口都可通过GPIO交换矩阵灵活配置使用任意GPIO引脚。对于LCD模块的信号,即图21.1.2.1右下角部分,在这里也简单介绍一下,如下表所示。

QQ截图20260508110240

表21.1.2.1 LCD模块信号描述
使用不同接口,使用的数据线会有差异。通过RGB接口去驱动正点原子的RGBLCD屏幕,没有用到LCD_CD和LCD_CS引脚,且数据总线引脚只用到16根(RGB565格式)。
LCD模块还支持数据格式控制,比如:
① 反转数据位顺序,LCD_DATA_out[x:0]变为LCD_DATA_out[0:x],即MSB与LSB切换。
② 反转数据字节顺序,在16bit模式下有效,{Byte1,Byte0}变为{Byte0,Byte1}
③ 两个字节反转位置,在8bit模式下有效,{Byte1}{Byte0}变为{Byte0}{Byte1}
假如一款LCD模块RGB565像素数据是先发送低位后发送高位,若硬件不支持反转数据字节顺序,那么要显示正常颜色,就得通过代码切换高低字节发送;若硬件支持反转数据字节顺序的功能,便可以提高LCD的显示效率。在SPILCD例程中,就有用到这个功能配置。

21.2 硬件设计
21.2.1 例程功能
使用ESP32-P4开发板的RGB接口来驱动RGB屏,RGBLCD模块的接口底板上,通过40P的FPC排线连接RGBLCD模块,实现RGBLCD模块的驱动和显示,下载成功后,按下复位之后,就可以看到RGBLCD模块不停的显示一些信息并不断切换底色。同时,屏幕上会显示LCD的ID。注意:若想支持正点原子的7寸1024600 RGBLCD和10.1寸 1280800 RGBLCD,则需要修改ESP-IDF v5.4版本的lcd_hal.c文件,该文件位于esp-idf-v5.4\components\hal目录下,然后找到lcd_hal_cal_pclk_freq函数屏蔽一下代码段。

image013

21.2.2 硬件资源
1)LED灯
LED 0 - IO51
2)RGBLCD
LCD_R3 - IO18 LCD_R4 - IO17 LCD_R5 - IO16
LCD_R6 - IO15 LCD_R7 - IO14
LCD_G2 - IO13 LCD_G3 - IO12 LCD_G4 - IO11
LCD_G5 - IO10 LCD_G6 - IO9 LCD_G7 - IO8
LCD_B3 - IO7 LCD_B4 - IO6 LCD_B5 - IO5
LCD_B6 - IO4 LCD_B7 - IO3
LCD_CLK - IO20 LCD_DE - IO22 LCD_BL - IO53
CT_RST - IO45 CT_INT - IO21
IIC_SCL - IO32 IIC_SDA - IO33

21.2.3 原理图
RGBLCD原理图,如下图所示。

image015

图21.2.3.1 RGBLCD原理图

21.3 程序设计
21.3.1 LCD的IDF驱动
LCD外设驱动位于ESP-IDF下的components/esp_lcd目录下。要使用esp_lcd功能,需要导入一下头文件:

#include "esp_lcd_panel_interface.h"	/* LCD面板结构体类型 */
#include "esp_lcd_panel_io.h"			/* 驱动芯片接收/发送命令,发送颜色数据等函数 */
#include "esp_lcd_panel_vendor.h"		/* 包含LCD外设驱动支持的几款驱动芯片 */
#include "esp_lcd_panel_ops.h"			/* LCD设备接口函数(reset/init/del等) */
#include "esp_lcd_panel_commands.h"	/* LCD驱动芯片的命令 */

RGB LCD 驱动流程可大致分为三个部分:初始化接口设备、移植驱动组件和初始化 LCD 设备。
初始化接口设备
初始化接口设备需要先初始化总线,再创建接口设备。
对于仅有RGB接口的LCD,不支持传输命令及参数,所以不需要初始化接口设备。
对于具备SPI接口和RGB接口的LCD,这里就需要创建SPI接口设备,如第20章操作一样,这里不做展开。
初始化接口设备,主要就是为了上层接口能够调用到底层接口函数进行数据发送。

移植驱动组件
对于仅有RGB接口的LCD,RGB接口驱动已经通过注册回调函数的方式实现了结构体esp_lcd_panel_t中的各项功能,即后续通过结构体esp_lcd_panel_t的调用即可访问到RGB底层接口函数。在LCD驱动组件中还提供esp_lcd_new_rgb_panel函数,该函数用于创建数据类型为esp_lcd_panel_handle_t的LCD设备,esp_lcd_panel_handle_t和esp_lcd_panel_t实际上是一样的。通过esp_lcd_new_rgb_panel函数,便可以使得应用程序能够使用LCD通用API函数(初始化/画点/打开显示)去操作LCD设备。因此,这种LCD不需要移植驱动组件,直接可以跳到初始化LCD设备步骤。
对于具备SPI接口和RGB接口的LCD,初始化接口后,还需要通过SPI接口发送命令以及参数,如第20章操作一样,这里不做展开。

初始化LCD设备
1, 为RGBLCD创建LCD面板esp_lcd_new_rgb_panel
该函数用于为RGBLCD创建LCD面板并对LCD设备配置,其函数原型如下:

esp_err_t esp_lcd_new_rgb_panel(const esp_lcd_rgb_panel_config_t *rgb_panel_config, esp_lcd_panel_handle_t *ret_panel)

函数形参:

QQ截图20260508110253

表21.3.1.1 esp_lcd_new_rgb_panel函数形参描述
函数返回值:
ESP_OK表示创建成功。
ESP_ERR_INVALID_ARG表示参数有误。
ESP_ERR_NO_MEM表示内存不足。
ESP_ERR_NOT_FOUND表示找不到硬件资源。
esp_panel_config为指向LCD设备配置结构体指针,esp_lcd_rgb_panel_config_t结构体中包含非常多成员,如下代码所示。

typedef struct {lcd_clock_source_t clk_src;   		/* LCD模块时钟源 */esp_lcd_rgb_timing_t timings; 		/* RGBLCD时序参数*/size_t data_width;            		/* 数据位宽(RGB565格式即16位) */size_t bits_per_pixel;        		/* 帧缓冲区颜色深度(0时默认为data_width)  */size_t num_fbs;               		/* 整屏帧缓冲区数目(0/1表示1个,最多3个)  */size_t bounce_buffer_size_px; 		/* DRAM缓冲区供DMA使用(速度比PSRAM快)  */size_t sram_trans_align;      		/* SRAM申请的缓冲区对齐  */size_t psram_trans_align;     		/* PSRAM申请的缓冲区对齐  */int hsync_gpio_num;           		/* LCD_HSYNC信号引脚  */int vsync_gpio_num;           		/* LCD_VSYNC信号引脚  */int de_gpio_num;              		/* LCD_DE信号引脚  */int pclk_gpio_num;            		/* LCD_PCLK信号引脚  */int disp_gpio_num;            		/* LCD_BL信号引脚  */int data_gpio_nums[SOC_LCD_RGB_DATA_WIDTH]; 	/* 数据线信号引脚(16个IO)  */struct {uint32_t disp_active_low: 1;     /* disp信号引脚的有效电平  */uint32_t refresh_on_demand: 1;   /* 1时调用draw_bitmap函数刷新帧缓存区  */uint32_t fb_in_psram: 1;         /* 帧缓冲区优先从PSRAM中分配  */uint32_t double_fb: 1;           /* 分配两个整屏缓存区,与num_fbs一致效果  */uint32_t no_fb: 1;               /* 不分配帧缓冲区,需手动在回调函数中处理  */uint32_t bb_invalidate_cache: 1; /* 对读取数据执行缓存无效操作,释放缓存  */} flags;                             /* RGBLCD配置标志  */
} esp_lcd_rgb_panel_config_t;

esp_lcd_rgb_panel_config_t结构体中涉及到一个比较重要的结构体esp_lcd_rgb_timing_t,该结构体主要就是对RGBLCD的时序参数做配置,如下代码所示。

typedef struct {uint32_t pclk_hz;           		/* LCD_PCLK时钟频率  */uint32_t h_res;             		/* 水平分辨率,即一行中的像素数  */uint32_t v_res;             		/* 垂直分辨率,即帧中的行数  */uint32_t hsync_pulse_width; 		/* 水平同步宽度,单位为PCLK周期  */uint32_t hsync_back_porch;  		/* 水平后廊  */uint32_t hsync_front_porch;			/* 水平前廊  */uint32_t vsync_pulse_width; 		/* 垂直同步宽度,单位为行数  */uint32_t vsync_back_porch;  		/* 垂直后廊  */uint32_t vsync_front_porch; 		/* 垂直前廊  */struct {uint32_t hsync_idle_low: 1;  	/* 空闲状态下,LCD_HSYNC低电平  */uint32_t vsync_idle_low: 1;  	/* 空闲状态下,LCD_VSYNC低电平  */uint32_t de_idle_high: 1;    	/* 空闲状态下,LCD_DE高电平  */uint32_t pclk_active_neg: 1; 	/* 显示数据是否在LCD_PCLK下降沿上发送  */uint32_t pclk_idle_high: 1;  	/* 空闲状态下,LCD_PCLK高电平  */} flags;                         	/* RGBLCD时序参数  */
} esp_lcd_rgb_timing_t;

创建好RGBLCD屏幕后,就可以调用LCD设备的函数,对RGBLCD进行初始化以及做一些配置工作,如下代码所示。

    /* 复位屏幕 */ESP_ERROR_CHECK(esp_lcd_panel_reset(panel_handle));/* 初始化RGB */ESP_ERROR_CHECK(esp_lcd_panel_init(panel_handle));

初始化LCD设备完成之后,就得编写一些LCD操作的函数,比如设置横竖屏、画点、画线、画图形、显示字符等。

21.3.2 程序流程图

image018

图21.3.2.1 RGBLCD实验程序流程图

21.3.3 程序解析
在11_rgblcd例程中,作者在11_rgblcd\components\BSP路径下新建LCD文件夹,并且需要更改CMakeLists.txt内容,以便在其他文件上调用。
1. RGBLCD驱动代码
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。LCD驱动源码包括五个文件:lcd.c、lcd.h、rgblcd.c、rgblcd.h和lcdfont.h。
rgblcd.c文件存放的是RGBLCD的驱动函数,而rgblcd.h存放的是RGBLCD接口相关引脚宏以及管理RGBLCD屏幕的重要结构体类型。lcd.c文件主要lcd的一些绘图函数,而lcd.h则存放的是引脚接口宏定义以及函数声明。lcdfont.h存放的是4种字体大小不一样的ASCII字符集(1212、1616、2424和3232)。
这里主要给大家介绍一下RGBLCD的初始化函数rgblcd_init,如下代码所示:

/*** @brief   	初始化RGBLCD* @param    	无* @retval    	RGBLCD句柄*/
esp_lcd_panel_handle_t rgblcd_init(void)
{rgbdev.id = lcddev.id;               	/* 读取LCD面板ID *//* 配置VDDPST_1管理的IO电压 */esp_ldo_channel_handle_t ldo_rgblcd_phy = NULL;esp_ldo_channel_config_t ldo_rgblcd_phy_config = {.chan_id    = 4,                 	/* 选择内存LDO */.voltage_mv = 3300,             	/* 输出标准电压提供VDD_RGBLCD_DPHY */};
ESP_ERROR_CHECK(esp_ldo_acquire_channel(&ldo_rgblcd_phy_config, 
&ldo_rgblcd_phy));if (rgbdev.id == 0X4342)                    /* 4.3寸屏, 480*272 RGB屏 */{rgbdev.pwidth   = 480;                  /* 面板宽度,单位:像素 */rgbdev.pheight  = 272;                  /* 面板高度,单位:像素 */rgbdev.hsw      = 4;                    /* 水平同步宽度 */rgbdev.hbp      = 43;                   /* 水平后廊 */rgbdev.hfp      = 8;                    /* 水平前廊 */rgbdev.vsw      = 4;                    /* 垂直同步宽度 */rgbdev.vbp      = 12;                   /* 垂直后廊 */rgbdev.vfp      = 8;                    /* 垂直前廊 */rgbdev.pclk_hz  = 9 * 1000 * 1000;      /* 设置像素时钟 9Mhz */}else if (rgbdev.id == 0X4384){rgbdev.pwidth   = 800;                  /* 面板宽度,单位:像素 */rgbdev.pheight  = 480;                  /* 面板高度,单位:像素 */rgbdev.hsw      = 48;                   /* 水平同步宽度 */rgbdev.hbp      = 88;                   /* 水平后廊 */rgbdev.hfp      = 40;                   /* 水平前廊 */rgbdev.vsw      = 3;                    /* 垂直同步宽度 */rgbdev.vbp      = 32;                   /* 垂直后廊 */rgbdev.vfp      = 13;                   /* 垂直前廊 */rgbdev.pclk_hz  = 30 * 1000 * 1000;     /* 设置像素时钟 30Mhz */}else if (rgbdev.id == 0x7084)               /* ATK-MD0700R-800480 */{rgbdev.pwidth   = 800;                  /* LCD面板的宽度 */rgbdev.pheight  = 480;                  /* LCD面板的高度 */rgbdev.hsw      = 1;                    /* 水平同步宽度 */rgbdev.hbp      = 46;                   /* 水平后廊 */rgbdev.hfp      = 210;                  /* 水平前廊 */rgbdev.vsw      = 1;                    /* 垂直同步宽度 */rgbdev.vbp      = 23;                   /* 垂直后廊 */rgbdev.vfp      = 22;                   /* 垂直前廊 */rgbdev.pclk_hz  = 33 * 1000 * 1000;     /* 设置像素时钟 33Mhz */}else if (rgbdev.id == 0x7016)               /* ATK-MD0700R-1024600 */{rgbdev.pwidth   = 1024;                 /* LCD面板的宽度 */rgbdev.pheight  = 600;                  /* LCD面板的高度 */rgbdev.hsw      = 20;                   /* 水平同步宽度 */rgbdev.hbp      = 140;                  /* 水平后廊 */rgbdev.hfp      = 160;                  /* 水平前廊 */rgbdev.vsw      = 3;                    /* 垂直同步宽度 */rgbdev.vbp      = 20;                   /* 垂直后廊 */rgbdev.vfp      = 12;                   /* 垂直前廊 */rgbdev.pclk_hz  = 48 * 1000 * 1000;     /* 设置像素时钟 48Mhz */}else if (rgbdev.id == 0x1018)               /* ATK-MD1018R-1280800 */{rgbdev.pwidth   = 1280;                 /* LCD面板的宽度 */rgbdev.pheight  = 800;                  /* LCD面板的高度 */rgbdev.hsw      = 10;                   /* 水平同步宽度 */rgbdev.hbp      = 140;                  /* 水平后廊 */rgbdev.hfp      = 10;                   /* 水平前廊 */rgbdev.vsw      = 3;                    /* 垂直同步宽度 */rgbdev.vbp      = 23;                   /* 垂直后廊 */rgbdev.vfp      = 10;                   /* 垂直前廊 */rgbdev.pclk_hz  = 48 * 1000 * 1000;     /* 设置像素时钟 48Mhz */}/* 配置RGB参数 */esp_lcd_rgb_panel_config_t panel_config = {     /* RGBLCD配置结构体 */.num_fbs            = 2,                    /* 缓存区数量 */.data_width         = 16,                   /* 数据宽度为16位 */.psram_trans_align  = 64,                   /* 在PSRAM分配的缓冲区的对齐 */.clk_src            = LCD_CLK_SRC_DEFAULT,  /* RGBLCD外设时钟源 */.disp_gpio_num      = GPIO_NUM_NC,          /* 用于显示控制信号,不用设-1 */.pclk_gpio_num      = GPIO_LCD_PCLK,        /* PCLK信号引脚 */.hsync_gpio_num     = GPIO_NUM_NC,          /* HSYNC信号引脚,DE模式不用 */.vsync_gpio_num     = GPIO_NUM_NC,          /* VSYNC信号引脚,DE模式不用 */.de_gpio_num        = GPIO_LCD_DE,          /* DE信号引脚 */.data_gpio_nums = {                         /* 数据线引脚 */GPIO_LCD_B3, GPIO_LCD_B4, GPIO_LCD_B5, GPIO_LCD_B6, GPIO_LCD_B7,GPIO_LCD_G2, GPIO_LCD_G3, GPIO_LCD_G4, GPIO_LCD_G5, GPIO_LCD_G6, 
GPIO_LCD_G7,GPIO_LCD_R3, GPIO_LCD_R4, GPIO_LCD_R5, GPIO_LCD_R6, GPIO_LCD_R7,},.timings = {                                /* RGBLCD时序参数 */.pclk_hz            = rgbdev.pclk_hz,   /* 像素时钟频率 */.h_res              = rgbdev.pwidth,    /* 水平分辨率,即一行中的像素数 */.v_res              = rgbdev.pheight,   /* 垂直分辨率,即帧中的行数 */.hsync_back_porch   = rgbdev.hbp,       /* 水平后廊 */.hsync_front_porch  = rgbdev.hfp,       /* 水平前廊 */.hsync_pulse_width  = rgbdev.vsw,       /* 垂直同步宽度,单位:行数 */.vsync_back_porch   = rgbdev.vbp,       /* 垂直后廊 */.vsync_front_porch  = rgbdev.vfp,       /* 垂直前廊 */.vsync_pulse_width  = rgbdev.hsw,       /* 水平同步宽度 */.flags = {.pclk_active_neg = true,            /* RGB数据在下降沿计时 */},},.flags.fb_in_psram = true,                  /* 在PSRAM中分配帧缓冲区 */};esp_lcd_new_rgb_panel(&panel_config, &panel_handle);/* 创建RGB对象 */ESP_ERROR_CHECK(esp_lcd_panel_reset(panel_handle)); /* 复位RGB屏 */ESP_ERROR_CHECK(esp_lcd_panel_init(panel_handle));  /* 初始化RGB */rgblcd_display_dir(1);                              /* 设置横屏 */return panel_handle;                                /* RGBLCD句柄 */
}

该函数就是对RGBLCD进行初始化,整个过程就围绕着21.3.1小节中描述的驱动流程实现。通过esp_lcd_new_rgb_panel函数创建RGBLCD屏对象,后续通过LCD通用API接口对LCD进行配置,比如esp_lcd_panel_reset复位LCD屏,esp_lcd_panel_init初始化LCD屏。最后通过rgblcd_display_dir函数设置横屏显示。函数中还涉及到配置VDDPST_1管理的IO电压,通过调用esp_ldo_acquire_channel函数设置LDO_VO4输出3.3V。
上面RGBLCD对应的时序参数,可到对应的裸屏规格书中查找到,一般来说都是以典型值作为配置值。
接下来,介绍一下设置RGBLCD横竖屏的函数rgblcd_display_dir,如下代码所示:

/*** @brief     	RGBLCD显示方向设置* @param    	dir:0,竖屏;1,横屏* @retval  	无*/
void rgblcd_display_dir(uint8_t dir)
{rgbdev.dir = dir;              /* 显示方向 */if (rgbdev.dir == 0)           /* 竖屏 */{rgbdev.width = rgbdev.pheight;rgbdev.height = rgbdev.pwidth;esp_lcd_panel_swap_xy(panel_handle, true);          /* 交换X和Y轴 */ esp_lcd_panel_mirror(panel_handle, false, true);    /* Y轴进行镜像处理 */}else if (rgbdev.dir == 1)      /* 横屏 */{rgbdev.width = rgbdev.pwidth;rgbdev.height = rgbdev.pheight;esp_lcd_panel_swap_xy(panel_handle, false);         /* 不需要交换XY轴 */esp_lcd_panel_mirror(panel_handle, false, false);   /* XY轴不镜像处理 */}lcddev.width = rgbdev.width;   /* 宽度 */lcddev.height = rgbdev.height; /* 高度 */
}

该函数主要就是通过调用esp_lcd_panel_swap_xy函数和esp_lcd_panel_mirror函数去实现,前者将X轴和Y轴交换,而后者就是对X轴或Y轴进行镜像。
下面介绍在rgblcd.h文件定义的重要结构体:

/* LCD RGBLCD重要参数集 */
typedef struct  
{uint32_t pwidth;        /* RGBLCD面板的宽度,固定参数,不随显示方向改变 */uint32_t pheight;       /* RGBLCD面板的高度,固定参数,不随显示方向改变 */uint16_t hsw;           /* 水平同步宽度 */uint16_t vsw;           /* 垂直同步宽度 */uint16_t hbp;           /* 水平后廊 */uint16_t vbp;           /* 垂直后廊 */uint16_t hfp;           /* 水平前廊 */uint16_t vfp;           /* 垂直前廊  */uint8_t activelayer;    /* 当前层编号:0/1 */uint8_t dir;            /* 0,竖屏;1,横屏; */uint16_t id;            /* RGBLCD ID */uint32_t pclk_hz;       /* 设置像素时钟 */uint16_t width;         /* RGBLCD宽度 */uint16_t height;        /* RGBLCD高度 */
} _rgblcd_dev; 

_rgblcd_dev结构体用于保存一些RGBLCD重要参数信息,比如RGBLCD的ID、RGBLCD的长宽、RGBLCD的时序参数等。最后声明_rgblcd_dev结构体类型变量rgblcd_dev,rgblcd_dev在rgblcd.c中定义。
在lcd.h文件中也定义了一个重要参数结构体,如下代码所示。

/* LCD重要参数集 */
typedef struct  
{uint16_t id;                       	/* 读取ID */uint32_t width;                    	/* 面板宽度,固定参数,不随显示方向改变 */uint32_t height;                		/* 面板高度,固定参数,不随显示方向改变 */uint8_t  dir;                   		/* 0,竖屏(MIPI只能竖屏);1,横屏; */uint8_t  color_byte;              		/* 颜色格式 */esp_lcd_panel_handle_t lcd_panel_handle; 	/* LCD控制句柄 */struct{int lcd_rst;            			/* 复位引脚 */int lcd_bl;                    	/* 背光引脚 */} ctrl;
} _lcd_dev;

_lcd_dev结构体用于保存一些LCD重要参数信息,比如LCD的ID(RGBLCD)、LCD的长宽、LCD控制句柄等。最后声明_lcd_dev结构体类型变量lcddev,lcddev在lcd.c中定义。
下面我们再解析lcd.c的程序,看一下初始化函数lcd_init,代码如下:

/*** @brief     	初始化LCD* @param     	无* @retval     	无*/
void lcd_init(void)
{lcddev.id = lcd_panelid_read();           	/* 读取RGB LCD面板ID */lcddev.ctrl.lcd_rst = LCD_RST_PIN;       	/* 复位管脚 */lcddev.ctrl.lcd_bl  = LCD_BL_PIN;         	/* 背光管脚 */gpio_config_t gpio_init_struct = {0};gpio_init_struct.intr_type    = GPIO_INTR_DISABLE;          /* 失能引脚中断 */gpio_init_struct.mode         = GPIO_MODE_OUTPUT;           /* 输出模式 */gpio_init_struct.pull_up_en   = GPIO_PULLUP_DISABLE;        /* 失能上拉 */gpio_init_struct.pull_down_en = GPIO_PULLDOWN_DISABLE;      /* 失能下拉 */gpio_init_struct.pin_bit_mask = 1ull << lcddev.ctrl.lcd_bl; /* 设置的引脚 */ESP_ERROR_CHECK(gpio_config(&gpio_init_struct));            /* 配置GPIO */LCD_BL(0);      /* 背光关闭 */lcddev.lcd_panel_handle = rgblcd_init();  				/* 初始化RGB LCD */
ESP_ERROR_CHECK(esp_lcd_rgb_panel_get_frame_buffer(lcddev.lcd_panel_handle,
2, &lcd_buffer[0], &lcd_buffer[1])); 	/* 获取帧缓冲区 *//* 内部缓冲区刷新完成回调函数 */
const esp_lcd_rgb_panel_event_callbacks_t rgb_cbs = {.on_bounce_frame_finish = lcd_rgb_panel_refresh_done_callback,  };ESP_ERROR_CHECK(esp_lcd_rgb_panel_register_event_callbacks(
lcddev.lcd_panel_handle, &rgb_cbs, NULL));lcd_clear(WHITE);LCD_BL(1);      /* 打开背光 */
}

在lcd_init函数中,首先通过调用lcd_panelid_read函数读取RGBLCD的ID,把ID赋值给lcddev.id成员,然后对LCD背光控制引脚配置,后面再调用rgblcd_init函数初始化RGBLCD,紧接着通过esp_lcd_rgb_panel_get_frame_buffer函数接口把数据获取到。为了防止屏幕撕裂,这里还注册了刷新完成回调函数,在进行清屏函数中,需要等待一帧刷新完成,再进行下一帧刷新。最后调用lcd_clear函数清屏,拉高背光控制引脚,打开背光。
现在,来看一下读取RGBLCD的ID函数lcd_panelid_read,如下代码所示。

/*** @brief   	读取RGB LCD ID* @note     	利用LCD RGB线的最高位(R7,G7,B7)来识别面板ID*          	IO14 = R7(M0); IO8 = G7(M1); IO3 = B7(M2);*            	M2:M1:M0*           	0 :0 :0     4.3 寸480*272  RGB屏,ID = 0X4342*            	0 :0 :1     7   寸800*480  RGB屏,ID = 0X7084*           	0 :1 :0     7   寸1024*600 RGB屏,ID = 0X7016*           	0 :1 :1     7   寸1280*800 RGB屏,ID = 0X7018*           	1 :0 :0     4.3 寸800*480  RGB屏,ID = 0X4348*            	1 :0 :1     10.1寸1280*800 RGB屏,ID = 0X1018* * @param     	无* @retval  	0, 非法; *           	其他, LCD ID*/
uint16_t lcd_panelid_read(void)
{uint8_t idx = 0;gpio_config_t gpio_init_struct = {0};gpio_init_struct.intr_type      = GPIO_INTR_DISABLE;    	/* 失能引脚中断 */gpio_init_struct.mode           = GPIO_MODE_INPUT;      	/* 输入输出模式 */gpio_init_struct.pull_up_en     = GPIO_PULLUP_ENABLE;    	/* 使能上拉 */gpio_init_struct.pull_down_en   = GPIO_PULLDOWN_DISABLE;	/* 失能下拉 */
gpio_init_struct.pin_bit_mask   = 1ull << GPIO_LCD_R7 | 1ull << GPIO_LCD_G7 
| 1ull << GPIO_LCD_B7;gpio_config(&gpio_init_struct);                         	/* 配置GPIO */idx  = (uint8_t)gpio_get_level(GPIO_LCD_R7);        		/* 读取M0 */idx |= (uint8_t)gpio_get_level(GPIO_LCD_G7) << 1;   		/* 读取M1 */idx |= (uint8_t)gpio_get_level(GPIO_LCD_B7) << 2;   		/* 读取M2 *//* 正点原子其他的RGB LCD自行匹配 */switch (idx){case 0:{return 0x4342;                      /* ATK-MD0430R-480272 */}case 1:{return 0x7084;                      /* ATK-MD0700R-800480 */}case 2:{return 0x7016;                      /* ATK-MD0700R-1024600 */}case 3:{return 0x7018;                      /* ATK-MD0700R-1280800 */}case 4:{return 0x4384;                      /* ATK-MD0430R-800480 */}case 5:{return 0x1018;                      /* ATK-MD1018R-1280800 */}default:{return 0;}}
}

在前面也说到,RGBLCD屏并没有读功能,但是在正点原子的RGBLCD模块上,利用数据线(R7/G7/B7)做了一个巧妙的设计,可让主控芯片读到RGBLCD模块的ID,从而执行不同的初始化,实现不同分辨率的RGBLCD模块的兼容。
接下来,再看看这个回调函数里面的实现,如下代码所示。

/*** @brief     	内部缓存刷新完成回调函数* @param    	panel_io: RGBLCD IO的句柄* @param  		edata: 事件数据类型* @param    	user_ctx: 传入参数* @retval    	无*/
IRAM_ATTR static bool lcd_rgb_panel_refresh_done_callback(esp_lcd_panel_handle_t panel, const esp_lcd_rgb_panel_event_data_t *edata, void *user_ctx)
{refresh_done_flag = 1;return false;
}

当发送一帧完成之后,就回进入到回调函数中,把refresh_done_flag标志置1。
现在看一下LCD的清屏函数lcd_clear,如下代码所示:

/*** @brief     	清屏* @param     	color :清屏颜色* @retval   	无*/
IRAM_ATTR void lcd_clear(uint16_t color)
{
/* 将 void* 转换为 uint16_t* */uint16_t *buffer = (uint16_t *)lcd_buffer[buffer_sw];  /* 制定缓存区填充颜色值 */for (uint32_t i = 0; i < lcddev.width * lcddev.height; i++){buffer[i] = color;}esp_lcd_panel_draw_bitmap(lcddev.lcd_panel_handle, 0, 0, lcddev.width, 
lcddev.height, buffer);refresh_done_flag = 0;do{vTaskDelay(1);		/* 等待内部缓存刷新完成 */}while (refresh_done_flag != 1);/* 使用异或操作在 0 和 1 之间切换,目的是为了切换另一个缓冲区 */buffer_sw ^= 1;
}

在清屏函数里面,按照熟悉的流程,先申请内存,然后往内存存入颜色数据,再调用esp_lcd_panel_draw_bitmap进行区域画点。只不过这里没有申请内存,而是直接使用LCD初始化时候申请到的整屏缓存,即esp_lcd_rgb_panel_config_t结构体中的num_fbs成员。在mipi_lcd_init函数里面,设置该成员为2,所以看到lcd_buffer有两个元素。当发送完esp_lcd_panel_draw_bitmap函数,这时候dma就开始搬运数据,这个过程需要一点时间,所以利用refresh_done_flag变量进行等待完成。完成一帧数据的显示,最后切换另一个buffer,避免出现LCD撕裂现象。
最后介绍一下字符显示函数lcd_show_char,该函数代码如下:

/*** @brief    	在指定位置显示一个字符* @param   	x,y  :坐标* @param    	chr  :要显示的字符:" "--->"~"* @param    	size :字体大小 12/16/24/32* @param    	mode :叠加方式(1); 非叠加方式(0);* @param    	color:字体颜色* @retval    	无*/
void lcd_show_char(uint16_t x, uint16_t y, char chr, uint8_t size, uint8_t mode, uint16_t color)
{uint8_t temp, t1, t;uint16_t y0 = y;uint8_t csize = 0;
uint8_t *pfont = 0;csize = (size / 8 + ((size % 8) ? 1 : 0)) * (size / 2);	/* 得到字体大小 */chr = (char)chr - ' ';      								/* 得到偏移后的值*/switch (size){case 12:pfont = (uint8_t *)asc2_1206[(uint8_t)chr];     	/* 调用1206字体 */break;case 16:pfont = (uint8_t *)asc2_1608[(uint8_t)chr];     	/* 调用1608字体 */break;case 24:pfont = (uint8_t *)asc2_2412[(uint8_t)chr];     	/* 调用2412字体 */break;case 32:pfont = (uint8_t *)asc2_3216[(uint8_t)chr];     	/* 调用3216字体 */break;default:return ;}for (t = 0; t < csize; t++){temp = pfont[t];                        	/* 获取字符的点阵数据 */for (t1 = 0; t1 < 8; t1++)            		/* 一个字节8个点 */{if (temp & 0x80)                  		/* 有效点,需要显示 */{lcd_draw_point(x, y, color);    	/* 画点出来,要显示这个点 */}else if (mode == 0)                	/* 无效点,不显示 */{lcd_draw_point(x, y, g_back_color); 	/* 画背景色 */}temp <<= 1;                           	/* 移位, 以便获取下一个位的状态 */y++;if (y >= lcddev.height) return;      	/* 超区域了 */if ((y - y0) == size)              	/* 显示完一列了? */{y = y0;                         	/* y坐标复位 */x++;                           	/* x坐标递增 */if (x >= lcddev.width){return;                      	/* x坐标超区域了 */}break;}}}
}

在lcd_show_char函数里面,我们用到了四个字符集点阵数据数组asc2_1206、asc2_1608、asc2_2412和asc2_3216,通过参数font决定。此外该函数增加以叠加方式显示,或者以非叠加方式显示。叠加方式显示多用于在显示的图片上再显示字符。非叠加方式一般用于普通的显示。
注意:字符点阵数据的生成是依靠正点原子的XFONT软件,取模方式设置为:阴码+逐列式+顺向+C51格式,具体教程请参考正点原子任何一个《STM32开发指南》的OLED显示实验章节的程序解析处。
lcd.c的函数比较多,其他函数请大家自行查看源码,都有详细的注释。

2. CMakeLists.txt文件
本例程的功能实现主要依靠LCD驱动。要在main函数中,成功调用LCD文件中的内容,就得需要修改BSP文件夹下的CMakeLists.txt文件,修改如下:

set(src_dirsLED
LCD)set(include_dirsLED
LCD)set(requiresdriveresp_lcd
esp_common)idf_component_register(	SRC_DIRS ${src_dirs} INCLUDE_DIRS ${include_dirs} REQUIRES ${requires})component_compile_options(-ffast-math -O3 -Wno-error=format=-Wno-format)
  1. main.c驱动代码
    在main.c里面编写如下代码。
void app_main(void)
{esp_err_t ret;uint8_t x = 0;ret = nvs_flash_init();  	/* 初始化NVS */if(ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND){ESP_ERROR_CHECK(nvs_flash_erase());ESP_ERROR_CHECK(nvs_flash_init());}led_init();     			/* LED初始化 */lcd_init();     			/* LCD屏初始化 */while (1){switch (x){case 0:{lcd_clear(WHITE);break;}case 1:{lcd_clear(BLACK);break;}case 2:{lcd_clear(BLUE);break;}case 3:{lcd_clear(RED);break;}case 4:{lcd_clear(MAGENTA);break;}case 5:{lcd_clear(GREEN);break;}case 6:{lcd_clear(CYAN);break;}case 7:{lcd_clear(YELLOW);break;}case 8:{lcd_clear(BRRED);break;}case 9:{lcd_clear(GRAY);break;}case 10:{lcd_clear(LGRAY);break;}case 11:{lcd_clear(BROWN);break;}}lcd_show_string(10, 40,  240, 32, 32, "ESP32-P4", RED);lcd_show_string(10, 80,  240, 24, 24, "RGBLCD TEST", RED);lcd_show_string(10, 110, 240, 16, 16, "ATOM@ALIENTEK", RED);x++;if (x == 12){x = 0;}LED0_TOGGLE();vTaskDelay(pdMS_TO_TICKS(1000));}
}

app_main函数功能主要是显示一些固定的字符,字体大小包括32、24和16三种,然后不停的切换背景颜色,每500毫秒切换一次。而LED0也会不停地闪烁,指示程序已经在运行了。

21.4 下载验证
下载代码后,LED0不停地闪烁,提示程序已经在运行了。同时可以看到RGBLCD屏幕模块显示背景色不停切换,如下图所示。

image019

图21.4.1 RGBLCD显示效果图

http://www.jsqmd.com/news/776339/

相关文章:

  • 【AISMM模型落地指南】:上市前90天合规冲刺清单与3大高频雷区避坑手册
  • 2026年清镇别墅装修与贵阳旧房翻新深度横评:从预算黑洞到透明决算的一站式整装完全指南 - 企业名录优选推荐
  • 2026年山东沥青筑路设备采购全攻略:德州霖垚与业界四大品牌深度横评 - 精选优质企业推荐官
  • 2026年郑州铝单板与氟碳铝单板市场深度横评:5大品牌选购指南与工程应用实测 - 年度推荐企业名录
  • 2026年郑州铝单板、氟碳铝单板、蜂窝铝单板全景选购指南:从幕墙到吊顶,官方联系与深度横评 - 年度推荐企业名录
  • 2026护线环批发定制:规格材质颜色灵活选,价格更便宜 - 品牌策略主理人
  • 终极解锁指南:zteOnu工具如何开启中兴光猫工厂模式与Telnet服务
  • 2026年山东沥青筑路设备全矩阵采购指南:源头厂家直达与道路养护完全方案 - 精选优质企业推荐官
  • 国内专业砖雕厂家排行 聚焦工程级供货与工艺品质 - 奔跑123
  • 行业评选2026年广州靠谱驾校 资质齐全正规推荐 - 企品推
  • 分布式爬虫平台架构设计:从权限控制到规模化数据采集实战
  • G-Helper终极指南:华硕笔记本性能优化神器,轻松降温15℃
  • 高校今年出手了!严查论文代写AI,这份期刊论文工具自救指南让你稳妥录用 - 逢君学术-AI论文写作
  • 【并购风控终极防线】:AISMM如何用动态语义映射替代传统DD问卷——来自奇点大会闭门实验的17.6倍ROI实证
  • WarcraftHelper实用指南:优化魔兽争霸3在现代系统上的游戏体验
  • Go QML高级特性:动态QML加载与运行时组件创建
  • LLMs-from-scratch-CN实战案例:构建垃圾邮件分类器与用户界面
  • 2026年乌鲁木齐断桥平开窗源头直供指南:本地工厂vs外地品牌真实对比 - 优质企业观察收录
  • 东营东城红星美凯龙欧派全屋定制:给东营人装出省心又安心的理想家 - 品牌企业推荐师(官方)
  • Element Plus项目实战:集成my-cron-vue3打造国际化定时任务管理后台
  • PyCharm里那个超大的java_error_in_pycharm.hprof文件,到底是个啥?教你一键清理释放几十G空间
  • QMCDecode:让QQ音乐加密音频在Mac上自由播放
  • openmpt是可以支持vsti插件和midi键盘的
  • 【AI面试八股文 Vol.1.4 | 专题1:Anthropic Tool Schema JSON】OpenAI / Anthropic Tool Schema JSON规范差异:逐字段拆解与面试应答
  • AI智能体规则设计:从原理到实践,构建可控高效Agent
  • 从.lib文件到实际应用:手把手教你调用STM32F4的DSP函数做FFT分析
  • 2026年清镇别墅装修与贵阳全屋整装:设计主材软装一体化深度横评指南 - 企业名录优选推荐
  • 2026年德州沥青筑路设备采购全攻略:霖垚与五大源头厂家深度横评 - 精选优质企业推荐官
  • AISMM模型与技术债务管理,20年架构师亲测:3个月内降低债务熵值47%的7项硬核实践
  • C++面向对象编程之继承