1. 开发环境说明
开发板:esp32
显示器驱动:ST7796(SPI接口)
触摸屏驱动:GT911
esp-idf:4.4.7(vscode集成)
LVGL:8.3.11
2. 配置项目
2.1 创建项目
- 新建项目,选择事例hello world 作为模板,编译无问题
2.2 置入LVGL库
- 新建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
3. 编辑项目
3.1 初始配置
- main文件夹下将主文件改为main.c,Cmakelists文件中有关变量同时更改,否则编译检查会报错
- 插入esp32开发板,选择com口以及开发板型号
3.2 在menuconfig中配置lvgl
打开menuconfig查看lvgl设置是否同步,若没同步则重启vscode。调整设置如下:
3.2.1 Serial flasher config
- Flash SPI speed:80MHz
- Flash size:8M
3.2.2 Color settings
- 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]]
3.2.3 Memory settings
- Size of the memory used by `lv_mem_alloc` in kilobytes (>= 2kB):48
3.2.4 LVGL ESP Drivers
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
-
编译后报错,下一步进行调试报错。
3.3 调试报错
3.3.1 在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)
- 打开lvgl_helpers.h查看
#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
继续编译报错
3.3.2 在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为例查阅芯片技术手册:
esp32内部设计了4个SPI控制器:
SPI0:cache访问外部存储单元接口
SPI1:主机使用
SPI2:主从两用(使用带前缀HSPI的信号总线)
SPI3:主从两用(使用带前缀VSPI的信号总线)
这些引脚是可以重新映射的,所以我购买的开发板进行了混用,因此该处报错的解决方法比较简单。
简单粗暴的可将该数值定义为 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
3.4 修改LVGL配置文件
按照lvgl官方使用说明修改配置文件,移植示例项目。
官方文档说明:
https://lvgl.100ask.net/8.3/porting/project.html
3.4.1 修改lv_conf.h
以设置LVGL项目
-
lv_conf.h
文件说明:
该文件为lvgl做了如下配置:
- 颜色设置:如1位、8位RGB332、16位RGB565、32位ARGB8888
- 内存设置:使用内外置内存及大小等配置
- 其他设置:屏幕读写频率、心跳频率
- 功能配置:绘图、GPU、日志、断点等
- 编译器设置
- 字体的使用
- 文本设置
- 小部件使用
- 额外的组件:部件、主题、布局等
- 例子
- 演示使用
- 上述配置可以在menuconfig进行设置。这里仅将
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*/
3.4.2 确认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]]
3.4.3 修改lv_port_disp.c
以移植显示屏
a. 原理分析
要为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的部分变量引用在该文件中进行驱动初始化、明确分辨率、屏幕刷新等替代文件功能,即可实现显示屏的移植。具体操作如下:
b. 使能及包含更名
- 在components文件夹下新建
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"改名
c. 屏幕分辨率引用
#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" //新增
d. 定义缓冲区
缓存方式选择:双缓存
代码分析:
/* 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;
e. 定义刷屏方式
将缓冲区的内容复制到显示器,需要定义一个屏幕刷新函数。在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文件在后面编辑
f. 增加屏幕初始化函数
在静态函数里定义了一个屏幕初始化函数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();
}
g. 修改lv_port_disp.h
头文件
- 使能,将templ有关表述删除(文件名和LV_PORT_DISP_TEMPL_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*/
h. 编辑Cmakelists文件
因为在disp函数里调用了lvgl和lvgl_esp32_drivers里的函数,所以还要修改CMakeList
file(GLOB_RECURSE SOURCES ./*.c
)
idf_component_register(SRCS ${SOURCES}
INCLUDE_DIRS
.
REQUIRES lvgl
lvgl_esp32_drivers
)
3.4.4 移植触摸屏
a. 原理分析
要注册输入设备,必须初始化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);
注册触摸板输入设备
b. 使能及包含更名
将文件复制到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*/
c. 配置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()
启动触摸驱动。
因为屏幕启动时触摸驱动也启动了,所以这里可以不进行配置了。
d. 配置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"
e. 编译调试
- 编译报错
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();
编译完成。
f. 修正GT911驱动
编译不报错后,烧录到开发板后输出错误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()
函数一开头调用即可。
3.5 编写main文件
上述修改完成后删除build然后重新编译,未发现有报错。
下面进行main文件编写。
3.5.1 LVGL使用说明
详官方文档: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相关的任务。
3.5.2 初始化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()
,说明第二行函数初始化了显示和输入设备驱动。
3.5.3 设置定时器
参考官方文档: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
- 将
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库提供时钟信号。
3.5.4 定时器处理程序
要处理LVGL的任务,您需要在以下命令之一中定期调用lv_timer_handler()
:
1. main()函数的while(1)
2. 定时器周期性中断(优先级低于`lv_tick_inc()`)
3. 定期执行操作系统任务
while (1) {
vTaskDelay(pdMS_TO_TICKS(10));
lv_task_handler();
}
3.5.5 使用DEMO例程
a. 加入demo
- 打开menuconfig,在demos中开启例程,这里以music demo为例。
Demos
√ Music player demo
- 打开lvgl-demos文件夹下的
lv_demos.h
文件,当开启上述选项后将包含music/lv_demo_music.h
文件。打开该文件,将例程函数加入到主函数中,同时要包含头文件
#include lv_demos.h
...
lv_demo_music();
b. 在menuconfig中增加music demo的字号
编译发现报错
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)
3.5.6 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();
}
}
4. 烧录测试
编译烧录后屏幕和触摸操作正常,串口监视器输出正常。
在这里插入图片描述
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