移植流程

LVGL(LittlevGL) 是一个功能强大的开源图形用户界面(GUI)库,主要应用于嵌入式设备。为了在不同的硬件平台上运行 LVGL,我们需要进行移植(porting)。移植的目标是让 LVGL 库能够在特定的硬件和操作环境下正常运行,从而显示图形界面并响应用户输入。

测试屏幕

在移植 LVGL 之前我们最好先测试一个我们的 LED 显示屏是否可以正常工作,如果可以正常工作再将 LVGL 生成的图像渲染到屏幕上;

我们这里使用的是 P169H002-CTP 型号的 1.69 寸显示屏;他的显示芯片为 ST7789V,触控芯片为 CST816T,我们可以先通过官方提供的驱动函数先试着点亮屏幕,看屏幕是否可以支持工作

显示驱动

拷贝官方 ST7789 的驱动程序等到我们工程中;

我们需要修改官方驱动程序的引脚与我们板子上所使用的是一致的:

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
42
// st7789.h
/* choose a Hardware SPI port to use. */
#define ST7789_SPIX_RCU RCU_SPI0
#define ST7789_SPIX_PORT SPI0

/* Define the pins tp connect */
#define ST7789_RST_RCU RCU_GPIOD
#define ST7789_RST_PORT GPIOD
#define ST7789_RST_PIN GPIO_PIN_6

#define ST7789_DC_RCU RCU_GPIOB
#define ST7789_DC_PORT GPIOB
#define ST7789_DC_PIN GPIO_PIN_1

#define ST7789_CS_RCU RCU_GPIOB
#define ST7789_CS_PORT GPIOB
#define ST7789_CS_PIN GPIO_PIN_0

#define ST7789_BLK_RCU RCU_GPIOB
#define ST7789_BLK_PORT GPIOB
#define ST7789_BLK_PIN GPIO_PIN_10

// st77789.c
#define SPI_CLK_RCU RCU_GPIOA
#define SPI_CLK_PORT GPIOA
#define SPI_CLK_PIN GPIO_PIN_5
#define SPI_CLK_AF GPIO_AF_5

#define SPI_SDA_RCU RCU_GPIOA
#define SPI_SDA_PORT GPIOA
#define SPI_SDA_PIN GPIO_PIN_7
#define SPI_SDA_AF GPIO_AF_5

// main.c
#include "st7789.h"
int main() {
ST7789_Init();
// 铺满颜色,也就是背景
ST7789_Fill_Color(0xFFFF);
// 画一条线
ST7789_DrawLine(50, 50, 100, 100, 0xFF00);
}

在修改时候一定要注意,如果官方提供的代码示例(SPI 实现)是通过硬件实现的,我们在修改引脚后一定不要忘记修改引脚的复用编号,和具体使用的复用功能(SPI几)的编号;

触控驱动

拷贝官方 CST816T 的驱动程序等到我们工程中,因为触控驱动使用的是 I2C 进行通信所以这里还需要拷贝 I2C 的实现代码;

修改驱动引脚;

这里要知道触控的实现是我们点击屏幕,然后它对应的引脚就会产生变化,我们需要去监听这个引脚的变化,也就是需要一个外部中断;所以官方提供的驱动中有引脚是需要外部中断的;

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// cst816t.h
#define CST816T_RESET_RCU RCU_GPIOD
#define CST816T_RESET_GPIO GPIOD
#define CST816T_RESET_GPIO_PIN GPIO_PIN_7
// PA7
// #define CST816T_IRQ PBout(4)
#define CST816T_IRQ_RCU RCU_GPIOB
#define CST816T_IRQ_GPIO GPIOB
#define CST816T_IRQ_GPIO_PIN GPIO_PIN_4
#define CST816T_EXTI_Line EXTI_4

// cst816t.c
void CST816T_GPIOInit(void) {
//PA_6(RST)
rcu_periph_clock_enable(CST816T_RESET_RCU);
gpio_mode_set(CST816T_RESET_GPIO, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, CST816T_RESET_GPIO_PIN);
gpio_output_options_set(CST816T_RESET_GPIO, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, CST816T_RESET_GPIO_PIN);
//PA_7(INT)
rcu_periph_clock_enable(CST816T_IRQ_RCU);
gpio_mode_set(CST816T_IRQ_GPIO, GPIO_MODE_INPUT, GPIO_PUPD_PULLUP, CST816T_IRQ_GPIO_PIN);

//1.2 开启系统配置时钟
rcu_periph_clock_enable(RCU_SYSCFG);

/*3.使能NVIC中断并配置优先级*/
//3.1 设置中断优先级分组(只需要配置一次 可以放在main中)
// nvic_priority_group_set(NVIC_PRIGROUP_PRE2_SUB2);
//3.2 配置中断的抢占优先级和响应优先级 抢占优先级3,子优先级3
nvic_irq_enable(EXTI4_IRQn,3,3);
/*4.配置GPIO中断 中断引脚资源端口和中断引脚*/
syscfg_exti_line_config(EXTI_SOURCE_GPIOB,EXTI_SOURCE_PIN4);
/*5.初始化中断线 第三个参数指定上升沿和下降沿均触发*/
exti_init(CST816T_EXTI_Line,EXTI_INTERRUPT,EXTI_TRIG_FALLING);
/*6.使能中断和清除中断标志位*/
//清除中断标志位
exti_interrupt_flag_clear(CST816T_EXTI_Line);
//使能中断
exti_interrupt_enable(CST816T_EXTI_Line);
/*7.编写中断服务函数*/

//初始化
_CST816T_RESET_HIGH_();
}

void EXTI4_IRQHandler(void){
if(exti_interrupt_flag_get(CST816T_EXTI_Line) == SET) {
exti_interrupt_flag_clear(CST816T_EXTI_Line);
CST816_GetAction();
printf("touch down\r\n");
}else{
CST816_GetAction();
//CST816T_ClrBaseDatas();
printf("touch release\r\n");
}
exti_interrupt_flag_clear(CST816T_EXTI_Line);
}

修改的时候细心些,修改完引脚后,中断配置中的一些也需要修改,不要漏了,否则会导致中断无法触发;

如果上面正确,会在屏幕上显示一条线,并且点击屏幕会在中断中有打印输出;

移植流程

获取 LVGL

获取 LVGL(为了保证与 LVGL 图形化工具生成的代码兼容,二者最好选择一样的版本 v8.3)

lvgl 图形库下包含多个子目录,要移植 LVGL,只需要 src 目录下的 .c.h 文件,以及根目录下的 lvgl.h 这个文件即可。

lvgl 官方库中还包含了 examplesdemos 目录。如果你的项目需要示例或演示,你可以将这些目录添加到你的项目中。如果使用 makeCMake 来处理 examplesdemos 目录,那么就不需要额外的操作。

LVGL 本身只是一个 GUI 的图形库,他将图像生成后,需要通过驱动程序将数据发送给屏幕;

为了检测移植是否成功所以我们需要保留 examples 和 demos 目录

创建 lv_conf.h

当你第一次设置项目时,首先将 lvgl 目录下的 lv_conf_template.h 重命名为 lv_conf.h。

接着,将文件中第一个 #if 0 改为 #if 1,以启用该文件的内容。

然后,根据你使用的显示屏的颜色深度,设置 LV_COLOR_DEPTH 这个宏。有关详细信息,请查看 lv_conf.h 文件中的注释。

这里我们需要去看下我们屏幕所使用的颜色格式或者说颜色深度,我们使用的屏幕显示部分驱动用的是 ST7789 芯片,颜色深度为可选择为 6/16/18 色,这里我们让二者保持一致都选择呢 16 色即可;

#define LV_COLOR_DEPTH 16

此时我们需要的目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
lvgl/
├── demos/ # 演示代码目录
│ └── ... # 演示代码

├── examples/ # 示例代码目录
│ └── ... # 示例代码

├── src/ # LVGL 核心源代码
│ └── ... # 核心实现

├── lvgl.h # LVGL 主头文件
├── lv_version.h # LVGL 版本信息 // 如果没有可以不拷贝
└── lv_conf.h # LVGL 配置文件

代码移植

  1. 此时我们需要将裁剪后的代码添加到我们的项目中去,将 lvgl 目录下裁剪后的代码复制到我们工程下的 Middleware/LVGL 目录中;

  2. 在 Keil 中添加 lvgl 相关文件时候,要注意我们并不是要拷贝 src 下的所有代码,他有些代码是为了兼容其他架构的我们就不需要添加进来;

兼容性驱动代码:

1
2
3
4
5
6
7
8
9
10
11
Middlewares/
└── lvgl/
│ ├── draw/ # 渲染方案
│ │ ├── arm2d/ # 针对 ARM2D 硬件加速库相关的代码
│ │ ├── nxp/ # 针对 NXP 处理器相关的代码
│ │ ├── renesas/ # 针对 Renesas 处理器相关的代码
│ │ ├── sdl/ # 针对 SDL 图形库相关的代码
│ │ ├── stm32_dma2d/ # 针对 STM32 的 DMA2D 图形加速的代码
│ │ ├── sw/ # 纯 CPU 软件渲染的实现(这个目录下的代码是需要添加进去的)
│ │ ├── swm341_dma2d/ # 针对 SWM341 的 DMA2D 图形加速的代码
└── └── └── ... # 其他必要的渲染实现代码

我们再添加代码时候,上述目录中与我们使用的硬件不相关管的代码可以不用添加到我们的 Keil 工程中去;

  1. src 目录下的代码负责生成 2D 图像的像素数据,但要将这些数据传输到屏幕并正确显示出来,还需要具体硬件的驱动程序来完成屏幕的初始化和数据传输。因此,我们需要添加并移植显示设备驱动和输入设备驱动的模板文件,以实现屏幕显示功能和输入交互功能。
1
2
3
4
5
6
7
8
9
10
Middleware/
└── LVGL/
└── examples/
└── porting/
└── lv_port_disp_template.c # 显示驱动的移植模板
└── lv_port_fs_template.c # 文件系统驱动的移植模板。
└── lv_port_indev_template.c # 输入设备驱动的移植模板。
└── lv_port_disp_template.h # 显示驱动的移植模板头文件
└── lv_port_fs_template.h # 文件系统驱动的移植模板头文件
└── lv_port_indev_template.h # 输入设备驱动的移植模板头文件

我们需要将图像显示在屏幕中,并通过触摸屏去操控屏幕,所以这里我们还需要添加 lv_port_disp_template.c 和 lv_port_indev_template.c 两个文件;

添加完成后记得将头文件目录加到项目中去;

lv_conf.h ;

Middleware/LVGL/examples/porting 下驱动程序相关的头文件

如果编译不报错,则代表初步移植完成,但它此时他还会爆一些警告,因为我们还没有添加一些必须要的参数信息;

配置驱动

显示驱动

启用输出模板代码

我们虽然添加了显示的驱动程序模板,但是它默认是不启用的,所以我们这里需要配置以启用它;

我们需要修改 lv_port_disp_template.c 和 lv_port_disp_template.h 中的条件编译选项以启用他的输出模板代码;

1
#if 0 修改为 #if 1

在输出模板中初始化我们的屏幕驱动

  1. 我们要先设置显示屏的分辨率,这样可以让 LVGL 生成的图像适配我们的屏幕
1
2
3
// lv_port_disp_template.h
#define MY_DISP_HOR_RES 240 //水平分辨率
#define MY_DISP_VER_RES 280 //垂直分辨率
  1. 初始化我们屏幕驱动
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// lv_port_disp_template.c
#include "st7789.h"

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

static void disp_init(void) {
/*You code here*/
ST7789_Init();
}

相当于官方给我们提供了一个统一的显示接口,所有的显示都放在这个模板文件中,在 LVGL 层面起到了一个屏蔽不同硬件差异的一个效果

屏幕显示驱动的代码一般是由硬件的供应商提供的,我们只需要引入到我们的工程目录下并在显示驱动文件中调用即可

  1. 配置我们的输出缓冲区

LVGL 的渲染流程是先将生成的图像存放在一个缓冲区中,然后通过硬件的显示驱动,将缓冲区中的数据显示到屏幕上。

LVGL 提供了三种不同的缓冲区类型,这里我们需要配置其中选择一个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// lv_port_disp_template.c
void lv_port_disp_init(void) {
// ...
/* Example for 1) */
static lv_disp_draw_buf_t draw_buf_dsc_1;
static lv_color_t buf_1[MY_DISP_HOR_RES * 10]; /*A buffer for 10 rows*/
/*Initialize the display buffer*/
lv_disp_draw_buf_init(&draw_buf_dsc_1, buf_1, NULL, MY_DISP_HOR_RES * 10);
/*
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]; // A buffer for 10 rows
static lv_color_t buf_2_2[MY_DISP_HOR_RES * 10]; // An other buffer for 10 rows
// Initialize the display buffer
lv_disp_draw_buf_init(&draw_buf_dsc_2, buf_2_1, buf_2_2, MY_DISP_HOR_RES * 10);

// Example for 3) also set disp_drv.full_refresh = 1 below
static lv_disp_draw_buf_t draw_buf_dsc_3;
static lv_color_t buf_3_1[MY_DISP_HOR_RES * MY_DISP_VER_RES]; // A screen sized buffer
static lv_color_t buf_3_2[MY_DISP_HOR_RES * MY_DISP_VER_RES]; // Another screen sized buffer
// Initialize the display buffer
lv_disp_draw_buf_init(&draw_buf_dsc_3, buf_3_1, buf_3_2,MY_DISP_VER_RES * LV_VER_RES_MAX);
*/
}

lv_port_disp_init 函数中包含了显示驱动和显示器绘图缓冲区的配置

在 disp_flush 函数中配置打点输出

在配置完后,它会调用 disp_flush 这个函数将图形缓冲区中的内容正式的推送或者说刷新到屏幕上进行显示;

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
// lv_port_disp_template.c
// 他的代码逻辑是这样的 如果需要刷新,那么将需要刷新的区域给我,(通过 x1,y1 和 x2, y2 确定)area
// 然后将你需要刷新到屏幕上的数据也就是缓冲区的地址给我 color_p
// 然后我们就可以调用屏幕驱动提供的代码将输入写入到屏幕中去了
// lv_disp_flush_ready 用于通知 LVGL 本次刷新已经完成,可以进行下一次绘制了
static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p) {
if(disp_flush_enabled) {
/*The most simple case (but also the slowest) to put all pixels to the screen one-by-one*/
int32_t x;
int32_t y;
for(y = area->y1; y <= area->y2; y++) {
for(x = area->x1; x <= area->x2; x++) {
/*Put a pixel to the display. For example:*/
/*put_px(x, y, *color_p)*/
color_p++;
}
}
}
/*IMPORTANT!!!
*Inform the graphics library that you are ready with the flushing*/
lv_disp_flush_ready(disp_drv);
}

// 因为 ST7789 提供了刷新屏幕的代码所以我们这里需要一下替换
static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p) {
if(disp_flush_enabled) {
ST7789_Fill(area->x1,area->y1,area->x2,area->y2,(uint16_t*)color_p);
}
/*IMPORTANT!!!
*Inform the graphics library that you are ready with the flushing*/
lv_disp_flush_ready(disp_drv);
}

我们需要在 LVGL 官方提供的输出模板中初始化我们屏幕的显示芯片,然后调用显示函数将 LVGL 生成的图像显示在我们的屏幕上;

触摸驱动

配置触摸驱动与显示流程是一样的,区别在于文件换成了 lv_port_indev_template.h 和 lv_port_indev_template.c

注释不必要的代码

这里要注意的是,LVGL 支持多种输入方式,而我们硬件只支持触摸的方式,所以我们需要先将除了触摸意外的输如配置给删掉

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
// lv_port_indev_template.c
// 只保留触屏相关的代码
static void touchpad_init(void);
static void touchpad_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data);
static bool touchpad_is_pressed(void);
static void touchpad_get_xy(lv_coord_t * x, lv_coord_t * y);
/*
static void mouse_init(void);
static void mouse_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data);
static bool mouse_is_pressed(void);
static void mouse_get_xy(lv_coord_t * x, lv_coord_t * y);

static void keypad_init(void);
static void keypad_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data);
static uint32_t keypad_get_key(void);

static void encoder_init(void);
static void encoder_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data);
static void encoder_handler(void);

static void button_init(void);
static void button_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data);
static int8_t button_get_pressed_id(void);
static bool button_is_pressed(uint8_t id);
*/
// 删除上面不需要的具体的方法

初始化驱动配置

1
2
3
4
5
/*Initialize your touchpad*/
static void touchpad_init(void) {
/*Your code comes here*/
CST816T_Init();
}

判断屏幕是否被按下

1
2
3
4
5
6
/*Return true is the touchpad is pressed*/
static bool touchpad_is_pressed(void) {
/*Your code comes here*/
return CST816T_is_pressed();
// return false;
}

获取被按下位置的坐标

1
2
3
4
5
6
7
/*Get the x and y coordinates if the touchpad is pressed*/
static void touchpad_get_xy(lv_coord_t * x, lv_coord_t * y) {
/*Your code comes here*/
CST816T_get_xy((uint16_t*)x,(uint16_t*)y);
//(*x) = 0;
//(*y) = 0;
}

我们可以通过触控芯片判断是否被按下和获取坐标,这里为什么还需要多此一举在封装一次呢?

1.规范化,LVGL 提供了一个统一的接口这样可以避免硬件之间的差异化,

2.LVGL 本身他也有一些事件的处理机制,如果我们直接通过触控芯片去判断是否有操作,那么就相当于绕过 LVGL 的事件处理机制,他不知道屏幕上有一些操作,也就会导致UI 元件的事件回调将无法被触发,我们需要手动触发,这样就太麻烦了;

LVGL 的事件处理机制是当他检测到有操作时候,会去执行对应的回调函数,然后等到 lv_timer_handler() 执行时就会将图像缓冲区的内容显示到屏幕上,如果绕过去,他就无法判断是否有操作,那么也就无法执行回调函数,那么他就会认为图像缓冲区中的内存没有改动,他也就不会将内容显示在屏幕上;

初始化 LVGL

按照官方示例,初始化 LVGL 步骤如下:

  1. 在系统执行早期通过调用初始化 LVGL 一次 lv_init()。这需要在进行任何其他 LVGL 调用之前完成。

    lv_init();

  2. 初始化您的驱动程序。

    lv_port_disp_init();、lv_port_indev_init();

  3. 连接 Tick 接口。

    lv_tick_inc(1)

  4. 连接显示接口。

  5. 连接输入设备接口。

  6. lv_timer_handler() 通过每隔几毫秒调用一次来管理 LVGL 计时器,从而驱动 LVGL 时间相关任务。请参阅计时器处理程序以了解执行此操作的不同方法。

    lv_timer_handler()

  7. 可选择使用 来设置主题 lv_display_set_theme()。

  8. 此后,在需要使用 LVGL 函数的源文件中 #include “lvgl/lvgl.h”。

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
// main.c
int main(void) {
int count = 0;
systick_config();
nvic_priority_group_set(NVIC_PRIGROUP_PRE2_SUB2);
usart0_init();

bsp_hard_iic_config();
// 1. LVGL 初始化
lv_init();
// 2. 驱动初始化
lv_port_disp_init();
lv_port_indev_init();

ST7789_Test();

printf("App_start...\n");

while(1){
if(count >= 5) {
count = 0;
// 6.调用回调函数
lv_timer_handler();
}
// 3. 连接 Tick 接口
lv_tick_inc(1);
delay_1ms(1);
count++;
}
}

1.我们要使用 LVGL 图像库,必须要在使用前需要先初始化 LVGL,他会初始化其内部的数据结构和功能模块(例如,任务管理器、显示缓冲区管理等)。不初始化会导致后续的 LVGL 代码无法执行。

2.我们要将图像显示在屏幕上,并通过触屏去操控屏幕,所以需要初始化屏幕和触控的驱动,他会初始化显示缓冲区等。

3.连接 Tick 接口,因为 LVGL 有些任务是和时间挂钩的,他需要感知时间的流逝,他不像我们板子可以依靠晶振去计数时间,所以需要我们给他提供一个计数的方式(时基或者心跳), lv_tick_inc(1) 中 1 表示 1ms 也就是说这个函数调用一次就表示过去了 1 ms,LVGL 通过这种函数调用的方式来进行计数;

4.连接显示接口,LVGL 只是一个图形库,它本身并不会直接操控屏幕;为了让 LVGL 能将绘制的内容显示到屏幕上,我们需要将使用的显示驱动添加或者说注册 LVGL 中,它会将一些配置,比如缓冲区的内容、屏幕像素的大小添加进 LVGL 中;这一步在初始化屏幕时候它会自动调用,我们通常不需要显示的去手动执行;lv_disp_drv_register(&disp_drv);

5.连接输入设备接口同上,但要注意的是我们需要给输出设备提供一个回调函数用于处理 LVGL 返回的数据;

6.定期调用 lv_timer_handler() 函数,这个函数它会检查并执行所有定时器任务、动画任务,以及更新显示缓冲区并触发渲染;

移植到 FreeRTOS

移植到 FreeRTOS 就是在项目中同时导入 FreeRTOS 和 LVGL,然后将 LVGL 的时基和中断回调函数放在 FreeRTOS 的任务中去执行;

修改 stm43fxxx.h 中 __NVIC_PRIO_BITS 的值

将 __NVIC_PRIO_BITS 的值从 4U 改为 4;

__NVIC_PRIO_BITS 是一个宏,它定义了中断优先级字段的位数;FreeRTOS 会通过这个宏去适配 STM32 的优先级,让二者能够保持一致,而在 FreeRTOS 中类型(5 << (8 - 4U))这种写法他是不识别的,在以前的 ARM 编译器中他是不支持这种写法的,他会认为这是两种不同的类(常量类型和一种未定义的类型)他会认为这二者是不可以进行逻辑运算的所以我们需要修改这个宏将 4U 改为4

在当前的版本中如果不报错一般可以不用进行修改,1 是在 STM32fxxx.h 中 __NVIC_PRIO_BITS 它被定义为了 4,二是在高版本的 ARM 编译器中,他已经可以识别 4U 这种写法并会去做一些优化;

在 lv_conf.h 文件中配置自定义时钟源

在不用 FreeRTOS 之前我们是将 LVGL 的时基或者说心跳直接写在我们的main函数或者定时器中的,既然使用了 FreeRTOS 我们就需要将时基交给 FreeRTOS 去处理;

LVGL 提供了自定义时钟源的配置,可以让我们指定时基时从哪产生的

1
2
3
4
5
6
7
8
9
10
#define LV_TICK_CUSTOM 1
#if LV_TICK_CUSTOM
// Header for the system time function
#define LV_TICK_CUSTOM_INCLUDE "FreeRTOS.h"
// Expression evaluating to current system time in ms
#define LV_TICK_CUSTOM_SYS_TIME_EXPR (xTaskGetCount())
/*If using lvgl as ESP32 component*/
// #define LV_TICK_CUSTOM_INCLUDE "esp_timer.h"
// #define LV_TICK_CUSTOM_SYS_TIME_EXPR ((esp_timer_get_time() / 1000LL))
#endif /*LV_TICK_CUSTOM*/

#define LV_TICK_CUSTOM 1,启用自定义时钟源

#if LV_TICK_CUSTOM,相当于一个开关,非 0 时执行下面的代码

#define LV_TICK_CUSTOM_INCLUDE “FreeRTOS.h”,定义一个宏指向 “FreeRTOS.h” 这个头文件,这个文件提供了 FreeRTOS 中的各种任务管理、调度器、时间管理等接口函数。

#define LV_TICK_CUSTOM_SYS_TIME_EXPR (xTaskGetCount()),指向了 xTaskGetCount()) 这个函数,该函数用于获取系统启动以来的 tick 数(时钟节拍次数),它相当于系统的 tick 计数器;每个 tick 通常是 1 毫秒(或更小,具体由 configTICK_RATE_HZ 配置决定)。

编写 FreeRTOS 任务函数进行 LVGL 任务调度

1
2
3
4
5
6
7
void lv_Timer_task(void* pvParameters) {
pvParameters = pvParameters;
while(1) {
lv_timer_handler();
vTaskDelay(4);
}
}

创建一个任务专门用于 LVGL 的任务调度,这样就成功将 FreeRTOS 与 LVGL 结合在一起了;

外部 SRAM

外部 SRAM 是个硬件,当内部 SRAM 不足以存放所需的数据时,我们就需要外接一个 SRAM 用来提供额外的存储空间;

由于 LVGL 是一个图形库,涉及大量的界面渲染和控件更新,这些操作会消耗较多的内存。如果系统内部的 SRAM 不够用,尤其是在处理较大的 GUI 元素或图形时,外部 SRAM 就变得非常重要。

我们就正常把 SRAM 当成一个外设芯片,将他的驱动添加到我们的工程中去使用就可以了;

使用

  1. 将 LVGL 管理的内存空间放到外部 SRAM 中(非常不推荐,因为速度会变慢)

    1. 确定外部 SDRAM 首地址,根据需求确定地址偏移
    2. 在 lv_conf.h 中将 LV_MEN_ADR 定义到外部的 SRAM 中去(不一定是首地址)
  2. 将 绘图缓冲区放到外部 SARM 中去(如果内存不够用时候可以采取)

    1. 确定外部 SDRAM 首地址,根据需求确定地址偏移
    2. 在 lv_port_disp_template.c 中创建全屏分辨率大小的数组,并将其定位到外部 SRAM 的地址

内存管理

LVGL 内存报错

LVGL 本身渲染会占用一定的内存如果报错我们可以从以下几个角度去分析:

1.修改 lv_conf.h,适当减小分配给 LVGL 管理的内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*1: use custom malloc/free, 0: use the built-in `lv_mem_alloc()` and `lv_mem_free()`*/
#define LV_MEM_CUSTOM 0
#if LV_MEM_CUSTOM == 0
/*Size of the memory available for `lv_mem_alloc()` in bytes (>= 2kB)*/
#define LV_MEM_SIZE (48U * 1024U) /*[bytes]*/
/*Set an address for the memory pool instead of allocating it as a normal array. Can be in external SRAM too.*/
#define LV_MEM_ADR 0 /*0: unused*/
/*Instead of an address give a memory allocator that will be called to get a memory pool for LVGL. E.g. my_malloc*/
#if LV_MEM_ADR == 0
#undef LV_MEM_POOL_INCLUDE
#undef LV_MEM_POOL_ALLOC
#endif
#else /*LV_MEM_CUSTOM*/
#define LV_MEM_CUSTOM_INCLUDE <stdlib.h> /*Header for the dynamic memory function*/
#define LV_MEM_CUSTOM_ALLOC malloc
#define LV_MEM_CUSTOM_FREE free
#define LV_MEM_CUSTOM_REALLOC realloc
#endif /*LV_MEM_CUSTOM*/

#define LV_MEM_CUSTOM 0 表示是使用自动内存管理的方式,我们不去动他;我们只需要修改 #define LV_MEM_SIZE (48U * 1024U) 定义的大小即可,要保证可以显示 LVGL 的所有小组件,但是又不能太大;可以试着一点一点去进行调试;

2.lv_port_diso_timplate.c,适当减小图像缓冲区的大小,同时需要兼顾运行效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
        /* Example for 1) */
static lv_disp_draw_buf_t draw_buf_dsc_1;
static lv_color_t buf_1[MY_DISP_HOR_RES * 10]; /*A buffer for 10 rows*/
/*Initialize the display buffer*/
lv_disp_draw_buf_init(&draw_buf_dsc_1, buf_1, NULL, MY_DISP_HOR_RES * 10);
/*
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]; // A buffer for 10 rows
static lv_color_t buf_2_2[MY_DISP_HOR_RES * 10]; // An other buffer for 10 rows
// Initialize the display buffer
lv_disp_draw_buf_init(&draw_buf_dsc_2, buf_2_1, buf_2_2, MY_DISP_HOR_RES * 10);

// Example for 3) also set disp_drv.full_refresh = 1 below
static lv_disp_draw_buf_t draw_buf_dsc_3;
static lv_color_t buf_3_1[MY_DISP_HOR_RES * MY_DISP_VER_RES]; // A screen sized buffer
static lv_color_t buf_3_2[MY_DISP_HOR_RES * MY_DISP_VER_RES]; // Another screen sized buffer
// Initialize the display buffer
lv_disp_draw_buf_init(&draw_buf_dsc_3, buf_3_1, buf_3_2,MY_DISP_VER_RES * LV_VER_RES_MAX);
*/

这个空间越大,理论上运行的效果越好,我们尽量不是去给他调的太小;

3.FreeRTOSConfig.h,适当减小分配给 FreeRTOS 的内存,简单的工程一般 10~20k 就够了

1
#define configTOTAL_HEAP_SIZE ( ( size_t ) ( 75 * 1024 ) ) // 堆的大小,单位是字节

这里设置减小但一般不要小于 10~20k;

错误

一、**../Middleware/LVGL/src/core../lv_conf_internal.h(41): error: ‘../../lv_conf.h’ file not found**

说明没有添加 lv_conf.h 头文件目录,添加即可;