ESP32-LVGL 开发笔记(二):设备注册
目标
本次的任务是
- 运行 lvgl demo
- 注册输入设备
- 注册按键
- 注册编码器(按键模拟)
- 开启性能检测
准备工作
运行 lvgl demo 之前,先来对 main.c 代码处理一下
- 改成横屏显示
- 分辨率,EXAMPLE_LCD_H_RES、EXAMPLE_LCD_V_RES
- 偏移,esp_lcd_panel_set_gap
- 反转 xy 坐标,swap_xy
- 删除
_app_button_cb函数 - 删除
app_main_display函数内的 ui 代码
static void app_main_display(void)
{
/* Task lock */
lvgl_port_lock(0);
/* Your LVGL objects code here .... */
/* Task unlock */
lvgl_port_unlock();
}
1. 运行 lvgl demo
- 根目录创建
components/ui组件 ui组件下创建01_demo.c、01_demo.h和CMakeLists.txt
![image]()
CMakeLists.txt:
idf_component_register(SRCS "01_demo.c"
INCLUDE_DIRS "."
REQUIRES espressif__esp_lvgl_port lvgl__lvgl)
相关说明下方补充
- 复制官方demo
复制 demo 代码到01_demo.c,在app_main_display调用,完整代码如下
01_demo.c
#include "01_demo.c"
void lv_example_get_started_1(void)
{
/*Change the active screen's background color*/
lv_obj_set_style_bg_color(lv_screen_active(), lv_color_hex(0x003a57), LV_PART_MAIN);
/*Create a white label, set its text and align it to the center*/
lv_obj_t * label = lv_label_create(lv_screen_active());
lv_label_set_text(label, "Hello world");
lv_obj_set_style_text_color(lv_screen_active(), lv_color_hex(0xffffff), LV_PART_MAIN);
lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);
}
01_demo.h
#pragma once
#include "lvgl.h"
void lv_example_get_started_1(void);
main.c,不要忘了引入头文件 和 CMakeLists 注册组件
static void app_main_display(void)
{
/* Task lock */
lvgl_port_lock(0);
/* Your LVGL objects code here .... */
lv_example_get_started_1();
/* Task unlock */
lvgl_port_unlock();
}
显示效果如下:

2. 注册输入设备
这里只写了 Button 和 Encoder 设备驱动,编码器使用按键模拟实现
2.1 按键设备
复制demo2
Example_2 A button with a label and react on click event
demo2是有关按键的,通过这个示例可以简单了解 输入设备 和 事件 的作用
static void btn_event_cb(lv_event_t * e)
{
lv_event_code_t code = lv_event_get_code(e);
lv_obj_t * btn = lv_event_get_target_obj(e);
if(code == LV_EVENT_CLICKED) {
static uint8_t cnt = 0;
cnt++;
/*Get the first child of the button which is the label and change its text*/
lv_obj_t * label = lv_obj_get_child(btn, 0);
lv_label_set_text_fmt(label, "Button: %d", cnt);
}
}
/**
* Create a button with a label and react on click event.
*/
void lv_example_get_started_2(void)
{
lv_obj_t * btn = lv_button_create(lv_screen_active()); /*Add a button the current screen*/
lv_obj_set_pos(btn, 10, 10); /*Set its position*/
lv_obj_set_size(btn, 120, 50); /*Set its size*/
lv_obj_add_event_cb(btn, btn_event_cb, LV_EVENT_ALL, NULL); /*Assign a callback to the button*/
lv_obj_t * label = lv_label_create(btn); /*Add a label to the button*/
lv_label_set_text(label, "Button"); /*Set the labels text*/
lv_obj_center(label);
}
不要忘了在头文件、CMakeLists引入
创建 Indev 组件
- 创建
indev组件 - 复制
lv_port_indev_template设备驱动模板复制到indev组件,并重命名为lv_port_indev
模板路径:
managed_components\lvgl__lvgl\examples\porting - 修改头文件,见下方
按键驱动实现:
- 添加头文件
/*********************
* INCLUDES
*********************/
#if defined(LV_LVGL_H_INCLUDE_SIMPLE)
#include "lvgl.h"
#else
// #include "lvgl/lvgl.h" <<------------- 改这里
#endif
#include "esp_lvgl_port.h"
#include "driver/gpio.h"
- 完成
button_init初始化
static void button_init(void)
{
/*Your code comes here*/
gpio_config_t button_gpio_config = {
.pin_bit_mask = (1ULL<<12),
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
gpio_config(&button_gpio_config);
}
- 完善
button_is_pressed函数
主要操作还是编码器完成的,所以这里只设置了一个按键,多个按键可以自行修改代码。
static bool button_is_pressed(uint8_t id)
{
// 过于简陋🙄,但是没事,这不是重点
/*Your code comes here*/
if(id == 0) {
return gpio_get_level(12) == 0;
}
return false;
}
- 调用
lv_port_indev_init
void app_main(void)
{
/* LCD HW initialization */
ESP_ERROR_CHECK(app_lcd_init());
/* LVGL initialization */
ESP_ERROR_CHECK(app_lvgl_init());
// 一定是在 app_lvgl_init 之后调用
/* input device initialization */
lv_port_indev_init();
/* Show LVGL objects */
app_main_display();
}
效果展示

2.2 编码器设备
复制 demo4
Examples 4 - Create a slider and write its value on a label
编码器驱动实现:
编码器使用三个按键模拟实现
encoder_init初始化
#define ENCODER_LEFT GPIO_NUM_1
#define ENCODER_RIGHT GPIO_NUM_2
#define ENCODER_ENTER GPIO_NUM_42
static void encoder_init(void)
{
/*Your code comes here*/
gpio_config_t io_conf = {
.pin_bit_mask = (1ULL << ENCODER_LEFT) | (1ULL << ENCODER_RIGHT) | (1ULL << ENCODER_ENTER),
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE
};
gpio_config(&io_conf);
}
- 实现
encoder_read方法
/*Will be called by the library to read the encoder*/
static void encoder_read(lv_indev_t * indev_drv, lv_indev_data_t * data)
{
int left = gpio_get_level(ENCODER_LEFT);
int right = gpio_get_level(ENCODER_RIGHT);
int enter = gpio_get_level(ENCODER_ENTER);
// 左右旋转
if (!left) encoder_diff--; // 按下低电平
if (!right) encoder_diff++; // 按下低电平
// 按压状态
if (!enter)
encoder_state = LV_INDEV_STATE_PRESSED;
else
encoder_state = LV_INDEV_STATE_RELEASED;
// 传递给 LVGL
data->enc_diff = encoder_diff;
data->state = encoder_state;
// LVGL 会读取完后清空这个 diff,下次重新计算
encoder_diff = 0;
}
有关 编码器 如何控制 组件,这就要引入几个概念
- Group
- 页面可能会包含很多组件,为了保证 导航模式 下在有效的组件中切换,就需要创建一个 group,把能够交互的组件添加进去
- 编辑模式 和 导航模式
- 导航模式,就像上面提到的,导航模式下前进或后退会在绑定在 group 的组件切换
- 编辑模式,遇到可以交互的组件,如滑块,点击 enter 按键会从 导航模式 切换至 编辑模式,此时前进或后退就会改变滑块的值
完整 demo4 代码如下
void lv_example_get_started_4(void)
{
/*Create a slider in the center of the display*/
lv_obj_t * slider = lv_slider_create(lv_screen_active());
lv_obj_set_width(slider, 200); /*Set the width*/
lv_obj_center(slider); /*Align to the center of the parent (screen)*/
lv_obj_add_event_cb(slider, slider_event_cb, LV_EVENT_VALUE_CHANGED, NULL); /*Assign an event function*/
/*Create a label above the slider*/
label = lv_label_create(lv_screen_active());
lv_label_set_text(label, "0");
lv_obj_align_to(label, slider, LV_ALIGN_OUT_TOP_MID, 0, -15); /*Align top of the slider*/
// 👉 创建 group , 并添加 slider
lv_group_t * g = lv_group_create();
lv_group_add_obj(g, slider);
lv_indev_t * indev_encoder = lv_indev_get_next(NULL);
while(indev_encoder) {
if(lv_indev_get_type(indev_encoder) == LV_INDEV_TYPE_ENCODER) {
lv_indev_set_group(indev_encoder, g);
break;
}
indev_encoder = lv_indev_get_next(indev_encoder);
}
}
效果展示

编码器驱动完善
现在的问题是:
导航模式下,按下左右键会持续触发,导致无法准确切换组件
所以想要设计成点击第一次的一定时间内不会再次触发,之后持续触发
基本解决思路是:
- 记录第一次按下按键的时间
- 持续检测按下的时间
- 判断按下事件是否超过设置的延迟(500ms)
- 超过的话持续触发,每次 input device read 轮询就会 "前加" 或 "后退"
- 松开重置
完整代码如下,可以自行参考:
/*------------------
* Encoder
* -----------------*/
// 按键触发参数
#define INITIAL_DELAY_MS 500 // 初次触发后的等待时间 500ms
// LV_DEF_REFR_PERIOD:Default display refresh, input device read and animation step period
#define REPEAT_INTERVAL_MS 40 // 初始重复触发间隔 LV_DEF_REFR_PERIOD(33) + 40ms
#define MIN_REPEAT_INTERVAL_MS 0 // 最小重复触发间隔 LV_DEF_REFR_PERIOD + 0ms
typedef struct
{
int gpio;
bool last_state;
int64_t press_time;
int64_t last_repeat_time;
int16_t repeat_interval_ms;
} key_state_t;
static key_state_t key_left = {
.last_state = false,
.gpio = ENCODER_LEFT,
.repeat_interval_ms = REPEAT_INTERVAL_MS
};
static key_state_t key_right = {
.last_state = false,
.gpio = ENCODER_RIGHT,
.repeat_interval_ms = REPEAT_INTERVAL_MS
};
/*Initialize your encoder*/
static void encoder_init(void)
{
/*Your code comes here*/
gpio_config_t io_conf = {
.pin_bit_mask = (1ULL << ENCODER_LEFT) | (1ULL << ENCODER_RIGHT) | (1ULL << ENCODER_ENTER),
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE};
gpio_config(&io_conf);
}
// 检查单个方向键是否该触发
static bool check_key_trigger(key_state_t *key)
{
bool level = !gpio_get_level(key->gpio); // 低电平为按下
int64_t now = esp_timer_get_time() / 1000; // 当前时间 ms
if (level)
{
if (!key->last_state)
{
// 首次按下
key->press_time = now;
key->last_repeat_time = now;
key->last_state = true;
return true; // 立即触发一次
}
else
{
int64_t held_time = now - key->press_time;
if (held_time < INITIAL_DELAY_MS)
{
// 在首次延迟阶段,不触发
return false;
}
else
{
// 超过延迟,开始周期触发
if (now - key->last_repeat_time >= key->repeat_interval_ms)
{
key->last_repeat_time = now;
key->repeat_interval_ms -= 2; // 长按后加速
if (key->repeat_interval_ms < MIN_REPEAT_INTERVAL_MS)
key->repeat_interval_ms = MIN_REPEAT_INTERVAL_MS; // 最小间隔 10ms
return true;
}
}
}
}
else
{
// 松开
key->last_state = false;
key->repeat_interval_ms = REPEAT_INTERVAL_MS; // 松开后重置间隔
}
return false;
}
/*Will be called by the library to read the encoder*/
static void encoder_read(lv_indev_t *indev_drv, lv_indev_data_t *data)
{
encoder_diff = 0;
// 左右按键:控制旋转
if (check_key_trigger(&key_left))
encoder_diff--;
if (check_key_trigger(&key_right))
encoder_diff++;
// 中键:控制按压状态
bool enter_pressed = !gpio_get_level(ENCODER_ENTER);
encoder_state = enter_pressed ? LV_INDEV_STATE_PRESSED : LV_INDEV_STATE_RELEASED;
// 输出给 LVGL
data->enc_diff = encoder_diff;
data->state = encoder_state;
}
功能验证
为了验证导航模式下不同组件之间可以正常切换,我们需要再次添加一个组件来测试
// 创建一个 button 组件
lv_obj_t* button = lv_button_create(lv_screen_active());
lv_obj_set_width(button, LV_SIZE_CONTENT);
lv_obj_set_height(button, LV_SIZE_CONTENT);
lv_obj_align(button, LV_ALIGN_BOTTOM_MID, 0, -15); /*Align bottom */
lv_obj_t* button_label = lv_label_create(button);
lv_label_set_text(button_label, "Button");
lv_obj_center(button_label);
lv_obj_align_to(button_label, button, LV_ALIGN_CENTER, 0, 0);
// 添加进 group
lv_group_add_obj(g, button);
效果展示

现在已经算的上是相当丝滑了🥳🥳
其他、补充
CMakeLists 相关参数的简单说明:
SRCS源文件列表,添加需要编译的文件INCLUDE_DIRS目录列表,头文件搜索路径REQUIRES不是必需的,声明该组件依赖的组件,组件所在的文件夹名 即为 组件名。如ui组件依赖的espressif__esp_lvgl_port,还有mian.c要使用到ui组件的代码,也需要在REQUIRES添加 ui 组件的依赖(不过好像会自动依赖 components 下的组件
相关文档说明:- 最小组件 CMakeLists 文件
- 组件 CMakeLists 文件
group 说明:
测试过程中发现按下按键频率过快的话,可能会栈溢出
当然栈空间给大点就没事了

浙公网安备 33010602011771号