本文记录以platformio为开发环境,esp32-arduino为框架下ESP32及ESP32-C3上LVGL的移植、调试与帧率优化。
硬件说明
ESP32-S核心板
ESP32-C3核心板
SPI接口TFT屏幕(2.8寸320*240,带电阻屏,ST7789驱动)
电阻屏驱动(NS2009)
屏幕显示部分所需信号:
SDA(SPI输入)
SDO(SPI输出,可选)
SCL (SPI时钟)
CSX (SPI片选,可直接拉高)
DC(数据/指令选择)
RST(复位)
屏幕触控部分所需信号:
XL、YU、XR、YD,构成4线电阻屏,原理见ref
ESP32
0.开发环境
platformio + arduino框架
新建ESP32-Arduino工程:
添加三个包:
lvgl-8.1.0
lvgl_examples(demo合集)
tft_eSPI(提供SPI屏驱动)
1.配置SPI屏驱动
编辑TFT_eSPI的配置文件:
项目路径/.pio/libdeps/esp32dev/TFT_eSPI/User_Setup.h:
根据注释的提示修改,对于本文的ST7789-TFT屏幕,修改项包括:
#define ST7789_DRIVER
//色彩
#define ESP32_DMA
#define TFT_RGB_ORDER TFT_BGR
#define TFT_INVERSION_OFF
//分辨率
#define TFT_WIDTH 240
#define TFT_HEIGHT 320
//管脚
#define TFT_MISO 19
#define TFT_MOSI 23
#define TFT_SCLK 18
// #define TFT_CS 15 // Chip select control pin
#define TFT_CS -1
#define TFT_DC 2 // Data Command control pin
#define TFT_RST 4 // Reset pin (could connect to RST pin)
//字体
#define SMOOTH_FONT
//SPI速率
#define SPI_FREQUENCY 40000000
#define SPI_READ_FREQUENCY 6000000
注:
- SPI读/写速率被限制在6MHz/40MHz,为ST7789驱动芯片的时序限制,ESP32硬件SPI接口可支持到80MHz。
- 关于I/O口的选择问题,高速SPI不可使用I/O映射,见Justice_Gao的博客。
- 由于不使用tft_eSPI的绘图函数,故关闭所有字库以节约ROM。
2.配置LVGL接口函数
ref: 项目路径.pio/libdeps/esp32dev/lvgl/examples/arduino/LVGL_Arduino/LVGL_Arduino.ino
核心是实现my_disp_flush,my_print和my_touchpad_read(可选)
项目路径/src/my_lv_ports.h
#ifndef _MY_LV_PORTS
#define _MY_LV_PORTS
#include <TFT_eSPI.h>
#include <lvgl.h>
// 触控
#include "NS2009.h"
#define ESP32_I2C_SDA 33
#define ESP32_I2C_SCL 25
/*Change to your screen resolution*/
const uint16_t screenWidth = 320;
const uint16_t screenHeight = 240;
void my_disp_init(void); // 挂载lvgl接口,设置buffer
#endif
项目路径/src/my_lv_ports.cpp
#include "my_lv_ports.h"
// TFT_eSPI tft = TFT_eSPI(screenWidth, screenHeight); /* TFT instance */
TFT_eSPI tft = TFT_eSPI(screenHeight,screenWidth); /* TFT instance */
/*Read the touchpad*/
void my_touchpad_read(lv_indev_drv_t *indev_driver, lv_indev_data_t *data) {
int16_t touchX, touchY, touchZ;
touchZ = ns2009_read(NS2009_LOW_POWER_READ_Z1); //压力值
touchX = ns2009_read(NS2009_LOW_POWER_READ_X);
touchY = ns2009_read(NS2009_LOW_POWER_READ_Y);
touchX = touchX * SCREEN_X_PIXEL / 4096; // 4096 = 2 ^ 12
touchY = SCREEN_Y_PIXEL - touchY * SCREEN_Y_PIXEL / 4096;
if (touchZ < 30) {
data->state = LV_INDEV_STATE_REL;
} else {
data->state = LV_INDEV_STATE_PR;
/*Set the coordinates*/
data->point.x = touchY;
data->point.y = touchX;
#if LV_USE_LOG != 0
Serial.printf("Touch: x=%d y=%d\r\n", touchX, touchY);
#endif
}
}
/* Display flushing */
void my_disp_flush(lv_disp_drv_t *disp, const lv_area_t *area,
lv_color_t *color_p) {
uint32_t w = (area->x2 - area->x1 + 1);
uint32_t h = (area->y2 - area->y1 + 1);
tft.setSwapBytes(true);
tft.pushImageDMA(area->x1, area->y1, w, h,(uint16_t *)&color_p->full);
// tft.startWrite();
// tft.setAddrWindow( area->x1, area->y1, w, h );
// tft.pushColors( ( uint16_t * )&color_p->full, w * h, true );
// tft.endWrite();
lv_disp_flush_ready(disp);
}
#if LV_USE_LOG != 0
void my_print(const char *buf) { Serial.printf("%s \r\n", buf); }
#endif
void my_disp_init(void) {
// 绘图缓冲初始化
// static lv_disp_draw_buf_t draw_buf;
// static lv_color_t buf[screenWidth * 10];
// lv_disp_draw_buf_init(&draw_buf, buf, NULL, screenWidth * 10);
static lv_disp_draw_buf_t draw_buf;
static lv_color_t buf_2_1[screenWidth * 30]; /*A buffer for 10 rows*/
static lv_color_t buf_2_2[screenWidth * 30]; /*An other buffer for 10
rows*/
lv_disp_draw_buf_init(&draw_buf, buf_2_1, buf_2_2,
screenWidth * 30); /*Initialize
the display buffer*/
// TFT驱动初始化
tft.begin(); /* TFT init */
tft.initDMA();
tft.setRotation(3); /* Landscape orientation, flipped */
// 设置LVGL显示设备
static lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);
/*Change the following line to your display resolution*/
disp_drv.hor_res = screenWidth;
disp_drv.ver_res = screenHeight;
disp_drv.flush_cb = my_disp_flush;
disp_drv.draw_buf = &draw_buf;
lv_disp_drv_register(&disp_drv);
// 设置LVGL输入设备(电阻屏)
static lv_indev_drv_t indev_drv;
lv_indev_drv_init(&indev_drv);
indev_drv.type = LV_INDEV_TYPE_POINTER;
indev_drv.read_cb = my_touchpad_read;
lv_indev_drv_register(&indev_drv);
// 设置LVGL串口输出设备(调试用)
#if LV_USE_LOG != 0
lv_log_register_print_cb(my_print);
#endif
}
注:
- my_disp_flush的实现中,使用pushImageDMA函数替换LVGL原例程中的pushColors函数,以实现非阻塞DMA传输,同时,应开启双绘图缓冲。此修改可带来20%~40%的帧率提升。
- my_print函数用于支持LVGL-Debug模块向串口输出调试信息,LVGL v8修改了该回调函数接口,LVGL原例程中的实现已不再适用,此处做了修改。
- 在my_disp_init的开始,开辟了两块绘图缓冲,以配合非阻塞DMA传输。缓冲区容量设为30行,大于30行时帧率提升较小。
3.LVGL设置
将 项目路径/.pio/libdeps/esp32dev/lvgl/lv_conf_template.h复制一份到 项目路径/.pio/libdeps/esp32dev/lvgl/src,更名为lv_conf.h,将第15行修改为1,使能该配置文件,做如下修改:
// 30行 调整RGB565数据格式
#define LV_COLOR_16_SWAP 1
// 52行(增大lvgl内存池32k->64k)
#define LV_MEM_SIZE (64U * 1024U)
// 88行 (使能Arduino tick source)
#define LV_TICK_CUSTOM 1
//174行,使能调试日志
#define LV_USE_LOG 1
4.运行benchmark测试
首先,开启lv_examples库的bench_mark部分:
将*/项目路径.pio/libdeps/esp32dev/lv_examples/lv_demo_conf_template.h复制为/项目路径.pio/libdeps/esp32dev/lv_examples/lv_demo_conf.h*,修改:
//11行
#if 1
//41行
#define LV_USE_DEMO_BENCHMARK 1
//47行
#define LV_USE_DEMO_MUSIC 0
最后,编写main函数:项目路径/src/main.cpp:
#include <Arduino.h>
#include "my_lv_ports.h"
#include <TFT_eSPI.h>
#include <lv_demo.h>
#include <lvgl.h>
void setup() {
Serial.begin(115200); /* prepare for possible serial debug */
Wire.begin(ESP32_I2C_SDA, ESP32_I2C_SCL);
String LVGL_Arduino = "Hello Arduino! ";
LVGL_Arduino += String('V') + lv_version_major() + "." + lv_version_minor() +
"." + lv_version_patch();
Serial.println(LVGL_Arduino);
Serial.println("I am LVGL_Arduino");
lv_init();
my_disp_init();
#if 0
/* Create simple label */
lv_obj_t *label = lv_label_create(lv_scr_act());
lv_label_set_text(label, LVGL_Arduino.c_str());
lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);
#else
/* Try an example from the lv_examples Arduino library
make sure to include it as written above.
lv_example_btn_1();
*/
// uncomment one of these demos
// lv_demo_widgets(); // OK
lv_demo_benchmark(); // OK
// lv_demo_keypad_encoder(); // works, but I haven't an encoder
// lv_demo_music(); // NOK
// lv_demo_printer();
// lv_demo_stress(); // seems to be OK
#endif
Serial.println("Setup done");
}
void loop() {
lv_timer_handler(); /* let the GUI do its work */
delay(5);
}
Benchmark加权FPS可达50。
ESP32-C3
0.开发环境
2022年最新版本的Arduino-ESP32 2.0.2框架已经添加了对C3芯片的支持,但platformio平台尚未完全适配,故在新建项目时需要手动配置ref。
首先新建ESP32-Arduino工程,将platformio.ini文件替换为如下内容:
[env:arduino-esp32c3]
platform = https://github.com/platformio/platform-espressif32.git#feature/arduino-upstream
board = esp32-c3-devkitm-1
framework = arduino
platform_packages =
framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32#master
lib_ldf_mode = deep
lib_deps =
lvgl/lvgl@^8.1.0
moononournation/GFX Library for Arduino@^1.2.0
lvgl/lv_examples@^8.1.1-dev
monitor_speed = 115200
board_build.flash_mode = dio
注:
- 添加lib_ldf_mode=deep解决编译时找不到库文件的问题
- 添加board_build.flash_mode=dio解决boot失败的问题
- 由于tft_eSPI库尚未完成对C3芯片适配,使用GFX Library for Arduino库代替tft_eSPI库作为SPI屏幕的驱动
1.配置SPI屏驱动
无。
2.配置LVGL接口函数
由于更换了SPI驱动库,需将上面的第二部分做如下修改:
ref: 项目路径.pio/libdeps/esp32dev/lvgl/examples/arduino/LVGL_Arduino/LVGL_Arduino.ino
核心是实现my_disp_flush,my_print和my_touchpad_read(可选)
项目路径/src/my_lv_ports.h
#ifndef _MY_LV_PORTS
#define _MY_LV_PORTS
#include <Arduino.h>
#include <Arduino_GFX_Library.h>
#include <lvgl.h>
// 触控
#include "NS2009.h"
#define ESP32_I2C_SDA 33
#define ESP32_I2C_SCL 25
/*Change to your screen resolution*/
const uint16_t screenWidth = 320;
const uint16_t screenHeight = 240;
void my_disp_init(void); // 挂载lvgl接口,设置buffer
#endif
项目路径/src/my_lv_ports.cpp
#include "my_lv_ports.h"
//接口I/O TFT_DC: 2, TFT_RST: 1 TFT_SCK: 4 TFT_MOSI : 6 TFT_CS: 拉高
/* More data bus class: https://github.com/moononournation/Arduino_GFX/wiki/Data-Bus-Class */
// DC pin is optional (-1 means using 9-bit SPI)
// MISO pin is optional (Arduino_GFX not yet have any features read from display)
// ESP32-C3使用FSPI
Arduino_DataBus *bus = new Arduino_ESP32SPI(2 /* DC */, -1 /* CS */, 4 /* SCK */, 6 /* MOSI */, -1 /* MISO */, FSPI /* spi_num */);
/* More display class: https://github.com/moononournation/Arduino_GFX/wiki/Display-Class */
Arduino_GFX *gfx = new Arduino_ST7789(bus, 1 /* RST */, 3 /* rotation */);
/*Read the touchpad*/
void my_touchpad_read(lv_indev_drv_t *indev_driver, lv_indev_data_t *data) {
int16_t touchX, touchY, touchZ;
touchZ = ns2009_read(NS2009_LOW_POWER_READ_Z1); //压力值
touchX = ns2009_read(NS2009_LOW_POWER_READ_X);
touchY = ns2009_read(NS2009_LOW_POWER_READ_Y);
touchX = touchX * SCREEN_X_PIXEL / 4096; // 4096 = 2 ^ 12
touchY = SCREEN_Y_PIXEL - touchY * SCREEN_Y_PIXEL / 4096;
if (touchZ < 30) {
data->state = LV_INDEV_STATE_REL;
} else {
data->state = LV_INDEV_STATE_PR;
/*Set the coordinates*/
data->point.x = touchY;
data->point.y = touchX;
#if LV_USE_LOG != 0
Serial.printf("Touch: x=%d y=%d\r\n", touchX, touchY);
#endif
}
}
/* Display flushing */
void my_disp_flush(lv_disp_drv_t *disp, const lv_area_t *area,
lv_color_t *color_p) {
uint32_t w = (area->x2 - area->x1 + 1);
uint32_t h = (area->y2 - area->y1 + 1);
gfx->draw16bitBeRGBBitmap(area->x1, area->y1, (uint16_t *)&color_p->full, w, h);
lv_disp_flush_ready(disp);
}
#if LV_USE_LOG != 0
void my_print(const char *buf) { Serial.printf("%s \r\n", buf); }
#endif
void my_disp_init(void) {
// 绘图缓冲初始化
static lv_disp_draw_buf_t draw_buf;
static lv_color_t buf[screenWidth * 30];
lv_disp_draw_buf_init(&draw_buf, buf, NULL, screenWidth * 30);
// TFT驱动初始化
// Init Display
gfx->begin();
gfx->fillScreen(BLACK);
// 设置LVGL显示设备
static lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);
/*Change the following line to your display resolution*/
disp_drv.hor_res = screenWidth;
disp_drv.ver_res = screenHeight;
disp_drv.flush_cb = my_disp_flush;
disp_drv.draw_buf = &draw_buf;
lv_disp_drv_register(&disp_drv);
// 设置LVGL输入设备(电阻屏)
// static lv_indev_drv_t indev_drv;
// lv_indev_drv_init(&indev_drv);
// indev_drv.type = LV_INDEV_TYPE_POINTER;
// indev_drv.read_cb = my_touchpad_read;
// lv_indev_drv_register(&indev_drv);
// 设置LVGL串口输出设备(调试用)
#if LV_USE_LOG != 0
lv_log_register_print_cb(my_print);
#endif
}
注:
- Arduino_GFX库不支持DMA传输,故使用单显示缓存即可。
3.LVGL设置
将 项目路径/.pio/libdeps/esp32dev/lvgl/lv_conf_template.h复制一份到 项目路径/.pio/libdeps/esp32dev/lvgl/src,更名为lv_conf.h,将第15行修改为1,使能该配置文件,做如下修改:
// 30行 (调整色彩格式)
#define LV_COLOR_16_SWAP 1
// 52行(增大lvgl内存池32k->64k)
#define LV_MEM_SIZE (64U * 1024U)
// 88行 (使能Arduino tick source)
#define LV_TICK_CUSTOM 1
//174行,使能调试日志
#define LV_USE_LOG 1
4.运行benchmark测试
同ESP32部分
Benchmark加权FPS可达39。
通过platformio.ini文件进行库配置
在上面两节中,对LVGL库和TFT_sSPI库的配置是通过直接修改源文件实现的,这种方法的缺点是当我们重新拉取(更新)依赖库时,这些修改会被覆盖掉。为解决这一问题,可通过在platformio.ini文件中配置编译标志实现对库的配置。platformio.ini文件描述了一个platformio工程的所有依赖,只需在pio CLI中执行pio init --ide=vscode
便可令platformio系统按照platformio.ini文件的描述自动下载所需的编译套件、SDK,和依赖库文件。
通过platformio.ini文件对LVGL库和TFT_sSPI库进行配置的具体方法可见:
参考资料
- https://daumemo.com/how-to-use-lvgl-library-on-arduino-with-an-esp-32-and-spi-lcd/#config-file
- https://docs.lvgl.io/master/intro/index.html
- https://squareline.io/discover
- https://docs.espressif.com/projects/arduino-esp32/en/latest/getting_started.html
- https://www.jianshu.com/nb/47946852
- https://github.com/Makerfabs/Project_Touch-Screen-Camera