STM32 移植 LVGL -- 教程图解

news2024/10/5 19:28:52

( 编辑状态中,已完成80%,估计清明假期后完成更新 )

移植效果,先睹为快:


目录

一、LVGL 简述

二、准备一个STM32的工程

三、LVGL 官方下载

四、裁剪 源文件

五、添加 源文件

六、注册 显示

七、注册 触摸输入

八、提供 LVGL 心跳、任务处理

九、开跑 LVGL 

十、器件的事件添加、响应处理

十 一、进阶


一、LVGL 简述

  • 丰富且强大的模块化图形组件:按钮 、图表 、列表、滑动条、图片等
  • 高级的图形引擎:动画、抗锯齿、透明度、平滑滚动、图层混合等效果
  • 支持多种输入设备:触摸屏、 键盘、编码器、按键等
  • 不依赖特定的硬件平台
  • 配置可裁剪,最低资源占用:64 kB Flash,16 kB RAM
  • 基于UTF-8的多语种支持,例如中文、日文、韩文、阿拉伯文等
  • 可以通过类CSS的方式来设计、布局图形界面(例如:Flexbox、Grid)
  • 支持操作系统、外置内存、以及硬件加速(已内建支持STM32 DMA2D)
  • 即便仅有单缓冲区(frame buffer)的情况下,也可保证渲染如丝般顺滑
  • 支持模拟器仿真,可以无硬件依托进行开发


二、复制一个STM32工程

1、硬件要求

  • 芯片资源:Flash>128K, RAM>64K; (LVGL至少占用: Flash>64K, RAM>16K); 
  • 与芯片型号无关,F1、F4、H7等系列的芯片,满足上述资源的都行;
  • 不建议使用常用的STM32F103C8,资源太小,裁剪难度大,强行移植了也会很卡。
  • 显示屏:建议使用16位色深的彩屏, 1.44寸、2.8寸、4.3寸等等;
  • 不建议使用常用的0.96寸OLED屏,指甲大小的单色屏,耗100K资源去撑它,没搞头。

2、软件环境

  • 代码:标准库、寄存器、手撸HAL库、CubeMX生成的HAL库、LL库,都可以;
  • 开发环境:Keil、CubeIDE,都可以; 

3、复制一个触摸屏的STM32工程

这个工程,必须先调试好下面四项:

  • Heap和Stack,都设置为:0x1000; 
  • 显示:画点函数;
  • 触摸:触摸状态检测函数 (返回:0-未按下、1-按下)、坐标获取函数;  
  • 1ms时基:可以用Systick、TIM6、TIM1等,只要能产生1ms的都行,建议TIM6。

上述四项功能,是玩LVGL最基本要求。

如果有一项你没看懂,就先把它盘透,回头再盘LVGL。

本篇,复制了开发板的一个示例作移植的基础工程:"显示屏_2.8寸_触摸检测_XPT2046"。

此示例已带上述前三项功能,最后那个1ms时基,在文中增加。

4、对这个工程,编译一次以确保 0 Error;  烧录测试,以确保能正常触摸和显示。


三、下载 LVGL

LVGL尽管已发布了v9.0、v9.1等,但v8.3版,应该是目前玩家们的至爱。

v8.3版本,网上教程众多、移植简单;

更重要的是:好几款主流的可视化设计工具,都支持v8.3版本!

因此,推荐使用v8.3版的LVGL。

 官方下载链接:https://github.com/lvgl/lvgl

1、选择版本

2、下载

3、下载后,解压缩得到文件夹:lvgl-release-v8.3


四、裁剪 源文件

解压后得到源文件夹: lvgl-release-v8.3。

源文件夹里头文件众多:源代码、帮助文档、官方示例等等。

我们只复制需要用到的文件:3个文件夹 + 2个h文件。

1、新建一个文件夹

因为LVGL源代码中的头文件,使用了相对路径,如在 "lvgl.h" 中:

为了令移植后的文件能直接使用这些相对路径,我们复制文件时,按下方目录结构来操作:

  • 在你喜欢的硬盘位置,新建文件夹:LVGL 
  • 在源文件夹中,把下图选中的 3个文件夹、2个h文件,  复制到新建的 LVGL文件夹中;

完成后,我们的 LVGL 文件夹,是这个样子的:

提醒:

  • 网上好些教程,在keil工程目录下新建 Middlewares 文件夹,在里面再新建LVGL文件夹。
  • 如果你使用的是标准库的工程,或者是自己手撸建立的HAL库工程,都可以那样操作。
  • 但是,如果使用CubeMX、CubeIDE生成的工程,就不要使用 “Middlewares” 作文件夹名称。
  • 因为 "Middlewares",刚好是CubeMX可能生成的文件夹,用来存放中间件,如:FreeRTOS、FatFS等支持文件。如果你没有使能这些中间件,那么 ,CubeMX重新生成工程时,"Middlewares"文件夹就会被认为不需要了,被删除掉。

 2、修改 lv_conf.h 文件名

在我们的 LVGL 文件夹中,有 h文件:"lv_conf_template.h",是LVGL配置参数的重要文件。

  • 原文件名:“lv_conf_template.h”,修改为: "lv_conf.h";

完成后,是这个样子的:

3、删除不需要的文件夹

打开文件夹:LVGL / examples:

  • 只保留 porting 文件夹,其它的文件夹和文件,都删除掉。

完成后,是这个样子的:

4、修改 porting 里面的文件名称

打开  porting 文件夹:

  • 6个文件的名称,都删除 "_template" 字样

完成后,是这个样子的:

好了,现在LVGL文件夹,已经是我们需要的结果。

这个LVGL文件夹,以后可以复制给各类的工程使用,不限于STM32的工程,通用。


五、工程添加 LVGL 文件 

现在,我们开始给STM32工程添加LVGL源文件。

1、复制 LVGL 文件夹,粘贴到工程目录下。

每个人的工程文件夹,几乎都不一样,没关系的。

  • 把上一步做好的 LVGL 文件夹,复制到工程目录下

完成后,是这个样子的:

2、打开Keil,在工程里,添加4个文件夹(Groups);

操作过程、完成后,是这个样子的:

文件夹名称 (Groups)存放文件的种类
LVGL_apps用户自己的界面代码文件、官方demo等
LVGL_confLVGL 的两个h文件
LVGL_portingLVGL 的接口文件, 如显示、触摸屏、键盘等
LVGL_srcLVGL 的所有底层c文件

提示:

  • 网上好些教程会新建近10个Group, 分开存放各个子功能文件。4个文件夹够了,简单直观。
  • 这里用下划线作名称分界线。尝试过使用“ / ”, 感觉没下划线直观。你可以用自己喜欢风格。 

3、给文件夹(Group),添加文件

这一步,最容易出错。

无聊、枯燥,估计耗时两分钟左右。

很多人在后面的编译中,出现error,提示缺少文件,基本是在这一步把某个文件添加漏了。

务必细心的操作!!

操作过程,步骤如下:

重要:每个文件夹(Group),需要添加的文件,如下表:

文件夹 (Group)添加文件
LVGL_apps不用添加
LVGL_confLVGL 文件夹下的: lv_conf.h、lvgl.h,共2个文件 (要选择文件类型才能看到)
LVGL_portingLVGL/ examples / porting 文件夹下的:lv_port_disp.c 、lv_port_disp.h、 lv_port_indev.c、lv_port_indev.h;共4个文件。(要选择文件类型才能看到 h 文件)
LVGL_src添加 LVGL / src 下的所有 c 文件; 重点:包括所有子、子子文件夹的 c 文件

关于 LVGL_src 的添加,特别地提醒,:

  • src文件夹下,会有多重的子文件夹,必须慢慢地、把每一个子文件夹的C文件全部添加进来;
  • 只须添加 c 文件,不用添加其它类型的文件,如:h、mk等;
  • 建议每添加完一个 Group,  停一停,放松一下,检查一下,再添加另一个Group。
  • 添加完毕后,必须点击"OK"保存,  不然,你会后悔。

完成后,Keil工程的资源管理器中,是这个样子的:

4、工程中,添加 LVGL 的头文件目录

打勾C99,并,添加3个头文件路径:

  • 添加:LVGL 文件夹的路径
  • 添加:LVGL\src 文件夹的路径
  • 添加:LVGL\examples\porting 文件夹的路径 

操作过程、完成后,是这个样子的:

5、编译验证

来到这一步,我们必须先编译一次,以验证文件是否都添加完整。

如果添加文件没有遗漏、添加头文件路径正确,那么,编译后应该是: 0 Error。

会有一大堆 Warning,不用管,不影响的。

如果,编译后,有  Error 报错:

  • 检查是否打勾: C99
  • 先检查头文件路径 ,是否添加完成;
  • 如果头文件路径添加正确,那么,基本是添加文件那一步有遗漏了,删了Group,重新添加。


六、注册 显示

1、启用 lv_conf.h

双击打开 lv_conf.h,对以下内容进行修改,以启用此文件。

  • 第15行,原:#if 0,修改为:#if 1 

完成后,是这个样子的:

2、启用 lv_port_disp.h

双击打开 lv_port_disp.h,修改以下内容,以启用此文件:

  • 第7行,原:#if 0, 修改为:#if 1 
  • 第22行,原:“lvgl/lvgl.h", 修改为:”lvgl.h"

完成后,是这个样子的:

3、启用  lv_port_disp.c 

双击打开 lv_port_disp.c,修改以下内容,以启用此文件:

  • 第7行,原:#if 0, 修改为:#if 1 
  • 第12行,原"lv_port_disp_template.h", 修改为:"lv_port_disp.h"

完成后,是这个样子的:

4、添加 LCD 驱动的头文件

在 lv_port_disp.c中:

  • 第14行,插入你的LCD驱动文件,如:#include "bsp_LCD_ILI341.h",写上你的h文件。
  • 第20行、第25行,是显示屏的宽、高度。填写 LCD实际像素。小篇用2.8寸屏,不用修改。

注意,LVGL默认使用横屏的方式,这一点要注意,宽度、高度的值别写反了。

完成后,是这个样子的

5、选择创建缓存的方式,3选1

还是在 lv_port_disp.c 中,向下滚动,

第86行到101行,是创建显示缓冲区的3种方式。

LVGL提供了3种方式,需要3选1。绝大多数情况下,使用第1种方法:创建1个缓冲区;

  • 注释掉第90~101行,即:不使用第2和第3种方法;

完成后,是这个样子的:

6、给 LVGL一个画点函数

还是在 lv_port_disp.c 中,向下滚动,

  • 第173行,disp_flush( )函数,添加 LCD 的画点函数; 参数:x坐标、y坐标、16位颜色值。

你的画点函数,可能和小篇所用的不一样,照样画瓢即可。

完成后,是这个样子的:

这里给LVGL一个画点函数后, LVGL就能完成需要的显示操作了。

进阶技巧(可以忽略):

一般地,画点函数底层操作是:发送X坐标指令、X值、Y坐标指令、Y值、像素点的颜色值。

假如要刷320x240的整屏,得传输14万次指令、14万次坐标值,7万次颜色值,相当耗时。

要是你的LCD驱动文件中,有区域填充颜色的函数,就能大量地减少指令、坐标值的发送次数。

下面是使用 魔女开发板 LCD驱动文件中所提供的 区域填充 函数,可以效仿参考。

  • LCD_DispFlush(area->x1, area->y1, area->x2, area->y2, (uint16_t*)color_p);

如果没有区域填充函数,不用强求,直接使用画点函数吧,先完成,再完善。

至此,显示部分的修改、注册,已完成。

点击保存,再做下一步操作。

提示:

细心的朋友,如果参考过其它LVGL教程,可能会有疑问。

为什么步骤明显少了?是不是漏了 disp_init()的那部分?

是的,我们没有为这个函数填入LCD的初始化函数。

没必要这样做。

在下面的第十部分,将会在main.c里直接初始化 LCD,更符合开发习惯,更清晰。

莫急~


七、注册 触摸屏

1、启用 "lv_port_indev.h"

打开"lv_port_indev.h", 修改以下内容,以启动此文件:

  • 第8行,原:#if 0,  修改成:#if 1
  • 第20行,原:"lvgl / lvgl.h", 修改成:"lvgl.h"

完成后,是这个样子的:

2、启动 "lv_port_indev.c"

打开"lv_port_indev.c", 修改以下内容,以启动此文件:

  • 第 7行,原:#if 0, 修改为:#if 1
  • 第12行,原:“lv_port_indev_template.h", 修改为:"lv_port_indev.h"
  • 第13行,原:"../../lvgl.h",修改为:"lvgl.h"

完成后,是这个样子的:

3、添加  触屏 的驱动头文件

还是在 "lv_port_indev.c" 中:

  • 第14行,#include "触摸屏的头文件",小编这边是:#include "bsp_XPT2046.h"

完成后,是这样子的:

4、注释掉不需要的输入任务注册

还是在 "lv_port_indev.c" 中,

向下滚动至大约70行,找到输入注册函数:lv_port_indev_init( ):

  • 函数内有5种输入方式的任务注册;
  • 保留触摸屏输入的任务注册;
  • 其它4种输入任务的注册,暂时不用,注释掉;

完成后,是这个样子的:

5、添加 触摸检测函数

还是在 "lv_port_indev.c" 中,

向下滚动到大约209行,找到触摸检测函数:touchpad_is_pressed(),

在函数内添加你的触摸屏状态检测函数; 

  • 第212行,即函数内,添加触屏状态检测函数,函数返回必须是:0-未按下、1-按下
  • return XPT2046_IsPressed();

完成后,是这个样子的:

6、添加 坐标获取函数

还是在 "lv_port_indev.c" 中,

在刚才触摸检测函数的下方,找到坐标获取函数:touchpad_get_xy();

  • 第221行,即函数内,为坐标 x、y 提供赋值方法,使LVGL能够获取到触摸按下时的坐标;
  • (*x) = XPT2046_GetX();
  • (*y) = XPT2046_GetY();

完成后,是这个样子的:

7、额外的测试预埋

(这一步,是非必须的,可以选择跳过。)

在后续的按钮测试中,有可能发生触摸坐标与显示坐标不符合的情况。

我们在这里先预埋一个操作,当后面发生问题,不用傻傻的盲猜原因。

就在刚才的那个 touchpad_get_xy( ) 函数中,增加加一行画点操作:

  • 第222行下方,插入新行,编写画点操作:LCD_DrawPoint( *x, *y, BLACK);

完成后,是这个样子的:

到此,触摸屏的注册,已经完成。

提示:

参考过其它教程的细心的朋友,这里又会发现,相比其它教程,这里又少了步骤!

在其它教程中,会把其它几种输入方式的相关获取函数,都注释掉。

即:大约第230行~408行,鼠标输入、键盘输入、编码器输入....,统统注释掉。

不需要这样操作!

你没有为那些输入注册任务,也不调用它们,它们就不起任何作用。

编译器聪明着呢!在编译时,将自动忽略死代码(即使是Level 0,死代码也不会产生影响)。


八、LVGL 心跳、任务刷新

根据官方移植文档的要求,我们要处理两个关于时间的问题:

  • 间隔精准地,调用时基函数:lv_tick_inc(),俗称心跳,让LVGL精准地知道的时间流逝;

  • 间隔5ms左右,调用周期性任务函数: lv_timer_handler() ,它的作用是检查所有注册任务的时间戳,执行那些已经到期的任务,如:屏显更新、动画更新、触控、定时器事件等;

1、给LVGL一个心跳时基

LVGL心跳函数(时基函数):lv_tick_inc(),每隔1ms调用一次;

这个函数对于图形界面的流畅运行至关重要,它令 LVGL 知道执行任务时流逝的时间。

如果 lv_tick_inc() 调用间隔不准确,可能会导致显示卡顿、任务处理不及时。

特别地,不建议使用滴答时钟SysTick产生这个时基,因为它常常需要被用于RTOS等。

建议使用TIM产生1ms中断,设置它的中断为高优先级,通过中断函数调用LVGL心跳时基。

你可以通过各个TIM、各种方法,产生1ms中断,如寄存器操作、标准库、手撸HAL等等。

本篇示例,通过CubeMX配置TIM6,产生1ms中断:

打勾TIM6的中断,并设置中断抢占级为:0,(默认也是0);

让CubeMX重新生成,令配置更新到工程代码后,在main.c中调用HAL函数:启动TIM6,并使能它的周期更新中断。

  • HAL_TIM_Base_Start_IT(&htim6);

 完成后,是这个样子的:

然后,编写周期更新中断的回调函数中,在里面调用lv_tick_inc( ),给LVGL提供心跳;

完成后,是这个样子的:

回调函数解释:

这是一个TIM的周期更新中断回调函数,它是定义在***_hal_tim.c中的一个弱定义函数。

所以TIM发生周期更新中断时,都会统一调用它。

我们需要在期待的位置,重写这个函数。本篇写在了main.c的尾部。

在回调函数中,我们调用:lv_tick_inc(1),参数为1,即让LVGL知道,1ms已经过去了。

如果你设置TIM产生的是2ms的中断,也可以:lv_tick_inc(2),效果是一样的。

另外 :

在这个中断回调函数中,我们额外地添加了LED每0.5ms闪烁的代码。

目的是为了调试时,可以肉眼判断定时器TIM是否按预期正常工作。

只有TIM6按预期正常工作了,才能给 LVGL 一个准确的心跳时基。

2、每隔5ms左右,调用任务函数 lv_timer_handler()

这个函数的作用:让LVGL检查所有已注册任务的时间戳,执行那些已经到期的任务,如刷屏、检测触摸等;

官方描述:大约5ms左右、在while循环中调用;

特别地:不要使用TIM产生5ms中断去调用它,因为它的执行时间有点长,不适合霸占中断资源。

  • 在msin.c的while中,每隔5ms调用:lv_timer_handler()

完成后,是这样子的:

至此,时间需求也处理完毕。

LVGL的移植,已全部完成 。


九、开跑 LVGL 

之前的几个部分,完成了底层显示、触摸的支持。

现在,正式让LVGL在工程中“应用”。

1、给工程,添加 LVGL 的头文件

打开 main.c,在顶部, #include 三个头文件:

  • #include "lvgl.h"                       // 它为整个LVGL提供了更完整的头文件引用
  • #include "lv_port_disp.h"         // LVGL的显示支持
  • #include "lv_port_indev.h"       // LVGL的触屏支持

完成后,是这个样子的:

2、初始化LCD、触摸屏

在main函数内、 while 循环之前,调用LCD初始化函数、触摸屏初始化函数。

删除示例中多余的显示代码、触摸代码。

下图,是小篇所用开发板的LCD驱动函数:

  • LCD_Init();                                                                    // 初始化 LCD
  • LCD_SetDir(1);                                                             // 设置LCD的显示方向:横屏
  • XPT2046_Init(xLCD.width, xLCD.height,  xLCD.dir);   // 初始化触摸屏

还记得前几步时,我们没有像其它教程那样,给disp_init() 填入LCD的初始化函数。

现在,随着其它的设备,把初始化放在一起,更符合习惯、更直观。

完成后,是这个样子的:

提示:

在上图的第187行:W25Q128_Init();

它是外部Flash设备W25Q128的的初始化函数。

开发板的触摸屏校准数据,存储在它里面。每次上电,要从它里面读取之前的校准数据。

如果你用的不是魔女开发板,或者,有其它的储存渠道,可以不用对它初始化。

3、初始化LVGL、显示、触屏

在开启TIM6的那行代码上面,进行LVGL的初始化:

  • lv_init();                                     // LVGL 初始化
  • lv_port_disp_init();                    // 注册LVGL的显示任务
  • lv_port_indev_init();                  // 注册LVGL的触屏检测任务

完成后,是这个样子的:

4、显示按钮控件、文本控件

在开启TIM6的那行代码下面,添加LVGL控件 ,开始测试LVGL的显示:

  • 添加一个按钮
  • 为按钮添加文本
  • 添加一个独立的标签文本

具体代码如下:

    // 按钮
    lv_obj_t *myBtn = lv_btn_create(lv_scr_act());                               // 创建按钮; 父对象:当前活动屏幕
    lv_obj_set_pos(myBtn, 10, 10);                                               // 设置坐标
    lv_obj_set_size(myBtn, 120, 50);                                             // 设置大小
   
    // 按钮上的文本
    lv_obj_t *label_btn = lv_label_create(myBtn);                                // 创建文本标签,父对象:上面的btn按钮
    lv_obj_align(label_btn, LV_ALIGN_CENTER, 0, 0);                              // 对齐于:父对象
    lv_label_set_text(label_btn, "Test");                                        // 设置标签的文本

    // 独立的标签
    lv_obj_t *myLabel = lv_label_create(lv_scr_act());                           // 创建文本标签; 父对象:当前活动屏幕
    lv_label_set_text(myLabel, "Hello world!");                                  // 设置标签的文本
    lv_obj_align(myLabel, LV_ALIGN_CENTER, 0, 0);                                // 对齐于:父对象
    lv_obj_align_to(myBtn, myLabel, LV_ALIGN_OUT_TOP_MID, 0, -20);               // 对齐于:某对象

完成后,是这个样子的:

好了,已经编写好让LVGL显示控件的代码,如果顺利,LVGL马上就要绽放了!

先编译一下!

0 Error, 35 Warning。

没有错误。那35个警告,不用管它。

这里要探讨的重点是:Flash和RAM的资源占用!

  • 程序 FLASH 占用 = Code + RO-data + RW-data  = 163172 + 31808 + 592 = 190K
  • 程序 RAM 占用    =  RW-data + ZI-data = 592 + 80504 = 80K

现在,你要回到所用芯片资源的焦点上了。

常用的几款STM32芯片资源:

芯片型号FlashRam
STM32F103RC256 K48 K
STM32F103VE512 K 64 K
STM32F407VE512 K192 K
STM32H750VB128 K1056 K

很明显,程序需要80K 的RAM,F103RC、F103VE,不够哗。

如何办?

( 放完假 回来后再更新吧,顶不住了,太累人了)

好了,现在烧录程序到开发板!!

一百多K, 烧录的时间,会有点长,大约耗时十来秒。

运行效果如下:

显示正常,显示部分已移植成功!

触摸正常,按下时按钮的状态生产了变化,触摸部分也移植成功!

恭喜你,运气太TM的好了。

但是,一次就成功的机率太低了。

更大可能出现的情况是:显示正常,触摸没反应!

5、触摸没反应的排查

就如上面动图所显示的,点击按钮时,按下、释放,按钮的状态是不一样的。

如果按钮在按下时没有反应、不会产生状态变化 ,3个排查范围:

  • 触摸检测:lv_port_indev.c 第209行:touchpad_is_pressed(),状态判断有问题;
  • 坐标获取:lv_port_indev.c 第217行:touchpad_get_xy();
  • 坐标不符:触摸屏坐标与显示屏坐标不符,需要重新校准;

如果,已按上面步骤在touchpad_get_xy()函数预埋了画点函数:

那么,可以这样测试:在显示屏空白的地方,用指甲,慢慢地,划几道线。

  • 如果划不出黑线(会是断断续续的黑线),那么就是触摸检测函数有问题了;
  • 如果划出了黑线,但是坐标不对,那就是触摸屏需要重新校准了;

关于第一种错误,检查触摸检测函数里的触摸状态返回值,是否正确。

  • 用printf大法!把返回值printf出来!!
  • 必须确认触摸时是有返回值的,而且返回值正确(0-未按下、1-按下),
  • 如果返回值对了,再确认坐标获取是否正常。
  • 最后,就是从本篇开头,一步步对归照,是哪一步出现漏做了。

关于第二种,重新校准

  • 不同的开发板,各施各法,问一问商家,如何重新校准触摸屏
  • 如果和本篇同款的“魔女开发板”,打开串口助手,发送:XPT2046,  即可进入重新校准。

十、控件的事件添加、响应处理

当上述问题都解决了,按钮能正常触控后,再操作这一步部分。

这里,以按钮的点击处理为例,展示控件的:事件添加、响应处理。

回到main函数,在添加按钮的三行代码下方,增加一行,为控件添加事件:

  • lv_obj_add_event_cb(myBtn, myBtn_event, LV_EVENT_CLICKED, NULL);     

这行有点复杂,对参数稍作解释:

myBtn:控件的名称(不限于按钮);

myBtn_event:事件响应时,LVGL自动调用的函数,等一会儿要手动编写这个函数;

LV_EVENT_CLICKED:点击事件; 不同的控件,有不同的事件类型;

NULL:传递给回调函数的可选用户数据,这里暂时不用;

完成后,是这个样子的:      

然后,开始编写刚才说的那个事件回调函数。

在main函数的上方,注释BEGIN 0 与 END 0之间,编写回调函数:myBtn_event();

// 按钮的事件回调函数
static void myBtn_event(lv_event_t *event)
{
    lv_obj_t *btn = lv_event_get_target(event);                    // 获得调用这个回调函数的对象
    if (event->code == LV_EVENT_CLICKED)
    {
        static uint8_t cnt = 0;
        cnt++;
        lv_obj_t *label = lv_obj_get_child(btn, NULL);             // 获取第1个子对象(我们在设计时,已安排了它的第1个子对象是一个label对象)
        lv_label_set_text_fmt(label, "Button: %d", cnt);           // 设置标签的文本,写法类似printf
    }
}

完成后,是这个样子的:

编译,烧录,运行效果如下:


十 一、进阶

本部分,讨论一些轻松一点的、可能对你有用的事情。

1、显示内存使用率

2、显示刷屏帧数

3、查询 LVGL 某个函数、某个变量的作用

直接问AI,没有更快了,下面是Kimi的网址:

https://kimi.moonshot.cn

4、获取控件的各种用法

玩LVGL,最好的网站,没有之一:

LVGL 中文开发手册 -- https://lvgl.100ask.net/master/index.html

想实现某个功能,都可以在里面找到答案。如,想实现一个下拉列表:

打开上面网址, 找到 EXamples / Widgets / Dropdown(下拉列表)。

点击,右侧会展示各种下拉列表的效果(有点延时,要稍等),通用鼠标点击操作它。

在效果的下方,“Show C code", 点击它,可以展开这个效果的代码,复制到工程中,即可测试。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1566610.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

014——超声波模块驱动开发Plus(基于I.MX6uLL、SR04和poll机制)

目录 一、基础知识 二、分析为什么打印会影响中断 三、驱动程序 四、应用程序 五、验证及其它 一、基础知识 013——超声波模块驱动开发(基于I.MX6uLL与SR04)-CSDN博客 二、分析为什么打印会影响中断 asmlinkage __visible int printk(const ch…

如何在本地搭建集成大语言模型Llama 2的聊天机器人并实现无公网IP远程访问

文章目录 1. 拉取相关的Docker镜像2. 运行Ollama 镜像3. 运行Chatbot Ollama镜像4. 本地访问5. 群晖安装Cpolar6. 配置公网地址7. 公网访问8. 固定公网地址 随着ChatGPT 和open Sora 的热度剧增,大语言模型时代,开启了AI新篇章,大语言模型的应用非常广泛,包括聊天机…

Python字典操作

假设我们有一个学生信息数据库,其中存储了每个学生的姓名、年龄、性别和成绩。我们可以使用字典来表示每个学生的信息,并将所有学生存储在一个字典列表中。 设计者:ISDF 版本:v1.0 日期:03/29/2024# 定义学生信息字典列…

Ruby 之交租阶段信息生成

题目 我看了一下,这个题目应该不是什么机密,所以先放上来了。大概意思是根据合同信息生成交租阶段信息。 解答 要求是要使用 Ruby 生成交租阶段信息,由于时间比较仓促,变量名那些就用得随意了些。要点主要有下面这些&#xff1a…

高级IO/多路转接-select/poll(1)

概念背景 IO的本质就是输入输出 刚开始学网络的时候,我们简单的写过一些网络服务,其中用到了read,write这样的接口,当时我们用的就是基础IO,高级IO主要就是效率问题。 我们在应用层调用read&&write的时候&…

八、从0开始卷出一个新项目之瑞萨RZN2L 3.1.7 debug调试和下载

目录 3.1.7 debug调试和下载 3.1.7.1 官方介绍 3.1.7.2 e2studio debug变量实时监控 3.1.7.3 Iar debug变量实时监控 3.1.7.4 debug经验总结 八、从0开始卷出一个新项目之瑞萨RZN2L 3.1.7 debug调试和下载 3.1.7 debug调试和下载 3.1.7.1 官方介绍 官网: d…

day02-java类型转换和运算符

1.温故而知新 整形 byte 1字节 8位 short 2字节 16位 int 4字节 32位 long 8字节 64位 内存存储时 X符号位 byte X0000000 short X0000000 00000000 int X0000000 00000000 00000000 long X0000000 00000000 00000000 00000000 long longNum 10000L;//l或者L 查看源码最大值…

数据库之迁移常规操作(Mysql篇)

借鉴的文章 》》https://blog.csdn.net/weixin_65685029/article/details/132413482?ops_request_misc&request_id&biz_id102&utm_termmysql备份与还原&utm_mediumdistribute.pc_search_result.none-task-blog-2~all~sobaiduweb~default-1-132413482.nonecase…

C语言 | Leetcode C语言题解之第6题Z字形变换

题目&#xff1a; 题解&#xff1a; char * convert(char * s, int numRows){int n strlen(s), r numRows;if (r 1 || r > n) {return s;}int t r * 2 - 2;char * ans (char *)malloc(sizeof(char) * (n 1));int pos 0;for (int i 0; i < r; i) { // 枚举矩阵的…

语言模型进化史(下)

由于篇幅原因&#xff0c;本文分为上下两篇&#xff0c;上篇主要讲解语言模型从朴素语言模型到基于神经网络的语言模型&#xff0c;下篇主要讲解现代大语言模型以及基于指令微调的LLM。文章来源是&#xff1a;https://www.numind.ai/blog/what-are-large-language-models 四、现…

Multi-Head Attention 代码实现

Multi-Head Attention 代码实现 flyfish MultiHead ( Q , K , V ) Concat ( head 1 , . . . , head h ) W O \text{MultiHead}(Q, K, V) \text{Concat}(\text{head}_1, ..., \text{head}_h)W^O MultiHead(Q,K,V)Concat(head1​,...,headh​)WO head i Attention ( Q W i Q…

kettle使用MD5加密增量获取接口数据

kettle使用MD5加密增量获取接口数据 场景介绍&#xff1a; 使用JavaScript组件进行MD5加密得到Http header&#xff0c;调用API接口增量获取接口数据&#xff0c;使用json input组件解析数据入库 案例适用范围&#xff1a; MD5加密可参考、增量过程可参考、调用API接口获取…

C++(set和map详解,包含常用函数的分析)

set set是关联性容器 set的底层是在极端情况下都不会退化成单只的红黑树,也就是平衡树,本质是二叉搜索树. set的性质:set的key是不允许被修改的 使用set需要包含头文件 set<int> s;s.insert(1);s.insert(1);s.insert(1);s.insert(1);s.insert(2);s.insert(56);s.inser…

面试题:RabbitMQ 消息队列中间件

1. 确保消息不丢失 生产者确认机制 确保生产者的消息能到达队列&#xff0c;如果报错可以先记录到日志中&#xff0c;再去修复数据持久化功能 确保消息未消费前在队列中不会丢失&#xff0c;其中的交换机、队列、和消息都要做持久化消费者确认机制 由spring确认消息处理成功后…

Java基础之流程控制语句(循环)

文章目录 Java基础之流程控制语句(循环)1.顺序结构2.分支结构if语句的第一种格式if语句的第二种格式if语句的第三种格式Switch语句格式Switch的其他知识点default的位置和省略case穿透Switch的新特性 3.循环结构循环的分类for 循环while 循环for循环 与 while循环 的对比 4.do.…

RAG原理、综述与论文应用全解析

1. 背景 1.1 定义 检索增强生成 (Retrieval-Augmented Generation, RAG) 是指在利用大语言模型回答问题之前&#xff0c;先从外部知识库检索相关信息。 早在2020年就已经有人提及RAG的概念&#xff08;paper&#xff1a;Retrieval-augmented generation for knowledge-inten…

IDEA 解决 java: 找不到符号 符号: 类 __ (使用了lombok的注解)

原因IDEA版本太高&#xff0c;在 ProcessingEnvironement 预编译的时候是以代理的方式来执行的&#xff0c;不再是直接 javac方式, lombok依赖的 javac方式的 annotation processors 不再生效了 解决办法&#xff1a;下面这一句&#xff0c;加在下图中 -Djps.track.ap.depen…

八口快速以太网交换机芯片方案分享/JL5110

以太网交换机(switch)是一种网络设备&#xff0c;用于在局域网中连接多个计算机和其他网络设备。它可以实现多个端口之间的同时传输&#xff0c;并根据MAC地址进行帧过滤和转发。交换机通过自学习的方式&#xff0c;将MAC地址与相应的接口关联起来&#xff0c;以便将数据帧准确…

C语言中的数组与函数指针:深入解析与应用

文章目录 一、引言二、数组的定义1、数组的定义与初始化2、char*与char[]的区别1. 存储与表示2. 修改内容3. 作为函数参数 三、字符串指针数组1. 定义与概念2. 使用示例3. 内存管理 四、从字符串指针数组到函数指针的过渡1、字符串指针数组的应用场景2、函数指针的基本概念3、如…

【RedHat9.0】Timer定时器——创建单调定时器实例

一个timer&#xff08;定时器&#xff09;的单元类型&#xff0c;用来定时触发用户定义的操作。要使用timer的定时器&#xff0c;关键是要创建一个定时器单元文件和一个配套的服务单元文件&#xff0c;然后启动这些单元文件。 定时器类型&#xff1a; 单调定时器&#xff1a;即…