轻量级图形库LVGL入门手册

LVGL入门

简介

什么是LVGL

LVGL的项目作者是来自匈牙利首都布达佩斯的 Gábor Kiss-Vámosi 。Kiss 在2016年将其并发布在 GitHub 上。

当时叫 LittlevGL而不是LVGL,后来作者统一修改为 LVGL 甚至连仓库地址都改了。 像一般的开源项目的那样,它是作为一个人的项目开始的。 从那时起,陆续有近 100 名贡献者参与了项目开发,使得 LVGL 逐渐成为最受欢迎的嵌入式图形库之一。

特性

  1. 丰富且强大的模块化图形组件:按钮 (buttons)、图表 (charts)、列表 (lists)、滑动条 (sliders)、图片 (images) 等

  1. 高级的图形引擎:动画、抗锯齿、透明度、平滑滚动、图层混合等效果

  1. 支持多种输入设备:触摸屏、 键盘、编码器、按键等

  1. 支持多显示设备

  1. 不依赖特定的硬件平台,可以在任何显示屏上运行

  1. 配置可裁剪(最低资源占用:64 kB Flash,16 kB RAM)

  1. 基于UTF-8的多语种支持,例如中文、日文、韩文、阿拉伯文等

  1. 可以通过类CSS的方式来设计、布局图形界面(例如:Flexbox、Grid)

  1. 支持操作系统、外置内存、以及硬件加速(LVGL已内建支持STM32 DMA2D、NXP PXP和VGLite)

  1. 即便仅有单缓冲区(frame buffer)的情况下,也可保证渲染如丝般顺滑

  1. 全部由C编写完成,并支持C++调用

  1. 支持Micropython编程,参见:LVGL API in Micropython

  1. 支持模拟器仿真,可以无硬件依托进行开发

  1. 丰富详实的例程

  1. 详尽的文档以及API参考手册,可线上查阅或可下载为PDF格式

  1. 在 MIT 许可下免费和开源

商用

LVGL 项目(包括所有存储库)在 MIT license 许可下获得许可。 这意味着您甚至可以在商业项目中使用它。

这不是强制性的,但如果您在论坛的 My projects 类别或来自 lvgl.io 的私人消息中写下有关您的项目的几句话,我们将不胜感激。

尽管您可以免费获得 LVGL,但它背后的工作量很大。它由一群志愿者创建,他们在空闲时间为您提供。

为了使 LVGL 项目可持续,请考虑为该项目做 贡献 。您可以从 多种投稿方式 中进行选择,例如简单地写一条关于您正在使用 LVGL 的推文、修复错误、翻译文档,甚至成为维护者。

配置要求

  1. 16、32 或 64 位微控制器或处理器

  1. 建议使用 >16 MHz 时钟速度

  1. 闪存/ROM: > 64 kB 用于非常重要的组件 (> 建议使用 180 kB)

  1. RAM:静态 RAM 使用量:~2 kB,取决于使用的功能和对象类型

堆: > 2kB (> 建议使用 8 kB)

动态数据(堆): > 2 KB (> 如果使用多个对象,建议使用 16 kB). 在 lv_conf.h 文件中配置 LV_MEM_SIZE 生效。

显示缓冲区:> “水平分辨率”像素(推荐 >10 × 10ד 水平分辨率”)

MCU或外部显示控制器中的一个帧缓冲区

  1. C99 或更新的编译器

  1. 具备基本的 C(或 C++)知识: pointers, structs, callbacks

LVGL的工作模式(个人理解for GimXR)及移植

在我的理解中LVGL的工作方式十分类似FreeRTOS,都是由一个timer来维持心跳,二者的区别是FreeRTOS由心跳来维持任务轮转,而LVGL由心跳来刷新屏幕以及检查输入。

在LVGL中心跳可以由一个任务来进行,例如:使用FreeRTOS的一个Task来为LVGL提供心跳。

void startheartbeatTask(void * pvParameters)
{
    printf("============heartbeat task is running!============\r\n");
    for(;;)
    {
        lv_timer_handler(); /* let the GUI do its work */
        vTaskDelay(10);
    }
}

当数据堆有数据,心跳会使LVGL按照堆中数据调用多次屏幕填充函数来刷新屏幕,这个函数以及屏幕的尺寸需要使用者提供给LVGL

static lv_disp_drv_t disp_drv;
lv_disp_drv_init( &disp_drv );
disp_drv.hor_res = LCD_W;
disp_drv.ver_res = LCD_H;
disp_drv.flush_cb = my_disp_flush;
disp_drv.draw_buf = &draw_buf;
lv_disp_drv_register( &disp_drv );

my_disp_flush函数需要调用我们屏幕的填充函数(这一步主要是为了将LVGL接口的参数与屏幕驱动的参数适配)

/*****************************************************************************
*函数名:my_disp_flush
*输  入:disp:显示设备 area:坐标(区域) color_p:色彩指针(指向显存)
*输  出:无
*返回值:无
*功  能:填充一次屏幕
*****************************************************************************/
void my_disp_flush(lv_disp_drv_t * disp, const lv_area_t * area, lv_color_t * color_p)
{
    LCD_Fill(area->x1, area->y1, area->x2, area->y2, color_p);
    lv_disp_flush_ready( disp );
}

LCD_Fill函数,这种方式为区域填充,也可以多次调用点填充(速度慢,不推荐)

/*****************************************************************************
*函数名:LCD_Fill
*输  入:xsta:x起始
*		ysta:y起始
*		xend:x结束
*		yend:y结束
*		color:颜色的指针,指向显存中
*输  出:无
*返回值:无
*功  能:填充颜色
*****************************************************************************/
void LCD_Fill(uint16_t xsta,uint16_t ysta,uint16_t xend,uint16_t yend, lv_color_t * color)
{          
	uint16_t i, j; 
	LCD_Address_Set(xsta, ysta, xend, yend);//设置显示范围
	for(i = ysta; i <= yend; i ++)
	{													   	 	
		for(j = xsta; j <= xend; j ++)
		{
			LCD_WR_DATA(color->full);
			color++;
		}
	}	  	    
}

当适配好显示驱动后既可以开始在屏幕上显示控件了,由lvgl的配置文件lv_conf.h中的

/*Default display refresh period. LVG will redraw changed areas with this period time*/
#define LV_DISP_DEF_REFR_PERIOD 30      /*[ms]*/

来确定屏幕刷新率

LVGL 音乐播放器demo

开始使用

除去上文提到的显示设备适配,我们还需要进行界面交互,这就少不了输入设备的适配。在LVGL中输入设备与显示设备类似,我们需要将我们的输入设备扫码函数提供给LVGL

输入设备声明(注册)

/*Initialize the (dummy) input device driver*/
static lv_indev_drv_t indev_drv;
lv_indev_drv_init( &indev_drv );
indev_drv.type = LV_INDEV_TYPE_POINTER;
indev_drv.read_cb = my_touchpad_read;
lv_indev_drv_register(&indev_drv);

这里的my_touchpad_read函数即为我们的输入设备的扫描函数,除了pointer,LVGL支持以下几种输入设备

typedef enum {
    LV_INDEV_TYPE_NONE,    /**< Uninitialized state*/
    LV_INDEV_TYPE_POINTER, /**< Touch pad, mouse, external button*/
    LV_INDEV_TYPE_KEYPAD,  /**< Keypad or keyboard*/
    LV_INDEV_TYPE_BUTTON,  /**< External (hardware button) which is assigned to a specific point of the screen*/
    LV_INDEV_TYPE_ENCODER, /**< Encoder with only Left, Right turn and a Button*/
} lv_indev_type_t;

my_touchpad_read函数

/*****************************************************************************
*函数名:my_touchpad_read
*输  入:indev_driver:输入设备 data:触摸相关的数据
*输  出:data
*返回值:无
*功  能:读一次触摸
*****************************************************************************/
void my_touchpad_read( lv_indev_drv_t * indev_driver, lv_indev_data_t * data )
{
    uint16_t touchX, touchY;
    bool touched = lvgl_touch_scan(&touchX,  &touchY);
    if( !touched )
    {
        data->state = LV_INDEV_STATE_REL;
    }
    else
    {
        data->state = LV_INDEV_STATE_PR;
        /*Set the coordinates*/
        switch (g_display_direction)
        {
        case TURN_LEFT_0_DEGREES:
            data->point.x = (LCD_W - touchX);
            data->point.y = touchY;
            break;
        
        default:
            break;
        }
        #if LV_TOUCH_DEBUG
            printf( "Data x " );
            printf("%d\r\n", data->point.x);

            printf( "Data y " );
            printf("%d\r\n", data->point.y );
        #endif
    }
}

与显示设备类似,这个函数主要是与我们的底层扫描驱动同步参数,这里lvgl_touch_scan函数为底层提供给LVGL的函数

我们的输入设备为触摸屏幕,所以是用IIC读取寄存器来返回触摸信息(LVGL8不支持多点触控,所以以返回的手指数来判断是否触摸)。

bool lvgl_touch_scan(uint16_t * x, uint16_t * y)
{
	bool retval = false;
	uint8_t FingerNum;
	uint8_t XposH;
	uint8_t XposL;
	uint8_t YposH;
	uint8_t YposL;
	CST816S_RD_DATA(0x02, 1, &FingerNum);
	CST816S_RD_DATA(0x03, 1, &XposH);
	CST816S_RD_DATA(0x04, 1, &XposL);
	CST816S_RD_DATA(0x05, 1, &YposH);
	CST816S_RD_DATA(0x06, 1, &YposL);
	if (FingerNum == 1)
	{
		* x = (XposH & 0x0F) << 8 | XposL;
		* y = (YposH & 0x0F) << 8 | YposL;
		retval = true;
	}
	else
	{
		* x = 0;
		* y = 0;
		retval = false;
	}
	return retval;
}

将函数提供给LVGL后如何确定扫描的频率?,与显示设备类似,在lvgl的配置文件lv_conf.h中

/*Input device read period in milliseconds*/
#define LV_INDEV_DEF_READ_PERIOD 100     /*[ms]*/

改变这个宏定义的值,如代码所示,就将在系统每心跳100ms时进行一次扫描

开始

当将显示设备与输入设备注册好后,即可开始使用LVGL了

首先调用lvgl初始化函数,进行初始化

lv_init();

在LVGL中控件是存在父子关系的,在创建一个控件时需要将他的父对象以参数的形式传递。例如创建一个lable

lv_obj_t * test_lable = lv_label_create(lv_scr_act());

其中lv_scr_act()即为test_lable的父对象,而lv_scr_act()函数是用来返回当前活动的屏幕,即显示在最上层的屏幕

在LVGL中屏幕是一个特殊的对象,它没有父对象所以我们可以以

lv_obj_t * test_screen = lv_obj_create(NULL);

的方式来创建一个新的屏幕。即创建一个没有父对象的LVGL对象,这个对象就是屏幕

标签

回到刚刚的标签,我们可以设置标签的属性

例如:

设置标签的文本

lv_label_set_text(test_lable, "this is test lable");
lv_label_set_text_fmt(test_lable, "%s[%d]" ,"this is test lable" ,1);

设置标签的对齐方式以及位置

lv_obj_align(test_lable, LV_ALIGN_CENTER, 0, 0);

其中LV_ALIGN_CENTER参数是用来选择对齐方式的,对其的目标即为对象的父对象。如上代码即为与屏幕中央对其(0, 0)

对齐方式有多种

/** Alignments*/
enum {
    LV_ALIGN_DEFAULT = 0,
    LV_ALIGN_TOP_LEFT,
    LV_ALIGN_TOP_MID,
    LV_ALIGN_TOP_RIGHT,
    LV_ALIGN_BOTTOM_LEFT,
    LV_ALIGN_BOTTOM_MID,
    LV_ALIGN_BOTTOM_RIGHT,
    LV_ALIGN_LEFT_MID,
    LV_ALIGN_RIGHT_MID,
    LV_ALIGN_CENTER,

    LV_ALIGN_OUT_TOP_LEFT,
    LV_ALIGN_OUT_TOP_MID,
    LV_ALIGN_OUT_TOP_RIGHT,
    LV_ALIGN_OUT_BOTTOM_LEFT,
    LV_ALIGN_OUT_BOTTOM_MID,
    LV_ALIGN_OUT_BOTTOM_RIGHT,
    LV_ALIGN_OUT_LEFT_TOP,
    LV_ALIGN_OUT_LEFT_MID,
    LV_ALIGN_OUT_LEFT_BOTTOM,
    LV_ALIGN_OUT_RIGHT_TOP,
    LV_ALIGN_OUT_RIGHT_MID,
    LV_ALIGN_OUT_RIGHT_BOTTOM,
};

设置标签的颜色

在LVGL中设置文本的颜色其实就是通过类似有色printf的方式,将颜色写在字符串中,色彩格式是8位RGB

lv_label_set_text_fmt(test_lable, "#%s %s#" ,"000000" ,"test lable color");

在使用这种方法之前需要调用

lv_label_set_recolor(test_lable, true);

函数来使能重设颜色

效果

当然也可以在字符之间使用, 以及还有另外的一些属性,比如滚动

lv_label_set_text(test_lable, "#FF0000 test# #00FF00 lable# #0000FF color#");

按钮

在LVGL中按钮是使用的非常多的一种控件,毕竟最常见的交互就是点击和滑动

创建按钮

可以使用lv_btn_create函数来创建按钮

lv_obj_t * test_btn = lv_btn_create(lv_scr_act());

设置文本

lv_obj_t * test_btn_label = lv_label_create(test_btn);
lv_label_set_text(test_btn_label, "Button");
lv_obj_center(test_btn_label);

为按钮添加文本,即创建一个以按钮为父对象的标签,并与按钮中心对齐。

添加回调

用lv_obj_add_event_cb函数来为按钮添加回调函数

lv_obj_add_event_cb(test_btn, event_handler, LV_EVENT_ALL, NULL);

这里的LV_EVENT_ALL参数表示所有事件触发都会调用回调,在LVGL中有多种event

typedef enum {
    LV_EVENT_ALL = 0,

    /** Input device events*/
    LV_EVENT_PRESSED,             /**< The object has been pressed*/
    LV_EVENT_PRESSING,            /**< The object is being pressed (called continuously while pressing)*/
    LV_EVENT_PRESS_LOST,          /**< The object is still being pressed but slid cursor/finger off of the object */
    LV_EVENT_SHORT_CLICKED,       /**< The object was pressed for a short period of time, then released it. Not called if scrolled.*/
    LV_EVENT_LONG_PRESSED,        /**< Object has been pressed for at least `long_press_time`.  Not called if scrolled.*/
    LV_EVENT_LONG_PRESSED_REPEAT, /**< Called after `long_press_time` in every `long_press_repeat_time` ms.  Not called if scrolled.*/
    LV_EVENT_CLICKED,             /**< Called on release if not scrolled (regardless to long press)*/
    LV_EVENT_RELEASED,            /**< Called in every cases when the object has been released*/
    LV_EVENT_SCROLL_BEGIN,        /**< Scrolling begins*/
    LV_EVENT_SCROLL_END,          /**< Scrolling ends*/
    LV_EVENT_SCROLL,              /**< Scrolling*/
    LV_EVENT_GESTURE,             /**< A gesture is detected. Get the gesture with `lv_indev_get_gesture_dir(lv_indev_get_act());` */
    LV_EVENT_KEY,                 /**< A key is sent to the object. Get the key with `lv_indev_get_key(lv_indev_get_act());`*/
    LV_EVENT_FOCUSED,             /**< The object is focused*/
    LV_EVENT_DEFOCUSED,           /**< The object is defocused*/
    LV_EVENT_LEAVE,               /**< The object is defocused but still selected*/
    LV_EVENT_HIT_TEST,            /**< Perform advanced hit-testing*/

    /** Drawing events*/
    LV_EVENT_COVER_CHECK,        /**< Check if the object fully covers an area. The event parameter is `lv_cover_check_info_t *`.*/
    LV_EVENT_REFR_EXT_DRAW_SIZE, /**< Get the required extra draw area around the object (e.g. for shadow). The event parameter is `lv_coord_t *` to store the size.*/
    LV_EVENT_DRAW_MAIN_BEGIN,    /**< Starting the main drawing phase*/
    LV_EVENT_DRAW_MAIN,          /**< Perform the main drawing*/
    LV_EVENT_DRAW_MAIN_END,      /**< Finishing the main drawing phase*/
    LV_EVENT_DRAW_POST_BEGIN,    /**< Starting the post draw phase (when all children are drawn)*/
    LV_EVENT_DRAW_POST,          /**< Perform the post draw phase (when all children are drawn)*/
    LV_EVENT_DRAW_POST_END,      /**< Finishing the post draw phase (when all children are drawn)*/
    LV_EVENT_DRAW_PART_BEGIN,    /**< Starting to draw a part. The event parameter is `lv_obj_draw_dsc_t *`. */
    LV_EVENT_DRAW_PART_END,      /**< Finishing to draw a part. The event parameter is `lv_obj_draw_dsc_t *`. */

    /** Special events*/
    LV_EVENT_VALUE_CHANGED,       /**< The object's value has changed (i.e. slider moved)*/
    LV_EVENT_INSERT,              /**< A text is inserted to the object. The event data is `char *` being inserted.*/
    LV_EVENT_REFRESH,             /**< Notify the object to refresh something on it (for the user)*/
    LV_EVENT_READY,               /**< A process has finished*/
    LV_EVENT_CANCEL,              /**< A process has been cancelled */

    /** Other events*/
    LV_EVENT_DELETE,              /**< Object is being deleted*/
    LV_EVENT_CHILD_CHANGED,       /**< Child was removed/added*/
    LV_EVENT_SIZE_CHANGED,        /**< Object coordinates/size have changed*/
    LV_EVENT_STYLE_CHANGED,       /**< Object's style has changed*/
    LV_EVENT_LAYOUT_CHANGED,      /**< The children position has changed due to a layout recalculation*/
    LV_EVENT_GET_SELF_SIZE,       /**< Get the internal size of a widget*/

    _LV_EVENT_LAST                /** Number of default events*/
}lv_event_code_t;

虽然有很多,但是我们用的只有点击和滑动,以及长按

event_handler即为回调函数的实现

static void event_handler(lv_event_t * e)
{
    lv_event_code_t code = lv_event_get_code(e);

    if(code == LV_EVENT_CLICKED) {
        LV_LOG_USER("Clicked");
    }
    else if(code == LV_EVENT_VALUE_CHANGED) {
        LV_LOG_USER("Toggled");
    }
}

在回调函数中可以使用lv_event_get_code();函数来获取当前被触发的事件,而上面提到的LV_EVENT_ALL参数即为该函数的参数

运行效果如下:

当按钮被点击则打印行号及文件名

[User]  (111.790, +120)  event_handler: Clicked         (in main.c line #58)

当然LVGL不仅仅只有这些内容,由于篇幅关系就简单看看效果

按钮矩阵

圆弧

键盘

物联沃分享整理
物联沃-IOTWORD物联网 » 轻量级图形库LVGL入门手册

发表评论