向ESP32-S3移植LVGL并测试性能
乐鑫最新的ESP32-S3芯片发布了,正好一直有做一个信息牌的想法,ESP32-S3就成了最好的选择。本文介绍用于测试的硬件,以及如何基于ESP32-S3移植LVGL并测试运行效果。
ESP32-S3提供了专用的LCD控制器,可以控制8080并口与16位RGB接口的LCD面板。由于ESP32-S3的内部RAM容量实在是有限,放不下完整的全屏缓冲区,并且PSRAM的带宽非常有限,因此最终还是选用了16位8080接口的LCD。
为了测试,我先设计了一块基于ESP32-S3-WROOM-1
模组的PCB。PCB采用四层板结构,PCB上除了模组之外主要有USB接口、DC-DC、蜂鸣器、按键以及屏幕排线接口。屏幕选用的是40Pin的480*320
标准16位并口屏,这种屏幕有各种主控芯片与面板的组合可供选择,灵活性较大。
软件方面,自然还是使用老朋友ESP-IDF。随着测试版4.4的发布,还带来了一个新的模块ESP-LCD
。这个模块总共分成了3层,分别是总线层、屏幕设备层与屏幕面板层,内部封装了各种总线的LCD的总线驱动,使用起来非常方便。
驱动方面,我并没有使用ESP-LCD提供的第三层屏幕面板层,而是在第二层的基础上直接进行开发。下面是ESP-LCD的总线层与屏幕设备层的初始化代码,该代码接收总线频率、单次传输最大数据量与传输完成回调函数为参数,返回屏幕面板的句柄。
esp_lcd_panel_io_handle_t lcd_i80_bus_io_init(uint16_t pclk_mhz, uint16_t transfer_size, esp_lcd_panel_io_color_trans_done_cb_t trans_done_cb)
{
/* 初始化背光 */
lcd_bl_init();
/* 初始化8080总线:16位数据,DC与WR脚 */
esp_lcd_i80_bus_handle_t i80_bus = NULL;
esp_lcd_i80_bus_config_t bus_config = {
.dc_gpio_num = BSP_LCD_DC_PIN,
.wr_gpio_num = BSP_LCD_WR_PIN, //DC与WR引脚
.data_gpio_nums = BSP_LCD_DATA_PINS,
.bus_width = 16, //总线宽度16位
.max_transfer_bytes = transfer_size, //缓冲区大小
};
esp_lcd_new_i80_bus(&bus_config, &i80_bus);
/* 初始化LCD面板,使用上述8080总线,并设置CS引脚以及总线频率 */
esp_lcd_panel_io_handle_t io_handle;
esp_lcd_panel_io_i80_config_t io_config = {
.cs_gpio_num = BSP_LCD_CS_PIN, //CS引脚
.pclk_hz = pclk_mhz*1000000, //总线时钟频率
.trans_queue_depth = 1,
.dc_levels = {
.dc_idle_level = 1,
.dc_cmd_level = 0,
.dc_dummy_level = 1,
.dc_data_level = 1,
},
.lcd_cmd_bits = 8,
.lcd_param_bits = 8, //指令与指令参数的长度
.on_color_trans_done = trans_done_cb,
};
esp_lcd_new_panel_io_i80(i80_bus, &io_config, &io_handle);
return io_handle;
}
由于40Pin接口是通用接口,有非常多型号的驱动IC与面板可选,同时对于最常见的ST7796
与ILI9488
,他们的关键指令(例如控制显存指针与写入的0x2A
0x2B
0x2C
等)都是通用的,因此理论上只需要更换初始化代码即可兼容不同的屏幕面板。
void lcd_init_reg(const esp_lcd_panel_io_handle_t io, const lcd_panel_reg_t reg_table[])
{
uint8_t i = 0;
while(reg_table[i].len != 0xFF) {
esp_lcd_panel_io_tx_param(io, reg_table[i].reg, reg_table[i].val, reg_table[i].len);
i ++;
}
}
为了最大化提升兼容性,我将屏幕初始化所使用的寄存器数据封装为了结构体,在初始化的时候传入对应屏幕的结构体,即可实现对不同面板的初始化。下面是一个面板的初始化寄存器表的示例:
const lcd_panel_reg_t panel_st7796s_w350ce024a_40z_reg_table[] = {
{ 0x11, NULL, 0 }, //退出休眠模式
{ 0x21, NULL, 0 }, //开启反色显示
{ 0x35, (uint8_t[]){0x00}, 1 }, //开启TE信号输出,信号只包括垂直同步信息
{ 0x36, (uint8_t[]){0x48}, 1 }, //显存访问方式: MY=0, MX=1, MV=0, ML=0, RGB=1, MH=0
{ 0x3A, (uint8_t[]){0x55}, 1 }, //像素颜色格式: RGB模式16位, MCU模式16位
{ 0xF0, (uint8_t[]){0xC3}, 1 }, //使能指令集2第1部分
{ 0xF0, (uint8_t[]){0x96}, 1 }, //使能指令集2第2部分
{ 0xB4, (uint8_t[]){0x01}, 1 }, //显示反转控制
{ 0xB7, (uint8_t[]){0xC6}, 1 }, //Entry Mode设置
{ 0xC0, (uint8_t[]){0x80, 0x64}, 2 }, //AVDD&AVCL&VGH&VGL设置
{ 0xC1, (uint8_t[]){0x13}, 1 }, //GVDD&GVCL设置
{ 0xC2, (uint8_t[]){0xA7}, 1 }, //源极&gamma驱动电流设置
{ 0xC5, (uint8_t[]){0x08}, 1 }, //VCOM设置
{ 0xE0, (uint8_t[]){0xF0, 0x06, 0x0B, 0x07, 0x06, 0x05, 0x2E, 0x33, 0x47, 0x3A, 0x17, 0x16, 0x2E, 0x31}, 14 }, //正gamma控制
{ 0xE1, (uint8_t[]){0xF0, 0x09, 0x0D, 0x09, 0x08, 0x23, 0x2E, 0x33, 0x46, 0x38, 0x13, 0x13, 0x2C, 0x32}, 14 }, //负gamma控制
{ 0xE8, (uint8_t[]){0x40, 0x8A, 0x00, 0x00, 0x29, 0x19, 0xA5, 0x33}, 8 }, //显示参数配置
{ 0xF0, (uint8_t[]){0x3C}, 1 }, //失能指令集2第1部分
{ 0xF0, (uint8_t[]){0x69}, 1 }, //失能指令集2第2部分
{ 0, NULL, 0xFF } //寄存器列表结束
};
与此同时,我还在Kconfig
中设置了菜单,方便用户快速切换对应的屏幕面板型号与总线频率。
然后,在程序中包含sdkconfig.h
,并对宏进行判断即可:
/* 初始化寄存器 */
#if defined(CONFIG_LVGL_LCD_PANEL_W350CE024A_40Z)
lcd_init_reg(panel_io, panel_st7796s_w350ce024a_40z_reg_table);
#elif defined(CONFIG_LVGL_LCD_PANEL_CL35BC1017_40A)
lcd_init_reg(panel_io, panel_st7796s_cl35bc1017_40a_reg_table);
#elif defined(CONFIG_LVGL_LCD_PANEL_CL35BC106_40A)
lcd_init_reg(panel_io, panel_ili9488_cl35bc106_40a_reg_table);
#elif defined(CONFIG_LVGL_LCD_PANEL_LHC03540)
lcd_init_reg(panel_io, panel_ili9481_lhc03540_reg_table);
#elif defined(CONFIG_LVGL_LCD_PANEL_CUSTOM)
lcd_init_reg(panel_io, panel_custom_reg_table);
#endif
有了初始化函数与矩形写入函数之后,移植LVGL便变得很简单了。ESP-LCD内部是使用DMA传输的,通过前面总线初始化函数中传入的回调函数,结合FreeRTOS
的信号量与双缓冲,即可在传输过程中开始渲染下一个缓冲区,并在渲染结束后交出CPU使用权,直到发送操作执行完毕。
static void disp_flush(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p)
{
/* 启动新的传输 */
lcd_draw_rect(disp_drv->user_data, area->x1, area->y1, area->x2, area->y2, color_p);
}
static void disp_wait(lv_disp_drv_t *disp_drv)
{
/* 等待传输完成 */
xSemaphoreTake(trans_done_semphr, portMAX_DELAY);
/* 通知lvgl传输已完成 */
lv_disp_flush_ready(disp_drv);
}
static bool lcd_trans_done_cb(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_io_event_data_t *edata, void *user_ctx)
{
BaseType_t woken = false;
xSemaphoreGiveFromISR(trans_done_semphr, &woken);
return woken;
}
关于程序中其他的细节部分,请参考文末的完整工程链接。请使用esp-idf 4.4
以上的版本进行编译,因为ESP-LCD组件是自这个版本引入的。
为了测试ESP32-S3运行LVGL的性能,我使用了LVGL官方提供的例程lv_demo_music
进行测试。
将整个仓库拉到工程的components/lvgl_demo
目录下,并在此目录下新建一个CMakeList.txt
文件,加入下列内容
file(GLOB_RECURSE SOURCES lv_demos/*.c)
idf_component_register(SRCS ${SOURCES}
INCLUDE_DIRS lv_demos/
REQUIRES lvgl)
将所有文件注册为工程的组件,然后修改components/lvgl_demo/lv_demos/lv_demo_conf.h
,打开宏LV_USE_DEMO_MUSIC
与LV_DEMO_MUSIC_AUTO_PLAY
,保存之后在主函数中调用lv_demo_music()
即可。
测试过程中手动将CPU的频率设置为了240Mhz
,并将Flash设置为了80Mhz
QIO
。同时,因为ESP32-S3新增了Cache大小自定义功能,我将ICache设置为32k
,DCache设置为64k
。
测试效果如视频所示。LVGL给出的帧率为33fps,相比初代ESP32的22fps有了很大的进步。提升的主要原因是翻倍的Cache带来的巨大提升,以及LX7
内核相比LX6
内核的IPC提升。