前言: 好久没写博客了,一方面是平时着实没有时间,另一方面是知识还是欠缺,实在没啥技术拿得出手(其实更主要的还是懒!!!)最近玩的比较多的就是LVGL了,自己也是做了几个小项目(后续考虑开源),考虑到网上LVGL入门教程还是比较少,特此出来写篇博客。
对于LVGL就不过多介绍了,能点进来的应该都知道LVGL是什么吧,本篇博客不讲UI中的相关组件,而侧重于讲解对于LVGL中的输入设备,什么是输入设备呢?对于LVGL来说,输入设备有:
LV_INDEV_TYPE_POINTER:触摸板或鼠标 LV_INDEV_TYPE_KEYPAD: 键盘 LV_INDEV_TYPE_ENCODER:编码器 LV_INDEV_TYPE_BUTTON:外部虚拟按钮 而对于大多数项目来说,用触摸屏,实体按键,编码器的比较多(打死我也不说是其他的我都没用过),那么如何将这些设备与LVGL中的组件相关联就是本篇博客的主要目的。
前期准备 环境 VScode+Platformio+LVGL工程(相关教程见【传送门 】) 注:本博客虽是基于Arduino写的,但掌握原理,在其他平台一样能使用
硬件 ESP32 带触摸或不带触摸的TFT显示屏 物理按键 / 编码器(如EC11)/ 多功能按键(如SLLB120200) 知识储备 流程讲解 在摆代码之前,先过一下流程,因为所有的输入设备都是基于这一套流程走的,代码都大同小异传送门 】
实例 所有的实例都是基于LVGL官方模板所写的,模板详见【传送门 】,以下实例的使用方法就是将其建成一个.cpp文件,并新建一个同名.h文件,.h文件用于函数声明且和在其他文件中调用,.c文件用于函数定义(这些都是c语言的基础知识,实在不懂的自行百度吧),这里的输入设备初识化只需要在你工程初识化的地方调用lv_port_indev_init()函数即可
触摸屏 触摸屏反馈及初识化函数用的是第三方库【TFT_eSPI 】中的相关内容
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 #include  <lvgl.h>  #include  <TFT_eSPI.h>  static  void  my_touchpad_read ( lv_indev_drv_t  * indev_driver, lv_indev_data_t  * data ) {     uint16_t  touchX, touchY;     bool  touched = tft.getTouch( &touchX, &touchY, 600  );     if ( !touched )     {         data->state = LV_INDEV_STATE_REL;     }     else      {         data->state = LV_INDEV_STATE_PR;                  data->point.x = touchX;         data->point.y = touchY;                                         } } static  void  my_touchpad_init () { 	 	  	 touch_calibrate(); 	 	 } void  lv_port_indev_init (void ) { 	 	my_touchpad_init(); 	 	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 ); } 
实体按键 实体按键可以使用第三方库【MD_UISwitch 】作为按键的反馈(支持长按阶段性反馈,即长按的话隔一段时间就切换下一个组件,但这样就不能响应LV_EVENT_LONG_PRESSED中的长按响应事件了),也可以直接使用Arduino中digitalWrite()读取引脚电平或者其他单片机中读取引脚电平的方法(这种方式支持LV_EVENT_LONG_PRESSED中的长按响应事件,但不能长按阶段性反馈),看各位取舍吧,我的建议就是一般导航键都有3个按键,左右及确定键,左右键使用MD_UISwitch,中间确定键使用digitalWrite()
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 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 #include  <lvgl.h>  #include  "MD_UISwitch.h"  #define  PinA 1	 #define  PinB 2   #define  PinC 3   MD_UISwitch_Digital Key_L (PinA, (uint8_t )LOW) ; MD_UISwitch_Digital Key_R (PinB, (uint8_t )LOW) ; uint8_t  Key_Scan () { 	if  (digitalRead(PinC) == LOW) 	{ 		 		return  1 ; 	} 	MD_UISwitch::keyResult_t k_r = Key_R.read(); 	MD_UISwitch::keyResult_t k_l = Key_L.read(); 	if  (k_l == MD_UISwitch::KEY_PRESS) 	{ 		 		return  2 ; 	} 	else  if  (k_r == MD_UISwitch::KEY_PRESS) 	{ 		 		return  3 ;			 	} 	 	return  0 ; } static  void  my_key_read ( lv_indev_drv_t  * indev_driver, lv_indev_data_t  * data ) { 	static  uint32_t  last_key = 0 ; 	uint8_t  act_enc = Key_Scan(); 	 	if (act_enc != 0 ) { 	    switch (act_enc) { 	        case  1 : 	            act_enc = LV_KEY_ENTER; 	            data->state = LV_INDEV_STATE_PRESSED;	 	            break ; 	        case  2 : 	            act_enc = LV_KEY_RIGHT; 	            data->state = LV_INDEV_STATE_RELEASED; 	            data->enc_diff++; 	            break ; 	        case  3 : 	            act_enc = LV_KEY_LEFT; 	            data->state = LV_INDEV_STATE_RELEASED; 	            data->enc_diff--; 	            break ; 	    } 	    last_key = (uint32_t )act_enc; 	} 	data->key = last_key; } static  void  my_key_init () { 	Key_L.enableDoublePress(false );	 	Key_L.enableLongPress(false );	 	Key_R.enableDoublePress(false );	 	Key_R.enableLongPress(false );	 	pinMode(PinC, INPUT_PULLUP); } void  lv_port_indev_init (void ) { 	 	my_key_init(); 	 	static  lv_indev_drv_t  indev_drv; 	lv_indev_drv_init( &indev_drv ); 	 	 	indev_drv.type = LV_INDEV_TYPE_ENCODER; 	indev_drv.read_cb = my_key_read; 	lv_indev_drv_register( &indev_drv ); } 
多功能按键(或叫波轮按键) 多功能按键并不属于编码器类型,虽然它们之间有些形状类似,但原理截然不同,多功能按键本质上就是按键
知道了多功能按键的原理,使用起来也非常简单,只需要把多功能按键看做按键使用,将1,4,T(或2,3,T)引脚分别接到单片机IO口上,C引脚接地就能组成上述实体按键一样的效果,代码就不展示了,稍微变通一下就行
编码器 编码器的类型有各式各样,但原理都相同(原理我也没怎么明白,就不丢人了),其都有ABC及S四个引脚(有些有多个S引脚),A,B引脚为左右旋的时候触发,C引脚为按下时触发,因此可以将S引脚接地,AB及C引脚接单片机IO口组成导航键,这里的编码器反馈用到了第三方库【MD_REncoder 】,没办法,Arduino好就好在第三方库多,完全不用懂原理就能用(窃喜)
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 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 #include  <lvgl.h>  #include  "MD_REncoder.h"  #define  PinA 1	 #define  PinB 2   #define  PinC 3   static  MD_REncoder R = MD_REncoder(PinA, PinB);		uint8_t  Encoder_Scan () { 	if  (digitalRead(PinC) == LOW) 	{ 		 		return  1 ; 	} 	uint8_t  x = R.read(); 	if  (x) 	{ 		 		if  (x == DIR_CW ) { 			 			return  2 ; 		} 		else  		{ 			 			return  3 ; 		} 	} 	return  0 ; } static  void  my_encoder_read ( lv_indev_drv_t  * indev_driver, lv_indev_data_t  * data ) { 	static  uint32_t  last_key = 0 ; 	uint8_t  act_enc = Encoder_Scan(); 	 	if (act_enc != 0 ) { 	    switch (act_enc) { 	        case  1 : 	            act_enc = LV_KEY_ENTER; 	            data->state = LV_INDEV_STATE_PRESSED;	 	            break ; 	        case  2 : 	            act_enc = LV_KEY_RIGHT; 	            data->state = LV_INDEV_STATE_RELEASED; 	            data->enc_diff++; 	            break ; 	        case  3 : 	            act_enc = LV_KEY_LEFT; 	            data->state = LV_INDEV_STATE_RELEASED; 	            data->enc_diff--; 	            break ; 	    } 	    last_key = (uint32_t )act_enc; 	} 	data->key = last_key; } static  void  my_encoder_init () { 	R.begin(); 	pinMode(PinC, INPUT_PULLUP); } void  lv_port_indev_init (void ) { 	 	my_encoder_init(); 	 	static  lv_indev_drv_t  indev_drv; 	lv_indev_drv_init( &indev_drv ); 	indev_drv.type = LV_INDEV_TYPE_ENCODER; 	indev_drv.read_cb = my_encoder_read; 	lv_indev_drv_register( &indev_drv ); } 
将输入设备与组件绑定 输入设备初识化成功后如何将其与LVGL中的组件进行绑定呢?这是很多教程中没有说明的,也是我踩了很多坑之后才知道的。这里就需要知道一个lv_group_t的概念,group是LVGL中很重要的一部分,其作用就是将许多LVGL中的组件划分为不同的组,输入设备可以通过切换绑定的组从而控制不同组中的组件,这里介绍几个group中常用的函数,更多函数见【传送门 】
lv_group_t * lv_group_create(void)作用:创建一个lv_group_t实例,如 lv_group_t* group = lv_group_create()  void lv_group_del(lv_group_t* group) void lv_group_set_default(lv_group_t * group) lv_group_t * lv_group_get_default(void) void lv_group_add_obj(lv_group_t * group, lv_obj_t * obj)作用:group填加组件,只有在group中添加的组件才能受到控制  void lv_group_remove_obj(struct _lv_obj_t * obj) void lv_group_remove_all_objs(lv_group_t * group) void lv_group_set_editing(lv_group_t * group, bool edit)作用:设置group为编辑模式或者导航模式,这里的编辑模式只对如下拉列表,按键矩阵等有二级控件时有用,一般来说这些在导航模式都需要先点击确定才能编辑,而在编辑模式下,无需确定即可编辑  void lv_indev_set_group(lv_indev_t * indev, lv_group_t * group)作用:将输入设备与group相绑定,这里的输入设备指lv_indev_drv_register()返回的值,这个最重要了, 前面初识化,添加组件都弄了,要是最后没绑定,一切都白搭  这里给个例子吧
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 lv_obj_t * btn1 = lv_btn_create(NULL );lv_obj_t * btn2 = lv_btn_create(NULL );group = lv_group_create(); lv_group_set_default(group); lv_group_remove_all_objs(group); lv_group_add_obj(group, btn1); lv_group_add_obj(group, btn2); lv_indev_set_group(indev_encoder, group); 
最后 LVGL官方最近出了一个图形化工具,根据简单的拖拽即可导出UI代码,支持最新的LVGL8.2版本,不过目前支持的组件不多,而且是付费应用(有30天试用期),感兴趣的见【传送门 】。