LVGL(Light and Versatile Embedded Graphics Library)是一个轻量级的嵌入式图形库。LVGL的项目作者是来自匈牙利首都布达佩斯的 Gábor Kiss-Vámosi,他在2016年将其发布在 GitHub上。
当时叫 LittlevGL而不是LVGL,后来作者重新命名为 LVGL,甚至连仓库地址都改了。 像一般的开源项目的那样,它是作为一个人的项目开始的。 从那时起,陆续有近 100 名贡献者参与了项目开发,使得 LVGL 逐渐成为最受欢迎的嵌入式图形库之一。LVGL 项目(包括所有存储库)在 MIT license 许可下获得许可。这意味着您甚至可以在商业项目中使用它。
LVGL用C语言编写,以实现最大的兼容性(与C ++兼容),模拟器可在没有嵌入式硬件的PC上启动嵌入式GUI设计,同时LVGL作为一个图形库,它自带着接近三十多种小工具可以供开发者使用。这些强大的构建块按钮搭配上带有非常丝滑的动画以及可以做到平滑滚动的高级图形,同时兼具着不高的配置要求以及开源属性,显著的优势使得LVGL蔚然成风,成为广大开发者在选择GUI时的第一选择。
基本上,每个能够驱动显示器的现代控制器都适合运行LVGL。最低要求是:
注意: 内存使用情况可能因体系结构、编译器和构建选项而异。
lvgl核心存储库遵循语义版本控制规则:
核心存储库至少具有以下分支:
版本支持情况
在 v8 之前,每个主要系列的最后一个次要版本支持 1 年。 从 v8 开始,每个次要版本都支持 1 年。
3.0.1是lv_arduino的最后一个版本,与之对应的 lvgl 版本是7.11.0。lv_arduino最后一次提交是2020.12.4,github仓库已经存档了。为什么讲这个lv_arduino,原因很简单,就是版本更高的lvgl在arduino上使用rp2040没用起来。
简单来说,就是由于编译器生成的汇编代码中存在一些不对齐的指令造成的。汇编代码中的指令需要按照特定的规则对齐,以确保正确的执行。不对齐的指令可能会导致处理器无法正确解析和执行这些指令,从而引发错误。lvgl的开发人员将它归结为编译器问题,并且说降低优化级别可以避开此问题,github的lvgl仓库中,关于此问题的issue。我暂时没找到简单的办法实现这种解决办法:
1.找到编译器命令行中的优化选项,通常是类似于“-O2”或“-Os”的选项。
2.将优化选项修改为较低的级别,例如“-O1”或“-O0”。
补充:
1.我有些疑问,如果仅是编译器问题,为什么低版本的lvgl(7.11.0)不会引发此问题?如果和lvgl库本身有关,那么改变那个地方是不是也能避免这个报错?但是我没有找到这个地方,在这里提到主要是为了让之后也遇到这个问题,然后又特别想用lvgl最新版本(兼容性更好,附加功能更加完善)的人,多一条解决道路。
2.gcc编译流程
注:后续内容大多基于lvgl 7.11.0版本,有高版本的会做说明,原因前面已经说了。
LVGL的系统框架主要包括以下几个核心组件:
通过这些组件的协作,LVGL提供了一个完整的系统框架,用于开发嵌入式系统的图形界面。开发者可以根据自己的需求和硬件平台,使用LVGL构建出功能丰富、易于使用的用户界面。
LVGL本身是一个图形库。
我们的应用程序通过调用LVGL库来创建GUI。它包含一个HAL(硬件抽象层)接口,用于注册显示和输入设备驱动程序。驱动程序除特定的驱动程序外,它还有其他的功能,可驱动显示器GPU、读取触摸板或按钮的输入。
MCU有两种典型的硬件设置。一个带有内置LCD/TFT驱动器的外围设备,而另一种是没有内置LCD/TFT驱动器的外围设备。相同的是,这两种情况都需要一个帧缓冲区来存储屏幕的当前图像。
集成了TFT/LCD驱动器的MCU如果MCU集成了TFT/LCD驱动器外围设备,则可以直接通过RGB接口连接显示器。在这种情况下,帧缓冲区可以位于内部RAM(如果MCU有足够的RAM)中,也可以位于外部RAM(如果MCU具有存储器接口)中。
如果MCU没有集成TFT/LCD驱动程序接口,则必须使用外部显示控制器(例如SSD1963、SSD1306、ILI9341 )。在这种情况下,MCU可以通过并行端口,SPI或通过I2C与显示控制器进行通信。帧缓冲区通常位于显示控制器中,从而为MCU节省了大量RAM。
上面的三个库中有一个类似名为 lv_conf_template.h 的配置头文件(template就是模板的意思)。通过它可以设置库的基本行为,裁剪不需要模块和功能,在编译时调整内存缓冲区的大小等等。
lv_conf.h
。打开文件并将开头的 #if 0
更改为 #if 1
以使能其内容。lv_drv_conf.h
。打开文件并将开头的 #if 0
更改为 #if 1
以使能其内容。lv_ex_conf.h
。打开文件并将开头的 #if 0
更改为 #if 1
以使能其内容。在配置文件中,注释说明了各个选项的含义。我们在移植时至少要检查以下三个配置选项,其他配置根据具体的需要进行修改:
LV_HOR_RES_MAX
显示器的水平分辨率。LV_VER_RES_MAX
显示器的垂直分辨率。LV_COLOR_DEPTH
颜色深度,其可以是:
- 8 - RG332
- 16 - RGB565
- 32 - (RGB888和ARGB8888)
【LVGL库】: lv_arduino
【屏幕驱动】: TFT_eSPI
这里我使用的是rp2040,并且没有触摸(如果有触摸可以使用【触摸驱动】: TFT_Touch)。
I、lv_arduino默认就有lv_conf.h
并且已经使能,不过还是至少应该打开确认分辨率和色深。
II、屏幕我这里使用的是st7789,并且rp2040引脚也是自定义的就没有采用已经写好的配置文件,而是直接使用User_Setup.h,自己修改。
User_Setup_Select.h默认如下:
// Only ONE line below should be uncommented to define your setup. Add extra lines and files as needed.
#include // Default setup is root library folder
//#include // Setup file for ESP8266 configured for my ILI9341
//#include // Setup file for ESP8266 configured for my ST7735
//#include // Setup file for ESP8266 configured for my ILI9163
//#include // Setup file for ESP8266 configured for my S6D02A1
//#include // Setup file for ESP8266 configured for my stock RPi TFT
//#include // Setup file for ESP8266 configured for my modified RPi TFT
//#include // Setup file for ESP8266 configured for my ST7735 128x128 display
//#include // Setup file for ESP8266 configured for my ILI9163 128x128 display
//#include // Setup file for ESP8266 configured for my ST7735
//#include // Setup file for ESP8266 configured for ESP8266 and RPi TFT with touch
...
User_Setup.h修改如下:
根据屏幕型号,选择屏幕驱动:
// Only define one driver, the other ones must be commented out
// #define ILI9341_DRIVER // Generic driver for common displays
//#define ILI9341_2_DRIVER // Alternative ILI9341 driver, see https://github.com/Bodmer/TFT_eSPI/issues/1172
//#define ST7735_DRIVER // Define additional parameters below for this display
//#define ILI9163_DRIVER // Define additional parameters below for this display
//#define S6D02A1_DRIVER
//#define RPI_ILI9486_DRIVER // 20MHz maximum SPI
//#define HX8357D_DRIVER
//#define ILI9481_DRIVER
//#define ILI9486_DRIVER
//#define ILI9488_DRIVER // WARNING: Do not connect ILI9488 display SDO to MISO if other devices share the SPI bus (TFT SDO does NOT tristate when CS is high)
//#define ST7789_DRIVER // Full configuration option, define additional parameters below for this display
#define ST7789_2_DRIVER // Minimal configuration option, define additional parameters below for this display
//#define R61581_DRIVER
//#define RM68140_DRIVER
//#define ST7796_DRIVER
//#define SSD1351_DRIVER
//#define SSD1963_480_DRIVER
//#define SSD1963_800_DRIVER
//#define SSD1963_800ALT_DRIVER
//#define ILI9225_DRIVER
//#define GC9A01_DRIVER
如果显示器红蓝颠倒,可以调整如下宏:
// For ST7735, ST7789 and ILI9341 ONLY, define the colour order IF the blue and red are swapped on your display
// Try ONE option at a time to find the correct colour order for your display
#define TFT_RGB_ORDER TFT_RGB // Colour order Red-Green-Blue
// #define TFT_RGB_ORDER TFT_BGR // Colour order Blue-Green-Red
根据屏幕尺寸选择:
// For ST7789, ST7735, ILI9163 and GC9A01 ONLY, define the pixel width and height in portrait orientation
// #define TFT_WIDTH 80
// #define TFT_WIDTH 128
// #define TFT_WIDTH 172 // ST7789 172 x 320
#define TFT_WIDTH 240 // ST7789 240 x 240 and 240 x 320
// #define TFT_HEIGHT 160
// #define TFT_HEIGHT 128
// #define TFT_HEIGHT 240 // ST7789 240 x 240
#define TFT_HEIGHT 320 // ST7789 240 x 320
// #define TFT_HEIGHT 240 // GC9A01 240 x 240
根据原理图修改屏幕的驱动引脚:
#define TFT_MISO 0
#define TFT_MOSI 3 // In some display driver board, it might be written as "SDA" and so on.
#define TFT_SCLK 2
#define TFT_CS 1 // Chip select control pin
#define TFT_DC 7 // Data Command control pin
#define TFT_RST 6 // Reset pin (could connect to Arduino RESET pin)
#define TFT_BL 12 // LED back-light
使用rp2040需打开此宏:
// For RP2040 processor and SPI displays, uncomment the following line to use the PIO interface.
#define RP2040_PIO_SPI // Leave commented out to use standard RP2040 SPI port interface
III、第三步自然是打开示例ino,由于lv_arduino自带了使用TFT_eSPI的示例 ESP32_TFT_eSPI.ino可以直接参考,不需要下载lv_examples。
因为我这里不仅需要使用lvgl的各种控件,还需要显示U盘的图片。也就是说,需要访问文件系统,还需要图片文件的解码。
下图是lv_arduino和lvgl(8.3.0)文件系统接口文件对比,可以明显看到,新版的lvgl包含的功能更加全面,所以能够使用最好使用最新版的lvgl。
lv_arduino没有图片解码的文件,但是lvgl有;并且lv_fs_fatfs.c比lv_port_fs_template.c更详尽,于是一并拿过来修改。
主要就是完善以下文件系统的接口:
/**********************
* STATIC PROTOTYPES
**********************/
static void fs_init(void);
static void * fs_open(lv_fs_drv_t * drv, const char * path, lv_fs_mode_t mode);
static lv_fs_res_t fs_close(lv_fs_drv_t * drv, void * file_p);
static lv_fs_res_t fs_read(lv_fs_drv_t * drv, void * file_p, void * buf, uint32_t btr, uint32_t * br);
static lv_fs_res_t fs_write(lv_fs_drv_t * drv, void * file_p, const void * buf, uint32_t btw, uint32_t * bw);
static lv_fs_res_t fs_seek(lv_fs_drv_t * drv, void * file_p, uint32_t pos, lv_fs_whence_t whence);
static lv_fs_res_t fs_tell(lv_fs_drv_t * drv, void * file_p, uint32_t * pos_p);
static void * fs_dir_open(lv_fs_drv_t * drv, const char * path);
static lv_fs_res_t fs_dir_read(lv_fs_drv_t * drv, void * dir_p, char * fn);
static lv_fs_res_t fs_dir_close(lv_fs_drv_t * drv, void * dir_p);
在调用如下函数后,即可在LVGL中注册文件系统接口:
/**********************
* GLOBAL FUNCTIONS
**********************/
void lv_fs_fatfs_init(void)
{
/*----------------------------------------------------
* Initialize your storage device and File System
* -------------------------------------------------*/
fs_init();
/*---------------------------------------------------
* Register the file system interface in LVGL
*--------------------------------------------------*/
/*Add a simple drive to open images*/
static lv_fs_drv_t fs_drv; /*A driver descriptor*/
lv_fs_drv_init(&fs_drv);
/*Set up fields...*/
fs_drv.letter = LV_FS_FATFS_LETTER;
fs_drv.cache_size = LV_FS_FATFS_CACHE_SIZE;
fs_drv.open_cb = fs_open;
fs_drv.close_cb = fs_close;
fs_drv.read_cb = fs_read;
fs_drv.write_cb = fs_write;
fs_drv.seek_cb = fs_seek;
fs_drv.tell_cb = fs_tell;
fs_drv.dir_close_cb = fs_dir_close;
fs_drv.dir_open_cb = fs_dir_open;
fs_drv.dir_read_cb = fs_dir_read;
lv_fs_drv_register(&fs_drv);
}
至于像bmp和png图片解码器的注册则是更简单,因为lvgl已经完成了这些解码器,直接调用初始化接口:
/**********************
* GLOBAL FUNCTIONS
**********************/
void lv_bmp_init(void)
{
lv_img_decoder_t * dec = lv_img_decoder_create();
lv_img_decoder_set_info_cb(dec, decoder_info);
lv_img_decoder_set_open_cb(dec, decoder_open);
lv_img_decoder_set_read_line_cb(dec, decoder_read_line);
lv_img_decoder_set_close_cb(dec, decoder_close);
}
当然,如果你和我一样使用老版本的lvgl,比如lv_arduino,要使用这些功能就还得修改一下版本兼容问题,如果使用最新版的lvgl,则是直接在lv_conf.h使能和配置即可:
/*API for FATFS (needs to be added separately). Uses f_open, f_read, etc*/
#define LV_USE_FS_FATFS 0
#if LV_USE_FS_FATFS
#define LV_FS_FATFS_LETTER '\0' /*Set an upper cased letter on which the drive will accessible (e.g. 'A')*/
#define LV_FS_FATFS_CACHE_SIZE 0 /*>0 to cache this number of bytes in lv_fs_read()*/
#endif
/*PNG decoder library*/
#define LV_USE_PNG 0
/*BMP decoder library*/
#define LV_USE_BMP 0
#include
#include
TFT_eSPI tft = TFT_eSPI(); /* TFT instance */
static lv_disp_buf_t disp_buf; // 用于表示LVGL库中的显示缓冲区
static lv_color_t buf[LV_HOR_RES_MAX * 10]; // 用于存储显示缓冲区的像素数据
// 使用打印的话,不止使能配置这里,还要记得去lv_conf.h中配置 LV_USE_LOG 、 LV_LOG_LEVEL 和 LV_LOG_PRINTF
#define USE_LV_LOG 1 /* 启用/禁用日志模块 */
#if USE_LV_LOG != 0
/* 串口调试
*/
void my_print(lv_log_level_t level, const char * file, uint32_t line, const char * dsc)
{
Serial.printf("%s@%d->%s\r\n", file, line, dsc);
Serial.flush();
}
#endif
// 这将会是注册到lvgl系统的显示驱动接口,该驱动接口使用的就是 TFT_eSPI
/* 屏幕刷新 */
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.startWrite();
tft.setAddrWindow(area->x1, area->y1, w, h);
tft.pushColors(&color_p->full, w * h, true);
tft.endWrite();
lv_disp_flush_ready(disp);
}
/* 读取输入设备(这里是模拟编码器) */
bool read_encoder(lv_indev_drv_t * indev, lv_indev_data_t * data)
{
static int32_t last_diff = 0;
int32_t diff = 0; /* Dummy - no movement */
int btn_state = LV_INDEV_STATE_REL; /* Dummy - no press */
data->enc_diff = diff - last_diff;;
data->state = btn_state;
last_diff = diff;
return false;
}
void setup()
{
Serial.begin(115200); /* prepare for possible serial debug */
lv_init();
#if USE_LV_LOG != 0
lv_log_register_print_cb(my_print); /* register print function for debugging */
#endif
// 显示器驱动的初始化依然要在这里完成
tft.begin(); /* TFT init */
tft.setRotation(1); /* Landscape orientation */
/*
disp_buf 变量用于管理缓冲区,这里进行初始化,将实际的单个缓冲区buf放入其中。
关于缓冲区大小,有 3 种情况:
一个缓冲区 LVGL将屏幕的内保存到缓冲区中并将其发送到显示器。缓冲区可以小于屏幕。在这种情况下,较大的区域将被重画成多个部分。
如果只有很小的区域发生变化(例如按下按钮),则只会刷新该部分的区域。
两个非屏幕大小的缓冲区 具有两个缓冲区的 LVGL 可以将其中一个作为显示缓冲区,而另一缓冲区的内容发送到后台显示。应该使用 DMA
或其他硬件将数据传输到显示器,以让CPU同时绘图。这样,渲染和刷新并行处理。与 一个缓冲区 的情况类似,如果缓冲区小于要刷新的区
域,LVGL将按块绘制显示内容
两个屏幕大小的缓冲区 与两个非屏幕大小的缓冲区相反,LVGL将始终提供整个屏幕的内容,而不仅仅是块。这样,驱动程序可以简单地将帧缓
冲区的地址更改为从 LVGL 接收的缓冲区。因此,当MCU具有 LCD/TFT 接口且帧缓冲区只是 RAM 中的一个位置时,这种方法的效果很好。
*/
lv_disp_buf_init(&disp_buf, buf, NULL, LV_HOR_RES_MAX * 10);
/* 将缓冲区和显示器驱动接口注册到lvgl */
lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);
disp_drv.hor_res = 320;
disp_drv.ver_res = 240;
disp_drv.flush_cb = my_disp_flush;
disp_drv.buffer = &disp_buf;
lv_disp_drv_register(&disp_drv);
/* 初始化(虚拟)输入设备驱动程序 */
lv_indev_drv_t indev_drv;
lv_indev_drv_init(&indev_drv);
indev_drv.type = LV_INDEV_TYPE_ENCODER;
indev_drv.read_cb = read_encoder;
lv_indev_drv_register(&indev_drv);
/* 创建简单字符标签 */
lv_obj_t *label = lv_label_create(lv_scr_act(), NULL);
lv_label_set_text(label, "Hello Arduino! (V7.0.X)");
lv_obj_align(label, NULL, LV_ALIGN_CENTER, 0, 0);
/* 在屏幕上显示文件系统里面的图片,wink.png 。当然,你得确保你完成了解码器初始化,以及文件系统接口的完善和初始化*/
lv_obj_t * img;
img = lv_img_create(lv_scr_act(), NULL);
lv_img_set_src(img, "A:/home/libs/png/wink.png");
lv_obj_set_size(img, 150, 150);
}
void loop()
{
lv_task_handler(); /* 让GUI完成它的工作 */
delay(5);
}
lv_task_handler()
是 LVGL(LittlevGL)库中的一个函数,用于处理 LVGL 的任务调度。
LVGL 是一个事件驱动的图形库,它使用任务调度器来管理和执行各种任务。任务可以是定时器、动画、界面刷新等操作,通过任务调度器进行管理可以确保这些任务按照预定的时间和顺序执行。
lv_task_handler()
函数的作用是在主循环中调用,它会处理和执行所有已注册的 LVGL 任务。在每次调用时,lv_task_handler()
函数会检查每个任务的状态和时间,然后决定是否执行该任务。
具体的功能和意义包括:
lv_task_handler()
函数会检查定时器任务的时间是否到达,如果到达则执行相应的操作。lv_task_handler()
函数会检查动画任务的状态和时间,更新动画的状态,以实现平滑的动画效果。lv_task_handler()
函数会检查界面刷新任务的状态和时间,根据需要刷新界面,确保显示内容的准确性和实时性。lv_task_handler()
函数会检查事件任务的状态和时间,处理接收到的事件,并触发相应的回调函数或操作。通过调用 lv_task_handler()
函数,可以确保 LVGL 中的各种任务得到及时执行,从而保证图形界面的正常运行和响应。一般情况下,lv_task_handler()
函数需要在主循环中以适当的频率被调用,以满足任务调度的需求。
LVGL(LittlevGL)和其他一些嵌入式图形库相比,各自具有一些优点和缺点。以下是对LVGL和其他几个常见嵌入式图形库的优缺点的简要概述:
优点:
缺点:
优点:
缺点:
优点:
缺点:
优点:
缺点:
优点:
缺点:
优点:
缺点:
优点:
缺点:
优点:
缺点:
效的图形界面显示和操作体验,它采用了精简的设计和高效的渲染机制,具有较小的内存占用和快速的响应速度。
2. 可定制性强:MiniGUI提供了灵活的配置选项和扩展机制,开发者可以根据项目需求和硬件平台的特点进行定制,包括选择需要的图形引擎、控件库和主题等,以满足不同应用场景的需求。
3. 多平台支持:MiniGUI支持多种操作系统和平台,包括Linux、Windows、Android等,具有较好的跨平台兼容性,方便在不同平台上进行开发和移植。
4. 完善的控件库和工具:MiniGUI提供了丰富的控件库,包括按钮、标签、文本框、列表框等常用控件,以及图形绘制、字体管理等功能模块。此外,MiniGUI还提供了可视化界面编辑工具和调试工具,方便开发者进行界面设计和调试。
缺点: