【立创·实战派ESP32-S3】文档教程
第 9 章 液晶显示
开发板上的液晶屏是 2.0 寸的 IPS 高清液晶屏,分辨率 240320。市场上最常见的 240320 的液晶屏,尺寸大小一般有 2.0 寸、2.4 寸、2.8 寸和 3.2 寸,同样的分辨率,尺寸越小,像素点越小,显示越清晰,所以以上 4 个型号中,2.0 寸的屏幕显示效果最好。
液晶屏驱动芯片 ST7789,采用 SPI 通信方式与 ESP32-S3 连接。
本例实现了液晶屏的驱动、整屏显示一个颜色、以及显示图片。
9.1 使用例程
把开发板提供的【06-lcd】例程复制到你的实验文件夹当中,并使用 VSCode 打开工程。
连接开发板到电脑,在 VSCode 上选择串口号,选择目标芯片为 esp32s3,串口下载方式,然后点击“一键三联”按钮,等待编译下载打开终端。
终端显示:
I (879) main_task: Calling app_main()
I (879) gpio: GPIO[39]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:0
I (1009) esp32_s3_szp: Setting LCD backlight: 100%
I (1449) main_task: Returned from app_main()
2
3
4
开发板液晶屏上最终会显示一张图片。
如果想显示自己的图片,请看第 3 小节最后面。
9.2 例程讲解
点击打开 main.c 文件,找到 app_main 函数。
void app_main(void)
{
bsp_i2c_init(); // I2C初始化
pca9557_init(); // IO扩展芯片初始化
bsp_lcd_init(); // 液晶屏初始化
//lcd_draw_pictrue(0, 0, 240, 240, logo_en_240x240_lcd); // 显示乐鑫logo图片
lcd_draw_pictrue(0, 0, 320, 240, gImage_yingwu); // 显示3只鹦鹉图片
}
2
3
4
5
6
7
8
液晶屏的 LCD_CS 引脚由 IO 扩展芯片 pca9557 控制,所以需要先初始化 pca9557,而 pca9557 是 i2c 通信芯片,所以又需要先初始化 i2c。关于 i2c 的初始化和 pca9557 的初始化函数介绍,前面章节已经介绍过,这里就不作讲解了。
下面我们看一下 bsp_lcd_init()
液晶屏初始化函数和 lcd_draw_pictrue()
绘制图片函数。
鼠标选中 bsp_lcd_init()
函数按 F12 定位到这个函数的定义位置,位于 esp32_s3_szp.c 文件中。
esp_err_t bsp_lcd_init(void)
{
esp_err_t ret = ESP_OK;
ret = bsp_display_new(); // 液晶屏驱动初始化
app_lcd_set_color(0x0000); // 设置整屏背景色
ret = esp_lcd_panel_disp_on_off(panel_handle, true); // 打开液晶屏显示
ret = bsp_display_backlight_on(); // 打开背光显示
return ret;
}
2
3
4
5
6
7
8
9
10
11
12
液晶屏显示的开关有两个,一个是 esp_lcd_panel_disp_on_off()
,一个是 bsp_display_backlight_on()
。
它们的区别是:
esp_lcd_panel_disp_on_off()
用来控制的是液晶屏的驱动芯片 ST7789 中的寄存器,这个寄存器控制液晶屏显示与否。
bsp_display_backlight_on()
用来控制液晶屏 LED 背光,通过调节 PWM 占空比调节亮度,使用的是 LEDC 外设产生的 PWM 信号。
这两个都打开,才能看到液晶屏显示的内容。
bsp_display_new()
函数用于初始化液晶屏驱动,该函数也位于 esp32_s3_szp.c 文件中。
// 液晶屏初始化
esp_err_t bsp_display_new(void)
{
esp_lcd_panel_io_handle_t io_handle = NULL;
esp_err_t ret = ESP_OK;
// 背光初始化
ESP_RETURN_ON_ERROR(bsp_display_brightness_init(), TAG, "Brightness init failed");
// 初始化SPI总线
ESP_LOGD(TAG, "Initialize SPI bus");
const spi_bus_config_t buscfg = {
.sclk_io_num = BSP_LCD_SPI_CLK,
.mosi_io_num = BSP_LCD_SPI_MOSI,
.miso_io_num = GPIO_NUM_NC,
.quadwp_io_num = GPIO_NUM_NC,
.quadhd_io_num = GPIO_NUM_NC,
.max_transfer_sz = BSP_LCD_H_RES * BSP_LCD_V_RES * sizeof(uint16_t),
};
ESP_RETURN_ON_ERROR(spi_bus_initialize(BSP_LCD_SPI_NUM, &buscfg, SPI_DMA_CH_AUTO), TAG, "SPI init failed");
// 液晶屏控制IO初始化
ESP_LOGD(TAG, "Install panel IO");
const esp_lcd_panel_io_spi_config_t io_config = {
.dc_gpio_num = BSP_LCD_DC,
.cs_gpio_num = BSP_LCD_SPI_CS,
.pclk_hz = BSP_LCD_PIXEL_CLOCK_HZ,
.lcd_cmd_bits = LCD_CMD_BITS,
.lcd_param_bits = LCD_PARAM_BITS,
.spi_mode = 2,
.trans_queue_depth = 10,
};
ESP_GOTO_ON_ERROR(esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)BSP_LCD_SPI_NUM, &io_config, &io_handle), err, TAG, "New panel IO failed");
// 初始化液晶屏驱动芯片ST7789
ESP_LOGD(TAG, "Install LCD driver");
const esp_lcd_panel_dev_config_t panel_config = {
.reset_gpio_num = BSP_LCD_RST,
.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB,
.bits_per_pixel = BSP_LCD_BITS_PER_PIXEL,
};
ESP_GOTO_ON_ERROR(esp_lcd_new_panel_st7789(io_handle, &panel_config, &panel_handle), err, TAG, "New panel failed");
esp_lcd_panel_reset(panel_handle); // 液晶屏复位
lcd_cs(0); // 拉低CS引脚
esp_lcd_panel_init(panel_handle); // 初始化配置寄存器
esp_lcd_panel_invert_color(panel_handle, true); // 颜色反转
esp_lcd_panel_swap_xy(panel_handle, true); // xy坐标翻转
esp_lcd_panel_mirror(panel_handle, true, false); // 镜像
return ret;
err:
if (panel_handle) {
esp_lcd_panel_del(panel_handle);
}
if (io_handle) {
esp_lcd_panel_io_del(io_handle);
}
spi_bus_free(BSP_LCD_SPI_NUM);
return ret;
}
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
先是背光初始化,然后初始化 SPI 总线,液晶屏与 esp32 采用 SPI 通信,然后初始化其它控制引脚以及配置参数,最后控制液晶屏显示方式。
背光初始化 bsp_display_brightness_init()
函数,也位于 esp32_s3_szp.c 文件中,原理是把控制背光的引脚初始化成 PWM 引脚,通过控制 PWM 的占空比,控制液晶屏的亮度。
最后几个控制显示的函数,是根据我们开发板的显示调整了一下方向。
esp_lcd_panel_swap_xy()
函数控制 xy 坐标翻转,第 2 个参数,true 表示翻转,false 表示不翻转。
esp_lcd_panel_mirror()
函数控制 xy 方向是否镜像。第 2 个参数控制 x 方向,第 3 个参数控制 y 方向,true 表示镜像,false 表示不镜像。
bsp_lcd_init()
函数里面还有一个显示背景色的函数 lcd_set_color()
,这个函数也位于 esp32_s3_szp.c 文件中,代码如下所示:
// 设置液晶屏颜色
void lcd_set_color(uint16_t color)
{
// 分配内存 这里分配了液晶屏一行数据需要的大小
uint16_t *buffer = (uint16_t *)heap_caps_malloc(BSP_LCD_H_RES * sizeof(uint16_t), MALLOC_CAP_8BIT | MALLOC_CAP_SPIRAM);
if (NULL == buffer)
{
ESP_LOGE(TAG, "Memory for bitmap is not enough");
}
else
{
for (size_t i = 0; i < BSP_LCD_H_RES; i++) // 给缓存中放入颜色数据
{
buffer[i] = color;
}
for (int y = 0; y < 240; y++) // 显示整屏颜色
{
esp_lcd_panel_draw_bitmap(panel_handle, 0, y, 320, y+1, buffer);
}
free(buffer); // 释放内存
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
这个函数,用来给整个液晶屏显示同一个颜色。
最开始分配了一块内存,BSP_LCD_H_RES 是液晶屏的宽度,即 320。RGB565 模式,每个像素点占用 2 个字节,所以 BSP_LCD_H_RES * sizeof(uint16_t)实际上就是液晶屏显示一行数据需要的内存大小。
MALLOC_CAP_8BIT 表示数据允许以 8 位或 16 位访问。MALLOC_CAP_SPIRAM 表示数据存储到 SPIRAM 中。
后面第 1 个 for 循环,给刚才开辟的内容都写上要显示的颜色数据。第 2 个 for 循环,显示整屏颜色。
esp_lcd_panel_draw_bitmap()
函数和刚才的 esp_lcd_panel_swap_xy()
等函数,都位于 esp-idf 的组件中,打开 esp-idf 整个工程,可以找到它们的源代码。
esp_lcd_panel_draw_bitmap()
函数,第 1 个参数指向液晶屏句柄,第 2 个参数表示 x 坐标的开始位置,第 3 个参数表示 y 坐标的开始位置,第 4 个参数表示 x 坐标的结束位置,第 5 个参数表示 y 坐标的结束位置,第 6 个参数表示要写入的颜色数据。分析这个 for 循环,每次调用一次这个函数,都会显示一行颜色,一共调用 240 次,就显示了整屏颜色。
接下来再回到 app_main 函数中。
lcd_draw_pictrue()
函数用来显示一张图片,这个代码定义也位于 esp32_s3_szp.c 中。
// 显示图片
void lcd_draw_pictrue(int x_start, int y_start, int x_end, int y_end, const unsigned char *gImage)
{
// 分配内存 分配了需要的字节大小 且指定在外部SPIRAM中分配
size_t pixels_byte_size = (x_end - x_start)*(y_end - y_start) * 2;
uint16_t *pixels = (uint16_t *)heap_caps_malloc(pixels_byte_size, MALLOC_CAP_8BIT | MALLOC_CAP_SPIRAM);
if (NULL == pixels)
{
ESP_LOGE(TAG, "Memory for bitmap is not enough");
return;
}
memcpy(pixels, gImage, pixels_byte_size); // 把图片数据拷贝到内存
esp_lcd_panel_draw_bitmap(panel_handle, x_start, y_start, x_end, y_end, (uint16_t *)pixels); // 显示整张图片数据
heap_caps_free(pixels); // 释放内存
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
函数的前 4 个参数,可以指定图片的起始位置和结束位置,第 5 个参数,传入图片数组名称。
这个显示图片的函数,和刚才显示整屏颜色的函数,有些类似。
首先,计算这张图片的像素大小,赋值给 pixels_byte_size,后面分配内存和拷贝内存的时候要用。
然后,分配整张图片所需字节大小的内存,使用 heap_caps_malloc。
接下来,memcpy()函数把图片数据拷贝到内存,第 1 个参数是目标内存地址,第 2 个参数是源数据地址,第 3 个参数是需要拷贝的字节数。
esp_lcd_panel_draw_bitmap()
函数在上面显示整屏颜色的函数里面也用过,每个参数的意义在前面讲解过,当时是每执行一次,显示一行的颜色,现在这里,是直接显示整屏的数据。
9.3 例程制作过程
我们还是使用 sample project 作为模板,复制 sample_project 这个工程到我们的实验文件夹,然后把这个文件夹的名称修改为 06-lcd,修改后我的工程路径为 D:\esp32s3\06-lcd。
使用 VSCode 打开 lcd 工程,点击打开 lcd 工程目录下的 CMakeList.txt 文件,修改工程的名称为 lcd,然后保存关闭此文件。
project(lcd)
这次,我们要参考的是乐鑫的 esp-bsp 库,这个库包含了各种乐鑫和第三方开发板的板级支持包 (BSP)。
esp-bsp 代码开源地址:https://github.com/espressif/esp-bsp
下载 esp-bsp 代码到你的硬盘,然后使用 VSCode 把它打开。
我们把上一章中制作的 esp32_s3_szp.c 和 esp32_s3_szp.h 文件复制粘贴到本例程的 main 文件夹下。
然后点击打开 main 下面的 CMakeLists.txt 文件,添加 esp32_s3_szp.c 源文件,如下所示:
idf_component_register(SRCS "esp32_s3_szp.c" "main.c"
INCLUDE_DIRS ".")
2
从原理图上可以看到,IO42 引脚控制液晶屏的背光。当 IO42 引脚输出高电平时,关闭背光,当 IO42 引脚输出低电平时,打开背光。如果 IO42 引脚输出 PWM 信号,就可以通过调节占空比,均匀的控制液晶屏的背光亮度。
esp32-s3 具有 LED PWM 控制器外设,专门用于控制 LED,我们将使用这个外设控制背光亮度。
在 esp-bsp 工程中,依次打开 esp-bsp\bsp\esp32_s3_eye\esp32_s3_eye.c 文件,它的第 195 行到 247 行,是背光初始化函数和背光调节函数,一共 4 个函数,我们把这 4 个函数复制到我们例程的 esp32_s3_szp.c 文件中。
先看背光初始化函数,代码如下:
esp_err_t bsp_display_brightness_init(void)
{
// Setup LEDC peripheral for PWM backlight control
const ledc_channel_config_t LCD_backlight_channel = {
.gpio_num = BSP_LCD_BACKLIGHT,
.speed_mode = LEDC_LOW_SPEED_MODE,
.channel = LCD_LEDC_CH,
.intr_type = LEDC_INTR_DISABLE,
.timer_sel = 1,
.duty = 0,
.hpoint = 0,
.flags.output_invert = true
};
const ledc_timer_config_t LCD_backlight_timer = {
.speed_mode = LEDC_LOW_SPEED_MODE,
.duty_resolution = LEDC_TIMER_10_BIT,
.timer_num = 1,
.freq_hz = 5000,
.clk_cfg = LEDC_AUTO_CLK
};
BSP_ERROR_CHECK_RETURN_ERR(ledc_timer_config(&LCD_backlight_timer));
BSP_ERROR_CHECK_RETURN_ERR(ledc_channel_config(&LCD_backlight_channel));
return ESP_OK;
}
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
上面函数代码,是控制 ESP32 的某个引脚输出 PWM 信号的标准写法,分别配置定时器和通道就可以。你也可以在 esp-idf 工程的 examples\peripherals\ledc\ledc_basic 例程中,看到这些语句,如果想查看 ledc_channel_config_t 、ledc_timer_config_t 以及 ledc_timer_config 和 ledc_channel_config 的原始定义,可以在 esp-idf 工程中查看。
上面配置 ledc 通道的结构体中,有一个.gpio_num 成员,我们需要把 BSP_LCD_BACKLIGHT 这个宏定义写到 esp32_s3_szp.h 中。
#define BSP_LCD_BACKLIGHT (GPIO_NUM_42)
其中的.channel,用来选择通道,ESP32 上的 LEDC 通道有 8 个,分别是 LEDC_CHANNEL_0 到 LEDC_CHANNEL_7,这些通道名称,是枚举类型,它的原始定义,位于 IDF 工程中的 ledc_types.h 文件中。我们在 esp32_s3_szp.h 文件中把 LCD_LEDC_CH 定义为 LEDC_CHANNEL_0。
#define LCD_LEDC_CH LEDC_CHANNEL_0
上面代码中的 BSP_ERROR_CHECK_RETURN_ERR,我们可以在 esp-bsp 的工程中的这个语句上面单击右键,选择转到定义,可以看到它是 bsp 文件中自己定义的一个检查错误的宏定义函数,这里我们把它改成 ESP_ERROR_CHECK 就可以了。且在 esp32_s3_szp.h 文件中,包含 esp_check.h 头文件。
#include "esp_check.h"
修改好之后的初始化函数如下所示:
esp_err_t bsp_display_brightness_init(void)
{
// Setup LEDC peripheral for PWM backlight control
const ledc_channel_config_t LCD_backlight_channel = {
.gpio_num = BSP_LCD_BACKLIGHT,
.speed_mode = LEDC_LOW_SPEED_MODE,
.channel = LCD_LEDC_CH,
.intr_type = LEDC_INTR_DISABLE,
.timer_sel = 1,
.duty = 0,
.hpoint = 0,
.flags.output_invert = true
};
const ledc_timer_config_t LCD_backlight_timer = {
.speed_mode = LEDC_LOW_SPEED_MODE,
.duty_resolution = LEDC_TIMER_10_BIT,
.timer_num = 1,
.freq_hz = 5000,
.clk_cfg = LEDC_AUTO_CLK
};
ESP_ERROR_CHECK(ledc_timer_config(&LCD_backlight_timer));
ESP_ERROR_CHECK(ledc_channel_config(&LCD_backlight_channel));
return ESP_OK;
}
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
使用 LEDC 外设,需要包含 ledc.h 头文件,把下面头文件添加到 esp32_s3_szp.h 文件中。
#include "driver/ledc.h"
下一个函数,是亮度调节函数,其实就是 PWM 占空比设置函数,如下所示:
esp_err_t bsp_display_brightness_set(int brightness_percent)
{
if (brightness_percent > 100) {
brightness_percent = 100;
} else if (brightness_percent < 0) {
brightness_percent = 0;
}
ESP_LOGI(TAG, "Setting LCD backlight: %d%%", brightness_percent);
// LEDC resolution set to 10bits, thus: 100% = 1023
uint32_t duty_cycle = (1023 * brightness_percent) / 100;
BSP_ERROR_CHECK_RETURN_ERR(ledc_set_duty(LEDC_LOW_SPEED_MODE, LCD_LEDC_CH, duty_cycle));
BSP_ERROR_CHECK_RETURN_ERR(ledc_update_duty(LEDC_LOW_SPEED_MODE, LCD_LEDC_CH));
return ESP_OK;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
设置占空比,首先执行“设置占空比”函数 ledc_set_duty,然后执行“更新占空比”函数 ledc_update_duty。函数的输入参数为 0 到 100。
和刚才的初始化函数一样,上面函数中,我们也把 BSP_ERROR_CHECK_RETURN_ERR 修改成 ESP_ERROR_CHECK,修改之后的函数为:
esp_err_t bsp_display_brightness_set(int brightness_percent)
{
if (brightness_percent > 100) {
brightness_percent = 100;
} else if (brightness_percent < 0) {
brightness_percent = 0;
}
ESP_LOGI(TAG, "Setting LCD backlight: %d%%", brightness_percent);
// LEDC resolution set to 10bits, thus: 100% = 1023
uint32_t duty_cycle = (1023 * brightness_percent) / 100;
ESP_ERROR_CHECK(ledc_set_duty(LEDC_LOW_SPEED_MODE, LCD_LEDC_CH, duty_cycle));
ESP_ERROR_CHECK(ledc_update_duty(LEDC_LOW_SPEED_MODE, LCD_LEDC_CH));
return ESP_OK;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
再接下来的两个函数,如下所示,分别是把背光设置为最亮,和关闭背光的函数。
esp_err_t bsp_display_backlight_off(void)
{
return bsp_display_brightness_set(0);
}
esp_err_t bsp_display_backlight_on(void)
{
return bsp_display_brightness_set(100);
}
2
3
4
5
6
7
8
9
这两个函数不用做修改。
接下来,我们把这 4 个函数,声明到 esp32_s3_szp.h 文件中,放到 i2c 初始化函数的声明后面就可以。
esp_err_t bsp_display_brightness_init(void);
esp_err_t bsp_display_brightness_set(int brightness_percent);
esp_err_t bsp_display_backlight_off(void);
esp_err_t bsp_display_backlight_on(void);
2
3
4
回到 esp-bsp 工程中,在 esp-bsp\bsp\esp32_s3_eye\esp32_s3_eye.c 文件中,刚才复制的 4 个函数的下面一个函数,就是 lcd 的初始化函数,名称为 bsp_display_new,我们把它复制到我们例程的 esp32_s3_szp.c 文件中。
esp_err_t bsp_display_new(const bsp_display_config_t *config, esp_lcd_panel_handle_t *ret_panel, esp_lcd_panel_io_handle_t *ret_io)
{
esp_err_t ret = ESP_OK;
assert(config != NULL && config->max_transfer_sz > 0);
ESP_RETURN_ON_ERROR(bsp_display_brightness_init(), TAG, "Brightness init failed");
ESP_LOGD(TAG, "Initialize SPI bus");
const spi_bus_config_t buscfg = {
.sclk_io_num = BSP_LCD_SPI_CLK,
.mosi_io_num = BSP_LCD_SPI_MOSI,
.miso_io_num = GPIO_NUM_NC,
.quadwp_io_num = GPIO_NUM_NC,
.quadhd_io_num = GPIO_NUM_NC,
.max_transfer_sz = config->max_transfer_sz,
};
ESP_RETURN_ON_ERROR(spi_bus_initialize(BSP_LCD_SPI_NUM, &buscfg, SPI_DMA_CH_AUTO), TAG, "SPI init failed");
ESP_LOGD(TAG, "Install panel IO");
const esp_lcd_panel_io_spi_config_t io_config = {
.dc_gpio_num = BSP_LCD_DC,
.cs_gpio_num = BSP_LCD_SPI_CS,
.pclk_hz = BSP_LCD_PIXEL_CLOCK_HZ,
.lcd_cmd_bits = LCD_CMD_BITS,
.lcd_param_bits = LCD_PARAM_BITS,
.spi_mode = 2,
.trans_queue_depth = 10,
};
ESP_GOTO_ON_ERROR(esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)BSP_LCD_SPI_NUM, &io_config, ret_io), err, TAG, "New panel IO failed");
ESP_LOGD(TAG, "Install LCD driver");
const esp_lcd_panel_dev_config_t panel_config = {
.reset_gpio_num = BSP_LCD_RST,
.color_space = BSP_LCD_COLOR_SPACE,
.bits_per_pixel = BSP_LCD_BITS_PER_PIXEL,
};
ESP_GOTO_ON_ERROR(esp_lcd_new_panel_st7789(*ret_io, &panel_config, ret_panel), err, TAG, "New panel failed");
esp_lcd_panel_reset(*ret_panel);
esp_lcd_panel_init(*ret_panel);
esp_lcd_panel_invert_color(*ret_panel, true);
return ret;
err:
if (*ret_panel) {
esp_lcd_panel_del(*ret_panel);
}
if (*ret_io) {
esp_lcd_panel_io_del(*ret_io);
}
spi_bus_free(BSP_LCD_SPI_NUM);
return ret;
}
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
这个函数中,有几处地方需要修改。
第 1 处修改: 在 buscfg
结构体里面,有一个 .max_transfer_sz
成员变量,它的值目前是 config->max_transfer_sz
,config
是该初始化函数 bsp_display_new()的第 1 个参数,这个参数,只起到这一个作用。
我们在 bsp_display_new()函数后面,看到有一个 bsp_display_lcd_init()函数,在这个函数中,调用了 bsp_display_new()函数,我们看到它的使用方法,相关语句如下:
static lv_display_t *bsp_display_lcd_init(const bsp_display_cfg_t *cfg)
{
assert(cfg != NULL);
esp_lcd_panel_io_handle_t io_handle = NULL;
esp_lcd_panel_handle_t panel_handle = NULL;
const bsp_display_config_t bsp_disp_cfg = {
.max_transfer_sz = BSP_LCD_DRAW_BUFF_SIZE * sizeof(uint16_t),
};
BSP_ERROR_CHECK_RETURN_NULL(bsp_display_new(&bsp_disp_cfg, &panel_handle, &io_handle));
......
2
3
4
5
6
7
8
9
10
这里,看一下 bsp_display_new()函数的第一个参数是怎么带进去的,通过分析,我们发现,它仅仅是把 .max_transfer_sz
成员赋值为 BSP_LCD_DRAW_BUFF_SIZE * sizeof(uint16_t)
,那这样的化,我们不如直接在 bsp_display_new()函数里面,直接给 .max_transfer_sz
成员赋值为 BSP_LCD_DRAW_BUFF_SIZE * sizeof(uint16_t)
,这里的 BSP_LCD_DRAW_BUFF_SIZE
被定义成了 240240,这个是 esp32_eye 开发板配套 LCD 屏幕的分辨率大小,这里我们修改为我们的 320240 就可以。所以这里我们把 bsp_display_new()函数的 .max_transfer_sz
成员赋值为 BSP_LCD_H_RES * BSP_LCD_V_RES * sizeof(uint16_t)
,然后把 bsp_display_new()函数的第 1 个入口参数去掉。
第 2 处修改:panel_config
结构体配置里面,把 .color_space
成员修为 .rgb_ele_order
成员,并且给这个成员赋值为 LCD_RGB_ELEMENT_ORDER_RGB
。这是为什么呢?我们可以在 esp-idf 工程中,找到 esp_lcd_panel_dev_config_t 的原始结构体定义,如下所示。
该代码位于 esp_lcd_panel_vendor.h 中,路径是 esp-idf\components\esp_lcd\include
/**
* @brief Configuration structure for panel device
*/
typedef struct {
int reset_gpio_num; /*!< GPIO used to reset the LCD panel, set to -1 if it's not used */
union {
lcd_rgb_element_order_t color_space; /*!< @deprecated Set RGB color space, please use rgb_ele_order instead */
lcd_rgb_element_order_t rgb_endian; /*!< @deprecated Set RGB data endian, please use rgb_ele_order instead */
lcd_rgb_element_order_t rgb_ele_order; /*!< Set RGB element order, RGB or BGR */
};
lcd_rgb_data_endian_t data_endian; /*!< Set the data endian for color data larger than 1 byte */
unsigned int bits_per_pixel; /*!< Color depth, in bpp */
struct {
unsigned int reset_active_high: 1; /*!< Setting this if the panel reset is high level active */
} flags; /*!< LCD panel config flags */
void *vendor_config; /*!< vendor specific configuration, optional, left as NULL if not used */
} esp_lcd_panel_dev_config_t;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
我们看这个结构体中的共用体,实际上,color_space、rgb_endian 和 rgb_ele_order,这 3 个都是一样的,我们看它后面的注释,前两个注释,都是让使用 rgb_ele_order,这就是刚才我们要把 color_space 修改成 rgb_ele_order 的原因。
第 3 处修改: 在 esp_lcd_panel_reset()函数下面,加入让 lcd_cs 引脚拉低的函数,这个函数就是在 pca9557.c 文件中定义的函数,lcd_cs(0)。这个 CS 拉低的动作,必须要放到复位之后和初始化之前,否则液晶屏无法显示。
第 4 处修改: 这个初始化完成之后,液晶屏的显示为竖屏,为了调整液晶屏的显示方向为横屏,且以按键一方为上,外扩接口为下,我们需要加入两个函数,一个是翻转函数 esp_lcd_panel_swap_xy(),一个是镜像函数 esp_lcd_panel_mirror()。
第 5 处修改: 去掉 bsp_display_new()的第 2 个参数,且在函数前面使用 esp_lcd_panel_handle_t 定义一个全局变量 panel_handle,然后把函数中的 ret_panel 替换成 panel_handle。panel_handle 用来指定液晶屏面板,如果开发板连接了多个液晶屏,用这个参数,就可以指定哪个液晶屏显示。这里把 esp_lcd_panel_handle_t 作为参数,可以在多个文件中,使用指针传递显示。我们这里把所有显示相关的都写到 esp32_s3_szp.c 文件中,就不需要传递了,所以直接把它写成全局变量。
第 6 处修改: 去掉 bsp_display_new()的第 3 个参数,且在函数前面定义一个 esp_lcd_panel_io_handle_t 全局变量。esp_lcd_new_panel_io_spi()函数和 esp_lcd_new_panel_st7789()函数需要这个作为参数,它定义的名称写为 io_handle,所以需要把函数中的 ret_io 替换为 io_handle。
最后修改好的初始化函数如下所示:
// 定义液晶屏句柄
static esp_lcd_panel_handle_t panel_handle = NULL;
esp_lcd_panel_io_handle_t io_handle = NULL;
// 液晶屏初始化
esp_err_t bsp_display_new(void)
{
esp_err_t ret = ESP_OK;
// 背光初始化
ESP_RETURN_ON_ERROR(bsp_display_brightness_init(), TAG, "Brightness init failed");
// 初始化SPI总线
ESP_LOGD(TAG, "Initialize SPI bus");
const spi_bus_config_t buscfg = {
.sclk_io_num = BSP_LCD_SPI_CLK,
.mosi_io_num = BSP_LCD_SPI_MOSI,
.miso_io_num = GPIO_NUM_NC,
.quadwp_io_num = GPIO_NUM_NC,
.quadhd_io_num = GPIO_NUM_NC,
.max_transfer_sz = BSP_LCD_H_RES * BSP_LCD_V_RES * sizeof(uint16_t),
};
ESP_RETURN_ON_ERROR(spi_bus_initialize(BSP_LCD_SPI_NUM, &buscfg, SPI_DMA_CH_AUTO), TAG, "SPI init failed");
// 液晶屏控制IO初始化
ESP_LOGD(TAG, "Install panel IO");
const esp_lcd_panel_io_spi_config_t io_config = {
.dc_gpio_num = BSP_LCD_DC,
.cs_gpio_num = BSP_LCD_SPI_CS,
.pclk_hz = BSP_LCD_PIXEL_CLOCK_HZ,
.lcd_cmd_bits = LCD_CMD_BITS,
.lcd_param_bits = LCD_PARAM_BITS,
.spi_mode = 2,
.trans_queue_depth = 10,
};
ESP_GOTO_ON_ERROR(esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)BSP_LCD_SPI_NUM, &io_config, &io_handle), err, TAG, "New panel IO failed");
// 初始化液晶屏驱动芯片ST7789
ESP_LOGD(TAG, "Install LCD driver");
const esp_lcd_panel_dev_config_t panel_config = {
.reset_gpio_num = BSP_LCD_RST,
.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB,
.bits_per_pixel = BSP_LCD_BITS_PER_PIXEL,
};
ESP_GOTO_ON_ERROR(esp_lcd_new_panel_st7789(io_handle, &panel_config, &panel_handle), err, TAG, "New panel failed");
esp_lcd_panel_reset(panel_handle); // 液晶屏复位
lcd_cs(0); // 拉低CS引脚
esp_lcd_panel_init(panel_handle); // 初始化配置寄存器
esp_lcd_panel_invert_color(panel_handle, true); // 颜色反转
esp_lcd_panel_swap_xy(panel_handle, true); // 显示翻转
esp_lcd_panel_mirror(panel_handle, true, false); // 镜像
return ret;
err:
if (panel_handle) {
esp_lcd_panel_del(panel_handle);
}
if (io_handle) {
esp_lcd_panel_io_del(io_handle);
}
spi_bus_free(BSP_LCD_SPI_NUM);
return ret;
}
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
函数里面用到的一些宏定义,我们需要在 esp32_s3_szp.h 文件中定义,引脚序号参照原理图写。
#define BSP_LCD_PIXEL_CLOCK_HZ (80 * 1000 * 1000)
#define BSP_LCD_SPI_NUM (SPI3_HOST)
#define LCD_CMD_BITS (8)
#define LCD_PARAM_BITS (8)
#define BSP_LCD_BITS_PER_PIXEL (16)
#define BSP_LCD_SPI_MOSI (GPIO_NUM_40)
#define BSP_LCD_SPI_CLK (GPIO_NUM_41)
#define BSP_LCD_SPI_CS (GPIO_NUM_NC)
#define BSP_LCD_DC (GPIO_NUM_39)
#define BSP_LCD_RST (GPIO_NUM_NC)
2
3
4
5
6
7
8
9
10
11
然后在 esp32_s3_szp.h 中,加入相关的头文件。
#include "driver/spi_master.h"
#include "esp_lcd_panel_io.h"
#include "esp_lcd_panel_vendor.h"
#include "esp_lcd_panel_ops.h"
#include "esp_lcd_types.h"
2
3
4
5
液晶屏刷屏(整屏显示同一个颜色)以及显示图片等功能,在 esp32 上一般使用 LVGL 实现,可以提高液晶屏显示代码制作效率。本例程中,还不涉及 LVGL 的使用,但是我们总得让 LCD 显示东西出来,才能看到液晶屏驱动成功与否。
现在我们再参考乐鑫官方的另外一个库 esp-who 的源代码,这里面有刷屏函数和显示图片的函数。
esp-who 开源地址:https://github.com/espressif/esp-who
我们要参考的是 who_lcd.c 这个文件中的代码,路径是:esp-who\components\modules\lcd。
在 who_lcd.c 这个文件中,第 64 行~第 75 行是 app_lcd_draw_wallpaper()
函数,就是显示乐鑫 logo 图片的函数。第 77 到 98 行是 app_lcd_set_color()
函数,就是全屏显示某个颜色的函数。把这两个函数复制到 esp32_s3_szp.c 文件中。
显示图片的函数的代码如下所示:
void app_lcd_draw_wallpaper()
{
uint16_t *pixels = (uint16_t *)heap_caps_malloc((logo_en_240x240_lcd_width * logo_en_240x240_lcd_height) * sizeof(uint16_t), MALLOC_CAP_8BIT | MALLOC_CAP_SPIRAM);
if (NULL == pixels)
{
ESP_LOGE(TAG, "Memory for bitmap is not enough");
return;
}
memcpy(pixels, logo_en_240x240_lcd, (logo_en_240x240_lcd_width * logo_en_240x240_lcd_height) * sizeof(uint16_t));
esp_lcd_panel_draw_bitmap(panel_handle, 0, 0, logo_en_240x240_lcd_width, logo_en_240x240_lcd_height, (uint16_t *)pixels);
heap_caps_free(pixels);
}
2
3
4
5
6
7
8
9
10
11
12
这个函数显示的图片是乐鑫 logo,240X240 像素大小,图片数据位于 logo_en_240x240_lcd.h 文件中,这个.h 文件就在 who_lcd.c 文件的旁边,我们把这个文件复制到我们例程的 main 文件夹下。
函数中使用了 memcpy 函数,需要包含 string.h 头文件。
#include <string.h>
这个函数现在只能显示它指定的 logo 图片,现在我们把它修改为可以显示任意图片。
首先,把函数名称修改为 lcd_draw_pictrue。
然后,给函数增加 5 个参数,分别是图片的左上角 xy 坐标和图片的右下角 xy 坐标,以及图片指针。
最后,把函数内部的关于坐标的位置,以及长宽的大小都计算出来代入。
修改后的函数如下所示:
// 显示图片
void lcd_draw_pictrue(int x_start, int y_start, int x_end, int y_end, const unsigned char *gImage)
{
// 分配内存 分配了需要的字节大小 且指定在外部SPIRAM中分配
size_t pixels_byte_size = (x_end - x_start)*(y_end - y_start) * 2;
uint16_t *pixels = (uint16_t *)heap_caps_malloc(pixels_byte_size, MALLOC_CAP_8BIT | MALLOC_CAP_SPIRAM);
if (NULL == pixels)
{
ESP_LOGE(TAG, "Memory for bitmap is not enough");
return;
}
memcpy(pixels, gImage, pixels_byte_size); // 把图片数据拷贝到内存
esp_lcd_panel_draw_bitmap(panel_handle, x_start, y_start, x_end, y_end, (uint16_t *)pixels); // 显示整张图片数据
heap_caps_free(pixels); // 释放内存
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
全屏显示函数代码为:
void app_lcd_set_color(int color)
{
uint16_t *buffer = (uint16_t *)malloc(BSP_LCD_H_RES * sizeof(uint16_t));
if (NULL == buffer)
{
ESP_LOGE(TAG, "Memory for bitmap is not enough");
}
else
{
for (size_t i = 0; i < BSP_LCD_H_RES; i++)
{
buffer[i] = color;
}
for (int y = 0; y < BSP_LCD_V_RES; y++)
{
esp_lcd_panel_draw_bitmap(panel_handle, 0, y, BSP_LCD_H_RES, y+1, buffer);
}
free(buffer);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
把入口参数的 int 改成 uint16_t,因为函数里面要把它赋值给 uint16_t 数组,这里严谨一点。
把函数名称由 app_lcd_set_color 修改为 lcd_set_color。
这里使用的内存分配函数是 malloc,我们把它修改为使用 heap_caps_malloc 函数,参考图片显示函数中的语句就可以。
修改好以后的设置整屏颜色的函数为:
// 设置液晶屏颜色
void lcd_set_color(uint16_t color)
{
// 分配内存 这里分配了液晶屏一行数据需要的大小
uint16_t *buffer = (uint16_t *)heap_caps_malloc(BSP_LCD_H_RES * sizeof(uint16_t), MALLOC_CAP_8BIT | MALLOC_CAP_SPIRAM);
if (NULL == buffer)
{
ESP_LOGE(TAG, "Memory for bitmap is not enough");
}
else
{
for (size_t i = 0; i < BSP_LCD_H_RES; i++) // 给缓存中放入颜色数据
{
buffer[i] = color;
}
for (int y = 0; y < 240; y++) // 显示整屏颜色
{
esp_lcd_panel_draw_bitmap(panel_handle, 0, y, 320, y+1, buffer);
}
free(buffer); // 释放内存
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
接下来,我们再写一个用于主函数中调用的 lcd 初始化函数,放到 esp32_s3_szp.c 文件中,如下所示:
// LCD显示初始化
esp_err_t bsp_lcd_init(void)
{
esp_err_t ret = ESP_OK;
ret = bsp_display_new(); // 液晶屏驱动初始化
lcd_set_color(0x0000); // 设置整屏背景黑色
ret = esp_lcd_panel_disp_on_off(panel_handle, true); // 打开液晶屏显示
ret = bsp_display_backlight_on(); // 打开背光显示
return ret;
}
2
3
4
5
6
7
8
9
10
11
12
13
然后把这个函数以及前面的两个显示颜色和图片的函数声明到 esp32_s3_szp.h 文件中。
esp_err_t bsp_lcd_init(void);
void lcd_set_color(uint16_t color);
void lcd_draw_pictrue(int x_start, int y_start, int x_end, int y_end, const unsigned char *gImage);
2
3
现在给 mian.c 中添加程序。
void app_main(void)
{
bsp_i2c_init(); // I2C初始化
pca9557_init(); // IO扩展芯片初始化
bsp_lcd_init(); // 液晶屏初始化
//lcd_draw_pictrue(0, 0, 240, 240, logo_en_240x240_lcd); // 显示乐鑫logo图片
lcd_draw_pictrue(0, 0, 320, 240, gImage_yingwu); // 显示3只鹦鹉图片
}
2
3
4
5
6
7
8
上面显示图片函数,一个用来显示乐鑫 logo 图片,一个用来显示 3 只鹦鹉图片,你想显示哪个,就打开一个注释另外一个。
需要在 main.c 文件的前面,调用鹦鹉图片和乐鑫 logo 图片的头文件。
#include "yingwu.h"
#include "logo_en_240x240_lcd.h"
2
如果显示 3 只鹦鹉而没有显示乐鑫 logo,编译时会有如下警告提示:
warning: 'logo_en_240x240_lcd_height' defined but not used [-Wunused-variable]
warning: 'logo_en_240x240_lcd_width' defined but not used [-Wunused-variable]
2
这是因为在 logo_en_240x240_lcd.h 文件前面,定义了 logo_en_240x240_lcd_height 和 logo_en_240x240_lcd_width 两个变量,但是没有用到。这里我们进入文件把这两个变量定义删除,就可以去掉这个警告了。
到现在为止,就可以编译试试了,正常情况下,编译是不会出现错误的。
编译之前,先设置目标芯片为 esp32s3,然后设置 menuconfig,把 flash 大小设置为 16MB,然后打开 PSRAM,因为程序中需要使用外部内存。
我们看上图中第 ③ 步这里,有两个选项,默认是 Quad,我们改成 Octal。Quad 是 4 线,Octal 是 8 线,因为我们的模组所使用的 ESP32-S3 芯片内部是 8 线 SPI,所以这里要选 Octal。第 ④ 步设置速度,默认 40M,我们改成 80M。
接下来就可以编译了,正常没有问题。如果你编译有错误,依据终端提示修改即可。
编译完成之后,可以下载到开发板,液晶屏会显示乐鑫 logo 图标,因为图片尺寸是 240240,而我们的液晶屏大小是 320240,所以还可以看到背景色是黑色。
背景颜色修改
lcd_set_color()
函数设置背景色,背景色是一个 16 位数,格式为 RGB565。
在计算机的各种画图软件中,颜色一般用 RGB888 表示,用 3 个字节表示颜色,顺序为红绿蓝,每个字节的大小用十进制表示,就是 0~255。例如,白色的表示就是(255,255,255),黑色的表示就是(0,0,0),纯红色(255,0,0),纯绿色(0,255,0),纯蓝色(0,0,255),其它颜色,都是改变这些数字的合成,例如黄绿色(154 205 50)。
我们想在液晶屏上显示自己的颜色,可以把 RGB888 转成 RGB565,转换原理是,R 取字节的前 5 位,G 取字节的前 6 位,B 取字节的前 5 位。例如,黄绿色(154 205 50),转换过程如下:
红:154,写成二进制为 1001 1010,取前 5 位为 10011
绿:205,写成二进制为 1100 1101,取前 6 位为 110011
蓝:50,写成二进制为 0011 0010,取前 5 位为 00110
把 RGB565 合起来,即 10011 110011 00110,4 位对齐显示 1001 1110 0110 0110,正好是 2 个字节,写成十六进制就是 0x9E66。
使用 lcd_set_color 函数显示颜色,需要将高低字节对调,即写成:
lcd_set_color(0x669E);
把 bsp_lcd_init()
函数中,lcd_set_color()
函数的参数,由原来的 0x0000 修改为 0x669E,再编译下载到开发板,可以看到它的颜色,黄绿色在电脑上的显示效果如下图所示,因为 RGB888 转换成 RGB565,所以肯定会有一些颜色损失,不过普通人基本上看不出来区别,大家可以对比一下。
自定义图片
如果想显示自己的图片,按照下面步骤实现。
准备一张你要显示的图片,把它搞成 320*240 大小,或者小于这个大小,常见的图片格式都可以。
一共需要两步,先制作图片的数组文件,再修改程序让图片显示。
制作图片数组文件,我们需要借助一款软件:Image2LCD(软件仅供交流学习用,自己去百度下载即可)。
遵循下面步骤,一定要先配置好参数,再打开图片,才能生成正确的数组数据。
如上图所示,左边设置:输出数据类型为“C 语言数组”,扫描方式为“水平扫描”,输出灰度选择“16 位真彩色”,最大宽度和高度设置为 320 和 240,底下勾选“高位在前”。
然后点击“打开”菜单,打开你要制作的图片,然后点击“保存”菜单,设置名称为“yingwu.h”,或者你自己起一个好听的,保存到我们例程的 main 文件夹下面。
在 VSCode 中点击打开这个文件,我们可以看到它的数组名称和数组大小,共 153600 个字节。
给数组定义的最前面加 static 关键字,修改后如下所示:
在 main.c 文件中包含 yingwu.h 头文件(你的名称不是 yingwu 的话,改成你自己的)。
#include "yingwu.h"
然后按照你自己图片的大小和名称,修改主函数中的 lcd_draw_pictrue()函数中的参数,就可以了。
修改后就可以编译下载看结果了,正常情况下,会看到液晶屏显示了你的图片。
最后使用 idf.py save-defconfig 命令生成 sdkconfig.defaults 文件,此文件保存了你在 menuconfig 中做的所有改动配置,不包含默认的配置。