通过前2篇的文章《MCU编程7:移植ST的LTDC官方例程驱动LCD显示器》和《MCU编程8:ST的LTDC结合SDRAM交替显示RGB三原色》,我们大概了解了STM32F429I-DISCO开发板上的TFT LCD的驱动方法。从这2篇文章我们可以知道,TFT LCD驱动的核心主要就是要配置好STM32的LTDC驱动,使其驱动时序和LCD显示器所需要的时序相匹配,同时分配一个帧缓存,该缓存保存LCD要显示的颜色数据,之后LTDC驱动器就会按照配置好的时序,周期地从帧缓存中读取数据,并发送给LCD显示器来显示对应的图像。
有了前面TFT LCD驱动的基础代码后,我们现在就可以开始移植开源的LVGL图像界面库的代码到我们的工程中了。这篇文章我们就来讲解LVGL代码的移植步骤。
LVGL的官方网址是:LVGL。从官网上我们可以获取到LVGL的相关说明文档,也可以获取到源代码的下载链接。
LVGL的源代码的仓库地址是:LVGL repository
截至目前最新的release版本的下载地址是:LVGL Release v9.4.0
这篇文章就以目前最新的v9.4.0版本为例,介绍LVGL代码的移植步骤。我们先从上面LVGL Release v9.4.0地址下载对应的源代码。
将下载下来的源码压缩包解压之后,LVGL源码的主要目录结构如下图所示。

其中src目录是LVGL源码的核心目录,包含了LVGL所有的功能部件代码。另外,根目录下的lv_conf_template.h、lv_version.h、lvgl.h和lvgl_private.h文件也是后续移植所需要的文件。我们这次移植也只需要图中高亮显示的文件夹和文件即可,所以实际移植工作其实非常简单。
LVGL的代码移植工作非常简单,我们只需要将第2节中提到的那些高亮文件拷贝到我们自己工程的对应目录下即可。
LVGL为第三方开源代码,所以我们在自己工程STM32F429I_PRJ的Middlewares目录下新建文件夹lvgl,用于放置LVGL的相关代码文件。
接着,我们将LVGL源码的src整个目录拷贝到我们新建的lvgl目录下,然后再将第2节中提到的lv_conf_template.h、lv_version.h、lvgl.h和lvgl_private.h这4个文件也拷贝到我们新建的lvgl目录下。
将lv_conf_template.h改名为lv_conf.h,然后打开该文件,将最开始的#if 0代码改成#if 1,使该文件下的代码生效。该文件用来配置LVGL的相关设置项,我们这次移植使用默认配置即可。lv_version.h文件定义LVGL当前的版本号。lvgl.h文件用于包含LVGL所需要的所有头文件。lvgl_private.h文件用于包含LVGL所需要的所有私有头文件。
以上文件就是我们本次移植所需的所有文件了,接着我们开始编写LVGL的应用代码。
LVGL应用代码编写我们可以直接参考前面下载的LVGL源代码根目录下的README.md文档中的使用说明,直接按照Integrating LVGL章节中的说明进行操作即可,集成操作也很简单。
我们先在lcd_func.h文件中包含lvgl.h文件,并定义LCD显示器的水平和垂直分辨率,之后声明函数vLvglCfg()用于对LVGL进行初始化。对应的代码示例如下。
#include"lvgl/lvgl.h"/*Define LV_LVGL_H_INCLUDE_SIMPLE to include as "lvgl.h"*/#define TFT_HOR_RES 240#define TFT_VER_RES 320voidvLvglCfg(void);
我们在lcd_func.c文件中实现上面提到的LVGL的初始化函数vLvglCfg(),具体的函数代码如下。
voidvLvglCfg(void){/*Initialize LVGL*/lv_init();/*Set millisecond-based tick source for LVGL so that it can track time.*/lv_tick_set_cb(HAL_GetTick);/*Create a display where screens and widgets can be added*/lv_display_t * display = lv_display_create(TFT_HOR_RES, TFT_VER_RES);/*Add rendering buffers to the screen.*Here adding a smaller partial buffer assuming 16-bit (RGB565 color format)*/static uint8_t buf[TFT_HOR_RES * TFT_VER_RES / 10 * 2]; /* x2 because of 16-bit color depth */lv_display_set_buffers(display, buf, NULL, sizeof(buf), LV_DISPLAY_RENDER_MODE_PARTIAL);/*Add a callback that can flush the content from `buf` when it has been rendered*/lv_display_set_flush_cb(display, my_flush_cb);/*Create an input device for touch handling*/lv_indev_t * indev = lv_indev_create();lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER);lv_indev_set_read_cb(indev, my_touch_read_cb);/*The drivers are in place; now we can create the UI*/#if 1lv_obj_t * label = lv_label_create(lv_screen_active());lv_label_set_text(label, "Hello world");lv_obj_center(label);#elif 1lv_obj_t * button = lv_button_create(lv_screen_active());lv_obj_center(button);lv_obj_add_event_cb(button, button_clicked_cb, LV_EVENT_CLICKED, NULL);lv_obj_t * label = lv_label_create(button);lv_label_set_text(label, "Hello from LVGL!");#elif 1static lv_subject_t subject_value;lv_subject_init_int(&subject_value, 35);lv_subject_add_observer(&subject_value, my_observer_cb, NULL);lv_style_t style_base;lv_style_init(&style_base);lv_style_set_bg_color(&style_base, lv_color_hex(0xff8800));lv_style_set_bg_opa(&style_base, 255);lv_style_set_radius(&style_base, 4);lv_obj_t * slider = lv_slider_create(lv_screen_active());lv_obj_center(slider);lv_obj_set_size(slider, lv_pct(80), 16);lv_obj_add_style(slider, &style_base, LV_PART_INDICATOR);lv_obj_add_style(slider, &style_base, LV_PART_KNOB);lv_obj_add_style(slider, &style_base, 0);lv_obj_set_style_bg_opa(slider, LV_OPA_50, 0);lv_obj_set_style_border_width(slider, 3, LV_PART_KNOB);lv_obj_set_style_border_color(slider, lv_color_hex3(0xfff), LV_PART_KNOB);lv_slider_bind_value(slider, &subject_value);lv_obj_t * label = lv_label_create(lv_screen_active());lv_obj_align(label, LV_ALIGN_CENTER, 0, -30);lv_label_bind_text(label, &subject_value, "Temperature: %d °C");#elif 1/*Create a new screen and load it*/lv_obj_t * scr = lv_obj_create(NULL);lv_screen_load(scr);/*Set a column layout*/lv_obj_set_flex_flow(scr, LV_FLEX_FLOW_COLUMN);lv_obj_set_flex_align(scr, LV_FLEX_ALIGN_SPACE_EVENLY, /*Vertical alignment*/LV_FLEX_ALIGN_START, /*Horizontal alignment in the track*/LV_FLEX_ALIGN_CENTER); /*Horizontal alignment of the track*//*Create 5 checkboxes*/const char * texts[5] = {"Input 1", "Input 2", "Input 3", "Output 1", "Output 2"};for(int i = 0; i < 5; i++) {lv_obj_t * cb = lv_checkbox_create(scr);lv_checkbox_set_text(cb, texts[i]);}/*Change some states*/lv_obj_add_state(lv_obj_get_child(scr, 1), LV_STATE_CHECKED);lv_obj_add_state(lv_obj_get_child(scr, 3), LV_STATE_DISABLED);#endif}
上面代码涉及到一些回调函数的注册,需要我们实现对应的回调函数功能。vLvglCfg()函数的第7行代码用于设置获取时钟心跳个数的回调函数,LVGL需要有个心跳时钟来实现显示和输入的处理功能。这里我们将回调函数设置成ST官方提供的获取系统时钟心跳数的函数HAL_GetTick()。vLvglCfg()函数的第18行代码用于设置LVGL图形渲染的回调函数,LVGL通过该回调函数将要更新的显示更新到帧缓存中,然后在LCD上显示出来。该回调函数需要我们自己实现,这里实现的渲染回调函数my_flush_cb()代码如下。
staticvoidmy_flush_cb(lv_display_t * disp, constlv_area_t * area, uint8_t * px_map){/*Write px_map to the area->x1, area->x2, area->y1, area->y2 area of the*frame buffer or external display controller. *//*Return if the area is out the screen*/if(area->x2 < 0) return;if(area->y2 < 0) return;if(area->x1 > TFT_HOR_RES - 1) return;if(area->y1 > TFT_VER_RES - 1) return;/*Truncate the area to the screen*/int32_t act_x1 = area->x1 < 0 ? 0 : area->x1;int32_t act_y1 = area->y1 < 0 ? 0 : area->y1;int32_t act_x2 = area->x2 > TFT_HOR_RES - 1 ? TFT_HOR_RES - 1 : area->x2;int32_t act_y2 = area->y2 > TFT_VER_RES - 1 ? TFT_VER_RES - 1 : area->y2;int32_t pxDataIdx = 0;for(int32_t r = act_y1; r <= act_y2; r++) {for(int32_t c = act_x1; c <= act_x2; c++) {my_fb[r * TFT_HOR_RES + c] = *((uint16_t*)px_map + pxDataIdx++);}}lv_disp_flush_ready(disp);}
my_flush_cb()函数有3个参数,其中第一个参数disp为显示对象的指针,第2个参数area为要更新的显示区域,第3个参数px_map为要更新的显示区域对应的显示数据的起始地址指针。my_flush_cb()函数第2个参数的结构体类型lv_area_t定义如下。该结构体类型定义了一个显示的矩形区域,其中(x1,y1)对应矩形的左上角的点,(x2,y2)对应矩形的右下角的点。如果我们把LCD的像素点阵看成行列表示的阵列的话,那水平的240个像素点对应240列,垂直的320个像素点对应320行,整个LCD可以看成是320行*240列的阵列。那lv_area_t的x1对应LCD行列阵列的起始列,x2对应终止列,y1对应行列阵列的起始行,y2对应终止行。
/** Represents an area of the screen.*/typedef struct {int32_t x1;int32_t y1;int32_t x2;int32_t y2;} lv_area_t;
my_flush_cb()函数代码的第6-9行用于判断显示区域是否在LCD的显示范围内,如果超出显示范围,则直接退出。第12-15行将显示区域限制在有效范围内。第16-22行将LVGL传递过来的显示数据更新到LTDC驱动器的帧缓存my_fb中。需要注意的是,这里的显示颜色数据为16位,所以需要将px_map指针类型强制转换成uint16_t类型。另外,area区域实际只是对应帧缓存my_fb中的一块区域,所以我们需要找到该area区域在帧缓存my_fb中的对应位置,然后将数据复制进去,而第16-22行的代码就是实现这个映射并复制数据。第24行代码用于通知LVGL已经完成了将要更新区域的数据复制到帧缓存中了,可以继续后续的数据更新操作。
vLvglCfg()函数第23行代码用于设置读取输入设备的输入位置和输入状态的回调函数,我们这次主要要实现LVGL的显示,暂时不需要实现输入功能,所以我们将输入的位置坐标固定为(0,0),将输入状态设置为释放状态。输入回调函数my_touch_read_cb()的具体代码如下。
int32_tmy_touch_is_pressed(void){return 0;}staticvoidmy_touch_read_cb(lv_indev_t * indev, lv_indev_data_t * data){int32_t touchpad_x = 0, touchpad_y = 0;if (my_touch_is_pressed()){data->point.x = touchpad_x;data->point.y = touchpad_y;data->state = LV_INDEV_STATE_PRESSED;}else{data->state = LV_INDEV_STATE_RELEASED;}}
vLvglCfg()函数第14-15行代码用于设置LVGL的渲染缓存大小,这里设置了一个最小的渲染缓存。当LCD上有要更新的区域时,LVGL会将更新的显示数据存放在该缓存中,再通过前面的渲染回调函数my_flush_cb()将显示数据复制到LTDC的帧缓存中。vLvglCfg()函数第26-83行代码用于显示具体的控件,这里的代码复制了LVGL工程目录下README.md文件中的一些例程代码,用于验证移植的代码是否能正常显示例程中的控件。
最后,我们需要在main.c文件中修改一些代码,以实现LVGL的初始化操作,并让LVGL的显示功能正常运行。首先,我们需要在main()函数中调用vLvglCfg()函数,实现LVGL的初始化操作,并创建显示对象及要显示的控件。最后,在while(1)主循环中周期调用LVGL的定时器处理函数lv_timer_handler(),使LVGL的各个功能能够正常地执行起来。对应代码片段如下。
int main(void){// ... 省略其他代码vLvglCfg();/* Infinite loop */while (1){endTick = HAL_GetTick();// ... 省略其他代码if(endTick - lvglTick >= 5) {lvglTick = endTick;lv_timer_handler();}}}
为了确保编译链接能够正常执行,需要对CMakeLists.txt进行修改,主要添加LVGL的头文件包含路径以及源代码路径。
LVGL的头文件包含路径添加比较简单,只需要在include_directories中添加以下的头文件路径即可。
${PROJECT_SOURCE_DIR}/../Middlewares${PROJECT_SOURCE_DIR}/../Middlewares/lvgl
由于LVGL的源码文件非常多,手动添加对应的路径效率很低,所以我们参照网上的方法,编写了python脚本来自动生成源代码的包含路径,然后将生成的路径复制到DRV_SRCS列表中即可。该python脚本代码我们放置在新建的Script目录下,对应代码文件为gen_dir.py。如需查看具体的代码,可以下载附录链接中的代码进行查看。
使用Ctrl+Shift+B的快捷键或者Terminal/Run Build Task...菜单执行代码的编译链接操作。
如果想了解详细的编译和运行操作,可以参考之前的文章《MCU编程3:使用VS Code+GCC+OpenOCD搭建MCU程序开发环境(下)》。
开发板通过USB线连接到电脑的USB口,这时开发板正常上电,按下F5快捷键或Run/Start Debugging菜单执行代码在线调试,VS Code会自动通过ST-Link将代码下载到开发板的MCU上,并暂停在main()函数的开始处,再次按下F5按键全速运行代码即可。
这里对LVGL工程目录下的README.md文件中提到的4个显示例程分别进行了测试,对应的执行结果如下。
更新后的工程代码依然保存在gitee的以下路径。
https://gitee.com/goodrenze/STM32F429I_PRJ
1. https://blog.csdn.net/m0_57585228/article/details/146442897。
2. https://github.com/lvgl/lv_port_stm32f429_disco