My post [Luckfox RV1106 Linux Development Board Review] link:
1. Unboxing and testing
2. SDK acquisition and image compilation
3. GPIO Lighting
4. Access the Internet through PC shared network and light up Python in Ubuntu
5. Compile the Buildroot system and test the camera
6. PWM control - command and C mode
7. PWM Control - Python and Device Tree Method
8. ADC and UART test
9. Python controls I2C to drive OLED
10. C program controls I2C to drive OLED
11. SPI drives LCD
12. Implement FrameBuffer device and LVGL application
Since I am currently using a Hezhou 0.96-inch LCD as the display device for Luckfox Pro Max evaluation, and the LCD module has a five-way switch on board, I tried to use the five-way switch as the input device for LVGL.
1. LVGL internal beat principle
In the previous evaluation, the input implementation scheme of Luckfox official case was actually used to independently judge the key presses and generate different logical functions. As a popular GUI library, LVGL provides a rich event callback interface for controls. If the input device cannot be registered for the project, the event mechanism of its controls will not be usable.
For the LVGL framework itself, it needs to rely on lv_timer_handler() to generate its system beat, which is essentially a "big round-robin" after LVGL runs. In the current case, the lv_timer_handler() function is called approximately every 5ms after the program starts, and this function traverses the timer linked list _lv_timer_ll.
Figure 13-1 lv_timer_handler() core code
The code above contains two layers of loops.
First, the outer loop controls the number of loops through the while(LV_GC_ROOT(_lv_timer_act)) condition. The purpose of this loop is to get an element from the timer list and execute the timer's execution function lv_timer_exec().
In the inner loop, the code first gets the next element through the _lv_ll_get_next() function so that the next timer can be loaded after the current timer is processed. Then, it checks the return value of the lv_timer_exec() function to determine whether the timer creation or deletion operation has been performed. If these operations have been performed, then the current timer or the next timer may have been destroyed, so the processing needs to be restarted from the first timer. This needs to be achieved by jumping out of the inner loop.
Therefore, the logic of the two-layer loop is: the outer loop gets an element from the timer list and executes it, and the inner loop checks whether it needs to restart processing after processing the current timer. This design ensures that all timers are processed and can be correctly restarted when a timer is created or deleted.
After analysis, lv_timer_handler() is a software timer linked list processor, which is executed every 5ms in the project, which means that the LVGL thread is maintained every 5ms.
The two most important points in the maintenance work of the specific LVGL thread are: one is UI refresh (the process of updating and drawing the UI interface to respond to user operations and data changes), and the other is input event capture (when you press a button or click the touch screen, the GUI framework will automatically call the corresponding event processing callback and pass the event object to the function) .
LVGL's UI refresh, or display function, is already known in the case code to be implemented using the lv_disp_drv_t structure variable, which includes: screen size, UI cache, and refresh callback. The lv_disp_drv_register() function that registers this structure variable creates a timer (with a default period of 50ms). Similarly, the input device registration function lv_indev_drv_register() also creates a timer (with a default period of 50ms).
Therefore, the user code implements the timer linked list processing every 5ms. By default, every 10 times, that is, 50ms, the timers for UI refresh and input capture will overflow and execute callbacks, thereby completing the basic maintenance work of LVGL. This is the principle of LVGL's internal beat.
Figure 13-2 lv_indev_drv_register core code (the macro LV_INDEV_DEF_READ_PERIOD defaults to 10, i.e. 10 beats)
Figure 13-3 Input device timer callback lv_indev_read_timer_cb() core code
Figure 13-4 _lv_indev_read() code implementation
From the above analysis, we can get that lv_indev_drv_register() registers the input driver - indev_drv is the input and output parameter (type: lv_indev_drv_t), and this driver will be further encapsulated into lv_indev_t * indev (let's call it input device descriptor), and the created input timer will also be encapsulated in indev_drv (see indev->driver->read_timer part).
Next, the callback of the input timer lv_indev_read_timer_cb() will call _lv_indev_read() to get the input event from the registered input device descriptor, and then call different functions to handle the input event according to the type of the registered input device. In this example, a five-way switch is used, and the input device is registered as type LV_INDEV_TYPE_KEYPAD, which means that the function indev_keypad_proc is used to handle the input event. The specific five-way switch input event is finally generated by the custom input device driver callback.
2. Input case implementation
From the analysis in the previous section, we can know that the five-way switch, as an input device, needs to be defined as LV_INDEV_TYPE_KEYPAD, and write a read callback, and then register the input device. The final main.c code is as follows (based on Luckfox official case lvgl_demo and my previous review, only main.c is modified):
#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;
}
In order to test the input function, this example builds two buttons "Btn" and "Tog" on the interface. Because switches are used as input instead of pointer devices such as mice or touch screens, the interactive controls must have the "focused" attribute set, and the Tog button also has the "checkable" attribute set - the button will switch to the checked state and change color after clicking. However, LVGL requires that selectable controls must be placed in a "group" for management.
The effect of the case is that you can circle different buttons by toggling the up and down switches, and click the circled buttons by pressing the switch. After clicking the two buttons, the callback is triggered to output to the console.
Note: In addition to _lv_timer_ll, LVGL has many global data structures, but their definition source codes are all nested with multiple parameter macros. Interested friends can check the two files "lvgl/src/misc/lv_gc.h, lvgl/src/misc/lv_gc.c".
VID_20240220_155334