ESP32-LVGL 开发笔记(二):设备注册

目标

本次的任务是

  1. 运行 lvgl demo
  2. 注册输入设备
    1. 注册按键
    2. 注册编码器(按键模拟)
  3. 开启性能检测

准备工作

运行 lvgl demo 之前,先来对 main.c 代码处理一下

  1. 改成横屏显示
    1. 分辨率,EXAMPLE_LCD_H_RES、EXAMPLE_LCD_V_RES
    2. 偏移,esp_lcd_panel_set_gap
    3. 反转 xy 坐标,swap_xy
  2. 删除 _app_button_cb 函数
  3. 删除 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

  1. 根目录创建 components/ui 组件
  2. ui 组件下创建 01_demo.c01_demo.hCMakeLists.txt
    image

CMakeLists.txt:

idf_component_register(SRCS "01_demo.c"
                    INCLUDE_DIRS "."
                    REQUIRES espressif__esp_lvgl_port lvgl__lvgl)

相关说明下方补充

  1. 复制官方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();
}

显示效果如下:
image

2. 注册输入设备

这里只写了 ButtonEncoder 设备驱动,编码器使用按键模拟实现

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 组件

  1. 创建 indev 组件
  2. 复制 lv_port_indev_template 设备驱动模板复制到 indev 组件,并重命名为 lv_port_indev
    模板路径:
    managed_components\lvgl__lvgl\examples\porting
  3. 修改头文件,见下方

按键驱动实现:

  1. 添加头文件
/*********************
 *      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"
  1. 完成 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);
}
  1. 完善 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;
}
  1. 调用 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();
}

效果展示
ESP32-LVGL 开发笔记(二):注册设备与性能监控-1762860031649

2.2 编码器设备

复制 demo4
Examples 4 - Create a slider and write its value on a label

编码器驱动实现:
编码器使用三个按键模拟实现

  1. 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);
}
  1. 实现 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;
}

有关 编码器 如何控制 组件,这就要引入几个概念

  1. Group
    1. 页面可能会包含很多组件,为了保证 导航模式 下在有效的组件中切换,就需要创建一个 group,把能够交互的组件添加进去
  2. 编辑模式导航模式
    1. 导航模式,就像上面提到的,导航模式下前进或后退会在绑定在 group 的组件切换
    2. 编辑模式,遇到可以交互的组件,如滑块,点击 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);
    }
}

效果展示
ESP32-LVGL 开发笔记(二):注册设备与性能监控-1762864537823

编码器驱动完善
现在的问题是:
导航模式下,按下左右键会持续触发,导致无法准确切换组件
所以想要设计成点击第一次的一定时间内不会再次触发,之后持续触发

基本解决思路是:

  1. 记录第一次按下按键的时间
  2. 持续检测按下的时间
  3. 判断按下事件是否超过设置的延迟(500ms)
  4. 超过的话持续触发,每次 input device read 轮询就会 "前加" 或 "后退"
  5. 松开重置

完整代码如下,可以自行参考:

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

效果展示
ESP32-LVGL 开发笔记(二):注册设备与性能监控-1762953029979

现在已经算的上是相当丝滑了🥳🥳

其他、补充

CMakeLists 相关参数的简单说明:

  • SRCS 源文件列表,添加需要编译的文件
  • INCLUDE_DIRS 目录列表,头文件搜索路径
  • REQUIRES 不是必需的,声明该组件依赖的组件组件所在的文件夹名 即为 组件名。如 ui 组件依赖的 espressif__esp_lvgl_port,还有 mian.c 要使用到 ui 组件的代码,也需要在 REQUIRES 添加 ui 组件的依赖(不过好像会自动依赖 components 下的组件
    相关文档说明:
  • 最小组件 CMakeLists 文件
  • 组件 CMakeLists 文件

group 说明:

测试过程中发现按下按键频率过快的话,可能会栈溢出
当然栈空间给大点就没事了

posted @ 2025-11-20 16:27  嗜睡河豚  阅读(0)  评论(0)    收藏  举报