ESP-IDF学习笔记-并口LCD

ESP32S3中提供了一堆外设,其中就包括LCD外设。该外设可以直接驱动屏幕,支持SPI,I2C,8080,RGB接口的屏幕。这与STM32不同(需要基于FSMC或者SPI写驱动),硬件完成了大部分操作。这里记录使用8080并口屏的方法,以及使用触摸作为输入。

官方文档

一,配置步骤概览

以8080总线驱动的HX8369A屏幕为例,ESP32中提供的API结构大概如下:

初始化总线

1
esp_lcd_new_i80_bus();

使用该函数定义时钟源,D/C引脚,WR引脚,总线宽度(8/16),数据引脚,单次最大传输数据量,DMA对齐参数。是对仅对8080总线的定义

定义一个i80panel的IO

1
esp_lcd_new_panel_io_i80();

在LCD驱动中,使用panel处理一个需要驱动的屏幕,需要我们去设置这个panel的io口。这个IO口是包含i80在内的片选等一个屏幕的io的合集。同时还定义了io口电平的含义(命令数据是高电平还是低电平等),定义了屏幕的数据时钟速度,是否交换颜色数据字节,刷新完成回调(用于GUI),命令长度和参数长度等定义

创建一个panel

1
esp_lcd_new_panel_st7789()

在这一步创建一个面板句柄,绑定了特定的屏幕驱动芯片,设置reset脚,色域深度等。

初始化屏幕

1
2
3
esp_lcd_panel_reset(panel_handle);
//在init前必须reset
esp_lcd_panel_init(panel_handle);

这一步将初始化屏幕。

设置其他参数

设置屏幕间隙,是否交换x/y轴等。

二,配置步骤

这里使用HX8369A驱动的480*800屏幕,利用8线8080数据线驱动的屏幕。

初始化总线

1
esp_err_t esp_lcd_new_i80_bus(const esp_lcd_i80_bus_config_t *bus_config, esp_lcd_i80_bus_handle_t *ret_bus)

该函数创建了一个i80句柄(ret_bus)。输入参数:

  • esp_lcd_i80_bus_config_t
1
2
3
4
5
6
7
8
9
10
typedef struct {
int dc_gpio_num; //D/C线
int wr_gpio_num; //WR线
lcd_clock_source_t clk_src; //选择时钟源
int data_gpio_nums[SOC_LCD_I80_BUS_WIDTH]; //数据线数组
size_t bus_width; //设定数据线宽度, 8 or 16
size_t max_transfer_bytes; //最大传输长度,这里决定了内部DMA的传输长度
size_t psram_trans_align; //PSRAM中的数据,DMA使用的数据对齐长度
size_t sram_trans_align; //SRAM中的数据,DMA使用的数据对齐长度
} esp_lcd_i80_bus_config_t;

其中:

  • clk_src

    时钟源选择。一般选择LCD_CLK_SRC_DEFAULT(LCD_CLK_SRC_PLL160M)。还可以选择LCD_CLK_SRC_PLL240M,LCD_CLK_SRC_XTAL

  • max_transfer_bytes

    决定了内部DMA的传输长度,一般为行的整数倍:EXAMPLE_LCD_H_RES * 100 * sizeof(uint16_t)

  • psram_trans_align

    PRAM中使用的数据对齐,支持16,32,64。

    Supported alignment: 16, 32, 64. A higher alignment can enables higher burst transfer size, thus a higher i80 bus throughput.

  • sram_trans_align

    一般为4

这个DMA对齐我还没搞明白,但应该是和STM32DMA设置中的字半字之类有关吧。

使用例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
    esp_lcd_i80_bus_handle_t i80_bus = NULL;
esp_lcd_i80_bus_config_t bus_config = {
.clk_src = LCD_CLK_SRC_DEFAULT,
.dc_gpio_num = EXAMPLE_PIN_NUM_DC,
.wr_gpio_num = EXAMPLE_PIN_NUM_PCLK,
.data_gpio_nums = {
EXAMPLE_PIN_NUM_DATA0,
EXAMPLE_PIN_NUM_DATA1,
EXAMPLE_PIN_NUM_DATA2,
EXAMPLE_PIN_NUM_DATA3,
EXAMPLE_PIN_NUM_DATA4,
EXAMPLE_PIN_NUM_DATA5,
EXAMPLE_PIN_NUM_DATA6,
EXAMPLE_PIN_NUM_DATA7,
#if CONFIG_EXAMPLE_LCD_I80_BUS_WIDTH > 8
EXAMPLE_PIN_NUM_DATA8,
EXAMPLE_PIN_NUM_DATA9,
EXAMPLE_PIN_NUM_DATA10,
EXAMPLE_PIN_NUM_DATA11,
EXAMPLE_PIN_NUM_DATA12,
EXAMPLE_PIN_NUM_DATA13,
EXAMPLE_PIN_NUM_DATA14,
EXAMPLE_PIN_NUM_DATA15,
#endif
},
.bus_width = CONFIG_EXAMPLE_LCD_I80_BUS_WIDTH,
.max_transfer_bytes = 480 * 100 * sizeof(uint16_t),
.psram_trans_align = 64,
.sram_trans_align = 4,
};
ESP_ERROR_CHECK(esp_lcd_new_i80_bus(&bus_config, &i80_bus));

创建IO设备句柄

1
esp_err_t esp_lcd_new_panel_io_i80(esp_lcd_i80_bus_handle_t bus, const esp_lcd_panel_io_i80_config_t *io_config, esp_lcd_panel_io_handle_t *ret_io)

在i80总线的基础上创建IO句柄。

参数:

  • bus

    esp_lcd_new_i80_bus()创建的句柄

  • ret_io

    创建出来的句柄

  • io_config

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
typedef struct {
int cs_gpio_num; /*!< 片选线,设为-1不使用*/
uint32_t pclk_hz; /*!< 像素时钟频率 */100ns
size_t trans_queue_depth; /*!< 传输队列长度,越大数据吞吐量越大 */
esp_lcd_panel_io_color_trans_done_cb_t on_color_trans_done; /*!< 数据传输完成回调,用于GUI */
void *user_ctx; /*!< 用户参数,传递给回调 on_color_trans_done's user_ctx */
int lcd_cmd_bits; /*!< 命令的位数 */
int lcd_param_bits; /*!< 参数的位数 */
struct {
unsigned int dc_idle_level: 1; /*!< 空闲时D/C的电平 */
unsigned int dc_cmd_level: 1; /*!< 命令时D/C的电平 */
unsigned int dc_dummy_level: 1; /*!< Level of DC line in DUMMY phase */
unsigned int dc_data_level: 1; /*!< 数据时D/C的电平 */
} dc_levels; /*!< 为每个8080定义自己的逻辑电平 */
struct {
unsigned int cs_active_high: 1; /*!< 片选有效电平 */
unsigned int reverse_color_bits: 1; /*!< 是否反转bit, D[N:0] -> D[0:N] */
unsigned int swap_color_bytes: 1; /*!< 交换两个颜色字节 */
unsigned int pclk_active_neg: 1; /*!< 是否使用wr下降沿传输数据 */
unsigned int pclk_idle_low: 1; /*!< 空闲时wr的电平 */
} flags; /*!< Panel IO config flags */
} esp_lcd_panel_io_i80_config_t;

其中回调函数的模板:

1
2
3
4
5
6
7
8
9
10
typedef bool (*esp_lcd_panel_io_color_trans_done_cb_t)(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_io_event_data_t *edata, void *user_ctx);


//example
static bool example_notify_lvgl_flush_ready(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_io_event_data_t *edata, void *user_ctx)
{
lv_disp_drv_t *disp_driver = (lv_disp_drv_t *)user_ctx;
lv_disp_flush_ready(disp_driver);
return false;
}

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include "esp_lcd_panel_io.h"

esp_lcd_panel_io_handle_t io_handle = NULL;
esp_lcd_panel_io_i80_config_t io_config = {
.cs_gpio_num = EXAMPLE_PIN_NUM_CS,
.pclk_hz = EXAMPLE_LCD_PIXEL_CLOCK_HZ,
.trans_queue_depth = 10,
.dc_levels = {
.dc_idle_level = 0,
.dc_cmd_level = 0,
.dc_dummy_level = 0,
.dc_data_level = 1,
},
.flags = {
.swap_color_bytes = !LV_COLOR_16_SWAP, // Swap can be done in LvGL (default) or DMA
},
.on_color_trans_done = example_notify_lvgl_flush_ready,
.user_ctx = &disp_drv,
.lcd_cmd_bits = EXAMPLE_LCD_CMD_BITS,
.lcd_param_bits = EXAMPLE_LCD_PARAM_BITS,
};
ESP_ERROR_CHECK(esp_lcd_new_panel_io_i80(i80_bus, &io_config, &io_handle));

创建panel句柄

创建完IO句柄后,驱动知道了怎么发数据,但是还不知道发什么数据/命令。因此,需要一层驱动层。

使用如下命令创建面板。

1
esp_err_t esp_lcd_new_panel_st7789(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)

创建了一个基于st7789的面板句柄。

  • io

    上面创建的io句柄

  • ret_panel

    创建出来的句柄

  • panel_dev_config

    设置结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "esp_lcd_panel_vendor.h"

typedef struct {
int reset_gpio_num; /*!< reset引脚,使用-1为不使用 */
union {
lcd_color_rgb_endian_t color_space; /*!< @弃用 Set RGB color space, please use rgb_endian instead */
lcd_color_rgb_endian_t rgb_endian; /*!< 颜色顺序,RGB或者BGR */
};
unsigned int bits_per_pixel; /*!< 颜色深度, in bpp(bits per pixel) */
struct {
unsigned int reset_active_high: 1; /*!< 设置reset有效电平 */
} flags; /*!< LCD panel config flags */
void *vendor_config; /*!< vendor specific configuration, optional, left as NULL if not used */
} esp_lcd_panel_dev_config_t;

其中:

  • rgb_endian:LCD_RGB_ENDIAN_RGB/LCD_RGB_ENDIAN_BGR

例子:

1
2
3
4
5
6
7
8
esp_lcd_panel_handle_t panel_handle = NULL;
ESP_LOGI(TAG, "Install LCD driver of st7789");
esp_lcd_panel_dev_config_t panel_config = {
.reset_gpio_num = EXAMPLE_PIN_NUM_RST,
.rgb_endian = LCD_RGB_ENDIAN_RGB,
.bits_per_pixel = 16,
};
ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(io_handle, &panel_config, &panel_handle));

然而,这是ST7789的驱动,我这个sb开发板是hx8369A驱动的,这个在官方的库中没支持。(虽然官方也只支持了ST7789,nt35510,ssd1306),在esp的组件库中也没有。但是在例程中给的库lvgl_esp32_drivers中有这个芯片,在该库中实现了一个hx8396a的panel生成,我这个开发板的店家也是直接用的那里面的驱动,然后好像改成了8080发送数据。然后我问店家怎么移植,他直接说用这个,然后说他们不教这个???

关于如果需要其他IC驱动的芯片(冷门的导致ESP官方没有支持的),需要自己添加驱动层代码,我会在另一篇文章细说。

调用函数初始化

以下函数都实际在上面的panel中实现的。

1
esp_err_t esp_lcd_panel_reset(esp_lcd_panel_handle_t panel);

重置屏幕,该函数需要在使用esp_lcd_panel_init()前被调用。

1
esp_err_t esp_lcd_panel_init(esp_lcd_panel_handle_t panel);

初始化屏幕。

1
esp_err_t esp_lcd_panel_mirror(esp_lcd_panel_handle_t panel, bool mirror_x, bool mirror_y);

在某个轴镜像屏幕。与esp_lcd_panel_swap_xy()协同使用。

1
esp_err_t esp_lcd_panel_swap_xy(esp_lcd_panel_handle_t panel, bool swap_axes);

交换x/y轴。与esp_lcd_panel_mirror()协同使用。

1
esp_err_t esp_lcd_panel_set_gap(esp_lcd_panel_handle_t panel, int x_gap, int y_gap)

设置x/y上的间隙(到边框的距离)。间隙是指液晶面板的左/顶部两侧分别与实际显示的第一行/列之间的空间(像素)。

当定位或对准比LCD小的框架时,设置间隙是很有用的。

1
esp_err_t esp_lcd_panel_invert_color(esp_lcd_panel_handle_t panel, bool invert_color_data)

颜色反转。

1
esp_err_t esp_lcd_panel_disp_on_off(esp_lcd_panel_handle_t panel, bool on_off)

打开或者关闭显示。

例子:

1
2
3
4
5
6
7
8
9
#include "esp_lcd_panel_ops.h"

esp_lcd_panel_reset(panel_handle);
esp_lcd_panel_init(panel_handle);
// Set inversion, x/y coordinate order, x/y mirror according to your LCD module spec
// the gap is LCD panel specific, even panels with the same driver IC, can have different gap value
esp_lcd_panel_invert_color(panel_handle, true);
esp_lcd_panel_set_gap(panel_handle, 0, 20);

三,使用屏幕

ESP库提供了最基础的画点函数。

1
esp_err_t esp_lcd_panel_draw_bitmap(esp_lcd_panel_handle_t panel, int x_start, int y_start, int x_end, int y_end, const void *color_data)

一个窗口中绘制像素点。

Tips:start点被包含而end点没有被包含

当初始化化完IO句柄后,其实已经可以实现发送和接收数据来驱动屏幕了,后面不过是封装了特定的驱动。

1
esp_err_t esp_lcd_panel_io_rx_param(esp_lcd_panel_io_handle_t io, int lcd_cmd, void *param, size_t param_size)

发送命令并接收参数。

1
esp_err_t esp_lcd_panel_io_tx_param(esp_lcd_panel_io_handle_t io, int lcd_cmd, const void *param, size_t param_size)

发送命令和参数。

1
esp_err_t esp_lcd_panel_io_tx_color(esp_lcd_panel_io_handle_t io, int lcd_cmd, const void *color, size_t color_size)

发送颜色数据。

这个函数将要发送的数据加入到后台队列,由DMA+中断发送。

由于有缓存时间存在,因此需要回调处理数据的生命周期。

四,SDKconfig中的设置

LCD and Touch Panel--->LCD Peripheral Configuration中,需要注意这样一个设置LCD panel io foramt buffer size。该参数的值与io层单次最大发送数据量param_size有关,如果该值设置过小,会导致发送时报错

在本人移植驱动过程中,由于没有设置该值,导致在初始化时在下面函数中报错

1
esp_lcd_panel_io_tx_param(io, 0x2D, cmd_192,192);

192超过了默认值大小,导致报错。