前言:之前基本一直在搞STM32的开发,想换到别的平台(比如ESP32)试试,也算对自己的一种锻炼和提升,注意到网上关于LVGL移植的教程非常多,但是大部分是基于Arduino平台的,基本没有基于ESP-IDF框架的(即使有也已经过时)。最近入手了几块ESP32的开发板和一块带CTP(电容触摸)的3.5寸IPS屏幕,可视角度和分辨率都不错,作为一个对GUI界面有执着追求的人,我想把当下很火的嵌入式GUI项目LVGL - Light and Versatile Embedded Graphics Library移植到我的项目里,并实现一部分优化,让它即使在资源极端有限的情况下也能较为流畅地运行~

进度:

  • (已完成)实现正确显示 - 20240412
  • (已完成)跑demo、优化帧率 - 20240414
  • (未完成)基于SquareLine平台设计UI
  • (未完成)接入各种传感器

物料选择

  • ESP32S3开发板一块,比如我使用立创的ESP32-S3开发板(集成串口芯片)

    087b0e76d0dc9852b9dd31fb43531fa

  • 3.5寸电容触摸屏(显示驱动为ST7796U,电容触摸驱动为gt911)

    6dd6fbcc761ede3cc4c3065eba1070c

  • 杜邦线若干

  • Type-C数据线

环境配置

  • 电脑系统:Windows 11

  • 开发环境:Visual Studio Code(至于为什么不用CLion,因为CLion上没配起来IDF框架(抽象))、ESP-IDF_5.1.2

这里具体配置细节暂时略去,因为最近要重新装系统,到时候刚好重新配置一遍,写个教程补上 - 20240412

开工!

文件准备

  • 先找到示例文件夹,在IDF的安装目录下
    image-20240412150107877

  • 打开进入./get-started/,找到hello_world程序并复制出来

    image-20240412150249861

  • 再在./peripherals/i2c中找到i2c_tools,也和刚才的hello_world放在一起
    image-20240412150606976

  • 打开Visual Studio Code,选到hello_world目录

    image-20240412151819885

  • 这里配置一下开发板的信息:
    1. 插上开发板,点一下左下角的COM1,根据实际选择正确的COM
    1. 点左下角esp32,选择合适的esp芯片型号,比如我就选esp32s3
    1. 选择OpenOCD的配置文件,一般选择via ESP-PROG就好
    1. 点左下角的五角星,选择UART(我的开发板上带了TTL芯片)
    最终配置好应该是这样的:
    image-20240412152745759

  • 在main文件夹里新建一个idf_component.yml文件,并复制以下内容

    这个网站idf_component.yml的编写规则,这里我就直接用里面的框架了

1
2
dependencies:
pkg_name: "pkg_version"
  • 浏览器打开乐鑫官网,在SDKs下找到IDF Component Manager包管理器

    image-20240412153510715

    image-20240412154657218

  • IDF Component Manager中搜索LVGL,这次用的是lvglesp_lvgl_port,至于驱动为什么不用lvgl_esp32_drivers,是因为这玩意的代码已经很久没有维护了,不支持IDF5.x版本和较新的LVGL

    这次用的是1.4.0版本的esp_lvgl_port,在idf_component.yml中把pkg_name换成esp_lvgl_port,因为我们这次移植的是8.4.0LVGL,所以pkg_version改成1.4.0,把LVGLgt911触摸芯片ST7796显示芯片的驱动也加上,像下面这样

1
2
3
4
5
dependencies:
esp_lvgl_port: "1.4.0"
esp_lcd_touch_gt911: "1.1.0"
esp_lcd_st7796: "1.2.1"
lvgl/lvgl: "8.4.0"

然后编译一次就会自动下载相关依赖(这里即使最后编译报错了也没关系,因为依赖已经下好了,报错稍后再去解决)

待依赖下载完成之后,工作空间的根目录会出现managed_components文件夹,把这个文件夹改为components(这步是因为可能要修改某些依赖的源码,如果不这样的话修改后会报哈希不匹配的错),把里面的依赖整理一下,像这样image-20240412164748491

检查每个依赖中的其他关联依赖(idf_component.yml),把所有除了IDF以外的其他依赖全部删掉

  • ./esp_lvgl_port/examples/touchscreen/main/下的main.c文件的全部内容复制到hello_world_main.c里

  • 顺便说一下,如果VScode没法代码补全和代码跳转的话,按下Ctrl+Shift+P,搜索IntelliSense,选择第一个不带bootloader的,选好之后Full Clean,再点一下编译,就可以正常使用了

  • image-20240412180848835

  • image-20240412180922880

  • 编译一下,看看报错

    image-20240412165942792

    可以看到找不到对应的头文件,这里示例文件默认用的触摸驱动是tt21100,与我用的gt911不一样,所以我们要包含gt911的驱动进去,故要做的就是修改头文件,把tt21100的驱动换成gt911的,并且把程序里调用的函数进行对应的更改

    1
    #include "esp_lcd_touch_tt21100.h"

    更改为

    1
    #include "esp_lcd_touch_gt911.h"

    对应的函数改为gt911

    image-20240412181507168

    image-20240412181658868

    再次编译,看看报错

    image-20240412182013943

    注意到这里找不到lvgl.h的错误是esp_lvgl_port这个依赖发出的,所以进入该依赖的CmakeLists.txt,在PRIV_REQUIRES里加入lvgl

    image-20240412182143182

    再次编译,看看报错

    image-20240412182235878

    这里gt911的驱动报错找不到driver/gpio.h,与上面类似地,修改gt911的驱动里的CmakeLists.txt,在PRIV_REQUIRES里加入driver

    image-20240412182412458

    再编译,看看报错

    image-20240412182513212

    这里gt911的驱动报错找不到esp_lcd_touch.h,与上面类似地,修改gt911的驱动里的CmakeLists.txt,在PRIV_REQUIRES里加入esp_lcd_touch

    再次编译,发现可以编译通过了!(好吧,其实离能用还有很远)

    image-20240412182738026

    硬件连线

    查看开发板的原理图和乐鑫ESP32S3技术文档,应该按照下表接线

    屏幕+触摸 ESP32S3开发板
    VDD 3V3
    GND GND
    SCLK GPIO_NUM_12
    MOSI GPIO_NUM_11
    RST GPIO_NUM_3
    DC GPIO_NUM_9
    CS GPIO_NUM_10
    BL GPIO_NUM_46
    I2C_SCL GPIO_NUM_2
    I2C_SDA GPIO_NUM_1
    CTP_INT GPIO_NUM_45

注意:VDD和GND千万不能接错,否则烧毁!

接好线后烧录刚才的程序试一下,然后…咦?屏幕怎么没反应呢???

原来是接线没有在程序里映射出来,这里修改hello_world_main.c的第24-50行为,在不同硬件上需要做相应调整

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
/* LCD settings */
#define EXAMPLE_LCD_SPI_NUM (SPI3_HOST)
#define EXAMPLE_LCD_PIXEL_CLK_HZ (40 * 1000 * 1000)
#define EXAMPLE_LCD_CMD_BITS (8)
#define EXAMPLE_LCD_PARAM_BITS (8)
#define EXAMPLE_LCD_COLOR_SPACE (ESP_LCD_COLOR_SPACE_BGR)
#define EXAMPLE_LCD_BITS_PER_PIXEL (16)
#define EXAMPLE_LCD_DRAW_BUFF_DOUBLE (1)
#define EXAMPLE_LCD_DRAW_BUFF_HEIGHT (50)
#define EXAMPLE_LCD_BL_ON_LEVEL (1)

/* LCD pins */
#define EXAMPLE_LCD_GPIO_SCLK (GPIO_NUM_12)
#define EXAMPLE_LCD_GPIO_MOSI (GPIO_NUM_11)
#define EXAMPLE_LCD_GPIO_RST (GPIO_NUM_3)
#define EXAMPLE_LCD_GPIO_DC (GPIO_NUM_9)
#define EXAMPLE_LCD_GPIO_CS (GPIO_NUM_10)
#define EXAMPLE_LCD_GPIO_BL (GPIO_NUM_46)

/* Touch settings */
#define EXAMPLE_TOUCH_I2C_NUM (0)
#define EXAMPLE_TOUCH_I2C_CLK_HZ (400000)

/* LCD touch pins */
#define EXAMPLE_TOUCH_I2C_SCL (GPIO_NUM_2)
#define EXAMPLE_TOUCH_I2C_SDA (GPIO_NUM_1)
#define EXAMPLE_TOUCH_GPIO_INT (GPIO_NUM_45)

改完之后再次烧录,发现屏幕不断滚动报错

image-20240412185134048

看红字知道报错是因为I2C总线上没有找到gt911,这就比较奇怪了,难道是因为I2C地址错了吗?幸好乐鑫提供了一个专门的I2C检测工具,可以遍历所有I2C地址,还记得和hello_world工程一起复制过来的i2c_tools工程吗,现在需要用到它了

在VScode中打开i2c_tools工程,编译烧录,在终端中可以看到进入了i2c_tools

image-20240412193813115

输入i2cdetect即可遍历总线上的地址

image-20240412193850546

这里出现了0x14,当前挂载在I2C总线上的只有一个设备,所以gt911的地址是0x14,然后我们回到hello_world程序,打开/components/esp_lcd_touch_gt911/include/esp_lcd_touch_gt911.h

image-20240412194202971

可以看到定义了两个I2C地址,在结构体中调用了一个,这里看到调用的是ESP_LCD_TOUCH_IO_I2C_gt911_ADDRESS,值为0x5D,与实际地址0x14不符合,而ESP_LCD_TOUCH_IO_I2C_gt911_ADDRESS_BACKUP的值为0x14,所以结构体里改为ESP_LCD_TOUCH_IO_I2C_gt911_ADDRESS_BACKUP

image-20240412194432812

改代码

再次烧录,看看效果__结果还是不行,还是报I2C的错误,这里我们查看原理图,发现屏幕的I2C没有上拉__,所以我们需要打开芯片的内部上拉

image-20240412195427450

然后再次烧录,屏幕成功点亮,但是显示有点奇怪

0f0e9dbd753c2ea81331be530d4058f

现在发现:

  1. 长宽不对
  2. 字显示不清晰
  3. 屏幕镜像颠倒
  4. 触摸X、Y轴反向

现在开始逐一解决:

  1. 设置长宽,更改宏定义里的EXAMPLE_LCD_H_RES为320,EXAMPLE_LCD_V_RES为480
  2. 点左下角小齿轮,进入SDK Configuration editor,搜索Swap,找到这个选项,打上勾(改完记得保存)
    image-20240412200541312
  3. 这两个true随便改一个为false
    image-20240412202126956
  4. 把结构体里的mirror_x的1改为0,mirror_y的0改为1
    image-20240412204715167

然后再次烧录测试,发现颜色还是不对,背景是黑色的,但是理论上应该是白色c4ae5d37df49d5748cd673873cdaece

考虑到屏幕的面板是IPS的,而默认显示驱动可能支持的是TN屏,我这里选择魔改另外一款默认为IPS的显示驱动芯片(ili9341)的驱动程序,具体操作如下:

  1. 通过idf-component下载esp_lcd_ili9341并移动到components目录下,并修改esp_lcd_ili9341idf-component.yml

  2. 修改/esp_lcd_ili9341/esp_lcd_ili9341.cvendor_specific_init_default数组为(这里是参照屏幕厂商的驱动改的)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
     const ili9341_lcd_init_cmd_t vendor_specific_init_default[] = {
    // {cmd, { data }, data_size, delay_ms}
    /* Power contorl B, power control = 0, DC_ENA = 1 */
    // {0x11, (uint8_t []){0x00}, 0, 120},
    {0xB2, (uint8_t []){0x0C, 0x0C, 0x00, 0x33, 0x33}, 5, 0},
    {0xB0, (uint8_t []){0x00, 0xE0}, 2, 0},
    // {0x36, (uint8_t []){0x00}, 1, 0},
    // {0x3A, (uint8_t []){0x05}, 1, 0},
    {0xB7, (uint8_t []){0x56}, 1, 0},
    {0xBB, (uint8_t []){0x14}, 1, 0},
    {0xC0, (uint8_t []){0x2C}, 1, 0},
    {0xC2, (uint8_t []){0x01}, 1, 0},
    {0xC3, (uint8_t []){0x0B}, 1, 0},
    {0xC4, (uint8_t []){0x10}, 1, 0},
    {0xC6, (uint8_t []){0x0F}, 1, 0},
    {0xD0, (uint8_t []){0xA4, 0xA1}, 2, 0},
    {0xD6, (uint8_t []){0xA1}, 1, 0},
    {0xE0, (uint8_t []){0xD0, 0x08, 0x0A, 0x0D, 0x0B, 0x07, 0x21, 0x33, 0x39, 0x39, 0x16, 0x16, 0x1F, 0x3C}, 14, 0},
    {0xE1, (uint8_t []){0xD0, 0x00, 0x03, 0x01, 0x00, 0x10, 0x21, 0x32, 0x38, 0x16, 0x14, 0x14, 0x20, 0x3D}, 14, 0},
    {0x21, (uint8_t []){0x00}, 0, 120},
    {0x29, (uint8_t []){0x00}, 0, 1},
    {0x2C, (uint8_t []){0x00}, 0, 1},
    };

    至此驱动修改完毕,在hello_world_main.c中include一下esp_lcd_ili9341.h,把esp_lcd_new_panel_st7796函数改为esp_lcd_new_panel_ili9341,烧录看看效果

正常显示 - 20240412

可以看到,已经正确显示了(但其实旋转方向之后显示还是不太对,屏幕会镜像,不知道为什么,应该还是驱动的问题)61e5f02c1fdb5e1fd49e2a5a34a134b

跑demo - 20240414

Music音乐播放器demo

点左下角小齿轮,进入SDK Configuration editor,找到LVGL相关的设置

image-20240414141602770

Demos里面勾选Music player demo,别忘了保存

image-20240414141718058

然后回到hello_world_main.c里,找到函数app_main_display(),将里面的LabelButton注释掉,并在后面加上lv_demo_music函数的声明和调用

这里补充一点,C语言可以在函数里面声明函数,格式与在函数之外相同。 声明后的函数只在本函数内调用有效。其他函数需要重新声明

修改好后的app_main_display()函数如下

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
static void app_main_display(void)
{
lv_obj_t *scr = lv_scr_act();

/* Task lock */
lvgl_port_lock(0);

/* Your LVGL objects code here .... */

// /* Label */
// lv_obj_t *label = lv_label_create(scr);
// lv_label_set_recolor(label, true);
// lv_obj_set_width(label, EXAMPLE_LCD_H_RES);
// lv_obj_set_style_text_align(label, LV_TEXT_ALIGN_CENTER, 0);
// lv_label_set_text(label, "#FF0000 "LV_SYMBOL_BELL" Hello world Espressif and LVGL "LV_SYMBOL_BELL"#\n#FF9400 "LV_SYMBOL_WARNING" For simplier initialization, use BSP "LV_SYMBOL_WARNING" #");
// lv_obj_align(label, LV_ALIGN_CENTER, 0, -30);

// /* Button */
// lv_obj_t *btn = lv_btn_create(scr);
// label = lv_label_create(btn);
// lv_label_set_text_static(label, "Rotate screen");
// lv_obj_align(btn, LV_ALIGN_BOTTOM_MID, 0, -30);
// lv_obj_add_event_cb(btn, _app_button_cb, LV_EVENT_CLICKED, NULL);
void lv_demo_music(void);
lv_demo_music();

/* Task unlock */
lvgl_port_unlock();
}

我们编译试试,看会不会报错image-20240414142329954

还真报错了,这里说的是找不到需要的字体,点左下角小齿轮进入LVGL的配置界面,在Font usage里勾选上12和16号字体,记得保存

image-20240414142453070

再编译试试,可以看到已经编译过了image-20240414142923937

那就烧录看看效果吧(这里有可能触摸的X或者Y还会反,再去改tp_cfg结构体里的mirror_x或者mirror_y就行)0396dc12365b70b64f1728d4a349420

可以看到显示和触摸都正常了

跑组件(Widget)demo

优化帧率 - 20240414

打开乐鑫官网,点顶部的Support,在找到ESP-FAQ,搜索LVGL,即可找到如下内容

  • Taking ESP32-S3R8 as an example, the following ESP configuration items can improve the frame rate (ESP-IDF release/v5.1):

    • CONFIG_FREERTOS_HZ=1000
    • CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y
    • CONFIG_ESPTOOLPY_FLASHMODE_QIO=y
    • CONFIG_ESPTOOLPY_FLASHFREQ_120M=y [should be consistent with PSRAM]
    • CONFIG_SPIRAM_MODE_OCT=y
    • CONFIG_IDF_EXPERIMENTAL_FEATURES=y and CONFIG_SPIRAM_SPEED_120M=y [should be consistent with FLASH]
    • CONFIG_SPIRAM_FETCH_INSTRUCTIONS=y
    • CONFIG_SPIRAM_RODATA=y
    • CONFIG_ESP32S3_DATA_CACHE_LINE_64B=y
    • CONFIG_COMPILER_OPTIMIZATION_PERF=y
  • The following LVGL configuration items can improve the frame rate (LVGL v8.3):

    • #define LV_MEM_CUSTOM 1 or CONFIG_LV_MEM_CUSTOM=y

    • #define LV_MEMCPY_MEMSET_STD 1 or CONFIG_LV_MEMCPY_MEMSET_STD=y

    • #define LV_ATTRIBUTE_FAST_MEM IRAM_ATTR or CONFIG_LV_ATTRIBUTE_FAST_MEM=y

再加一个LV_DISP_DEF_REFR_PERIOD由30改为10

注:如果要显示帧率和CPU占用,可以吧Show CPU usage and FPS count勾上

这里就按照官网的推荐来进行修改,点左下角小齿轮,分别修改上面的这些参数(修改完记得保存),其中某些参数可能找不到,这是由于CSDK的版本和使用的MCU型号不同导致的,不用深究

修改hello_world_main.c里的SPI频率到80M(实测这个影响帧率最为明显)

image-20240414145809827

改完以后再来编译烧录,发现帧率提升了很多,滑动较为丝滑了