lvgl v8.3移植及组件使用

前言

最近在学习lvgl,网上的教程主要有韦东山和正点原子他们两家有做,我手上只有野火的开发板,但野火他们没做这个教程,不过问题不大,其实随便一个带屏幕的开发板就可以,移植过程都是差不多的,这里是分享一下把lvgl v8.3移植到野火霸天虎开发板(v2)的大概教程,并简单介绍了一下如何上手lvgl组件以及如何写UI,初次接触lvgl的话建议先看一下正点原子或韦东山的教程会比较好。

代码:https://github.com/CatIsBest/STM32F407_LVGL
演示视频:https://www.bilibili.com/video/BV1kv4y1S7p6/?spm_id_from=333.999.0.0&vd_source=272132e613b16a2fd3bd189c2fa3be6d

准备工作

先从github获取lvgl的源码
地址:https://github.com/lvgl/lvgl
这里选择v8.3下载
lvgl v8.3移植及组件使用_第1张图片
lvgl仓库各个分支的含义
lvgl v8.3移植及组件使用_第2张图片

目前最新的稳定版本是v8.3(2022.11.10)
在这里插入图片描述

接下来在野火的HAL库例程里面找到触摸画板的例程,复制一份出来,如果你是其他开发板的话把带有显示屏驱动和触摸屏驱动的那个工程复制一下,啥都没有的话就得看datasheet自己写一下驱动了
在这里插入图片描述
顺便改个名字
在这里插入图片描述

在复制后的文件夹User里面新增两个空文件夹lvgl(用来放lvgl源码)和GUI(用来放自己写的界面代码)lvgl v8.3移植及组件使用_第3张图片
打开刚刚下载的lvgl-release-v8.3,把里面的
demos
examples
src
lvgl.h
lv_conf_template.h
复制到工程文件的lvgl文件夹
lvgl v8.3移植及组件使用_第4张图片
lvgl v8.3移植及组件使用_第5张图片
如果不需要使用官方提供的示例代码,可以不复制 demos 目录。
把 lv_conf_template.h 重命名为 lv_conf.h ,打开这个文件,将开头的 #if 0 条件编译取消,启用文件包含的配置,并移动到上一级目录中。

把lvgl加入keil工程

打开keil工程,按下图步骤添加组,第4步那里把lvgl/src 中除了 draw 目录中的所有文件全部导入,而 draw 目录中除了根目录的 .c 文件外,只导入 sw 目录中的源文件。LVGL的目录深度较大,要耐心添加,不要遗漏文件。

lvgl v8.3移植及组件使用_第6张图片
接下来再创建一个组,添加examples\porting里面的驱动文件。
lvgl v8.3移植及组件使用_第7张图片

另外,注意在启动文件中修改堆、栈大小,建议各设置 8kB 空间:(大一些也没关系)
lvgl v8.3移植及组件使用_第8张图片
建议在设置这里用AC6,AC6用的Clang编译,比AC5的ARMCC快很多。(用AC5完整编译一遍不知道要等到猴年马月)
lvgl v8.3移植及组件使用_第9张图片
在这里我编译遇到一个错误
lvgl v8.3移植及组件使用_第10张图片
这个错误是由MicroLIB引起的,这个问题的描述和解决方法可以参考:KeilMDK编译错误Error: L6218E: Undefined symbol __aeabi_assert (referred from xxx.o).

我这里对标准错误输出进行了重定向
lvgl v8.3移植及组件使用_第11张图片

接下来修改一下main函数。

/**
  * @brief  主函数
  * @param  无  
  * @retval 无
  */
int main ( void )
{
  SystemClock_Config();

	while ( 1 )
	{
	}
}

把lv_conf.h放到项目组里面,方便修改。(下图lv_port_disp_template这三个文件名可改可不改,命名正式一点的话应该把文件名的“_template”去掉,如果改了的话记得在对应.c源文件里面把#include的头文件名改一下)
lvgl v8.3移植及组件使用_第12张图片
把编辑器的编码格式改为UTF-8,免得UI界面出现字符编码与编辑器字符编码不符的错误。(注:改完后例程里面的注释会变乱码,因为例程原来的注释是GB2312编码的)
lvgl v8.3移植及组件使用_第13张图片
lvgl v8.3移植及组件使用_第14张图片
这个时候编译是0 Error。

接入显示屏

在做这个之前最好先了解一下你的屏幕是怎么驱动的,然后再做下面的工作。

先打开 lv_conf.h,
第27行,根据屏幕颜色数据类型修改
第52行,指定lvgl的动态内存可用大小
第96行,每英寸的像素,根据自己屏幕尺寸和像素算一下就好
第258行,开启style初始化断言
其他的浏览一下即可,用到的时候再去深究。

打开 lv_port_disp_template.h,将开头的 #if 0 条件编译取消
打开 lv_port_disp_template.c,将开头的 #if 0 条件编译取消,设置一下屏幕宽和高的像素。

#define MY_DISP_HOR_RES    480
#define MY_DISP_VER_RES    800

看下面的 void lv_port_disp_init(void),LVGL缓冲区的图像写入有三种方式
第一种:一个缓冲区,默认存10行图像
第二种:两个缓冲区,默认每个缓冲区存10行图像
第三种:两个缓冲区,每个缓冲区存一整个屏幕的图像
这里我们用第二种,把第一种和第三种注释掉即可。第二种是为存在DMA之类的数据传送机制的硬件设计的,可以把传送缓冲区的内容交给DMA处理,而cpu去执行其他工作(比如渲染),这里我暂时没用DMA,这样的话效果和第一种是一样的。(注:缓冲区大小为屏幕的1/10效果会比较好,这里为了节约一点RAM没有改大小)

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

    /*-----------------------------
     * Create a buffer for drawing
     *----------------------------*/

    /**
     * LVGL requires a buffer where it internally draws the widgets.
     * Later this buffer will passed to your display driver's `flush_cb` to copy its content to your display.
     * The buffer has to be greater than 1 display row
     *
     * There are 3 buffering configurations:
     * 1. Create ONE buffer:
     *      LVGL will draw the display's content here and writes it to your display
     *
     * 2. Create TWO buffer:
     *      LVGL will draw the display's content to a buffer and writes it your display.
     *      You should use DMA to write the buffer's content to the display.
     *      It will enable LVGL to draw the next part of the screen to the other buffer while
     *      the data is being sent form the first buffer. It makes rendering and flushing parallel.
     *
     * 3. Double buffering
     *      Set 2 screens sized buffers and set disp_drv.full_refresh = 1.
     *      This way LVGL will always provide the whole rendered screen in `flush_cb`
     *      and you only need to change the frame buffer's address.
     */

    /* 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*/
//    lv_disp_draw_buf_init(&draw_buf_dsc_1, buf_1, NULL, MY_DISP_HOR_RES * 10);   /*Initialize the display buffer*/

    /* 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*/
    lv_disp_draw_buf_init(&draw_buf_dsc_2, buf_2_1, buf_2_2, MY_DISP_HOR_RES * 10);   /*Initialize the display buffer*/

    /* 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*/
//    lv_disp_draw_buf_init(&draw_buf_dsc_3, buf_3_1, buf_3_2,
//                          MY_DISP_VER_RES * LV_VER_RES_MAX);   /*Initialize the display buffer*/

    /*-----------------------------------
     * Register the display in LVGL
     *----------------------------------*/

    static lv_disp_drv_t disp_drv;                         /*Descriptor of a display driver*/
    lv_disp_drv_init(&disp_drv);                    /*Basic initialization*/

    /*Set up the functions to access to your display*/

    /*Set the resolution of the display*/
    disp_drv.hor_res = MY_DISP_HOR_RES;
    disp_drv.ver_res = MY_DISP_VER_RES;

    /*Used to copy the buffer's content to the display*/
    disp_drv.flush_cb = disp_flush;

    /*Set a display buffer*/
    disp_drv.draw_buf = &draw_buf_dsc_2;

    /*Required for Example 3)*/
    //disp_drv.full_refresh = 1;

    /* Fill a memory array with a color if you have GPU.
     * Note that, in lv_conf.h you can enable GPUs that has built-in support in LVGL.
     * But if you have a different GPU you can use with this callback.*/
    //disp_drv.gpu_fill_cb = gpu_fill;

    /*Finally register the driver*/
    lv_disp_drv_register(&disp_drv);
}

屏幕初始化

/*Initialize your display and the required peripherals.*/
static void disp_init(void)
{
	/*You code here*/
	NT35510_Init ();
}

刷新缓冲区

/*Flush the content of the internal buffer the specific area on the display
 *You can use DMA or any hardware acceleration to do this operation in the background but
 *'lv_disp_flush_ready()' has to be called when finished.*/
static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
    if(disp_flush_enabled) {
			
			uint32_t width = area->x2 - area->x1 + 1;
			uint32_t height = area->y2 - area->y1 + 1;
			NT35510_OpenWindow(area->x1,area->y1,width,height);
			
			NT35510_Write_Cmd ( CMD_SetPixel );	
			for(uint32_t i = 0;i < width * height;i++)
			{
				NT35510_Write_Data ( color_p->full );
				color_p++;
			}
			
//        /*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);
}

接下来修改main函数

int main ( void )
{
 	 SystemClock_Config();
  
  	/* SystemFrequency / 1000    1ms中断一次
	 * SystemFrequency / 100000	 10us中断一次
	 * SystemFrequency / 1000000 1us中断一次
	 */
	HAL_SYSTICK_Config(SystemCoreClock / 1000);

	/* USART config */
	DEBUG_USART_Config();
			 
  	lv_init();
	lv_port_disp_init();
	

	
	lv_obj_t* btn = lv_btn_create(lv_scr_act()); 
	lv_obj_set_pos(btn, 100, 100);
	lv_obj_set_size(btn, 120, 50);
	lv_obj_t* label = lv_label_create(btn);
	lv_label_set_text(label, "Button");
	lv_obj_center(label);



//	GTP_Init_Panel();

	while ( 1 )
	{
		lv_timer_handler();
	}
}

在stm32f4xx_it.c里面添加SysTick中断服务

/**
  * @brief  This function handles SysTick Handler.
  * @param  None
  * @retval None
  */
void SysTick_Handler(void)
{
	lv_tick_inc(1);
}

上述的有些操作调用了其他文件的函数,记得要包含相应函数的头文件,比如SysTick_Handler()调用了lv_tick_inc(),那么在stm32f4xx_it.c开头要 #include “lvgl/lvgl.h”

编译,下载,现象
lvgl v8.3移植及组件使用_第15张图片

接入触摸

在做这个之前最好先了解一下你的触摸屏是怎么驱动的,然后再做下面的工作。

打开 lv_port_indev_template.h,将开头的 #if 0 条件编译取消
打开 lv_port_indev_template.c,将开头的 #if 0 条件编译取消,然后翻到它的lv_port_indev_init()函数,把鼠标、键盘之类的输入驱动代码注释掉,只保留触摸的代码,如下

void lv_port_indev_init(void)
{
    /**
     * Here you will find example implementation of input devices supported by LittelvGL:
     *  - Touchpad
     *  - Mouse (with cursor support)
     *  - Keypad (supports GUI usage only with key)
     *  - Encoder (supports GUI usage only with: left, right, push)
     *  - Button (external buttons to press points on the screen)
     *
     *  The `..._read()` function are only examples.
     *  You should shape them according to your hardware
     */

    static lv_indev_drv_t indev_drv;

    /*------------------
     * Touchpad
     * -----------------*/

    /*Initialize your touchpad if you have*/
    touchpad_init();

    /*Register a touchpad input device*/
    lv_indev_drv_init(&indev_drv);
    indev_drv.type = LV_INDEV_TYPE_POINTER;
    indev_drv.read_cb = touchpad_read;
    indev_touchpad = lv_indev_drv_register(&indev_drv);

	/*
		其他输入设备的代码
	*/
}

初始化代码

/*Initialize your touchpad*/
static void touchpad_init(void)
{
    /*Your code comes here*/
		GTP_Init_Panel(); 
}

接下来lvgl需要两个函数,一个用来判断是否发生了触摸,一个用来获得触摸坐标。我在触摸驱动文件(我这里是gt5xx.c)里面添加了下面代码作为提供给lvgl的接口函数

volatile int16_t touchpad_x = 0;
volatile int16_t touchpad_y = 0;
volatile int16_t touchpad_is_down = 0;

int16_t Get_Touchpad_x(void)
{
	return touchpad_x;
}

int16_t Get_Touchpad_y(void)
{
	return touchpad_y;
}

int16_t Get_Touchpad_Is_Down(void)
{
	return touchpad_is_down;
}

在触摸处理函数中更新touchpad_x ,touchpad_y 和touchpad_is_down

/**
	* @brief  在按键按下时的中断服务中被调用
  * @param 	void
  * @retval void
  */
static void Goodix_TS_Work_Func(void)
{
    uint8_t  end_cmd[3] = {GTP_READ_COOR_ADDR >> 8, GTP_READ_COOR_ADDR & 0xFF, 0};
    uint8_t  point_data[2 + 1 + 8 * GTP_MAX_TOUCH + 1]={GTP_READ_COOR_ADDR >> 8, GTP_READ_COOR_ADDR & 0xFF};
    uint8_t  touch_num = 0;
    uint8_t  finger = 0;
    static uint16_t pre_touch = 0;
    static uint8_t pre_id[GTP_MAX_TOUCH] = {0};

    uint8_t client_addr=GTP_ADDRESS;
    uint8_t* coor_data = NULL;
    int32_t input_x = 0;
    int32_t input_y = 0;
    int32_t input_w = 0;
    uint8_t id = 0;
 
    int32_t i  = 0;
    int32_t ret = -1;

    GTP_DEBUG_FUNC();

    ret = GTP_I2C_Read(client_addr, point_data, 12);//10字节寄存器加2字节地址
    if (ret < 0)
    {
        GTP_ERROR("I2C transfer error. errno:%d\n ", ret);

        return;
    }
    
    finger = point_data[GTP_ADDR_LENGTH];//状态寄存器数据

    if (finger == 0x00)		//没有数据,退出
    {
        return;
    }

    if((finger & 0x80) == 0)//判断buffer status位
    {
        goto exit_work_func;//坐标未就绪,数据无效
    }

    touch_num = finger & 0x0f;//坐标点数
    if (touch_num > GTP_MAX_TOUCH)
    {
        goto exit_work_func;//大于最大支持点数,错误退出
    }

    if (touch_num)
    {
        for (i = 0; i < touch_num; i++)						//一个点一个点处理
        {
            coor_data = &point_data[i * 8 + 3];

            id = coor_data[0] & 0x0F;									//track id
            pre_id[i] = id;

            input_x  = coor_data[1] | (coor_data[2] << 8);	//x坐标
            input_y  = coor_data[3] | (coor_data[4] << 8);	//y坐标
            input_w  = coor_data[5] | (coor_data[6] << 8);	//size
        
            {
#if 0
										/*根据扫描模式更正X/Y起始方向*/
								switch(LCD_SCAN_MODE)
								{
									case 0:case 7:
										input_y  = LCD_Y_LENGTH - input_y;
										break;
									
									case 2:case 3: 
										input_x  = LCD_X_LENGTH - input_x;
										input_y  = LCD_Y_LENGTH - input_y;
										break;
									
									case 1:case 6:
										input_x  = LCD_X_LENGTH - input_x;
										break;	
									
									default:
									break;
								}
#endif
            }
        }
				touchpad_x = input_y;
				touchpad_y = NT35510_MORE_PIXEL - input_x;
				touchpad_is_down = 1;
    }
    else if (pre_touch)		//touch_ num=0 且pre_touch!=0
    {
			touchpad_is_down = 0;
    }

    pre_touch = touch_num;


exit_work_func:
    {
        ret = GTP_I2C_Write(client_addr, end_cmd, 3);
        if (ret < 0)
        {
            GTP_INFO("I2C write end_cmd error!");
        }
    }

}

然后补全 lv_port_indev_template.c 的触摸接口

/*Return true is the touchpad is pressed*/
static bool touchpad_is_pressed(void)
{
    /*Your code comes here*/
		if(Get_Touchpad_Is_Down())
			return true;
		else
			return false;
}

/*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*/

    (*x) = Get_Touchpad_x();
    (*y) = Get_Touchpad_y();
}

特别注意一下软件IIC的延时,不知道是不是开发板不好布线的原因,野火这个板子用的GPIO模拟IIC,说实话软件模拟通信时序的程序很容易遇到坑,首先它的延时往往会占用CPU的很多时间(尤其是低速通信),写应用程序的时候会带来一些限制和麻烦,而且软件延时往往是for循环执行空操作,会影响这种程序实际执行效果的因素可太多了,且不说更换MCU或者切换时钟源改变系统频率之类的麻烦事,光是编译环境(比如代码优化等级之类的一些编译选项)以及中断程序(延时可能会被中断打断,中断打断的耗时往往不是固定的)带来的影响可能都会让人很头秃,“明明另一个工程可行的程序为什么到这里就不行了呢?!”,相信找bug的时候这一定是大家都非常厌恶的情形(当然更严重的是一个能跑的代码都没有,有一个能跑好歹还能对比一下)。我移植的时候就因为把AC5换成了AC6编译而被这个IIC延时坑了一把,要把这个延时加长。这玩意儿太容易暴雷了,而且有的时候问题还不好找。既然说到这个,顺便提一嘴,要是你的硬件同事在资源足够的情况下这样设计,然后一句“这个都可以软件处理的”把锅甩给你,这种时候就应该先把他吊起来打一顿。

接下来写一下mian函数,加一个圆弧组件看看触摸效果

/**
  * @brief  main函数
  * @param  void  
  * @retval void
  */
int main ( void )
{
 	SystemClock_Config();
	
	/* SystemFrequency / 1000    1ms中断一次
	 * SystemFrequency / 100000	 10us中断一次
	 * SystemFrequency / 1000000 1us中断一次
	 */
	HAL_SYSTICK_Config(SystemCoreClock / 1000);
	
  	lv_init();
	lv_port_disp_init();
	lv_port_indev_init();
		
	lv_obj_t* btn = lv_btn_create(lv_scr_act()); 
	lv_obj_set_pos(btn, 200, 100);
	lv_obj_set_size(btn, 120, 50);
	lv_obj_t* label = lv_label_create(btn);
	lv_label_set_text(label, "Button");
	lv_obj_center(label);

	 /*Create an Arc*/
	 lv_obj_t * arc = lv_arc_create(lv_scr_act());
	 lv_obj_set_size(arc, 150, 150);
	 lv_arc_set_rotation(arc, 135);
	 lv_arc_set_bg_angles(arc, 0, 270);
	 lv_arc_set_value(arc, 40);
	 lv_obj_center(arc);
	
	
	while ( 1 )
	{
		lv_timer_handler();
	}
}

编译,下载,现象
lvgl v8.3移植及组件使用_第16张图片

如何上手lvgl的组件

在上手lvgl的组件之前,需要对lvgl有个大概的了解,建议先看一下lvgl文档的overview。lvgl官方文档有的时候访问起来会很慢,可以看百问网翻译的文档。看了overview之后,要用到什么组件就翻到那个组件去看它的说明和使用方法。

这里以Slider(滑动条)为例,先打开Slider的文档,把它的介绍浏览一下
lvgl v8.3移植及组件使用_第17张图片
可以看到example,有代码和代码效果示例
lvgl v8.3移植及组件使用_第18张图片
通过文档上面示例可以快速了解组件的基本用法
接下来就可以用这个组件写一些交互功能了,比如写一个简单RGB彩色灯控制交互界面,主要代码如下(通过TIMER生成PWM波,用占空比控制光强的代码可参考我之前的文章,这里不是重点,就不多作说明了)不过注意,改变TIMER的PWM占空比我这里直接操作了寄存器,其实给它套个皮然后放在tim.c会比较好(比如叫Set_PWM_Duty(timer,value))。

/**********************
 *  STATIC PROTOTYPES
 **********************/
static void slider_r_event_cb(lv_event_t * e);
static void slider_g_event_cb(lv_event_t * e);
static void slider_b_event_cb(lv_event_t * e);
/**********************
 *  STATIC VARIABLES
 **********************/

/**********************
 *      MACROS
 **********************/

/**********************
 *   GLOBAL FUNCTIONS
 **********************/

void GUI_RGB_Controller(void)
{
	  /*Create a Title*/
		lv_obj_t * title_label;
    title_label = lv_label_create(lv_scr_act());
    lv_label_set_text(title_label, "RGB Controller");
    lv_obj_set_align(title_label, LV_ALIGN_TOP_MID);
	
	
		/*RED LED*/
	  /*Create a slider of red LED*/
    lv_obj_t * slider_r = lv_slider_create(lv_scr_act());
    lv_obj_set_pos(slider_r,30,200);
    lv_obj_set_style_bg_color(slider_r,lv_palette_main(LV_PALETTE_RED),LV_PART_INDICATOR);
		lv_obj_set_style_bg_color(slider_r,lv_palette_main(LV_PALETTE_RED),LV_PART_KNOB);

    /*Create a label below the slider*/
		lv_obj_t * slider_label_r;
    slider_label_r = lv_label_create(lv_scr_act());
    lv_label_set_text(slider_label_r, "0%");
    lv_obj_align_to(slider_label_r, slider_r, LV_ALIGN_OUT_BOTTOM_MID, 0, 10);
	
    lv_obj_add_event_cb(slider_r, slider_r_event_cb, LV_EVENT_VALUE_CHANGED, slider_label_r);	
	
		
		/*GREEN LED*/
	  /*Create a slider of green LED*/
    lv_obj_t * slider_g = lv_slider_create(lv_scr_act());
    lv_obj_set_pos(slider_g,30,400);
    lv_obj_set_style_bg_color(slider_g,lv_palette_main(LV_PALETTE_GREEN),LV_PART_INDICATOR);
		lv_obj_set_style_bg_color(slider_g,lv_palette_main(LV_PALETTE_GREEN),LV_PART_KNOB);

    /*Create a label below the slider*/
		lv_obj_t * slider_label_g;
    slider_label_g = lv_label_create(lv_scr_act());
    lv_label_set_text(slider_label_g, "0%");
    lv_obj_align_to(slider_label_g, slider_g, LV_ALIGN_OUT_BOTTOM_MID, 0, 10);
	
    lv_obj_add_event_cb(slider_g, slider_g_event_cb, LV_EVENT_VALUE_CHANGED, slider_label_g);	
		
		
		/*BLUE LED*/
	  /*Create a slider of blue LED*/
    lv_obj_t * slider_b = lv_slider_create(lv_scr_act());
    lv_obj_set_pos(slider_b,30,600);
    lv_obj_set_style_bg_color(slider_b,lv_palette_main(LV_PALETTE_BLUE),LV_PART_INDICATOR);
		lv_obj_set_style_bg_color(slider_b,lv_palette_main(LV_PALETTE_BLUE),LV_PART_KNOB);

    /*Create a label below the slider*/
		lv_obj_t * slider_label_b;
    slider_label_b = lv_label_create(lv_scr_act());
    lv_label_set_text(slider_label_b, "0%");
    lv_obj_align_to(slider_label_b, slider_b, LV_ALIGN_OUT_BOTTOM_MID, 0, 10);
	
    lv_obj_add_event_cb(slider_b, slider_b_event_cb, LV_EVENT_VALUE_CHANGED, slider_label_b);	
}

static void slider_event_cb(lv_event_t * e)
{
    lv_obj_t * slider = lv_event_get_target(e);
		lv_obj_t * slider_label = lv_event_get_user_data(e);
    char buf[8];
    lv_snprintf(buf, sizeof(buf), "%d%%", (int)lv_slider_get_value(slider));
    lv_label_set_text(slider_label, buf);
    lv_obj_align_to(slider_label, slider, LV_ALIGN_OUT_BOTTOM_MID, 0, 10);
}

static void slider_r_event_cb(lv_event_t * e)
{
		slider_event_cb(e);
		lv_obj_t * slider = lv_event_get_target(e);
		TIM10->CCR1 = lv_slider_get_value(slider) * 25;
}

static void slider_g_event_cb(lv_event_t * e)
{
		slider_event_cb(e);
		lv_obj_t * slider = lv_event_get_target(e);
		TIM11->CCR1 = lv_slider_get_value(slider) * 25;		
}
static void slider_b_event_cb(lv_event_t * e)
{
		slider_event_cb(e);
		lv_obj_t * slider = lv_event_get_target(e);
		TIM13->CCR1 = lv_slider_get_value(slider) * 25;		
}

效果可以看我的演示视频的P2。

其他组件也是一样的使用过程,要用到什么组件就去找到组件的文档看一看示例代码,基本功能的使用应该就没问题了,想进一步深入了解的话可以打开组件的源文件看看它提供了一些什么样的GLOBAL FUNCTIONS供其他文件使用,然后实际用一用基本就没什么问题。值得一提的是,lvgl组件的使用跟你用什么平台没有关系(只要做好移植),所以在PC模拟器上面熟悉lvgl组件即可,毕竟在PC模拟器上面更方便。

如何写UI

上述代码写的UI非常简单简洁,就是3个滑动条加一个小标题。简单的工控界面基本这样写一下就可以了,它一般比较简洁,从UI能一眼看出来设备怎么操控与设备的状态参数之类的就好,按照上面的写法,处理好布局,加一加图标和提示,基本就没什么问题。不过消费电子的界面一般花哨一点会比较好,而且要流行的、易用的,现在流行的界面是一个菜单界面,上面要有APP图标,点这些图标打开应用这种(大伙儿也都习惯这种),这里简单地介绍一下怎么用lvgl写一个这种UI。

添加背景
添加背景其实关键就是添加图片,图片可以写在c文件里面作为数组,也可以保存在外部存储设备由MCU读取,而图片如果不压缩,那它会占用很大的空间,建议存在外部存储里面(比如SD卡)。我一开始本来打算把图片存放在SD卡里面的,移植了文件系统后发现读写有问题,排查问题的时候用SD卡的读写测试例程测试,发现例程偶尔可以跑通,大部分时候会失败,失败的时候程序经常卡在SDMMC_GetCmdResp1()的延时等待里面,推测是开发板硬件问题,大概率是SD卡座接触不良(去年用的时候没有问题)。没办法,只能把图片存为数组来读取了。

打开lvgl文档的 Image(图象)组件说明,浏览一下说明,了解用法
lvgl v8.3移植及组件使用_第19张图片
然后打开 在线图像转换工具,这里引用正点原子教程文档里面的说明
lvgl v8.3移植及组件使用_第20张图片

整体上来说,此工具还是比较简单的,我们现在来介绍一下每一个配置项的含义.
Image file: 从你的 PC 电脑上选择你想要转换的图片
Name: 生成的.c 文件名称
Color format: 颜色格式,有如下 14 个选项值,可以分为 4 类 [第 1 类,真彩色格式]
这类格式的好处就是显示出来的图片不失真,但是占用存储空间大
True color: 真彩色格式
True color with alpha: 带有 alpha 透明度的真彩色格式
True color chroma keyed: 带有 chroma keyed 透明度的真彩色格式
[第 2 类,调色板格式]
这类格式的好处就是占用存储空间小,缺点就是不能显示太鲜艳的图片
Indexed 2 colors: 当图片中的颜色种类不超过 2 种时,可以采用此格式
Indexed 4 colors: 当图片中的颜色种类不超过 4 种时,可以采用此格式
Indexed 16 colors: 当图片中的颜色种类不超过 16 种时,可以采用此格式
Indexed 256 colors: 当图片中的颜色种类不超过 256 种时,可以采用此格式
[第 3 类,纯阴影格式]
这类格式中的像素点只有 alpha 通道的数据,没有 R,G,B 等三个颜色通道的数据
Alpha only 2 shades: alpha 通道占 1 位,每一个像素有 2 种阴影等级
Alpha only 4 shades: alpha 通道占 2 位,每一个像素有 4 种阴影等级
Alpha only 16 shades: alpha 通道占 4 位,每一个像素有 16 种阴影等级
Alpha only 256 shades: alpha 通道占 8 位,每一个像素有 256 种阴影等级
[第 4 类,原始数据格式]
当你用到外部 png 解析库时,才会用到此类格式,它可以转换得到 png 图片的原始数据,
而且是以 C 数组的方式存储在微处理器的内部 flash 上,如果你是把 png 图片直接放到外
部存储介质上,如 SD 卡,U 盘等,那么你就用不到此类格式了
Raw: 原始数据格式
Raw with alpha: 带有 alpha 透明度的原始数据格式
Raw as chroma keyed: 带有 chroma keyed 透明度的原始数据格式
Output format: 输出格式,会根据选择的颜色格式不同出现不同的选项值,但整体上来说,它
具有如下 5 个选项值
C array: 输出.c 文件,即 C 数组格式
Binary RGB332: 输出.bin 文件,即二进制格式,颜色为 RGB332 格式
Binary RGB565: 输出.bin 文件,即二进制格式,颜色为 RGB565 格式
Binary RGB565 Swap: 输出.bin 文件,即二进制格式,颜色为 RGB565 Swap 格式
Binary RGB888: 输出.bin 文件,即二进制格式,颜色为 RGB888 格式
Binary: 输出.bin 文件,即二进制格式,颜色格式自动确定
Dithering: 是否使能真彩色图片的颜色抖动显示,使能之后,转换得到的数据是已经经过抖动
处理了的,所以它是不会增加运行时开销的,而 Dithering 抖动显示技术的好处就是
它欺骗你的眼睛,使用有限的色彩让你看到比实际图象更多色彩的显示方式,通过在
相邻像素间随机的加入不同的颜色来修饰图象,通常这种方式被用于颜色较少的情
况下
Convert: 当你的所有配置项都设置好后,你可以点击此按钮来进行转换

然后把转换得到的.c文件加入工程
lvgl v8.3移植及组件使用_第21张图片

把这个图片设为屏幕背景

	lv_obj_t * bg_top;
	LV_IMG_DECLARE(nahida);
	bg_top = lv_img_create(lv_scr_act());
	lv_img_set_src(bg_top, &nahida );

效果
lvgl v8.3移植及组件使用_第22张图片
APP按钮
制作带图标的按钮要用Image button组件,打开Image button的文档看一下,了解一下使用方法
lvgl v8.3移植及组件使用_第23张图片
准备一下图标,美观的图标最好是由美工来做,但如果是小公司的话可能就要自己用图片编辑软件做一下了(比如我之前所在的小公司就要自己做,我直接用Windows自带的画图3D来做,反正之前的项目像素点也不多,一个图标大概25*25,选好颜色对着一个个像素用鼠标点点点就是了)。
这里我就用PS简单地做了两个图标
lvgl v8.3移植及组件使用_第24张图片
用 在线图像转换工具转换一下
lvgl v8.3移植及组件使用_第25张图片
把图标加入工程,然后添加下面的代码创建 Image button 。(我做的图标好像大了点)

	/*Create an image button*/
	lv_obj_t * imgbtn1 = lv_imgbtn_create(lv_scr_act());
	lv_obj_set_size(imgbtn1,120,120);
	lv_obj_set_pos(imgbtn1,30,30);
	LV_IMG_DECLARE(icon_normal);
	LV_IMG_DECLARE(icon_press);
	lv_imgbtn_set_src(imgbtn1, LV_IMGBTN_STATE_RELEASED, NULL, &icon_normal, NULL);
	lv_imgbtn_set_src(imgbtn1, LV_IMGBTN_STATE_PRESSED, NULL, &icon_press, NULL);

lvgl v8.3移植及组件使用_第26张图片
lvgl v8.3移植及组件使用_第27张图片
写APP应用
这里我就直接用我上面写的 RGB控制 作为应用1,应用2随便写了一个,实际写什么应用根据你的需要来。现在的关键是桌面和APP界面的切换。

首先给 Image button 添加事件

	lv_obj_add_event_cb(imgbtn1, chang_to_app1, LV_EVENT_VALUE_CHANGED , NULL);

处理好界面切换,记得把退出应用时记得把当前屏幕给删掉,节约运行时对内存的使用。

 //要添加APP的话在这里加屏幕比如 SCREEN_APP3
typedef enum
{
	SCREEN_MENU,
	SCREEN_APP1,
	SCREEN_APP2,
}Screen_Type;
struct{
	Screen_Type current_screen;
	Screen_Type next_screen;
}screen_state;

//菜单屏幕
static lv_obj_t * menu_scr;
//APP1屏幕
static lv_obj_t * app1_scr;
//APP2屏幕
static lv_obj_t * app2_scr;
APP3屏幕
//static lv_obj_t * app3_scr;
//...

/**
  * @brief  切换界面
  */
void Change_Screen(void)
{
	if(screen_state.next_screen != screen_state.current_screen)
	{
		//加载新屏幕
		if(screen_state.next_screen == SCREEN_MENU)
		{
			Menu_Init();
		}
		else if(screen_state.next_screen == SCREEN_APP1)
		{
			APP1_Screen_Init();			
		}
		else if(screen_state.next_screen == SCREEN_APP2)
		{
			APP2_Screen_Init();			
		}
		
		//删除原来的屏幕
		if(screen_state.current_screen == SCREEN_MENU)
		{
			lv_obj_del(menu_scr);
		}		
		else if(screen_state.current_screen == SCREEN_APP1)
		{
			lv_obj_del(app1_scr);
		}
		else if(screen_state.current_screen == SCREEN_APP2)
		{
			lv_obj_del(app2_scr);
		}
		
		screen_state.current_screen = screen_state.next_screen;
	}
}

static void chang_to_app1(lv_event_t * e)
{
		screen_state.next_screen = SCREEN_APP1;
}

static void chang_to_app2(lv_event_t * e)
{
		screen_state.next_screen = SCREEN_APP2;
}

static void chang_to_menu(lv_event_t * e)
{
		screen_state.next_screen = SCREEN_MENU;
}

切换屏幕这部分代码其实可以用指针来优化一下,毕竟现在这样切换屏幕还是挺麻烦的,加一个屏幕要改好几个地方确实不方便。唔…算是挖个坑吧。(填不填就随缘啦~)

应用界面就根据自己的需求写一下吧,界面里面记得加退出按钮,还有就是记得在main函数的循环里面调用Change_Screen切换屏幕。

写这部分代码的时候我遇到了Hard Fault异常,经过排查发现用来指向屏幕的指针在经过lv_scr_load()之后会被释放掉,如果下次使用时不重新进行lv_obj_create()的话就会出现数据访问异常,导致Hard Fault。

另外,Flash不够用了,我把纳西妲的图片去掉了一部分。
lvgl v8.3移植及组件使用_第28张图片

如何使用SquareLine Studio设计UI

TODO
lvgl v8.3移植及组件使用_第29张图片

参考

关于lvgl的移植和组件使用参考了百问网与lvgl官方文档,以及正点原子和韦东山在B站的视频(正点原子B站视频、韦东山B站视频)

移植过程参考了:LVGL库入门教程01-移植到STM32(触摸屏)

你可能感兴趣的:(stm32,单片机)