赞
踩
开发板:esp32
显示器驱动:ST7796(SPI接口)
触摸屏驱动:GT911
esp-idf:4.4.7(vscode集成)
LVGL:8.3.11
新建component文件夹
下载lvgl 8.3.11和lvgl esp32 drivers文件,并解压至component文件夹下,改名去掉版本号。
下载地址
LVGL:
https://github.com/lvgl/lvgl/releases/tag/v8.3.11
LVGL esp32 drivers:
https://github.com/lvgl/lvgl_esp32_drivers
main文件夹下将主文件改为main.c,Cmakelists文件中有关变量同时更改,否则编译检查会报错
插入esp32开发板,选择com口以及开发板型号
打开menuconfig查看lvgl设置是否同步,若没同步则重启vscode。调整设置如下:
- Flash SPI speed:80MHz
- Flash size:8M
- Color depth:16:RGB565
- √ Swap the 2 bytes of RGB565 color. Useful if the display has an 8-bit interface (e.g. SPI).
“交换RGB565颜色的2个字节。如果显示器具有8位接口,则很有用”。该处2位RGB565设置原因详见[[2. 案例2:esp32移植LVGL#^3313c5]]
- Size of the memory used by `lv_mem_alloc` in kilobytes (>= 2kB):48
LVGL TFT Display controller:
- Display orientation:Portrait inverted(竖屏显示,根据显示情况调整)
- Select a display controller model:ST7796S(依据显示屏驱动选择型号,若无型号则需要自己手动移植)
- TFT SPI Bus:SPI2_HOST(选择TFT显示器连接到的SPI总线)
- TFT Data Transfer Mode:DIO (为TFT显示选择SPI SIO/DIO/QIO传输模式)
- √ Use custom SPI clock frequency.
- Select a custom frequency:40MHz
按照接线设置显示屏GPIO接口信息:
Display Pin Assignments:
- GPIO for MOSI (Master Out Slave In):19
- 取消勾选 GPIO for MISO (Master In Slave Out)
- GPIO for CLK (SCK / Serial Clock):23
- GPIO for CS (Slave Select):22
- GPIO for DC (Data / Command):14
- GPIO for Reset:12
- √ Is backlight turn on with a HIGH (1) logic level?(重要)
- GPIO for Backlight Control:2
-
设置触摸板信息:
LVGL Touch controller:
- Select a touch panel controller model.:GT911
- Touchpanel Configuration (GT911)
- 取消勾选 Swap X with Y coordinate.
- √ Invert X coordinate value.
- √ Invert Y coordinate value.
- Select an I2C port for the touch panel:I2C port0
- I2C Port 0
- √ Enable I2C port 0
- SDA (GPIO pin):18
- SCL (GPIO pin):16
- Frequency (Hz):100000
-
编译后报错,下一步进行调试报错。
lvgl_helpers.h
中增加显示屏分辨率参数^879332
H:/ESP32/ESP_IDF_develop_env/3_Fit_LVGL/components/lvgl_esp32_drivers/lvgl_helpers.h:57:25: error: 'LV_HOR_RES_MAX' undeclared (first use in this function); did you mean 'LV_HOR_RES'?
#define DISP_BUF_SIZE (LV_HOR_RES_MAX * 40)
#if defined (CONFIG_CUSTOM_DISPLAY_BUFFER_SIZE)
#define DISP_BUF_SIZE CONFIG_CUSTOM_DISPLAY_BUFFER_BYTES
#else
#if defined (CONFIG_LV_TFT_DISPLAY_CONTROLLER_ST7789)
#define DISP_BUF_SIZE (LV_HOR_RES_MAX * 40)
#elif defined CONFIG_LV_TFT_DISPLAY_CONTROLLER_ST7735S
#define DISP_BUF_SIZE (LV_HOR_RES_MAX * 40)
#elif defined CONFIG_LV_TFT_DISPLAY_CONTROLLER_ST7796S
#define DISP_BUF_SIZE (LV_HOR_RES_MAX * 40)
#elif defined CONFIG_LV_TFT_DISPLAY_CONTROLLER_HX8357
#define DISP_BUF_SIZE (LV_HOR_RES_MAX * 40)
...
其上部说明文档:
/* DISP_BUF_SIZE value doesn't have an special meaning, but it's the size * of the buffer(s) passed to LVGL as display buffers. The default values used * were the values working for the contributor of the display controller. * * As LVGL supports partial display updates the DISP_BUF_SIZE doesn't * necessarily need to be equal to the display size. * * When using RGB displays the display buffer size will also depends on the * color format being used, for RGB565 each pixel needs 2 bytes. * When using the mono theme, the display pixels can be represented in one bit, * so the buffer size can be divided by 8, e.g. see SSD1306 display size. * * DISP_BUF_SIZE值没有特殊的含义,但它是作为显示缓冲区传递给LVGL的缓冲区的大小。使用的默认值为显示控制器的贡献者工作的值。 * 由于LVGL支持部分显示更新,所以DISP_BUF_SIZE不一定需要等于显示大小。 * 当使用RGB显示时,显示缓冲区的大小也取决于所使用的颜色格式,对于RGB565,每个像素需要2字节。 * 当使用单声道主题时,显示像素可以用一位表示,因此缓冲区大小可以除以8,例如参见SSD1306显示大小。
^3313c5
这说明此段代码定义了一个名为缓冲区大小DISP_BUF_SIZE的值,该值因不同显示驱动取自不同值,但都与液晶屏水平长度LV_HOR_RES_MAX有关。因该值未设置导致报错。
解决方法:增加显示屏分辨率变量数值到此段代码前,代码如下:
//配置显示屏分辨率:
#ifndef LV_HOR_RES_MAX
#define LV_HOR_RES_MAX (320)
#endif
#ifndef LV_VER_RES_MAX
#define LV_VER_RES_MAX (480)
#endif
继续编译报错
lvgl_helpers.c
中明确SPI_Host报错信息
H:/ESP32/ESP_IDF_develop_env/3_Fit_LVGL/components/lvgl_esp32_drivers/lvgl_helpers.h:57:25: error: 'LV_HOR_RES_MAX' undeclared (first use in this function); did you mean 'LV_HOR_RES'?
#define DISP_BUF_SIZE (LV_HOR_RES_MAX * 40)
H:/ESP32/ESP_IDF_develop_env/3_Fit_LVGL/components/lvgl_esp32_drivers/lvgl_helpers.c:157:28: error: 'SPI_HOST_MAX' undeclared (first use in this function); did you mean 'GPIO_PORT_MAX'?
assert((0 <= host) && (SPI_HOST_MAX > host));
段代码所在位置如下:
bool lvgl_spi_driver_init(int host, int miso_pin, int mosi_pin, int sclk_pin, int max_transfer_sz, int dma_channel, int quadwp_pin, int quadhd_pin) { assert((0 <= host) && (SPI_HOST_MAX > host)); const char *spi_names[] = { "SPI1_HOST", "SPI2_HOST", "SPI3_HOST" }; ESP_LOGI(TAG, "Configuring SPI host %s", spi_names[host]); ESP_LOGI(TAG, "MISO pin: %d, MOSI pin: %d, SCLK pin: %d, IO2/WP pin: %d, IO3/HD pin: %d", miso_pin, mosi_pin, sclk_pin, quadwp_pin, quadhd_pin); ESP_LOGI(TAG, "Max transfer size: %d (bytes)", max_transfer_sz); spi_bus_config_t buscfg = { .miso_io_num = miso_pin, .mosi_io_num = mosi_pin, .sclk_io_num = sclk_pin, .quadwp_io_num = quadwp_pin, .quadhd_io_num = quadhd_pin, .max_transfer_sz = max_transfer_sz }; ESP_LOGI(TAG, "Initializing SPI bus..."); #if defined (CONFIG_IDF_TARGET_ESP32C3) dma_channel = SPI_DMA_CH_AUTO; #endif esp_err_t ret = spi_bus_initialize(host, &buscfg, (spi_dma_chan_t)dma_channel); assert(ret == ESP_OK); return ESP_OK != ret; }
该段代码定义了一个bool类型的lvgl_spi_driver_init()
函数用于初始化一个ESP32上的SPI总线。其中assert((0 <= host) && (SPI_HOST_MAX > host));
用于检查host
是否在有效范围内。
esp32内部设计了4个SPI控制器:
SPI0:cache访问外部存储单元接口
SPI1:主机使用
SPI2:主从两用(使用带前缀HSPI的信号总线)
SPI3:主从两用(使用带前缀VSPI的信号总线)
这篇博文对GPIO引脚进行了较好的解释说明:
https://blog.csdn.net/chentuo2000/article/details/128269394?spm=1001.2014.3001.5506
这些引脚是可以重新映射的,所以我购买的开发板进行了混用,因此该处报错的解决方法比较简单。
简单粗暴的可将该数值定义为 3 :
#define SPI_HOST_MAX (3)
实际上,不同esp32芯片初始化总线方式不同,该段代码
assert((0 <= host) && (SPI_HOST_MAX > host));
const char *spi_names[] = {
"SPI1_HOST", "SPI2_HOST", "SPI3_HOST"
};
可被修改为:
#if defined(CONFIG_IDF_TARGET_ESP32) assert((SPI_HOST <= host) && (VSPI_HOST >= host)); const char *spi_names[] = { "SPI_HOST", "HSPI_HOST", "VSPI_HOST"}; dma_channel = SPI_DMA_CH_AUTO; #elif defined(CONFIG_IDF_TARGET_ESP32S2) assert((SPI_HOST <= host) && (HSPI_HOST >= host)); const char *spi_names[] = { "SPI_HOST", "", ""}; dma_channel = SPI_DMA_CH_AUTO; #elif defined(CONFIG_IDF_TARGET_ESP32C3) assert((SPI1_HOST <= host) && (SPI3_HOST >= host)); const char *spi_names[] = { "SPI1_HOST", "SPI2_HOST", "SPI3_HOST"}; dma_channel = SPI_DMA_CH_AUTO; /* SPI_DMA_CH_AUTO */; #elif defined(CONFIG_IDF_TARGET_ESP32S3) assert((SPI1_HOST <= host) && (SPI3_HOST >= host)); const char *spi_names[] = { "SPI_HOST", "HSPI_HOST", "VSPI_HOST"}; dma_channel = SPI_DMA_CH_AUTO; #else #error "Target chip not selected" #endif
按照lvgl官方使用说明修改配置文件,移植示例项目。
官方文档说明:
https://lvgl.100ask.net/8.3/porting/project.html
lv_conf.h
以设置LVGL项目lv_conf.h
文件说明:该文件为lvgl做了如下配置:
- 颜色设置:如1位、8位RGB332、16位RGB565、32位ARGB8888
- 内存设置:使用内外置内存及大小等配置
- 其他设置:屏幕读写频率、心跳频率
- 功能配置:绘图、GPU、日志、断点等
- 编译器设置
- 字体的使用
- 文本设置
- 小部件使用
- 额外的组件:部件、主题、布局等
- 例子
- 演示使用
lv_conf_template.h
复制并重命名为lv_conf.h
文件,打开后查看说明:Copy this file as `lv_conf.h` * 1. simply next to the `lvgl` folder * 2. or any other places and * - define `LV_CONF_INCLUDE_SIMPLE` * - add the path as include path 将此文件复制为' lv_conf.h ' 1. 复制在“lvgl”文件夹旁边 2. 复制到其他地方时要进行如下调整: - 定义“LV_CONF_INCLUDE_SIMPLE” - 添加路径作为包含路径 修改后文件夹布局如下: |-lvgl |-lv_conf.h |-other files and folders
修改0为1以启用其内容。
#if 1 /*Set it to "1" to enable content*/
lvgl_helpers.h
以适配显示驱动打开lvgl_esp32_drivers
文件夹下的lvgl_spi_conf.h
文件,查看驱动文件有关配置。
lvgl_spi_conf.h
文件说明:该文件为lvgl做了如下驱动配置:
- 定义:显示引脚、触摸引脚、检测共享SPI总线
- 显示屏驱动型号(不同型号的if配置)
- 触摸驱动配置:触摸频率及SPI模式
- 其他宏等
上述配置可以在menuconfig进行设置。
该处仅确认显示器分辨率有关参数是否设置该处设置体现在了lvgl_helpers.h
中。详见[[2. 案例2:esp32移植LVGL#^879332]]
lv_port_disp.c
以移植显示屏要为LVGL注册显示,必须初始化lv_disp_draw_buf_t
和lv_disp_drv_t
变量。
- `lv_disp_draw_buf_t`包含称为绘制缓冲区的内部图形缓冲区。
- `lv_disp_drv_t`包含与显示器交互和操作低级绘图行为的回调函数。
该变量全局搜索后发现在lvgl-examples-porting
文件夹下的lv_port_disp_template.c
文件进行了上述变量定义,同时使用说明要求使能。
另外,也可以按照官方说明自行编写显示代码。总体来讲,需要如下步骤:
1. lv_disp_draw_buf_t变量初始化:
1. 定义用于存储缓冲区的静态或全局变量
static lv_disp_draw_buf_t disp_buf;
1. 静态或全局缓冲区。第二个缓冲区是可选的
static lv_color_t buf_1[MY_DISP_HOR_RES * 10];
static lv_color_t buf_2[MY_DISP_HOR_RES * 10];
2. 使用缓冲区初始化“disp_buf”
lv_disp_draw_buf_init(&disp_buf, buf_1, buf_2, MY_DISP_HOR_RES*10);
2. `lv_disp_drv_t`显示驱动程序(该部分在drivers已经配置完成,可以直接调用):
1. 用`lv_disp_drv_init(&disp_drv)`
2. 其字段需要设置(详官方文档)
3. 它需要在LVGL中注册`lv_disp_drv_register(&disp_drv)`
当然也可以使用lv_port_disp
移植。
lv_port_disp.c
文件说明:该文件进行了显示移植:
- void lv_port_disp_init(void)
- 初始化显示:disp_init();
- 创建一个用于绘图的缓冲区:分3类,解释如下
- 在LVGL中注册显示:
- 设置显示器的分辨率(已在drivers配置)
- 设置显示缓冲区模式
- 将缓冲区内容复制到显示器(屏幕刷新,drivers已配置)
- 注册驱动程序(已在drivers配置)
缓冲区解释如下(管网使用说明)https://lvgl.100ask.net/8.3/porting/display.html:
- 一个缓冲区
如果只使用一个缓冲区,LVGL将屏幕的内容绘制到该绘制缓冲区并将其发送到显示器。LVGL需要等到缓冲区的内容发送到显示器后再在其中绘制新内容。
- 两个缓冲区
如果使用两个缓冲区,LVGL可以绘制到一个缓冲区中,而另一个缓冲区的内容在后台发送到显示器。应该使用DMA或其他硬件将数据传输到显示器,以便MCU可以继续绘制。这样,显示器的渲染和刷新就变成了并行操作。
- 完全刷新
在显示驱动程序(`lv_disp_drv_t`)中,启用`full_refresh`位将迫使LVGL始终重绘整个屏幕。这适用于_一个缓冲区_和_两个缓冲区_模式。 如果启用`full_refresh`并提供两个屏幕大小的绘制缓冲区,LVGL的显示处理工作方式类似于“传统”双缓冲。 这意味着`flush_cb`回调只需要更新帧缓冲区的地址(`color_p`参数)。 如果MCU有LCD控制器外设,而不是通过串行链路访问的外部显示控制器(例如ILI9341或SSD1963),则应使用此配置。后者通常太慢,无法在全屏重绘时保持高帧速率。
- 另外,还有直接模式(略)
因此,该文件提供了一个实际可用的显示移植代码。仅需将绘图缓存部分进行适配使用,同时将drivers的部分变量引用在该文件中进行驱动初始化、明确分辨率、屏幕刷新等替代文件功能,即可实现显示屏的移植。具体操作如下:
lv_porting
文件夹,将.c和.h文件复制其中并改名为lv_port_disp.c
和lv_port_disp.h
。#if 1
将0改为1
#include "lv_port_disp.h"
将#include "lv_port_disp_template.h"改名
#ifndef MY_DISP_HOR_RES
#define MY_DISP_HOR_RES LV_HOR_RES_MAX
#endif
#ifndef MY_DISP_VER_RES
#define MY_DISP_VER_RES LV_VER_RES_MAX
#endif
注意到,该变量在文件中有两处引用,一处为缓冲区大小,一处为设置屏幕旋转。这里保留是在该处设置屏幕旋转。
这里要包含lvgl_helpers.h头文件。Cmakelists文件在后面编辑
#include "lv_port_disp.h"
#include <stdbool.h>
#include "lvgl_helpers.h" //新增
缓存方式选择:双缓存
代码分析:
/* Example for 2) */
static lv_disp_draw_buf_t draw_buf_dsc_2;
static lv_color_t buf_2_1[MY_DISP_HOR_RES * 10];
static lv_color_t buf_2_2[MY_DISP_HOR_RES * 10];
lv_disp_draw_buf_init(&draw_buf_dsc_2, buf_2_1, buf_2_2, MY_DISP_HOR_RES * 10);
这段代码定义了两个用于双缓冲绘图的缓冲区buf_2_1
和buf_2_2
,并初始化lv_disp_draw_buf_init()
了一个绘图缓冲区描述符draw_buf_dsc_2
来描述这两个缓冲区。
同时,将一个屏幕水平变量赋值给缓冲区buf_2_*
以定义缓冲区的大小。该处代码使用的尺寸为屏幕宽度*10
,而不同显示驱动支持的缓存区大小不同,需要依据驱动型号适配缓冲区大小:
查阅lvgl_helpers.h
文件,这里定义了一个DISP_BUF_SIZE
值,代表显示缓冲区传递给LVGL的缓冲区的大小。同时对不同驱动进行了适配:
#if defined (CONFIG_LV_TFT_DISPLAY_CONTROLLER_ST7789)
#define DISP_BUF_SIZE (LV_HOR_RES_MAX * 40)
#elif defined CONFIG_LV_TFT_DISPLAY_CONTROLLER_ST7735S
#define DISP_BUF_SIZE (LV_HOR_RES_MAX * 40)
#elif defined CONFIG_LV_TFT_DISPLAY_CONTROLLER_ST7796S
#define DISP_BUF_SIZE (LV_HOR_RES_MAX * 40)
...
因此,这里将中括号内的变量替换为DISP_BUF_SIZE
即可。修改后的代码如下:
/* Example for 2) */
static lv_disp_draw_buf_t draw_buf_dsc_2;
static lv_color_t buf_2_1[DISP_BUF_SIZE];
static lv_color_t buf_2_2[DISP_BUF_SIZE];
lv_disp_draw_buf_init(&draw_buf_dsc_2, buf_2_1, buf_2_2, DISP_BUF_SIZE);
这里要包含lvgl_helpers.h头文件。Cmakelists文件在后面编辑
其余注释掉或者删除(避免warning)
// /* Example for 1) */ // ... /* Example for 2) */ static lv_disp_draw_buf_t draw_buf_dsc_2; static lv_color_t buf_2_1[DISP_BUF_SIZE]; static lv_color_t buf_2_2[DISP_BUF_SIZE]; lv_disp_draw_buf_init(&draw_buf_dsc_2, buf_2_1, buf_2_2, DISP_BUF_SIZE); // /* Example for 3) // / ... ... /*Set a display buffer*/ disp_drv.draw_buf = &draw_buf_dsc_2;
将缓冲区的内容复制到显示器,需要定义一个屏幕刷新函数。在lv_port_disp.c
文件中定义了一个刷屏函数,其代码为:
/*Used to copy the buffer's content to the display*/ disp_drv.flush_cb = disp_flush; ... //定义刷屏函数 static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p) { if(disp_flush_enabled) { int32_t x; int32_t y; for(y = area->y1; y <= area->y2; y++) { for(x = area->x1; x <= area->x2; x++) { color_p++; } } } lv_disp_flush_ready(disp_drv); }
带段代码定义了一个名为disp_flush()
的函数,该函数传入了指向显示驱动器的指针、指向一个矩形区域的指针、指向颜色数组的指针。同时,从x=0/y=0
坐标处逐个像素刷新。该函数刷新效率最低。
查阅disp_driver.c
文件定义了disp_driver_flush()
函数,并且根据不同驱动有不同的刷屏方式。例如,显示屏驱动文件st7796s.c
也进行了刷新函数定义
void st7796s_flush(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_map) { uint8_t data[4]; /*Column addresses*/ st7796s_send_cmd(0x2A); //ST7796S_CMD_COLUMN_ADDRESS_SET data[0] = (area->x1 >> 8) & 0xFF; data[1] = area->x1 & 0xFF; data[2] = (area->x2 >> 8) & 0xFF; data[3] = area->x2 & 0xFF; st7796s_send_data(data, 4); /*Page addresses*/ st7796s_send_cmd(0x2B); //ST7796S_CMD_PAGE_ADDRESS_SET data[0] = (area->y1 >> 8) & 0xFF; data[1] = area->y1 & 0xFF; data[2] = (area->y2 >> 8) & 0xFF; data[3] = area->y2 & 0xFF; st7796s_send_data(data, 4); /*Memory write*/ st7796s_send_cmd(0x2C); //ST7796S_CMD_MEMORY_WRITE uint32_t size = lv_area_get_width(area) * lv_area_get_height(area); st7796s_send_color((void *)color_map, size * 2); }
该函数通过一个数组实现刷新指定的区域。这种方式可以显著提高效率,因此将该函数赋值给显示器刷屏函数并注释掉原函数即可。
/*Used to copy the buffer's content to the display*/
disp_drv.flush_cb = disp_driver_flush;
//注释掉静态变量声明
//static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p);
//注释掉函数
//void st7796s_flush(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_map)
//{
//}
这里要包含disp_driver.h头文件。Cmakelists文件在后面编辑
在静态函数里定义了一个屏幕初始化函数disp_init()
里面为空,目的是初始化显示器和所需的外围设备,需要自行添加。这里将驱动启动函数添加在内
查阅lvgl_helpers.c
文件,这里定义了一个lvgl_driver_init()
变量,其作用如下:
接口和驱动程序初始化lvgl_driver_init():
1. 打印屏幕分辨率;
2. if初始化FT81X的SPI主机和触摸(略);
3. if初始化共享SPI主机;
4. 显示控制器初始化
1. 初始化SPI和I2C总线:lvgl_spi_driver_init()
2. 初始化显示屏驱动:disp_driver_init()
5. 触摸控制器初始化
1. 初始化SPI和I2C总线:lvgl_spi_driver_init()
2. 初始化触摸驱动:touch_driver_init()
其中:
1. 初始化显示屏驱动在disp_driver.c
文件中定义;
2. 初始化触摸驱动touch_driver.c
文件中定义。
所以这里仅将lvgl_driver_init()
置入就可以了。修改后为
static void disp_init(void)
{
lvgl_driver_init();
}
lv_port_disp.h
头文件/*Copy this file as "lv_port_disp.h" and set this value to "1" to enable content*/
#if 1
#ifndef LV_PORT_DISP_H
#define LV_PORT_DISP_H
...
#endif /*LV_PORT_DISP_H*/
因为在disp函数里调用了lvgl和lvgl_esp32_drivers里的函数,所以还要修改CMakeList
file(GLOB_RECURSE SOURCES ./*.c
)
idf_component_register(SRCS ${SOURCES}
INCLUDE_DIRS
.
REQUIRES lvgl
lvgl_esp32_drivers
)
要注册输入设备,必须初始化lv_indev_drv_t
变量。注意:在注册任何输入设备之前,请务必注册至少一个显示器。
官方事例,初始化输入驱动,设置输入设备驱动的类型和读取回调函数,然后注册到lvgl中:
lv_disp_drv_register(&disp_drv); 注册输入设备前要注册显示器,3.4.3已完成 static lv_indev_drv_t indev_drv; lv_indev_drv_init(&indev_drv); indev_drv.type =... 该`type`成员可以是: - `LV_INDEV_TYPE_POINTER`触摸板或鼠标 - `LV_INDEV_TYPE_KEYPAD`键盘或小键盘 - `LV_INDEV_TYPE_ENCODER`编码器 - `LV_INDEV_TYPE_BUTTON`外部按钮虚拟地按压屏幕 indev_drv.read_cb =... `read_cb`是一个函数指针,它将被定期调用以报告输入设备的当前状态。 lv_indev_t * my_indev = lv_indev_drv_register(&indev_drv); 在LVGL中注册驱动程序并保存创建的输入设备对象
其中,对于触摸板设备配置参考如下,这里提供了一个触摸按下的read函数,用于读取触摸输入设备的状态并填充到给定的数据结构中:
indev_drv.type = LV_INDEV_TYPE_POINTER; indev_drv.read_cb = my_input_read; //上一段代码有关变量定义为触摸板参数 ... void my_input_read(lv_indev_drv_t * drv, lv_indev_data_t*data) { if(touchpad_pressed) { data->point.x = touchpad_x; data->point.y = touchpad_y; data->state = LV_INDEV_STATE_PRESSED; } else { data->state = LV_INDEV_STATE_RELEASED; } }
在lvgl-examples-porting
文件夹下的lv_port_indev_template.c
文件包含了上述操作,这里仅分析触摸屏有关代码,其结构如下,:
void lv_port_indev_init(void)
{
static lv_indev_drv_t indev_drv;
touchpad_init();
初始化触摸板,注意:这里是空函数,需要配置
lv_indev_drv_init(&indev_drv);
设置输入类型和读取参数
indev_drv.type = LV_INDEV_TYPE_POINTER;
indev_drv.read_cb = touchpad_read;
indev_touchpad = lv_indev_drv_register(&indev_drv);
注册触摸板输入设备
将文件复制到lv_porting
文件夹下并改名为lv_port_indev.c
和lv_port_indev.h
。
lv_port_indev.c
文件修改:#if 1
将0改为1
#include "#include "lv_port_indev.h"
将#include "lv_port_indev_template.h"改名
lv_port_indev.h
文件修改:使能,将templ有关表述删除(文件名和_TEMPL_H变量
#if 1
#ifndef LV_PORT_INDEV_H
#define LV_PORT_INDEV_H
...
#endif /*LV_PORT_INDEV_H*/
touchpad_init();
在lvgl_helpers.c
文件中,当执行屏幕初始化函数调用到lvgl_driver_init()
函数时,已经进行了屏幕和触摸的初始化。
关于触摸初始化调用函数如下:
#if CONFIG_LV_TOUCH_CONTROLLER != TOUCH_CONTROLLER_NONE #if defined (CONFIG_LV_TOUCH_DRIVER_PROTOCOL_SPI) ESP_LOGI(TAG, "Initializing SPI master for touch"); lvgl_spi_driver_init(TOUCH_SPI_HOST, TP_SPI_MISO, TP_SPI_MOSI, TP_SPI_CLK, 0 /* Defaults to 4094 */, 2, -1, -1); tp_spi_add_device(TOUCH_SPI_HOST); touch_driver_init(); #elif defined (CONFIG_LV_I2C_TOUCH) touch_driver_init(); #elif defined (CONFIG_LV_TOUCH_DRIVER_ADC) touch_driver_init(); #elif defined (CONFIG_LV_TOUCH_DRIVER_DISPLAY) touch_driver_init(); #else #error "No protocol defined for touch controller" #endif #else #endif
该函数进行了一些判断,不同接口进行不同方式的启动,最后调用touch_driver_init()
启动触摸驱动。
因为屏幕启动时触摸驱动也启动了,所以这里可以不进行配置了。
read()
回调函数在touch_driver.c
函数中定义了有关read()
函数如下:
#if LVGL_VERSION_MAJOR >= 8 void touch_driver_read(lv_indev_drv_t *drv, lv_indev_data_t *data) #else bool touch_driver_read(lv_indev_drv_t *drv, lv_indev_data_t *data) #endif { bool res = false; #if defined (CONFIG_LV_TOUCH_CONTROLLER_XPT2046) res = xpt2046_read(drv, data); #elif defined (CONFIG_LV_TOUCH_CONTROLLER_FT6X06) res = ft6x36_read(drv, data); #elif defined (CONFIG_LV_TOUCH_CONTROLLER_STMPE610) res = stmpe610_read(drv, data); #elif defined (CONFIG_LV_TOUCH_CONTROLLER_ADCRAW) res = adcraw_read(drv, data); #elif defined (CONFIG_LV_TOUCH_CONTROLLER_FT81X) res = FT81x_read(drv, data); #elif defined (CONFIG_LV_TOUCH_CONTROLLER_RA8875) res = ra8875_touch_read(drv, data); #elif defined (CONFIG_LV_TOUCH_CONTROLLER_GT911) res = gt911_read(drv, data); #endif #if LVGL_VERSION_MAJOR >= 8 data->continue_reading = res; #else return res; #endif }
这里将touch_driver_read
放入回调函数即可
/*Register a touchpad input device注册触摸板输入设备*/
lv_indev_drv_init(&indev_drv);
indev_drv.type = LV_INDEV_TYPE_POINTER;
indev_drv.read_cb = touch_driver_read; //这里
indev_touchpad = lv_indev_drv_register(&indev_drv);
其余函数可以通过 if 0 关闭掉。
注意包含头文件
#include "touch_driver.h"
H:/ESP32/ESP_IDF_develop_env/4_LVGL_Touch_Port/components/lv_porting/lv_port_indev.c:13:10: fatal error: ../../lvgl.h: No such file or directory
#include "../../lvgl.h"
此处lvgl的头文件引用错误,修改为
#include "lvgl/lvgl.h"
H:/ESP32/ESP_IDF_develop_env/4_LVGL_Touch_Port/components/lv_porting/lv_port_indev.c: In function 'lv_port_indev_init':
....
H:/ESP32/ESP_IDF_develop_env/4_LVGL_Touch_Port/components/lv_porting/lv_port_indev.c:61:16: warning: 'encoder_diff' defined but not used [-Wunused-variable]
static int32_t encoder_diff;
这里是以下几个函数未使用,编译时题型warning。将有关函数和定义注释掉即可
touchpad_init();
touchpad_read();
touchpad_is_pressed();
touchpad_get_xy();
编译完成。
编译不报错后,烧录到开发板后输出错误log:
...
I (969) lvgl_i2c: Initialised port 0 (SDA: 18, SCL: 16, speed: 100000 Hz.)
W (979) lvgl_i2c: Error: -1
E (979) GT911: Error reading from device: ERROR
I (1019) lvgl_i2c: Initialised port 0 (SDA: 18, SCL: 16, speed: 100000 Hz.)
I (1029) GT911: Product ID:
查阅出处为gt911.c
驱动代码:
void gt911_init(uint8_t dev_addr) {
if (!gt911_status.inited) {
gt911_status.i2c_dev_addr = dev_addr;
uint8_t data_buf;
esp_err_t ret;
ESP_LOGI(TAG, "Checking for GT911 Touch Controller");
if ((ret = gt911_i2c_read(dev_addr, GT911_PRODUCT_ID1, &data_buf, 1) != ESP_OK)) {
ESP_LOGE(TAG, "Error reading from device: %s",
esp_err_to_name(ret)); // Only show error the first time
return;
}
...
说明没有读取到触摸屏型号,因为GPIO没有完成初始化。因此,LVGL给出的GT911驱动没有进行初始化上电操作,要按照官方手册进行移植。
参考GT911编程指南,GT9 系列在通信中始终作为从设备,其 I2C 设备地址由 7 位设备地址加 1 位读写控制位组成,为高 7 位为地址, bit 0 为读写控制位。GT9 系列有两个从设备地址可供选择,每次上电或复位时需要使用 INT 脚进行 I2C 地址设置。如下表:
GT911的IIC从设备地址有两组,分别为0x28/0x29和0xBA和0xBB。不同设定地址的上电时序不同:
这里官方驱动声明了#define GT911_I2C_SLAVE_ADDR 0x5D
地址,经过高七位的换算,应该为第二种上电时序0xBA/0xBB
。这里按照上电时序编写复位驱动。驱动板设计接口为RTS=IO4, INT=IO17
,所以封装GT911_RST()
函数进行复位:
void GT911_RST() { // RTS IO4 and INT IO17 //设置4号和17号引脚为输出模式 gpio_set_direction( GPIO_NUM_4, GPIO_MODE_OUTPUT); gpio_set_direction( GPIO_NUM_17, GPIO_MODE_OUTPUT); //低电平复位 gpio_set_level(4, 0); gpio_set_level(17, 0); //延迟150毫秒,这里为避免时序过短,增加50毫秒 vTaskDelay(pdMS_TO_TICKS(15)); //RTS拉高 gpio_set_level(4, 1); //延迟5ms vTaskDelay(pdMS_TO_TICKS(50)); //将INT转为悬浮输入态 gpio_set_direction(17, (GPIO_MODE_INPUT)| (GPIO_MODE_DEF_OD));
注意,这里的有关函数调用和变量出自gpio.c
和gpio_types.h
。引脚模式变量如下:
typedef enum {
GPIO_MODE_DISABLE = GPIO_MODE_DEF_DISABLE, /*!< GPIO mode : disable input and output */
GPIO_MODE_INPUT = GPIO_MODE_DEF_INPUT, /*!< GPIO mode : input only */
GPIO_MODE_OUTPUT = GPIO_MODE_DEF_OUTPUT, /*!< GPIO mode : output only mode */
GPIO_MODE_OUTPUT_OD = ((GPIO_MODE_DEF_OUTPUT) | (GPIO_MODE_DEF_OD)), /*!< GPIO mode : output only with open-drain mode */
GPIO_MODE_INPUT_OUTPUT_OD = ((GPIO_MODE_DEF_INPUT) | (GPIO_MODE_DEF_OUTPUT) | (GPIO_MODE_DEF_OD)), /*!< GPIO mode : output and input with open-drain mode*/
GPIO_MODE_INPUT_OUTPUT = ((GPIO_MODE_DEF_INPUT) | (GPIO_MODE_DEF_OUTPUT)), /*!< GPIO mode : output and input mode */
} gpio_mode_t;
当然也可以设定为另一个地址,总体封装函数如下:
// 触摸复位操作以便于设定IIC地址 void GT911_RST() { // 设置4号RTS和17号INT引脚为输出模式 gpio_set_direction(GPIO_NUM_4, GPIO_MODE_OUTPUT); gpio_set_direction(GPIO_NUM_17, GPIO_MODE_OUTPUT); // 低电平复位 gpio_set_level(4, 0); gpio_set_level(17, 0); if (GT911_I2C_SLAVE_ADDR == 0x5D) { vTaskDelay(pdMS_TO_TICKS(15)); gpio_set_level(4, 1); // RTS拉高 vTaskDelay(pdMS_TO_TICKS(50)); gpio_set_direction(17, (GPIO_MODE_INPUT) | (GPIO_MODE_DEF_OD)); // 将INT转为悬浮输入态 } else if (GT911_I2C_SLAVE_ADDR == 0x14) { vTaskDelay(pdMS_TO_TICKS(5)); gpio_set_level(17, 1); vTaskDelay(pdMS_TO_TICKS(10)); gpio_set_level(4, 1);// RTS拉高 vTaskDelay(pdMS_TO_TICKS(50)); gpio_set_direction(17, (GPIO_MODE_INPUT) | (GPIO_MODE_DEF_OD)); // 将INT转为悬浮输入态 } }
将该函数放在gt911_init()
函数一开头调用即可。
上述修改完成后删除build然后重新编译,未发现有报错。
下面进行main文件编写。
详官方文档:https://lvgl.100ask.net/8.3/porting/project.html#initialization
要使用图形库,您必须对其进行初始化并设置所需的组件。初始化的顺序是:
1. 调用`lv_init()`。
2. 初始化您的驱动程序。
3. 在LVGL中注册显示和输入设备驱动程序。
4. 在中断中每隔`x`毫秒调用`lv_tick_inc(x)`以向LVGL报告经过的时间。
5. 每隔几毫秒调用`lv_timer_handler()`来处理与LVGL相关的任务。
首先,为确保lvgl以及有关设备能被调用,在main.c
中包含必要的头文件:
#include "lvgl.h"
#include "lv_port_disp.h"
#include "lv_port_indev.h"
同时,按照初始化顺序的1-3项,在main()
函数中添加初始化函数:
lv_init(); // 初始化lvgl
lv_port_disp_init(); // 初始化显示器
lv_port_indev_init(); // 初始化触摸屏
其中前面说到,lv_port_disp_init()
函数调用到disp_init()
即lvgl_driver_init()
和lv_disp_drv_register()
,说明第二行函数初始化了显示和输入设备驱动。
参考官方文档:https://lvgl.100ask.net/8.3/porting/tick.html
LVGL需要一个系统刻度来知道动画和其他任务的经过时间。所以要周期性调用lv_tick_inc(tick_period)
函数,并以毫秒为单位提供调用周期。例如,每毫秒调用一次时lv_tick_inc(1)
。
lv_tick_inc
应该在比lv_task_handler()
更高优先级的例程中调用(例如在中断中)以精确知道经过的毫秒,即使执行lv_task_handler
需要更多时间。
使用FreeRTOSlv_tick_inc
可以在vApplicationTickHook
中调用。注意,这里lv_tick_inc()
函数属于硬件层面心跳,不建议使用freeRTOS中的esp_register_freertos_tick_hook(lv_tick_task)
函数替代。在FreeRTOS的每个系统心跳中断执行可能会受到FreeRTOS系统滴答频率的限制,并且可能不是最精确的方法。
当不使用操作系统的实时时钟功能(如RTOS的tick中断)来自动调用 lv_task_handler 时,您可能需要自己实现一个定时器来定期调用 lv_tick_inc。
这里参考学习lvgl官方在github上面移植esp32案例库中的有关代码。https://github.com/lvgl/lv_port_esp32/blob/master/main/main.c
lv_tick_inc
进行函数封装static void lv_tick_task(void *arg) {
(void) arg;
lv_tick_inc(LV_TICK_PERIOD_MS);
}
其中LV_TICK_PERIOD_MS
要进行变量声明,此处设置为10ms
#define LV_TICK_PERIOD_MS 10
const esp_timer_create_args_t periodic_timer_args = {
.callback = &lv_tick_task,
.name = "periodic_gui"};
esp_timer_handle_t periodic_timer;
ESP_ERROR_CHECK(esp_timer_create(&periodic_timer_args, &periodic_timer));
ESP_ERROR_CHECK(esp_timer_start_periodic(periodic_timer, LV_TICK_PERIOD_MS * 1000));
上述代码表示创建一个ESP-IDF的硬件定时器,用于定期调用lv_tick_task函数,这里每10ms调用一次。从而为LVGL GUI库提供时钟信号。
要处理LVGL的任务,您需要在以下命令之一中定期调用lv_timer_handler()
:
1. main()函数的while(1)
2. 定时器周期性中断(优先级低于`lv_tick_inc()`)
3. 定期执行操作系统任务
while (1) {
vTaskDelay(pdMS_TO_TICKS(10));
lv_task_handler();
}
Demos
√ Music player demo
lv_demos.h
文件,当开启上述选项后将包含music/lv_demo_music.h
文件。打开该文件,将例程函数加入到主函数中,同时要包含头文件#include lv_demos.h
...
lv_demo_music();
编译发现报错
H:/ESP32/ESP_IDF_develop_env/3_Fit_LVGL/components/lvgl/demos/music/lv_demo_music_list.c: In function '_lv_demo_music_list_create':
H:/ESP32/ESP_IDF_develop_env/3_Fit_LVGL/components/lvgl/demos/music/lv_demo_music_list.c:59:19: error: 'lv_font_montserrat_12' undeclared (first use in this function); did you mean 'lv_font_montserrat_14'?
font_small = &lv_font_montserrat_12;
^~~~~~~~~~~~~~~~~~~~~
lv_font_montserrat_14
H:/ESP32/ESP_IDF_develop_env/3_Fit_LVGL/components/lvgl/demos/music/lv_demo_music_list.c:59:19: note: each undeclared identifier is reported only
once for each function it appears in
H:/ESP32/ESP_IDF_develop_env/3_Fit_LVGL/components/lvgl/demos/music/lv_demo_music_list.c:60:20: error: 'lv_font_montserrat_16' undeclared (first use in this function); did you mean 'lv_font_montserrat_14'?
font_medium = &lv_font_montserrat_16;
这是music demo 使用了12/14/16号字体,需要在menuconfig中修改。
修改后编译成功
PS-对于其它lvgl版本,在编译时会报错找不到lv_demo_music()路径,需要编辑Cmake使识别。报错案例:
H:/ESP32/ESP_IDF_develop_env/3_Fit_LVGL/main/main.c:56:5: error: implicit declaration of function 'lv_demo_music'; did you mean 'lv_mem_test'? [-Werror=implicit-function-declaration]
lv_demo_music();
存在demo事例未加入引用的情况,处理方式如下:
解决此方案可以使用多种方法:
1. 原位编辑lvgl中esp cmake使其包含demo;
2. 将demo文件复制到其它路径下并在主函数引用,并挨个包含。
打开lvgl-env_support-cmake-esp.cmake文件进行如下修改,添加demos路径下的.c文件并包含demos文件夹:
file(GLOB_RECURSE SOURCES
${LVGL_ROOT_DIR}/src/*.c
${LVGL_ROOT_DIR}/demos/*.c
)
和
if(LV_MICROPYTHON)
idf_component_register(
SRCS
${SOURCES}
INCLUDE_DIRS
${LVGL_ROOT_DIR}
${LVGL_ROOT_DIR}/src
${LVGL_ROOT_DIR}/../
${LVGL_ROOT_DIR}/demos
REQUIRES
main)
else()
...
和
idf_component_register(SRCS ${SOURCES} ${EXAMPLE_SOURCES} ${DEMO_SOURCES}
INCLUDE_DIRS ${LVGL_ROOT_DIR} ${LVGL_ROOT_DIR}/src ${LVGL_ROOT_DIR}/../
${LVGL_ROOT_DIR}/examples ${LVGL_ROOT_DIR}/demos
REQUIRES esp_timer)
main.c
案例代码#include <stdio.h> #include "sdkconfig.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "esp_system.h" #include "esp_spi_flash.h" // LVGL有关移植引用 #include "lvgl.h" #include "lv_port_disp.h" #include "lv_port_indev.h" #include "lv_demos.h" /********************* * DEFINES *********************/ // 定义一个心跳周期为10ms #define LV_TICK_PERIOD_MS 10 /********************** * STATIC PROTOTYPES **********************/ static void lv_tick_task(void *arg); /********************** * STATIC FUNCTIONS **********************/ /* 将定时器lv_tick_inc() 函数封装为lv_tick_task(),为LVGL提供模拟时钟信号。 * LVGL需要时钟信号来更新其内部状态,如动画等*/ static void lv_tick_task(void *arg) { (void)arg; lv_tick_inc(LV_TICK_PERIOD_MS); } /********************** * APPLICATION MAIN **********************/ // 主函数入口 void app_main(void) { printf("Hello world!\n"); lv_init(); // 初始化lvgl printf("LVGL完成初始化!\n"); lv_port_disp_init(); // 初始化显示器 printf("显示屏完成初始化!\n"); lv_port_indev_init(); // 初始化触摸屏 printf("触摸完成初始化!\n"); /* 创建并启动一个周期性的定时器中断以调用lv_tick_inc */ const esp_timer_create_args_t periodic_timer_args = { .callback = &lv_tick_task, // 定时器触发回调函数 .name = "periodic_gui"}; // 定时器名称 esp_timer_handle_t periodic_timer; // 定义句柄以引用 ESP_ERROR_CHECK(esp_timer_create(&periodic_timer_args, &periodic_timer)); // 创建定时器 printf("定时器创建完成!\n"); ESP_ERROR_CHECK(esp_timer_start_periodic(periodic_timer, LV_TICK_PERIOD_MS * 1000)); // 启动周期性定时器,每10ms调用一次 printf("周期性定时器启动,周期为10ms!\n"); // ESP_ERROR_CHECK 是一个宏,检查ESP-IDF API调用的错误,并在出现错误时停止程序 // 使用lvgl music的demo事例 lv_demo_music(); printf("加载music demo完成!\n"); /*调用 lv_task_handler 来处理LVGL的异步任务,并使用 vTaskDelay 延时10毫秒,这样不会阻塞CPU,同时允许LVGL和其他任务有机会运行*/ while (1) { /* 延迟1个刻度(假设FreeRTOS刻度为10ms) */ vTaskDelay(pdMS_TO_TICKS(10)); lv_task_handler(); } }
编译烧录后屏幕和触摸操作正常,串口监视器输出正常。
在这里插入图片描述
I (27) boot: ESP-IDF GIT-NOTFOUND 2nd stage bootloader I (27) boot: compile time 14:47:27 I (27) boot: Multicore bootloader I (31) boot: chip revision: v3.1 I (35) boot.esp32: SPI Speed : 80MHz I (40) boot.esp32: SPI Mode : DIO I (44) boot.esp32: SPI Flash Size : 8MB I (49) boot: Enabling RNG early entropy source... I (54) boot: Partition Table: I (58) boot: ## Label Usage Type ST Offset Length I (65) boot: 0 nvs WiFi data 01 02 00009000 00006000 I (72) boot: 1 phy_init RF data 01 01 0000f000 00001000 I (80) boot: 2 factory factory app 00 00 00010000 00100000 I (87) boot: End of partition table I (92) esp_image: segment 0: paddr=00010020 vaddr=3f400020 size=711b0h (463280) map I (240) esp_image: segment 1: paddr=000811d8 vaddr=3ffb0000 size=01ef4h ( 7924) load I (243) esp_image: segment 2: paddr=000830d4 vaddr=40080000 size=0cf44h ( 53060) load I (264) esp_image: segment 3: paddr=00090020 vaddr=400d0020 size=4c9b8h (313784) map I (359) esp_image: segment 4: paddr=000dc9e0 vaddr=4008cf44 size=01dd0h ( 7632) load I (369) boot: Loaded app from partition at offset 0x10000 I (370) boot: Disabling RNG early entropy source... I (382) cpu_start: Multicore app I (382) cpu_start: Pro cpu up. I (382) cpu_start: Starting app cpu, entry point is 0x400812d4 0x400812d4: call_start_cpu1 at C:/Users/l/esp/v4.4.7/esp-idf/v4.4.7/esp-idf/components/esp_system/port/cpu_start.c:151 I (0) cpu_start: App cpu up. I (402) cpu_start: Pro cpu start user code I (402) cpu_start: cpu freq: 160000000 I (402) cpu_start: Application information: I (407) cpu_start: Project name: hello_world I (412) cpu_start: App version: 1 I (417) cpu_start: Compile time: May 16 2024 15:34:15 I (423) cpu_start: ELF file SHA256: 4310bf1bfc5772fc... I (429) cpu_start: ESP-IDF: GIT-NOTFOUND I (434) cpu_start: Min chip rev: v0.0 I (439) cpu_start: Max chip rev: v3.99 I (443) cpu_start: Chip rev: v3.1 I (448) heap_init: Initializing. RAM available for dynamic allocation: I (455) heap_init: At 3FFAE6E0 len 00001920 (6 KiB): DRAM I (461) heap_init: At 3FFCB6C8 len 00014938 (82 KiB): DRAM I (468) heap_init: At 3FFE0440 len 00003AE0 (14 KiB): D/IRAM I (474) heap_init: At 3FFE4350 len 0001BCB0 (111 KiB): D/IRAM I (480) heap_init: At 4008ED14 len 000112EC (68 KiB): IRAM I (487) spi_flash: detected chip: gd I (491) spi_flash: flash io: dio I (496) cpu_start: Starting scheduler on PRO CPU. I (0) cpu_start: Starting scheduler on APP CPU. Hello world! LVGL完成初始化! I (509) lvgl_helpers: Display buffer size: 12800 I (519) lvgl_helpers: Initializing SPI master for display I (519) lvgl_helpers: Configuring SPI host SPI2_HOST I (529) lvgl_helpers: MISO pin: -1, MOSI pin: 19, SCLK pin: 23, IO2/WP pin: -1, IO3/HD pin: -1 I (539) lvgl_helpers: Max transfer size: 25600 (bytes) I (549) lvgl_helpers: Initializing SPI bus... I (549) disp_spi: Adding SPI device I (549) disp_spi: Clock speed: 40000000Hz, mode: 0, CS pin: 22 I (759) ST7796S: Initialization. I (959) ST7796S: Display orientation: PORTRAIT_INVERTED I (959) ST7796S: 0x36 command value: 0x88 I (959) disp_backlight: Setting LCD backlight: 100% I (1019) GT911: Checking for GT911 Touch Controller I (1019) lvgl_i2c: Starting I2C master at port 0. I (1019) lvgl_i2c: Initialised port 0 (SDA: 18, SCL: 16, speed: 100000 Hz.) I (1029) GT911: Product ID: 911 I (1029) GT911: Vendor ID: 0x00 I (1029) GT911: X Resolution: 320 I (1039) GT911: Y Resolution: 480 gt911触摸驱动初始化! 显示屏完成初始化! 触摸完成初始化! 定时器创建完成! 周期性定时器启动,周期为10ms! 加载music demo完成! I (10589) GT911: X=22 Y=57 I (10619) GT911: X=22 Y=57
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。