第二十二章 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原理图,如下图所示。
图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)
函数形参:
表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)
函数形参:
表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屏做了兼容可以通过组件仓库进行搜索,如下图所示。
图22.3.1.1 查看乐鑫支持MIPI LCD驱动芯片
在前面SPILCD章节中,直接使用乐鑫提供的st7789文件很方便就能驱动起来。RGBLCD章节更简单,由于不需要SPI接口配置,直接跳过该步骤。而在本章节,就得介绍一下怎么对屏幕驱动,知道要编写哪些函数,函数该怎么编写,以及怎么样与上层接口对应起来,了解这种方法之后,后续大家就可以兼容自己的屏幕。
接下来,我们了解一下LCD驱动组件需要要实现的函数接口以及我们要实现的函数,如下两表所示。
表22.3.1.3 LCD通用API函数
表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)
函数形参:
表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函数,该函数的功能就是根据结构体中的已知成员变量的地址,来寻求该结构体的首地址,如下图所示。
图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)
函数形参:
表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 程序流程图
图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.5寸720P屏幕 */
{
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.5寸1080P屏幕 */
{
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.1寸800P屏幕 */
{
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屏幕模块显示背景色不停切换,如下图所示。
图22.4.1 MIPILCD显示效果图