本人【Luckfox幸狐 RV1106 Linux 开发板测评】帖子链接:
一、开箱及测试
二、SDK获取与编译镜像
三、GPIO点灯
四、通过PC机共享网络接入Internet和Ubuntu下Python点灯
五、编译Buildroot系统并测试摄像头
六、PWM控制——命令和C方式
七、PWM控制——Python和设备树方式
八、ADC和UART测试
九、Python控制I2C驱动OLED
十、C程序控制I2C驱动OLED
十一、SPI驱动LCD
十二、实现FrameBuffer设备及LVGL应用
由于本人目前使用合宙0.96寸LCD作为Luckfox Pro Max测评的显示设备,且LCD模块板载五向开关,于是尝试利用五向开关作为LVGL的输入设备。
1、LVGL内部节拍原理
上一篇测评中实际使用了Luckfox官方案例的输入实现方案,自主判断按键并产生不同的逻辑功能。LVGL作为一款流行的GUI库,控件提供了丰富的事件回调接口,如果不能为项目注册输入设备,其控件的事件机制也就无法使用了。
对于LVGL框架本身,需要依靠lv_timer_handler()产生其系统节拍——本质上就是LVGL运行后的一个“大轮训”。目前的案例中,程序启动后大概每5ms调用一次lv_timer_handler()函数,而此函数就是对定时器链表_lv_timer_ll的遍历。
图13-1 lv_timer_handler()核心代码
上图这段代码包含了两层循环。
首先,外层循环通过while(LV_GC_ROOT(_lv_timer_act))条件来控制循环的次数。这个循环的作用是从定时器链表中获取一个元素,并执行定时器的执行函数lv_timer_exec()。
在内层循环中,代码首先通过_lv_ll_get_next()函数获取下一个元素,以便在处理当前定时器后能够加载下一个定时器。然后,它检查lv_timer_exec()函数的返回值来确定是否执行了定时器的创建或删除操作。如果执行了这些操作,那么当前定时器或下一个定时器可能已经被破坏,所以需要重新从第一个定时器开始处理。这就需要通过跳出内层循环来实现了。
因此,两层循环的逻辑是:外层循环从定时器链表中获取一个元素并执行,内层循环在处理完当前定时器后检查是否需要重新开始处理。这样设计可以确保所有的定时器都会被处理,并且在有定时器被创建或删除时能够正确地重新开始处理。
经过分析,lv_timer_handler()就是一个软件定时器链表的处理器,在项目中间隔5ms执行一次,也就意味着每5ms维护一次LVGL线程。
具体LVGL线程的维护工作中最重要的两点:一是UI刷新(更新和绘制UI界面的过程,以响应用户的操作和数据变化),二是输入事件捕获(按下按键或点击触屏,GUI框架会自动调用相应的事件处理回调,并将事件对象传递给该函数)。
LVGL的UI刷新,也就是显示功能,在案例代码中已经知道利用lv_disp_drv_t结构体变量实现——包含:屏幕尺寸、UI缓存、刷新回调。而注册这个结构体变量的lv_disp_drv_register()函数中就创建了定时器(默认周期50ms)。同样,输入设备注册函数lv_indev_drv_register()也创建了定时器(默认周期50ms)。
所以说,用户代码实现每5ms一次的定时器链表处理,默认情况下,每10次即50ms,UI刷新和输入捕获的定时器都会溢出并执行回调,进而完成LVGL基本维护工作。这就是LVGL内部节拍的原理。
图13-2 lv_indev_drv_register核心代码(宏LV_INDEV_DEF_READ_PERIOD默认10即10个节拍)
图13-3 输入设备定时器回调lv_indev_read_timer_cb()核心代码
图13-4 _lv_indev_read()代码实现
由上述分析可得到,lv_indev_drv_register()注册输入驱动——indev_drv即出入参数(类型:lv_indev_drv_t),而这个驱动会进一步封装到lv_indev_t * indev(姑且称为输入设备描述符)中,而且创建的输入定时器也会封装在indev_drv中(见indev->driver->read_timer部分)。
接着,输入定时器的回调lv_indev_read_timer_cb()会调用_lv_indev_read()由注册输入设备描述符得到输入事件,再根据注册输入设备的类型不同调用不同的函数实现用以处理输入事件。本例使用五向开关,输入设备注册为类型LV_INDEV_TYPE_KEYPAD,也就是最后依靠函数indev_keypad_proc来处理输入事件——而具体的五向开关输入事件则是依靠自定义的输入设备驱动回调最终产生。
2、输入案例实现
由上节分析可知,五向开关作为输入设备,需要定义其类型为LV_INDEV_TYPE_KEYPAD,并且编写读取回调,然后注册输入设备,最终的main.c代码如下(基于Luckfox官方案例lvgl_demo和本人上一篇测评,仅修改main.c):
#include "lvgl/lvgl.h"
#include "DEV_Config.h"
#include "lv_drivers/display/fbdev.h"
#include <unistd.h>
#include <pthread.h>
#include <time.h>
#include <sys/time.h>
/**
by Firr. 五向按键做LVGL输入设备
*/
// 按键读取函数——返回按键对应IO编号以区分不同的按键
uint8_t getKeyValue(void) {
if(GET_KEY_RIGHT == 0) return KEY_RIGHT_PIN;
else if(GET_KEY_LEFT == 0) return KEY_LEFT_PIN;
else if(GET_KEY_UP == 0) return KEY_UP_PIN;
else if(GET_KEY_DOWN == 0) return KEY_DOWN_PIN;
else if(GET_KEY_PRESS == 0)return KEY_PRESS_PIN;
else return 0;
}
// 自定义输入事件接口——输入读取回调
void keypadRead(lv_indev_drv_t *indev_drv, lv_indev_data_t *data) {
static uint32_t last_key = 0;
// 读取按键值——即按键对应IO编号,作为“输入事件的数值”
uint32_t act_key = getKeyValue();
if(act_key != 0) {
// 有按键按下,则修改“输入事件的状态”
data->state = LV_INDEV_STATE_PR;
/* 转换按键值为“LVGL控件字符(LVGL control characters)” */
switch(act_key) {
case KEY_LEFT_PIN:
act_key = LV_KEY_LEFT; // 减少值或向左移动
break;
case KEY_RIGHT_PIN:
act_key = LV_KEY_RIGHT; // 减少值或向右移动
break;
case KEY_UP_PIN:
act_key = LV_KEY_PREV; // 聚焦到上一对象
break;
case KEY_DOWN_PIN:
act_key = LV_KEY_NEXT; // 聚焦到下一对象
break;
case KEY_PRESS_PIN:
act_key = LV_KEY_ENTER; // 触发LV_EVENT_PRESSED/CLICKED/LONG_PRESSED
break;
}
last_key = act_key;
} else {
data->state = LV_INDEV_STATE_REL;
}
// 将不同按键转化为LVGL定义的“输入事件key”
data->key = last_key;
}
// LVGL显示缓存区及显示回调声明
#define DISP_BUF_SIZE (160 * 128)
void fbdev_flush(lv_disp_drv_t * drv, const lv_area_t * area, lv_color_t * color_p);
// 按键设备指针——用于设置给“按键设备”设置“(焦点)组”
lv_indev_t *indev_keypad;
// 按键回调函数
void event_handler(lv_event_t * e) {
lv_event_code_t code = lv_event_get_code(e);
if(code == LV_EVENT_CLICKED) printf("Clicked\n");
else if(code == LV_EVENT_VALUE_CHANGED) printf("Toggled\n");
}
// 构建两个按键的UI
void btnExample(void) {
// 按键上的文本
lv_obj_t * label;
// 创建按键btn1——父元素系统屏幕(lv_scr_act()返回)
lv_obj_t * btn1 = lv_btn_create(lv_scr_act());
// 按键btn1具备“可选中(focused)”属性
lv_obj_add_flag(btn1, LV_OBJ_FLAG_SCROLL_ON_FOCUS);
lv_obj_clear_flag(btn1, LV_OBJ_FLAG_SCROLLABLE);
// 按键btn1绑定事件(全部事件)回调
lv_obj_add_event_cb(btn1, event_handler, LV_EVENT_ALL, NULL);
// 按键btn1位于父元素(系统屏幕)中心向左40px位置
lv_obj_align(btn1, LV_ALIGN_CENTER, -40, 0);
// 按键btn1的文本“Btn”,且位于按键中心
label = lv_label_create(btn1);
lv_label_set_text(label, "Btn");
lv_obj_center(label);
// 创建按键btn2,位于屏幕中心向右40px位置,且可选中(选中后背景色改变)
lv_obj_t * btn2 = lv_btn_create(lv_scr_act());
// 按键btn2具备“可选中(focused)”属性
lv_obj_add_flag(btn2, LV_OBJ_FLAG_SCROLL_ON_FOCUS);
lv_obj_clear_flag(btn2, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_event_cb(btn2, event_handler, LV_EVENT_ALL, NULL);
lv_obj_align(btn2, LV_ALIGN_CENTER, 40, 0);
lv_obj_add_flag(btn2, LV_OBJ_FLAG_CHECKABLE);
lv_obj_set_height(btn2, LV_SIZE_CONTENT);
// 按键btn2文本“Toggle”
label = lv_label_create(btn2);
lv_label_set_text(label, "Tog");
lv_obj_center(label);
/* 创建组——界面中所有可选中的控件应以“组”进行管理 */
lv_group_t* Key_group;
Key_group = lv_group_create(); // 创建组实例
lv_indev_set_group(indev_keypad, Key_group); // 组-绑定-输入设备
lv_group_add_obj(Key_group, btn1); // btn1加入组
lv_group_add_obj(Key_group, btn2); // btn2加入组
lv_group_set_editing(Key_group, false); // 组内不可编辑
}
int main(void)
{
/*LittlevGL init*/
lv_init();
/*Linux frame buffer device init*/
fbdev_init();
/*A small buffer for LittlevGL to draw the screen's content*/
static lv_color_t buf[DISP_BUF_SIZE];
/*Initialize a descriptor for the buffer*/
static lv_disp_draw_buf_t disp_buf;
lv_disp_draw_buf_init(&disp_buf, buf, NULL, DISP_BUF_SIZE);
/*Initialize and register a display driver*/
static lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);
disp_drv.draw_buf = &disp_buf;
disp_drv.flush_cb = fbdev_flush;
disp_drv.hor_res = 160;
disp_drv.ver_res = 128;
lv_disp_drv_register(&disp_drv);
/*Initialize pin*/
DEV_ModuleInit();
/* lvgl输入初始化 */
static lv_indev_drv_t indev_drv; // 输入设备实例初始化
lv_indev_drv_init(&indev_drv);
indev_drv.type = LV_INDEV_TYPE_KEYPAD;
indev_drv.read_cb = keypadRead; // 输入事件回调
indev_keypad = lv_indev_drv_register(&indev_drv); // 注册输入设备并获取返回指针
// 显示应用UI
btnExample();
/*Handle LitlevGL tasks (tickless mode)*/
while(1) {
lv_timer_handler();
usleep(5000);
}
return 0;
}
/*Set in lv_conf.h as `LV_TICK_CUSTOM_SYS_TIME_EXPR`*/
uint32_t custom_tick_get(void)
{
static uint64_t start_ms = 0;
if(start_ms == 0) {
struct timeval tv_start;
gettimeofday(&tv_start, NULL);
start_ms = (tv_start.tv_sec * 1000000 + tv_start.tv_usec) / 1000;
}
struct timeval tv_now;
gettimeofday(&tv_now, NULL);
uint64_t now_ms;
now_ms = (tv_now.tv_sec * 1000000 + tv_now.tv_usec) / 1000;
uint32_t time_ms = now_ms - start_ms;
return time_ms;
}
为了测试输入功能,本例在界面上构建两个按钮“Btn”和“Tog”,因为使用开关作为输入而不是鼠标或触屏等Pointer类设备,所以能够交互的控件必须设置“可选中(focused)”属性,同时Tog按键还设置了“可Check”属性——按钮点击后会切换Checked状态并变色。而LVGL又要求可选中的控件必须放置在“组”中进行管理。
案例效果是拨动上、下开关可以圈选不同的按钮,按下开关可以点击圈中按钮。两个按钮点击后触发回调进行控制台输出,
注:除了_lv_timer_ll,LVGL还有不少全局数据结构,不过其定义源码都是多次参数宏嵌套而成的,有兴趣的朋友可以查看“lvgl/src/misc/lv_gc.h、lvgl/src/misc/lv_gc.c”这两个文件。
VID_20240220_155334