《DNESP32P4开发指南_V1.0》第二十二章 MIPILCD实验(下)

0 阅读29分钟

第二十二章 MIPILCD实验(下)

本章将继续学习ESP_IDF的LCD外设驱动,主要学习MIPI接口屏幕的驱动方法。本章用到的是ESP32-P4的MIPI_DSI接口驱动MIPILCD屏幕,实现和MIPILCD屏之间的通信,实现ASCII字符、图形和彩色的显示。
本章分为如下几个小节:
22.1 MIPI介绍和ESP32-P4的MIPI_DSI介绍
22.2 硬件设计
22.3 程序设计
22.4 下载验证

22.2 硬件设计

22.2.1 例程功能

使用ESP32-P4底板的MIPI屏幕FPC座实现MIPILCD模块的显示。通过把正点原子的MIPI屏幕模块插入底板上的MIPI屏幕FPC座,按下复位之后,就可以看到MIPILCD模块不停地显示一些信息并不断切换底色。LED0闪烁用于提示程序正在运行。

22.2.2 硬件资源

1)LED灯
LED 0 - IO51
2)MIPILCD
DSI_D0_P(固定引脚) DSI_D0_N(固定引脚)
DSI_D1_P(固定引脚) DSI_D1_N(固定引脚)
DSI_CK_P(固定引脚) DSI_CK_N(固定引脚)
LCD_RST - IO52
CT_RST - IO45 CT_INT - IO21
IIC_SCL - IO32 IIC_SDA - IO33

22.2.3 原理图

MIPILCD原理图,如下图所示。

image075.png

图22.2.3.1 MIPILCD原理图

从上图可知,正点原子ESP32-P4开发板的MIPI屏幕接口采用2Lane,其中LCD_RST是屏幕的复位引脚,通过一个电阻分压电路得到MIPI_RST_1V8,因为MIPI屏幕要求复位引脚电平为1.8V。

22.3 程序设计

22.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驱动芯片的命令 */
#include "esp_lcd_mipi_dsi.h"			/* MIPILCD的函数 */

MIPILCD 驱动流程可大致分为三个部分:初始化接口设备、移植驱动组件和初始化 LCD 设备。
接下来,作者就按照这三个部分分别介绍用到的函数。
初始化接口设备
初始化接口设备需要先初始化总线,再创建接口设备。
1, 初始化MIPI_DSI总线函数esp_lcd_new_dis_bus
该函数用于创建DSI总线,并对D-PHY进行初始化设置,其函数原型如下:

esp_err_t esp_lcd_new_dsi_bus(const esp_lcd_dsi_bus_config_t *bus_config, esp_lcd_dsi_bus_handle_t *ret_bus)

函数形参:

QQ截图20260513102019.png

表22.3.1.1 esp_lcd_new_dsi_bus函数形参描述

函数返回值:
ESP_OK表示MIPI_DSI总线初始化成功。
ESP_ERR_INVALID_ARG表示错误参数。
ESP_ERR_NO_MEM表示内存不足。
ESP_ERR_NOT_FOUND表示没有空闲的DSI总线。
ESP_FAIL表示创建MIPI_DSI发生其他错误。
bus_config为指向DSI总线配置结构体的指针,esp_lcd_dsi_bus_config_t结构体中包含很多成员,如下代码所示。

typedef struct {
    int bus_id;                          		/* 指定要使用的DSI主机 */
    uint8_t num_data_lanes;                  	/* 要使用的数据通道数 */
    mipi_dsi_phy_clock_source_t phy_clk_src; 	/* DPHY时钟源 */
    uint32_t lane_bit_rate_mbps;             	/* 数据通道的比特率(Mbps) */
} esp_lcd_dsi_bus_config_t;

esp_lcd_dsi_bus_config_t结构体的bus_id注意要从0开始编号;num_data_lanes要根据芯片支持的数量进行设置;DPHY时钟源是可选XTAL、F160M和F240M,都有对应的宏选择MIPI_DSI_DPI_CLK_SRC_XTAL、MIPI_DSI_DPI_CLK_SRC_PLL_F160M和MIPI_DSI_DPI_CLK_SRC_PLL_F240M,直接设置默认MIPI_DSI_DPI_CLK_SRC_DEFAULT即可,即选择F240M;而lane_bit_rate_mbps要根据dsi时钟计算进行设置。
ret_bus为指向esp_lcd_dsi_bus_handle_t结构体的指针,esp_lcd_dis_bus_handle_t结构体可以不需要了解。

创建接口设备esp_lcd_new_panel_io_dpi
该函数用于创建DBI接口LCD IO设备。DBI接口用于控制IO层,使用该接口可读写LCD设备内部的配置寄存器,其函数原型如下:

esp_err_t esp_lcd_new_panel_io_dbi(esp_lcd_dsi_bus_handle_t bus, const esp_lcd_dbi_io_config_t *io_config, esp_lcd_panel_io_handle_t *ret_io)

函数形参:

QQ截图20260513102029.png

表22.3.1.2 esp_lcd_new_panel_io_dpi函数形参描述

函数返回值:
ESP_OK表示创建接口设备成功。
ESP_ERR_INVALID_ARG表示错误参数。
ESP_ERR_NO_MEM表示内存不足。
ESP_FAIL表示其他错误。
bus为DSI总线句柄结构体,调用 esp_lcd_new_dsi_bus函数会创建DSI总线句柄结构体。
io_config为指向DBI接口的LCD IO设备配置结构体的指针,esp_lcd_dbi_io_config_t结构体中包含很多成员,如下代码所示。

typedef struct {
    uint8_t virtual_channel;  /* 设置虚拟通道号  */
    int lcd_cmd_bits;         /* 设置LCD控制芯片可识别的命令位宽  */
    int lcd_param_bits;       /* 设置LCD控制芯片可识别的参数位宽  */
} esp_lcd_dbi_io_config_t;

这里需要注意:① virtual_channel虚拟通道是一种逻辑通道,用于从不同来源多路复用数据。如果只连接了一个LCD,则将此值设置为0。② 根据LCD驱动芯片的实际情况,配置命令和参数位宽,对于正点原子的MIPI屏这两个参数位宽都是8位。
ret_io为指向LCD接口句柄结构体指针,esp_lcd_panel_io_handle_t实际是esp_lcd_panel_io_t,该结构体中包含一些函数接口,用于给LCD驱动芯片发送命令和图像数据,如下代码所示。

struct esp_lcd_panel_io_t {
    esp_err_t (*rx_param)(esp_lcd_panel_io_t *io, int lcd_cmd, void *param, size_t param_size);								/* 发送单个LCD命令并接收响应参数 */
esp_err_t (*tx_param)(esp_lcd_panel_io_t *io, int lcd_cmd, const void *param, size_t param_size);						/* 发送单个LCD命令及配套参数 */
esp_err_t (*tx_color)(esp_lcd_panel_io_t *io, int lcd_cmd, const void *color, size_t color_size);						/* 发送单次LCD刷屏命令和图像数据 */
esp_err_t (*del)(esp_lcd_panel_io_t *io);	/* 卸载LCD IO设备句柄 */
esp_err_t (*register_event_callbacks)(esp_lcd_panel_io_t *io, const esp_lcd_panel_io_callbacks_t *cbs, void *user_ctx);	/* 注册LCD IO设备回调 */
};

在esp_lcd_new_panel_io_dbi函数的内部就会将DBI底层接口与ret_io绑定起来,后续直接访问ret_io即可调用到DBI的底层接口,比如panel_io_dbi_rx_param、panel_io_ dbi _tx_param和panel_io_dbi_del。

移植驱动组件
移植MIPI LCD驱动组件的基本原理包含以下三点:
1、 基于数据类型为esp_lcd_panel_io_handle_t的接口设备句柄发送指定格式的命令及参数
2、 实现并创建一个LCD设备,然后通过注册回调函数的方式实现结构体esp_lcd_panel_t中的各项功能
3、 实现一个函数用于提供数据类型为esp_lcd_panel_handle_t的LCD设备句柄,使得应用程序能够利用LCD通用API来操作LCD设备
第一点已经在前面创建接口设备时完成了,可以调用DBI底层接口。而第二和第三点需要用到esp_lcd_new_panel_xxx函数去实现。xxx对应的是LCD驱动芯片,由于正点原子的MIPI屏幕有三款,虽然说乐鑫已经对两款做了支持,但是我们为了兼容所有MIPI屏幕,还是自己编写MIPI LCD的驱动组件。想要知道乐鑫已经对哪些MIPI屏做了兼容可以通过组件仓库进行搜索,如下图所示。

image077.png

图22.3.1.1 查看乐鑫支持MIPI LCD驱动芯片

在前面SPILCD章节中,直接使用乐鑫提供的st7789文件很方便就能驱动起来。RGBLCD章节更简单,由于不需要SPI接口配置,直接跳过该步骤。而在本章节,就得介绍一下怎么对屏幕驱动,知道要编写哪些函数,函数该怎么编写,以及怎么样与上层接口对应起来,了解这种方法之后,后续大家就可以兼容自己的屏幕。
接下来,我们了解一下LCD驱动组件需要要实现的函数接口以及我们要实现的函数,如下两表所示。

image079.png

表22.3.1.3 LCD通用API函数

QQ截图20260513102040.png

表22.3.1.4 MIPILCD函数列表

表22.3.1.1罗列的是通用API接口,而表22.3.1.2罗列的是我们在mipi_lcd.c文件中实现的函数接口。通过两表,大家大概也猜到一些函数里面的实现了,就是发送不同命令实现不同功能。draw_bitmap函数没有实现,后续在其他文件中实现。
首先介绍一下mipi_lcd_new_panel函数。
2, 为MIPI LCD驱动芯片创建LCD面板mipi_lcd_new_panel。
该函数用于为MIPI LCD驱动芯片创建LCD面板并对LCD设备配置,其函数原型如下:

esp_err_t mipi_lcd_new_panel(const esp_lcd_panel_io_handle_t io, const esp_lcd_panel_dev_config_t *panel_dev_config, esp_lcd_panel_handle_t *ret_panel)

函数形参:

QQ截图20260513102055.png

表22.3.1.5 mipi_lcd_new_panel函数形参描述

函数实现:

esp_err_t mipi_lcd_new_panel(const esp_lcd_panel_io_handle_t io, const esp_lcd_panel_dev_config_t *panel_dev_config, esp_lcd_panel_handle_t *ret_panel)
{
    esp_err_t ret = ESP_OK;

    ESP_RETURN_ON_FALSE(	io && panel_dev_config && ret_panel, ESP_ERR_INVALID_ARG, mipi_lcd_tag, "invalid argument");

    mipi_panel_t *mipi_lcd = (mipi_panel_t *)calloc(1, sizeof(mipi_panel_t));
ESP_RETURN_ON_FALSE( mipi_lcd, ESP_ERR_NO_MEM, mipi_lcd_tag, 
"no mem for mipi_lcd panel");

    if (panel_dev_config->reset_gpio_num >= 0) 	/* 配置LCD复位引脚 */
    {
        gpio_config_t io_conf = {
            .mode         = GPIO_MODE_OUTPUT,
            .pin_bit_mask = 1ULL << panel_dev_config->reset_gpio_num,
        };
        ESP_GOTO_ON_ERROR( gpio_config(&io_conf), err, mipi_lcd_tag, 
"configure GPIO for RST line failed");
    }

    switch (panel_dev_config->rgb_ele_order) 		/* 颜色顺序RGB/BGR */
    {
        case LCD_RGB_ELEMENT_ORDER_RGB:
            mipi_lcd->madctl_val = 0;
            break;
        case LCD_RGB_ELEMENT_ORDER_BGR:
            mipi_lcd->madctl_val |= LCD_CMD_BGR_BIT;
            break;
        default:
            ESP_GOTO_ON_FALSE(false, ESP_ERR_NOT_SUPPORTED, err, mipi_lcd_tag, "unsupported rgb element order");
            break;
    }

    switch (panel_dev_config->bits_per_pixel)       /* 像素格式 */
    {
        case 16:    /* RGB565 */
            mipi_lcd->colmod_val = 0x55;
            break;
        case 18:    /* RGB666 */
            mipi_lcd->colmod_val = 0x66;
            break;
        case 24:    /* RGB888 */
            mipi_lcd->colmod_val = 0x77;
            break;
        default:
            ESP_GOTO_ON_FALSE(false, ESP_ERR_NOT_SUPPORTED, err, mipi_lcd_tag, "unsupported pixel width");
            break;
    }

    mipi_lcd->io                = io;
    mipi_lcd->reset_gpio_num    = panel_dev_config->reset_gpio_num;	
    mipi_lcd->reset_level       = panel_dev_config->flags.reset_active_high;
    mipi_lcd->base.reset        = mipi_lcd_panelreset;			
    mipi_lcd->base.init         = mipi_lcd_panelinit;         	
    mipi_lcd->base.del          = mipi_lcd_paneldel;         	
    mipi_lcd->base.mirror       = mipi_lcd_panelmirror;       	
    mipi_lcd->base.swap_xy      = mipi_lcd_panelswap_xy;      	
    mipi_lcd->base.set_gap      = mipi_lcd_panelset_gap;      	
    mipi_lcd->base.invert_color = mipi_lcd_panelinvert_color; 	
    mipi_lcd->base.disp_on_off  = mipi_lcd_paneldisp_on_off;  	
    mipi_lcd->base.disp_sleep   = mipi_lcd_panelsleep;        	

    *ret_panel = &mipi_lcd->base;

    return ESP_OK;

err:
    if (mipi_lcd)
    {
        mipi_lcd_paneldel(&mipi_lcd->base);
    }

    return ret;
}

简单来说,就做了两件事情:
① 根据传参panel_dev_config对LCD做配置,比如说复位引脚配置和像素格式。
② 将LCD的配置信息和一些函数接口传递给esp_lcd_panel_handle_t结构体类型指针变量ret_panel。
函数返回值:
ESP_OK表示创建成功。
其他表示异常。
io为LCD接口句柄结构体esp_lcd_panel_io_t,该结构体在前面已经介绍了,这里不再展开。
panel_dev_config为指向LCD设备配置结构体指针,esp_lcd_panel_dev_config_t结构体中包含几个成员,如下代码所示。

typedef struct {
    int reset_gpio_num; 						/* 连接LCD复位信号的引脚 */
    union {
        esp_lcd_color_space_t color_space;   	/* RGB色彩空间,rgb_ele_order代替 */
        lcd_color_rgb_endian_t rgb_endian;   	/* 设置数据端序,rgb_ele_order代替 */
        lcd_rgb_element_order_t rgb_ele_order;	/* 像素色彩的元素顺序(RGB/BGR) */
    };
    lcd_rgb_data_endian_t data_endian;        	/* 设置>1字节的颜色数据的数据端序 */
    uint32_t bits_per_pixel;            		/* 色彩格式的位数 */
    struct {
        uint32_t reset_active_high: 1; 		/* 复位引脚有效电平 */
    } flags;                           
    void *vendor_config; 						/* 用于替换驱动组件的初始化序列 */
} esp_lcd_panel_dev_config_t;

esp_lcd_panel_dev_config_t结构体需要注意以下几点:
① 当发生颜色显示不对,比如想显示红色,最终是蓝色,这是像素色彩的元素顺序问题,通过调整rgb_ele_order的赋值。
② 颜色显示异常,并非①中描述的情况,除了白色、黑色显示出来,这是由于颜色数据的发送顺序错误,通过调整data_endian的赋值。
③ 若手上的屏幕驱动起来,色彩有点偏差,可通过vendor_config把厂家提供的初始化序列填充进去。
④ 若不是通过芯片的IO作为复位引脚,reset_gpio_num直接赋值GPIO_NUM_NC即可,硬件复位就得自己去实现。
ret_panel为指向LCD设备句柄结构体指针,esp_lcd_panel_handle_t其实是esp_lcd_panel_t,如下代码所示。

struct esp_lcd_panel_t {
esp_err_t (*reset)(esp_lcd_panel_t *panel);	/* LCD屏幕复位 */
esp_err_t (*init)(esp_lcd_panel_t *panel);		/* LCD屏幕初始化 */
esp_err_t (*del)(esp_lcd_panel_t *panel);		/* 卸载LCD屏幕 */
esp_err_t (*draw_bitmap)(esp_lcd_panel_t *panel, int x_start, int y_start, int x_end, int y_end, const void *color_data);		/* LCD屏幕绘画函数 */
/* LCD屏幕镜像X轴和Y轴 */
esp_err_t (*mirror)(esp_lcd_panel_t *panel, bool x_axis, bool y_axis);
/* LCD屏幕交换X轴和Y轴 */
esp_err_t (*swap_xy)(esp_lcd_panel_t *panel, bool swap_axes);
/* LCD屏幕设置画图的起始坐标 */
esp_err_t (*set_gap)(esp_lcd_panel_t *panel, int x_gap, int y_gap);
/* LCD屏幕像素颜色数据按位取反(0xF0F0->0x0F0F),即反显功能 */
esp_err_t (*invert_color)(esp_lcd_panel_t *panel, bool invert_color_data);
/* LCD屏幕显示开关 */
esp_err_t (*disp_on_off)(esp_lcd_panel_t *panel, bool on_off);
/* LCD屏幕休眠开关 */
    esp_err_t (*disp_sleep)(esp_lcd_panel_t *panel, bool sleep);
void *user_data;    /* 用户数据,用于存储外部自定义数据 */
};

esp_lcd_panel_handle_t结构体对应的就是表22.3.1.1的内容了。
接下来,就要介绍一下我们对应实现的函数接口。
第一个介绍的是mipi_lcd_panelinit函数,如下代码所示。

static esp_err_t mipi_lcd_panelinit(esp_lcd_panel_t *panel)
{
    mipi_panel_t *mipi_lcd = __containerof(panel, mipi_panel_t, base);
    esp_lcd_panel_io_handle_t io = mipi_lcd->io;
    
    const mipi_lcd_init_cmd_t *init_cmds = {0};
    uint16_t init_cmds_size = 0;
    bool mirror_x = true;
    bool mirror_y = false;

    if (mipidev.id == 0x8399)       /* 5寸,720P */
    {
        init_cmds = vendor_specific_init_code_default_1080p;
        init_cmds_size = sizeof(vendor_specific_init_code_default_1080p) / 
sizeof(mipi_lcd_init_cmd_t);
    }
    else if (mipidev.id == 0x8394)  /* 5寸,1080p */
    {
        init_cmds = vendor_specific_init_code_default_720p;
        init_cmds_size = sizeof(vendor_specific_init_code_default_720p) / 
sizeof(mipi_lcd_init_cmd_t);
    }
    else if (mipidev.id == 0x9881)  /* 10.1寸,800p */
    {
        init_cmds = vendor_specific_init_code_default_800p;
        init_cmds_size = sizeof(vendor_specific_init_code_default_800p) / 
sizeof(mipi_lcd_init_cmd_t);

        /* 返回 命令页 1 */
        ESP_RETURN_ON_ERROR(	esp_lcd_panel_io_tx_param(io, ILI9881C_CMD_CNDBKxSEL, (uint8_t[]) {
          	ILI9881C_CMD_BKxSEL_BYTE0, ILI9881C_CMD_BKxSEL_BYTE1, ILI9881C_CMD_BKxSEL_BYTE2_PAGE1
}, 3), mipi_lcd_tag, "send command failed");
        /* 设置2 lane */
        ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, ILI9881C_PAD_CONTROL, 
(uint8_t[]) {
            				ILI9881C_DSI_2_LANE,
        					}, 1), mipi_lcd_tag, "send command failed");
        /* 返回 命令页 0 */
        ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, 
ILI9881C_CMD_CNDBKxSEL, (uint8_t[]) {
       		ILI9881C_CMD_BKxSEL_BYTE0, ILI9881C_CMD_BKxSEL_BYTE1,
           		ILI9881C_CMD_BKxSEL_BYTE2_PAGE0
        		}, 3), mipi_lcd_tag, "send command failed");
        mirror_x = false;
    }

    /* 发送初始化序列 */
    for (int i = 0; i < init_cmds_size; i++)
    {
        ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, init_cmds[i].cmd,
init_cmds[i].data, init_cmds[i].data_bytes),mipi_lcd_tag,"send command failed");
    }

    vTaskDelay(pdMS_TO_TICKS(120));

    /* 退出睡眠 */
ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_SLPOUT, NULL, 0), 
mipi_lcd_tag, "io tx param failed");
    
    /* 根据MIPI屏放置位置调整显示方向(也可调用mipi_lcd_panelmirror函数设置) */
    if (mirror_x)
    {
        mipi_lcd->madctl_val |= HX_F_SS_PANEL;      	/* 扫描方向水平翻转 */
    }
    else
    {
        mipi_lcd->madctl_val &= ~HX_F_SS_PANEL;     	/* 扫描方向水平不翻转 */
    }

    if (mirror_y)
    {
        mipi_lcd->madctl_val |= HX_F_GS_PANEL;      	/* 扫描方向垂直翻转 */
    }
    else
    {
        mipi_lcd->madctl_val &= ~HX_F_GS_PANEL;     	/* 扫描方向垂直不翻转 */
    }
    ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io,LCD_CMD_MADCTL, (uint8_t[])
    {
        mipi_lcd->madctl_val,
    }, 1), mipi_lcd_tag, "send command failed");  		/* 配置MIPILCD的显示 */

    ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io,LCD_CMD_COLMOD, (uint8_t[])
    {
        mipi_lcd->colmod_val,
    }, 1), mipi_lcd_tag, "send command failed");  		/* 配置像素格式 */
    vTaskDelay(pdMS_TO_TICKS(120));

return ESP_OK;
}

mipi_lcd_panelinit函数主要就是通过发送厂家提供的初始化序列来初始化LCD设备,其次设置LCD的扫描方向以及配置像素格式。
在这里说明一下container_of函数,该函数的功能就是根据结构体中的已知成员变量的地址,来寻求该结构体的首地址,如下图所示。

image081.png

图22.3.1.2 container_of函数功能

通过container_of函数能把mipi_lcd结构体(自定义的结构体类型)中的base成员地址获取到,即结构体的首地址。通过mipi_lcd结构体便可以通过其成员,对LCD进行配置。
mipi_lcd结构体类型,如下代码所示。

/* 初始化屏幕结构体 */
typedef struct {
    esp_lcd_panel_t base;           /* LCD设备的基础接口函数 */
    esp_lcd_panel_io_handle_t io;   /* LCD设备的IO接口函数配置 */
    int reset_gpio_num;             /* 复位管脚 */
    int x_gap;                      /* x偏移 */
    int y_gap;                      /* y偏移 */
    uint8_t madctl_val;             /* 保存LCD CMD MADCTL寄存器的当前值 */
    uint8_t colmod_val;             /* 保存LCD_CMD_COLMOD寄存器的当前值 */
    uint16_t init_cmds_size;        /* 初始化序列大小 */
    bool reset_level;               /* 复位电平 */
} mipi_panel_t;

第二个介绍的是mipi_lcd_paneldisp_on_off函数,如下代码所示。

static esp_err_t mipi_lcd_paneldisp_on_off(esp_lcd_panel_t *panel, bool on_off)
{
    mipi_panel_t *mipi_lcd = __containerof(panel, mipi_panel_t, base);
    esp_lcd_panel_io_handle_t io = mipi_lcd->io;
    int command = 0;

    if (on_off)
    {
        command = LCD_CMD_DISPON;   /* 打开显示命令 */
    }
    else
    {
        command = LCD_CMD_DISPOFF;  /* 关闭显示命令 */
    }
ESP_RETURN_ON_ERROR( esp_lcd_panel_io_tx_param(io, command, NULL, 0), 
mipi_lcd_tag, "send command failed");
    
    return ESP_OK;
}

该函数的功能是LCD显示开关,通过传参进行判断,打开显示命令LCD_CMD_DISPON,关闭显示命令LCD_CMD_DISPOFF,最终,通过esp_lcd_panel_io_tx_param函数接口发送出去。
其他函数接口跟这里实现的代码逻辑是比较相似的,所以也不再一个个罗列说明,大家自行查看mipi_lcd.c文件即可。
初始化LCD设备
通过前面两个步骤的操作,算是完成了准备工作,接下来,就得调用LCD通用API接口对LCD进行初始化,如下代码所示。

ESP_ERROR_CHECK(esp_lcd_panel_reset(mipi_lcd_ctrl_panel));		/* 复位LCD */
ESP_ERROR_CHECK(esp_lcd_panel_init(mipi_lcd_ctrl_panel));  	/* 初始化LCD */
/* 打开LCD */
    ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(mipi_lcd_ctrl_panel, true));  

此时仍然无法通过esp_lcd_panel_draw_bitmap函数向LCD绘画,因为MIPI LCD具有高分辨率,而LCD控制器中没有GRAM。因此需要维护LCD帧buffer,并通过MIPI DSI DPI接口将其刷新到LCD屏幕上,所以在这里需要对DPI进行配置。
1, 为MIPI DSI DPI接口创建LCD面板函数esp_lcd_new_panel_dpi
该函数用于创建DPI接口,并创建LCD面板,其函数原型如下:

esp_err_t esp_lcd_new_panel_dpi(esp_lcd_dsi_bus_handle_t bus, const esp_lcd_dpi_panel_config_t *panel_config, esp_lcd_panel_handle_t *ret_panel)

函数形参:

QQ截图20260513102114.png

表22.3.1.6 esp_lcd_new_panel_dpi函数形参描述

函数返回值:
ESP_OK表示MIPI_DSI数据面板创建成功。
ESP_ERR_INVALID_ARG表示错误参数。
ESP_ERR_NO_MEM表示内存不足。
ESP_ERR_NOT_SUPPORTED表示不支持。
ESP_FAIL表示发生其他错误。
bus为DSI总线句柄结构体。
panel_config为指向DSI数据面板配置结构体指针,esp_lcd_dpi_panel_config_t 结构体如下代码所示。

typedef struct {
    uint8_t virtual_channel;                   /* 设置虚拟通道号 */
    mipi_dsi_dpi_clock_source_t dpi_clk_src;   /* 设置DPI接口的时钟源 */
    uint32_t dpi_clock_freq_mhz;               /* 设置DPI时钟频率 */
    lcd_color_rgb_pixel_format_t pixel_format; /* 设置像素数据的像素格式 */
    uint8_t num_fbs;                           /* 整屏大小的帧缓存区数量 */
    esp_lcd_video_timing_t video_timing;       /* LCD面板的特定时序参数 */

    struct extra_flags {
        uint32_t use_dma2d: 1; 					/* 是否启用DMA2D */
    } flags;                   
} esp_lcd_dpi_panel_config_t;

这里需要注意几点:
① 与DBI接口类似,DPI 接口也需要设置虚拟通道。如果只连接了一个LCD,则将此值设置为0。
② DPI时钟源dpi_clk_src有三个,分别是XTAL、F160M和F240M,都有对应的宏选择MIPI_DSI_DPI_CLK_SRC_XTAL、MIPI_DSI_DPI_CLK_SRC_PLL_F160M和MIPI_DSI_DPI_CLK_SRC_PLL_F240M,直接设置默认MIPI_DSI_DPI_CLK_SRC_DEFAULT即可,即选择F240M;
③ 在设置DPI时钟频率时需要注意:像素时钟频率越高,刷新率越高,但如果DMA带宽不足或LCD控制器芯片不支持高像素时钟频率,则可能会导致闪烁。
④ 设置像素数据格式pixel_format有三种选择,分别是LCD_COLOR_PIXEL_FORMAT_RGB565、LCD_COLOR_PIXEL_FORMAT_RGB666、LCD_COLOR_PIXEL_FORMAT_RGB888,为了兼容性,这里选择了RGB565。MIPI LCD通常使用RGB888来获得最佳色彩深度。
⑤ LCD面板的特定时序参数video_timing,主要是看屏幕的规格书,在前面22.1.9小节已经有说明了,配置时对号入座即可。
ret_panel为指向esp_lcd_panel_handle_t结构体的指针,esp_lcd_panel_handle_t结构体在前面介绍mipi_lcd_new_panel函数时,已经介绍过了,这里不再展开。
最后,再调用一下esp_lcd_panel_init函数初始化一下,这时,MIPILCD完全驱动好,可以使用esp_lcd_panel_draw_bitmap函数进行绘画了。

22.3.2 程序流程图

image084.png

图22.3.2.1 MIPILCD实验程序流程图

22.3.3 程序解析

在12_mipilcd例程中,作者在12_mipilcd\components\BSP路径下新建LCD文件夹,并且需要更改CMakeLists.txt内容,以便在其他文件上调用。

1.LCD驱动代码
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。LCD驱动源码包括五个文件:mipi_lcd.c、mipi_lcd.h、lcd.c、lcd.h和lcdfont.h。
mipi_lcd.c文件存放的是MIPILCD的驱动函数,而mipi_lcd.h存放的是LCD驱动芯片的命令宏、DSI总线配置宏以及自定义用于管理LCD的结构体类型,以及函数声明。lcd.c文件主要lcd的一些绘图函数,而lcd.h则存放的是引脚接口宏定义以及函数声明。lcdfont.h存放的是4种字体大小不一样的ASCII字符集(12 * 12、16 * 16、24 * 24和32 * 32)。lcd.c和lcd.h两个文件在11_rgblcd例程和12_mipilcd例程中存在差别,都是单独为某种类型屏幕进行驱动,而在13_lcd例程中对这两种类型屏幕做了兼容。
在前面22.3.1小节中,已经提及了不少mipi_lcd.c文件里面的函数,这里主要给大家介绍一下MIPILCD的初始化函数mipi_lcd_init,如下代码所示:

/**
 * @brief   	mipi_lcd初始化
 * @param   	无
 * @retval    	LCD控制句柄
 */
esp_lcd_panel_handle_t mipi_lcd_init(void)
{
    mipi_dev_bsp_enable_dsi_phy_power();  		/* 配置MIPI接口电压1.8V */

    /* 创建DSI总线 */
    esp_lcd_dsi_bus_handle_t mipi_dsi_bus;
    esp_lcd_dsi_bus_config_t bus_config = {
        .bus_id = 0,                                  		/* 总线ID */
        .num_data_lanes = MIPI_DSI_LANE_NUM,             	/* 2路数据信号 */
        .phy_clk_src = MIPI_DSI_PHY_CLK_SRC_DEFAULT,      	/* DPHY时钟源为20M */
        .lane_bit_rate_mbps = MIPI_DSI_LANE_BITRATE_MBPS,	/* 数据通道比特率 */
};
/* 新建DSI总线 */
    ESP_ERROR_CHECK(esp_lcd_new_dsi_bus(&bus_config, &mipi_dsi_bus));   

    /* 配置DSI总线的DBI接口 */
    esp_lcd_panel_io_handle_t mipi_dbi_io;
    esp_lcd_dbi_io_config_t dbi_config = {
        .virtual_channel = 0,   	/* 虚拟通道(只有一个LCD连接,设置0即可) */
        .lcd_cmd_bits    = 8,     	/* 根据MIPI LCD驱动IC规格设置 命令位宽度 */
        .lcd_param_bits  = 8,    	/* 根据MIPI LCD驱动IC规格设置 参数位宽度 */
    };
ESP_ERROR_CHECK(esp_lcd_new_panel_io_dbi(mipi_dsi_bus, &dbi_config, 
&mipi_dbi_io));

    /* 创建LCD控制器驱动 */
    esp_lcd_panel_handle_t mipi_lcd_ctrl_panel;  		/* MIPI控制句柄 */
    esp_lcd_panel_dev_config_t lcd_dev_config = {
        .bits_per_pixel = 16,  							/* MIPILCD的像素位宽度 */
        .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB,  	/* 像素数据的RGB元素顺序 */
        .reset_gpio_num = lcddev.ctrl.lcd_rst,      	/* MIPILCD屏的复位引脚 */
    };
ESP_ERROR_CHECK(mipi_lcd_new_panel(mipi_dbi_io, &lcd_dev_config, 
&mipi_lcd_ctrl_panel));

    ESP_ERROR_CHECK(esp_lcd_panel_reset(mipi_lcd_ctrl_panel));	 /* 复位LCD屏 */

    /* 读取屏幕ID */
    esp_lcd_panel_io_rx_param(mipi_dbi_io, 0xDA, &mipi_id[0], 1);
    vTaskDelay(pdMS_TO_TICKS(20));
    esp_lcd_panel_io_rx_param(mipi_dbi_io, 0xDB, &mipi_id[1], 1);
    vTaskDelay(pdMS_TO_TICKS(20));
    /* 不是HX8399和HX8394 */
    if (mipi_id[0] == 0x00 || mipi_id[1] == 0x00)
    {
        /* 读取ILI9881 ID */
        esp_lcd_panel_io_tx_param(mipi_dbi_io, ILI9881C_CMD_CNDBKxSEL,
        (uint8_t[]) {
   				ILI9881C_CMD_BKxSEL_BYTE0, ILI9881C_CMD_BKxSEL_BYTE1,
 				ILI9881C_CMD_BKxSEL_BYTE2_PAGE1
        }, 3);
        esp_lcd_panel_io_rx_param(mipi_dbi_io, 0x00, &mipi_id[0], 1);
        esp_lcd_panel_io_rx_param(mipi_dbi_io, 0x01, &mipi_id[1], 1);
    }

    mipidev.id = (uint16_t)(mipi_id[0] << 8) | mipi_id[1];
    ESP_LOGI(mipi_lcd_tag, "mipilcd_id:%#x ", mipidev.id);  /* 打印LCD的ID */

ESP_ERROR_CHECK(esp_lcd_panel_init(mipi_lcd_ctrl_panel));	/* 初始化LCD屏 */
    ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(mipi_lcd_ctrl_panel, true));  

    if (mipidev.id == 0x8394)  			/* 5.5720P屏幕 */
    {
        mipidev.pwidth = 720; 			/* 面板宽度,单位:像素 */
        mipidev.pheight = 1280;   		/* 面板高度,单位:像素 */
        mipidev.hbp = 52;        		/* 水平后廊 */
        mipidev.hfp = 48;      			/* 水平前廊 */
        mipidev.hsw = 8;          		/* 水平同步宽度 */
        mipidev.vbp = 15;         		/* 垂直后廊 */
        mipidev.vfp = 16;        		/* 垂直前廊 */
        mipidev.vsw = 5;       			/* 垂直同步宽度 */
        mipidev.pclk_mhz = 60;    		/* 设置像素时钟 60Mhz */
        mipidev.dir = 0;      			/* 只能竖屏 */
    }
    else if (mipidev.id == 0x8399) 	/* 5.51080P屏幕 */
    {
        mipidev.pwidth = 1080;        	/* 面板宽度,单位:像素 */
        mipidev.pheight = 1920;      	/* 面板高度,单位:像素 */
        mipidev.hbp = 22;           	/* 水平后廊 */
        mipidev.hfp = 22;             	/* 水平前廊 */
        mipidev.hsw = 20;             	/* 水平同步宽度 */
        mipidev.vbp = 9;              	/* 垂直后廊 */
        mipidev.vfp = 7;              	/* 垂直前廊 */
        mipidev.vsw = 7;              	/* 垂直同步宽度 */
        mipidev.pclk_mhz = 60;      	/* 设置像素时钟 60Mhz */
        mipidev.dir = 0;             	/* 只能竖屏 */
    }
    else if (mipidev.id == 0x9881)		/* 10.1800P屏幕 */
    {
        mipidev.pwidth = 800;      	/* 面板宽度,单位:像素 */
        mipidev.pheight = 1280;       	/* 面板高度,单位:像素 */
        mipidev.hbp = 24;             	/* 水平后廊 */
        mipidev.hfp = 15;             	/* 水平前廊 */
        mipidev.hsw = 24;             	/* 水平同步宽度 */
        mipidev.vbp = 9;              	/* 垂直后廊 */
        mipidev.vfp = 7;              	/* 垂直前廊 */
        mipidev.vsw = 2;             	/* 垂直同步宽度 */
        mipidev.pclk_mhz = 60;        	/* 设置像素时钟 60Mhz */
        mipidev.dir = 0;             	/* 支持横/竖屏,默认为竖屏 */
    }

    lcddev.id = mipidev.id;                                 /* LCD_ID */
    lcddev.width = mipidev.pwidth;                          /* 宽度 */
    lcddev.height = mipidev.pheight;                        /* 高度 */

    esp_lcd_panel_handle_t mipi_dpi_panel;                  /* MIPILCD控制句柄 */
    esp_lcd_dpi_panel_config_t dpi_config = {               /* DSI数据配置 */
        .virtual_channel    = 0,                            /* 虚拟通道 */
        .dpi_clk_src        = MIPI_DSI_DPI_CLK_SRC_DEFAULT, /* 时钟源 */
        .dpi_clock_freq_mhz = mipidev.pclk_mhz,             /* 像素时钟频率 */
        .pixel_format       = LCD_COLOR_PIXEL_FORMAT_RGB565,/* 颜色格式 */
        .num_fbs            = 2,                            /* 帧缓冲区数量 */
        .video_timing       = {                             /* 面板特定时序参数 */
            .h_size            = mipidev.pwidth,            /* 水平分辨率 */
            .v_size            = mipidev.pheight,           /* 垂直分辨率 */
            .hsync_back_porch  = mipidev.hbp,               /* 水平后廊 */
            .hsync_pulse_width = mipidev.hsw,               /* 水平同步宽度 */
            .hsync_front_porch = mipidev.hfp,               /* 水平前廊 */
            .vsync_back_porch  = mipidev.vbp,               /* 垂直后廊 */
            .vsync_pulse_width = mipidev.vsw,               /* 垂直同步宽度 */
            .vsync_front_porch = mipidev.vfp,               /* 垂直前廊 */
        },
    };
ESP_ERROR_CHECK(esp_lcd_new_panel_dpi(mipi_dsi_bus, &dpi_config, 
&mipi_dpi_panel));	/* 为MIPI DSI DPI接口创建LCD控制句柄 */
    /* 初始化MIPILCD */
ESP_ERROR_CHECK(esp_lcd_panel_init(mipi_dpi_panel));                                    
    return mipi_dpi_panel;
}

该函数就是对MIPILCD进行初始化,整个过程就围绕着22.3.1小节中描述的驱动流程实现。通过esp_lcd_new_dsi_bus函数和esp_lcd_new_panel_io_dbi函数初始化DBI接口设备;通过mipi_lcd_new_panel函数创建MIPI设备;后续通过LCD通用API接口对LCD进行配置,比如esp_lcd_panel_reset复位LCD屏,esp_lcd_panel_init初始化LCD屏,esp_lcd_panel_disp_on_off打开显示;最终还得通过esp_lcd_new_panel_dpi函数为DPI接口创建LCD面板,申请到对应的缓冲区即GRAM,后面还需要重新调用esp_lcd_panel_init再次初始化LCD屏。函数的返回值是esp_lcd_panel_handle_t类型的句柄,便于后续对LCD屏幕进行操作。函数中还涉及到配置DSI总线的电压,通过调用mipi_dev_bsp_enable_dsi_phy_power函数设置LDO_VO3输出1.8V给到MIPI_DSI总线。
下面介绍在mipi_lcd.h文件定义的两个重要结构体:

/* MIPI LCD重要参数集 */
typedef struct  
{
    uint16_t id;                    /* 720p/800p/1080p */
    uint32_t pwidth;                /* MIPI面板的宽度,固定参数,不随显示方向改变 */
    uint32_t pheight;               /* MIPI面板的高度,固定参数,不随显示方向改变 */
    uint16_t hsw;                   /* 水平同步宽度 */
    uint16_t vsw;                   /* 垂直同步宽度 */
    uint16_t hbp;                   /* 水平后廊 */
    uint16_t vbp;                   /* 垂直后廊 */
    uint16_t hfp;                   /* 水平前廊 */
    uint16_t vfp;                   /* 垂直前廊  */
    uint8_t dir;                    /* 0,竖屏;1,横屏; */
    uint32_t pclk_mhz;              /* 设置像素时钟 */
} _mipilcd_dev; 

/* 初始化屏幕结构体 */
typedef struct {
    esp_lcd_panel_t base;           /* LCD设备的基础接口函数 */
    esp_lcd_panel_io_handle_t io;   /* LCD设备的IO接口函数配置 */
    int reset_gpio_num;             /* 复位管脚 */
    int x_gap;                      /* x偏移 */
    int y_gap;                      /* y偏移 */
    uint8_t madctl_val;             /* 保存LCD CMD MADCTL寄存器的当前值 */
    uint8_t colmod_val;             /* 保存LCD_CMD_COLMOD寄存器的当前值 */
    uint16_t init_cmds_size;        /* 初始化序列大小 */
    bool reset_level;               /* 复位电平 */
} mipi_panel_t;

_mipilcd_dev结构体用于保存一些MIPILCD重要参数信息,比如MIPILCD的ID、MIPILCD的长宽、MIPILCD的时序参数等。最后声明_mipilcd_dev结构体类型变量mipilcd_dev,mipilcd_dev在mipi_lcd.c中定义。
_mipi_panel_t结构体用来保存MIPILCD相关的一些重要信息,比如LCD设备的基础接口结构体、LCD设备的IO接口结构体、复位引脚、初始化序列等。
在lcd.h文件中也定义了一个重要参数结构体_lcd_dev,在RGBLCD实验章节中介绍过,这里就不再展开。
下面我们再解析lcd.c的程序,看一下初始化函数lcd_init,代码如下:

/**
 * @brief     	初始化LCD
 * @param     	无
 * @retval     	无
 */
void lcd_init(void)
{
    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 = mipi_lcd_init();  			/* 初始化MIPI LCD */
ESP_ERROR_CHECK(esp_lcd_dpi_panel_get_frame_buffer(lcddev.lcd_panel_handle,
2, &lcd_buffer[0], &lcd_buffer[1])); 	/* 获取帧缓冲区 */
    
const esp_lcd_dpi_panel_event_callbacks_t mipi_cbs = {
	/* 内部缓冲区刷新完成回调函数 */
        .on_refresh_done = lcd_panel_refresh_done_callback,     
    };
esp_lcd_dpi_panel_register_event_callbacks(lcddev.lcd_panel_handle, 
&mipi_cbs, NULL);
    lcd_clear(WHITE);
    LCD_BL(1);      /* 打开背光 */
}

在lcd_init函数中,首先是对LCD背光控制引脚配置,然后调用mipi_lcd_init函数初始化MIPILCD,后面直接通过esp_lcd_dpi_panel_get_frame_buffer函数接口把数据获取到。为了防止屏幕撕裂,这里还注册了刷新完成回调函数,在进行清屏函数中,需要等待一帧刷新完成,再进行下一帧刷新。最后调用lcd_clear函数清屏,拉高背光控制引脚,打开背光。
接下来,再看看这个回调函数里面的实现,如下代码所示。

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

当发送一帧完成之后,就回进入到回调函数中,把refresh_done_flag标志置1。
lcd.c的其他函数与RGBLCD例程中的lcd.c是一致的,请大家自行查看源码,都有详细的注释。
13_lcd例程则是有对RGBLCD和MIPILCD的兼容,大家也可以自行学习查看。

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

set(src_dirs
           	LED
LCD)

set(include_dirs
           	LED
LCD)

set(requires
          	driver
			esp_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)

3. 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, "MIPILCD 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(500));
    }
}

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

22.4 下载验证

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

image085.png

图22.4.1 MIPILCD显示效果图