LVGL9.2移植至正点原子阿波罗STM32F429

参考文献:LVGL 移植到 STM32 通法 (https://www.cnblogs.com/Huae/p/18621614)
工程文件:Github

前言

LVGL(Light and Versatile Graphics Library)是一个开源的图形库,旨在为嵌入式系统提供高效、灵活的图形用户界面(GUI)解决方案。它具有小巧的内存占用和高性能的渲染能力,支持多种硬件平台,包括单片机、DSP 和 ARM 处理器等。LVGL 支持丰富的控件(如按钮、标签、图标、滚动条等),并提供强大的主题、样式和动画功能,使得开发者能够轻松设计出美观且响应迅速的界面。此外,LVGL 还支持触摸屏、图形加速(如 DMA2D)、多线程等特性,非常适合用于嵌入式应用程序,如智能家居、工业控制、医疗设备等领域。

本文将介绍一下最新的LVGL9.2在正点原子阿波罗STMF429IGT6开发板上的移植。正点原子自己的移植教程还是几年前的8.2版本,LVGL在这个版本之后对很多API进行了优化和裁剪,导致正点原子的教程现在已经不是很好用了。但是万变不离其宗,LVGL的主架构没有变,只需要一点细微的改变就可以进行移植。

我使用的屏幕是正点原子7寸RGB屏,分辨率为1024 * 600.编译器为IAR EW 9.6

在开始之前,确保你有:

  • 相关的外设
  • 正点原子例程提供的一系列外设驱动
  • LVGL9.2版本源码
  • LVGL官方的文档点击跳转

我个人认为LVGL官方的文档非常重要,而且它甚至是有完整的官方授权的中文版本的。大多数东西都可以在上面找到。

准备工作

拷贝外设驱动

将所有和显示屏、触摸屏、SDRAM等有关的外设驱动加入工程。换句话说,你的工程需要做到在没有LVGL的环境下也能够通过最基本的显示函数在屏幕上显示东西的状态。如果你的工程在没有LVGL的环境下都没法通过正点原子例程里提供的函数显示东西,那显然是没有办法继续的。

拷贝LVGL库至工程文件夹

LVGL下载并解压之后,文件夹根目录内会包含很多文件夹及文件。在不需要demo或例程的情况下,我们只需要用到其中的这些文件/文件夹:

  • examples文件夹中的porting文件夹
  • src文件夹
  • lv_conf_template.h
  • lvgl.h

在工程文件夹根目录创建一个lvgls文件夹:

然后:

  1. 创建一个子文件夹,名为lvgl
  2. 创建一个子文件夹,名为lvgl_app
  3. 将之前提到的lv_conf_template.h重命名为lv_conf.h,移动到lvgls文件夹的根目录
  4. 打开examples/porting文件夹,复制lv_port_disp_template.c、lv_port_disp_template.h、lv_port_indev_template.c、lv_port_indev_template.h四个文件到lvgls文件夹根目录,并且重命名四个文件,去掉"_template"。这是后面要用到的显示屏和触摸屏注册文件
  5. 把src文件夹和lvgl.h复制到lvgls/lvgl文件夹中

现在的lvgls文件夹是这样的:

lvgl文件夹:

添加LVGL库文件至工程

将以下文件全部添加进入工程(可以一股脑全部加,也可以按照原本的文件结构自己慢慢加):

  • src文件夹中的所有c文件,除了
    • draw中的nxp、renesas、sdl
    • drivers文件夹
  • lv_port_disp.c和lv_port_indev.c

(千万别忘了src文件夹根目录下还有个lv_init.c)

然后,添加头文件目录,到src为止就可以.

我的添加完之后的工程结构长这个样子:

修改Stack和Heap大小

LVGL需要至少2KB的Stack和Heap。

代码修改

定时器

LVGL需要一个tick源来为其提供心跳,这里使用基本定时器TIM6完成,PSC为10-1,ARR为6000-1.在tim.c中添加如下的回调函数:

1
2
3
4
5
6
7
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim == &htim6)
{
lv_tick_inc(1); /* LVGL systick */
}
}

别忘了在定时器初始化时打开TIM6的Time base,让定时器开始工作。

lv_conf.h

启用宏:#if 1 /*Set it to "1" to enable content*/
检查以下关键信息:

  • /*Color depth: 1 (I1), 8 (L8), 16 (RGB565), 24 (RGB888), 32 (XRGB8888)*/#define LV_COLOR_DEPTH 16:颜色深度,默认为RGB565
  • #define LV_MEM_SIZE (64U * 1024U) /*[bytes]*/缓冲区大小
    ,默认64KB

其他的东西理论上你看不懂也没关系,可以暂时不动,日后可以对照着注释慢慢研究。

lv_port_disp.c

  • 设置屏幕分辨率:
1
2
3
4
5
6
7
8
9
#ifndef MY_DISP_HOR_RES
#warning Please define or replace the macro MY_DISP_HOR_RES with the actual screen width, default value 320 is used for now.
#define MY_DISP_HOR_RES 1024
#endif

#ifndef MY_DISP_VER_RES
#warning Please define or replace the macro MY_DISP_VER_RES with the actual screen height, default value 240 is used for now.
#define MY_DISP_VER_RES 600
#endif

注意,这里必须根据自己使用的屏幕分辨率来设置!如果尺寸设置的比实际屏幕小会导致实际的图像占不满整个屏幕,如果设置的比实际大会导致花屏、撕裂或干脆无法显示

  • 设置缓冲区
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

void lv_port_disp_init(void)
{
/*-------------------------
* Initialize your display
* -----------------------*/
disp_init();

/*------------------------------------
* Create a display and set a flush_cb
* -----------------------------------*/
lv_display_t * disp = lv_display_create(MY_DISP_HOR_RES, MY_DISP_VER_RES);
lv_display_set_flush_cb(disp, disp_flush);

/* Example 1
* One buffer for partial rendering*/
LV_ATTRIBUTE_MEM_ALIGN
static uint8_t buf_1_1[MY_DISP_HOR_RES * 10 * BYTE_PER_PIXEL]; /*A buffer for 10 rows*/
lv_display_set_buffers(disp, buf_1_1, NULL, sizeof(buf_1_1), LV_DISPLAY_RENDER_MODE_PARTIAL);

// /* Example 2
// * Two buffers for partial rendering
// * In flush_cb DMA or similar hardware should be used to update the display in the background.*/
// LV_ATTRIBUTE_MEM_ALIGN
// static uint8_t buf_2_1[MY_DISP_HOR_RES * 10 * BYTE_PER_PIXEL];

// LV_ATTRIBUTE_MEM_ALIGN
// static uint8_t buf_2_2[MY_DISP_HOR_RES * 10 * BYTE_PER_PIXEL];
// lv_display_set_buffers(disp, buf_2_1, buf_2_2, sizeof(buf_2_1), LV_DISPLAY_RENDER_MODE_PARTIAL);

// /* Example 3
// * Two buffers screen sized buffer for double buffering.
// * Both LV_DISPLAY_RENDER_MODE_DIRECT and LV_DISPLAY_RENDER_MODE_FULL works, see their comments*/
// LV_ATTRIBUTE_MEM_ALIGN
// static uint8_t buf_3_1[MY_DISP_HOR_RES * MY_DISP_VER_RES * BYTE_PER_PIXEL];

// LV_ATTRIBUTE_MEM_ALIGN
// static uint8_t buf_3_2[MY_DISP_HOR_RES * MY_DISP_VER_RES * BYTE_PER_PIXEL];
// lv_display_set_buffers(disp, buf_3_1, buf_3_2, sizeof(buf_3_1), LV_DISPLAY_RENDER_MODE_DIRECT);

}

我这里设置的是单缓冲区,实际按照自己需求来就可以。如果不是干那些高刷新率的活,单缓冲足够用了。原例程提供了三个缓冲区方式,分别是单缓冲、双缓冲和全尺寸双缓冲,选择合适的一个之后注释掉其他的几个就可以。

  • 显示设备初始化
1
2
3
4
5
6
7
/*Initialize your display and the required peripherals.*/
static void disp_init(void)
{
/*You code here*/
lcd_init();
lcd_display_dir(1);
}

在这里添加屏幕的初始化函数。

  • 设置刷屏
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*Flush the content of the internal buffer the specific area on the display.
*`px_map` contains the rendered image as raw pixel map and it should be copied to `area` on the display.
*You can use DMA or any hardware acceleration to do this operation in the background but
*'lv_display_flush_ready()' has to be called when it's finished.*/
static void disp_flush(lv_display_t * disp_drv, const lv_area_t * area, uint8_t * px_map)
{
if(1) {
/*The most simple case (but also the slowest) to put all pixels to the screen one-by-one*/
lcd_color_fill(area->x1, area->y1, area->x2, area->y2, (uint16_t *)px_map);

}
/*IMPORTANT!!!
*Inform the graphics library that you are ready with the flushing*/
lv_display_flush_ready(disp_drv);
}

用正点原子例程中的lcd_color_fill()函数替代原来默认的画点函数。

lv_port_indev.c

  • 修改初始化函数,添加触摸屏的初始化函数
1
2
3
4
5
static void touchpad_init(void)
{
/*Your code comes here*/
tp_dev.init();
}
  • 修改触摸屏按下的识别动作
1
2
3
4
5
6
7
8
9
10
/*Return true is the touchpad is pressed*/
static bool touchpad_is_pressed(void)
{
/*Your code comes here*/
tp_dev.scan(0);
if (tp_dev.sta & TP_PRES_DOWN) {
return true;
}
return false;
}
  • 修改坐标读取函数
1
2
3
4
5
6
7
8
/*Get the x and y coordinates if the touchpad is pressed*/
static void touchpad_get_xy(int32_t * x, int32_t * y)
{
/*Your code comes here*/

(*x) = tp_dev.x[0];
(*y) = tp_dev.y[0];
}

main.c

初始化一系列外设,对于lvgl相关的头文件则要按顺序初始化:

1
2
3
lv_init();
lv_port_disp_init();
lv_port_indev_init();

在while(1)主循环中添加time_handler

1
2
3
4
5
6
7
8
while (1)
{
/* USER CODE END WHILE */

/* USER CODE BEGIN 3 */
lv_timer_handler();
delay_ms(5);
}

到这里为止,可以加一点自己的测试代码,然后编译烧录,LVGL就能正常显示了。

如果碰到问题欢迎站内私信。


LVGL9.2移植至正点原子阿波罗STM32F429
http://akichen891.github.io/2025/01/10/LVGL9.2移植至正点原子阿波罗STM32F429/
作者
Aki
发布于
2025年1月10日
更新于
2025年1月14日
许可协议