【esp32&lvgl】-2.5 #lvgl-输入设备

目录

一、前言

二、硬件

三、lvgl&输入设备交互框架

3.1 lvgl框架内的软件设定

3.2 输入设备与esp32间通过gpio端口的硬件设定

3.2.1 ESP-IDF框架下GPIO设定

3.2.2 ESP-IDF框架下中断函数

四、lv_port_input库代码

五、演示测试

参考资料


一、前言

        之前的工作中,我们已经实现了esp32上进行屏幕和文件系统(lvgl框架上)的驱动。下面将为这套系统安装驱动设备来实现与GUI界面之间的交互,本文简单记录一下这个过程中的坑(相比较屏幕和文件系统来说属实是有点多😪)。

二、硬件

        开发板:ESP-WROOM-32

        波轮按键:SIQ-02FVS3

三、lvgl&输入设备交互框架

        lvgl与输入设备的交互代码显然至少包括两部分:

  • lvgl框架内的软件设定部分
  • 输入设备与esp32间通过gpio端口的硬件设定部分

3.1 lvgl框架内的软件设定

        波轮按键的使用在lvgl中有参考例程,即lv_port_indev_template,当然由于不知道具体的硬件配置写的不会很详细,只作为代码的参考框架。本文参考lv_port_indev_template建立lv_port_indev库,其中主要包括四个函数:

void lv_port_indev_init(void);
static void encoder_init(void);
static void encoder_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data);
static void encoder_handler(void);

这几个函数的交互方式大致如下:

图一-lv_port_indev库内函数间交互方式

         简单来说,encoder_init在设定完输入设备与esp32之间的gpio端口后会不断扫描几个gpio端口的状态,特定的状态变化会触发encoder_handler这一函数,这一函数将更新输入设备的状态变量,循环调用encoder_read不断扫描输入设备的状态变量然后更新,从而实现lvgl与输入设备的交互。

3.2 输入设备与esp32间通过gpio端口的硬件设定

        在3.1中简单说了一下在lvgl中添加输入设备的逻辑,这里把硬件的设定部分继续展开。esp32的官方例程其实并没有这类波轮按键的使用例程,不过好消息是有编码器的使用例程并提供了一个rotary_encoder库来方便在esp32中使用编码器。这里简单提一下波轮按键与编码器的区别:简单来说拨轮按钮=编码器+按钮。而对于按钮,在esp32中可以通过中断函数来检测gpio的电平实现与对lvgl的控制。

        也就是说,无论是否使用rotary_encoder库,都需要使用中断函数来实现按钮。那感觉rotary_encoder库的使用必要性就不是很高,而且使用rotary_encoder库还需要专门写一个任务不断扫描编码器的值,占用内存且没必要,不如用另一个思路把波轮按键视为三个特殊的按钮就可以了,识别三个按钮的不同组合在翻译成上拨/下拨/push就可以。

        翻译方法大概是,设定编码器A和Push端为中断触发端口,在A端上升沿触发(拨动)时如果B端为低电平,则是向下拨动,反之B端为高电平则为上拨。这个思路主要是参考了这两篇大佬:

(24条消息) ESP32驱动编码器--SIQ-02FVS3 (Vscode + IDF)_请叫我啸鹏的博客-CSDN博客_esp32 编码器

(24条消息) lvgl使用旋转编码器做为外部输入设备_我来过了.的博客-CSDN博客_lvgl 编码器

        此外,也有通过rotary_encoder库或其他编码器库+一个按钮来实现波轮按键的硬件控制的:

(24条消息) 《ESP32-Arduino》LVGL之输入设备详解及实例(触摸屏,实体按键,编码器,多功能按键)_@MGod吾的博客-CSDN博客_lvgl 按键输入

2.Software/LVGL8_ESP32/components/encoder/encoder.c · 史达芬林/esp32_lvgl_board - Gitee.com

        之前提到了esp32与按钮的交互是通过中断函数来检测gpio的电平实现的,下面就展开说一下esp32中GPIO与中断函数的设定。

3.2.1 ESP-IDF框架下GPIO设定

        与Arduino框架中的pinMode函数类似,ESP-IDF框架下可以设定不同的gpio端口的模式。而且ESP-IDF框架对esp32毕竟是比较底层的一个应用框架,在ESP-IDF框架下可以实现比Arduino框架更详细的设定(当然也更麻烦一点😂)。

        如下代码所示,gpio端口模式的设定主要是通过 gpio_config_t来实现的,构建一个gpio_config_t类型的变量,再通过gpio_config函数来把这一变量配置应用到个端口即可实现ESP-IDF框架下对各个gpio端口的设定。

    gpio_config_t io_conf = {
        .intr_type = GPIO_INTR_ANYEDGE,                 /* 变化->中断*/
        .pin_bit_mask = ENCODER_PUSH_GPIO_PIN_SEL,
        .mode = GPIO_MODE_INPUT,
        .pull_down_en = 1,                              /* 允许下拉  */
        .pull_up_en = 0,
    };
    gpio_config(&io_conf);

        这里面需要注意两个地方:.pin_bit_mask和.intr_type的设定:

  • pin_bit_mask是gpio端口的位掩码,通过这个设定可以实现对单个gpio口的模式设定。而gpio端口的位掩码计算方法可以通过如下方法:
#define ENCODER_A       25          /* 编码器A端 */

#define ENCODER_A_GPIO_PIN_SEL      (1ULL<<ENCODER_A)
  • intr_type是gpio端口的中断类型设置,赋值的变量为gpio_int_type_t类型,如下代码为各种中断类型的说明。
enum gpio_int_type_t

Values:
GPIO_INTR_DISABLE = 0
Disable GPIO interrupt

GPIO_INTR_POSEDGE = 1
GPIO interrupt type : rising edge

GPIO_INTR_NEGEDGE = 2
GPIO interrupt type : falling edge

GPIO_INTR_ANYEDGE = 3
GPIO interrupt type : both rising and falling edge

GPIO_INTR_LOW_LEVEL = 4
GPIO interrupt type : input low level trigger

GPIO_INTR_HIGH_LEVEL = 5
GPIO interrupt type : input high level trigger

3.2.2 ESP-IDF框架下中断函数

        ESP-IDF框架下中断函数的使用官方提供了示例代码,主要包括以下几个重要函数:

gpio_install_isr_service(0);//安装终端服务并设置中断等级
gpio_isr_handler_add(GPIO_INPUT_IO_0, gpio_isr_handler, (void*) GPIO_INPUT_IO_0);
static void IRAM_ATTR gpio_isr_handler(void* arg);

        其中主要是gpio_isr_handler_add这一函数的使用,gpio_isr_handler_add这一函数类似于Arduino框架中的attachInterrupt()函数类似,其中gpio_isr_handler就是触发中断后调用的函数(在本文程序中更换为encoder_handler)。

        值得注意的是在ESP-IDF框架下触发函数gpio_isr_handler在声明的时候需要增加IRAM_ATTR字样,似乎是把这个函数写入esp32的内存的意思,反正是抄官方示例文件写的,详细细节有待继续学习。

        把上述gpio定义和中断函数的定义填充到3.1中说的encoder_init种即可完成波轮按键的设定:

/*Call this function in an interrupt to process encoder events (turn, press)*/
static void IRAM_ATTR encoder_handler(void* arg){
    /*根据gpio的的电平,修改encoder_diff和encoder_state*/
    //是否摁下拨盘的push
    encoder_state = (gpio_get_level(ENCODER_PUSH) == 0)? LV_INDEV_STATE_PRESSED : LV_INDEV_STATE_RELEASED;
    if(encoder_state == LV_INDEV_STATE_PRESSED){
        Encoder_Diff_Enable = false;//摁下拨盘的时候就不再检测拨盘是否转动
    }
    else if(encoder_state == LV_INDEV_STATE_RELEASED){
        Encoder_Diff_Enable = true;
    }
    //计算拨盘旋转持续了多久(中断函数每扫描一次加一,扫描频率???)
    if(Encoder_Diff_Enable){
        int dir = (gpio_get_level(ENCODER_B) == 0 ? -1 : +1);
        encoder_diff += dir;
    }

    /*测试拨轮gpio*/
    //发送出现变化的gpio口及变化情况
    uint32_t gpio_num = (uint32_t) arg;
    xQueueSendFromISR(gpio_evt_queue, &gpio_num, NULL);
}

static void encoder_init(void){    
    /* 1.配置编码器push GPIO */
    gpio_config_t io_conf = {
        .intr_type = GPIO_INTR_ANYEDGE,                 /* 变化->中断*/
        .pin_bit_mask = ENCODER_PUSH_GPIO_PIN_SEL,
        .mode = GPIO_MODE_INPUT,
        .pull_down_en = 1,                              /* 允许下拉  */
        .pull_up_en = 0,
    };
    gpio_config(&io_conf);
    /* 2.配置编码器A GPIO */
    io_conf.intr_type = GPIO_INTR_POSEDGE;              /* 上升->中断*/
    io_conf.pin_bit_mask = ENCODER_A_GPIO_PIN_SEL;
    gpio_config(&io_conf);
    /* 3.配置编码器B GPIO */
    io_conf.intr_type = GPIO_INTR_DISABLE;              
    io_conf.pin_bit_mask = ENCODER_B_GPIO_PIN_SEL;
    gpio_config(&io_conf);

    //create a queue to handle gpio event from isr
    gpio_evt_queue = xQueueCreate(10, sizeof(uint32_t));

    //创建任务-打印拨轮上出现变化的gpio口及变化情况(25/26),用于硬件测试
    xTaskCreate(gpio_task_example, "gpio_task_example", 2048, NULL, 10, NULL);

    //install gpio isr service
    gpio_install_isr_service(0);
    //hook isr handler for specific gpio pin
    gpio_isr_handler_add(ENCODER_PUSH,encoder_handler,(void*) ENCODER_PUSH);
    gpio_isr_handler_add(ENCODER_A,encoder_handler,(void*) ENCODER_A);
}

        此外,由于拨轮按钮是用来上下切换的,太过于灵敏反而会不太好用(小拨一下疯狂切换😂)。在encoder_read种在设置一个简单的滤波代码,拨的时间长一点再触发lvgl中输入设备状态的改变。

        其中encoder_diff的大小应该是与中断函数触发的次数有关(每触发一次+1/-1),因此这个变量的大小应该是与拨动编码器的幅度有关。也就是说,拨动的太小就当无事发生,拨动幅度达到一定大小,触发lvgl中输入设备状态的改变。

/*Will be called by the library to read the encoder*/
static void encoder_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data){
    int filter_flag = 8;
    /*简单滤波,减少误触的可能性*/
    if(encoder_diff >= filter_flag){
        encoder_diff = 1;
    }
    else if(encoder_diff <= -filter_flag){
        encoder_diff = -1;
    }
    else{
        encoder_diff = 0;
    }
    
    /*为拨轮状态赋值,以实现对lv控件的控制*/
    data->enc_diff = encoder_diff;
    data->state = encoder_state;

    encoder_diff = 0;//用完归零
}

四、lv_port_input库代码

        最后附上lv_port_input库总代码。

lv_port_indev.h

#ifndef LV_PORT_INDEV_H
#define LV_PORT_INDEV_H

/*==========================
           INCLUDE
 *==========================*/

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "driver/gpio.h"
#include "lvgl.h"

/*==========================
        VARIABLE DELARE
 *==========================*/

/*gpio端口定义*/
#define ENCODER_A       25          /* 编码器A端 */
#define ENCODER_B       27          /* 编码器B端 */
#define ENCODER_PUSH    26          /* 编码器KEY端 */

#define ENCODER_A_GPIO_PIN_SEL      (1ULL<<ENCODER_A)
#define ENCODER_B_GPIO_PIN_SEL      (1ULL<<ENCODER_B )
#define ENCODER_PUSH_GPIO_PIN_SEL   (1ULL<<ENCODER_PUSH)      /* 编码器KEY GPIO bit 掩码 */


/*==========================
        FUNCTION DELARE
 *==========================*/
/**
 * @brief 输入设备的初始化
 */
void lv_port_indev_init(void);

#endif

 lv_port_indev.c 


/*==========================
           INCLUDE
 *==========================*/

#include "lv_port_indev.h"

/*==========================
        VARIABLE DELARE
 *==========================*/
static const char *TAG = "LV_PORT_INDEV";
/*encoder状态中间量,用于在中断函数与lvgl之间传递encoder状态*/
static int32_t encoder_diff = 0;
static lv_indev_state_t encoder_state = LV_INDEV_STATE_RELEASED;

static bool  Encoder_Diff_Enable = true;//拨轮按钮的旋转功能是否开启(主要是摁下后关闭)
static xQueueHandle gpio_evt_queue = NULL;//数据队列:打印拨轮上出现变化的gpio口及变化情况(25/26),用于硬件测试

/*==========================
        FUNCTION DELARE
 *==========================*/
static void encoder_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data);//from lv_port_indev_template.c
static void IRAM_ATTR encoder_handler(void* arg);//from lv_port_indev_template.c(但是根据esp32的中断函数程序示例进行了修改)https://zhuanlan.zhihu.com/p/502315924
static void encoder_init(void);//拨轮的硬件初始化
static void gpio_task_example(void* arg);//打印拨轮上出现变化的gpio口及变化情况(25/26),用于硬件测试

/*==========================
      FUNCTION DEFINITION
 *==========================*/
///esp官方的编码器用法rotary库+自己写一个按钮,这个不设置中断函数什么的,就直接加一个task
///https://blog.csdn.net/believe666/article/details/123635445+push端也设一个中断函数
void lv_port_indev_init(void){
    static lv_indev_drv_t indev_drv;

    /*初始化拨轮按钮的硬件设置*/
    encoder_init();

    /*Register a encoder input device*/
    lv_indev_drv_init(&indev_drv);
    indev_drv.type = LV_INDEV_TYPE_ENCODER;
    indev_drv.read_cb = encoder_read;
    lv_indev_t* indev = lv_indev_drv_register(&indev_drv);

    /*创建lv_group并和输入设备绑定,使输入设备能控制group内的对象*/
    lv_group_t* group = lv_group_create();
    lv_indev_set_group(indev, group);//把这个group设为默认group,使得在gui的控制逻辑内可以通过lv_group_get_default()函数调用这个group
    lv_group_set_default(group);
}

/*Will be called by the library to read the encoder*/
static void encoder_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data){
    int filter_flag = 8;
    /*简单滤波,减少误触的可能性*/
    if(encoder_diff >= filter_flag){
        encoder_diff = 1;
    }
    else if(encoder_diff <= -filter_flag){
        encoder_diff = -1;
    }
    else{
        encoder_diff = 0;
    }
    
    /*为拨轮状态赋值,以实现对lv控件的控制*/
    data->enc_diff = encoder_diff;
    data->state = encoder_state;

    encoder_diff = 0;//用完归零
}

/*Call this function in an interrupt to process encoder events (turn, press)*/
static void IRAM_ATTR encoder_handler(void* arg){
    /*根据gpio的的电平,修改encoder_diff和encoder_state*/
    //是否摁下拨盘的push
    encoder_state = (gpio_get_level(ENCODER_PUSH) == 0)? LV_INDEV_STATE_PRESSED : LV_INDEV_STATE_RELEASED;
    if(encoder_state == LV_INDEV_STATE_PRESSED){
        Encoder_Diff_Enable = false;//摁下拨盘的时候就不再检测拨盘是否转动
    }
    else if(encoder_state == LV_INDEV_STATE_RELEASED){
        Encoder_Diff_Enable = true;
    }
    //计算拨盘旋转持续了多久(中断函数每扫描一次加一,扫描频率???)
    if(Encoder_Diff_Enable){
        int dir = (gpio_get_level(ENCODER_B) == 0 ? -1 : +1);
        encoder_diff += dir;
    }

    /*测试拨轮gpio*/
    //发送出现变化的gpio口及变化情况
    uint32_t gpio_num = (uint32_t) arg;
    xQueueSendFromISR(gpio_evt_queue, &gpio_num, NULL);
}

static void encoder_init(void){    
    /* 1.配置编码器push GPIO */
    gpio_config_t io_conf = {
        .intr_type = GPIO_INTR_ANYEDGE,                 /* 变化->中断*/
        .pin_bit_mask = ENCODER_PUSH_GPIO_PIN_SEL,
        .mode = GPIO_MODE_INPUT,
        .pull_down_en = 1,                              /* 允许下拉  */
        .pull_up_en = 0,
    };
    gpio_config(&io_conf);
    /* 2.配置编码器A GPIO */
    io_conf.intr_type = GPIO_INTR_POSEDGE;              /* 上升->中断*/
    io_conf.pin_bit_mask = ENCODER_A_GPIO_PIN_SEL;
    gpio_config(&io_conf);
    /* 3.配置编码器B GPIO */
    io_conf.intr_type = GPIO_INTR_DISABLE;              
    io_conf.pin_bit_mask = ENCODER_B_GPIO_PIN_SEL;
    gpio_config(&io_conf);

    //create a queue to handle gpio event from isr
    gpio_evt_queue = xQueueCreate(10, sizeof(uint32_t));

    //创建任务-打印拨轮上出现变化的gpio口及变化情况(25/26),用于硬件测试
    xTaskCreate(gpio_task_example, "gpio_task_example", 2048, NULL, 10, NULL);

    //install gpio isr service
    gpio_install_isr_service(0);
    //hook isr handler for specific gpio pin
    gpio_isr_handler_add(ENCODER_PUSH,encoder_handler,(void*) ENCODER_PUSH);
    gpio_isr_handler_add(ENCODER_A,encoder_handler,(void*) ENCODER_A);
}

static void gpio_task_example(void* arg){
    uint32_t io_num;
    while(1) {
        if(xQueueReceive(gpio_evt_queue, &io_num, portMAX_DELAY)) {
            printf("GPIO[%d] intr, val: %d\n", io_num, gpio_get_level(io_num));
        }
    }
}

五、演示测试

        为了测试拨轮按钮与lvgl的交互,这里简单写了一个包括几个按钮的界面来检验一下,界面编写的说明请移步我的这篇文章:

todo

        最后的结果如下:

拨轮按钮演示

参考资料

代码示例

Pulse Counter (PCNT) - ESP32 - — ESP-IDF 编程指南 v4.4.2 文档 (espressif.com)

esp-idf/gpio_example_main.c   espressif/esp-idf (github.com)

esp-idf/rotary_encoder_example_main.c at v4.4.2 · espressif/esp-idf (github.com)

lvgl/lv_port_indev_template.c at master · lvgl/lvgl (github.com)

思路参考

Input devices(输入设备) — 百问网LVGL中文教程文档 文档 (100ask.net)

(24条消息) 《ESP32-Arduino》LVGL之输入设备详解及实例(触摸屏,实体按键,编码器,多功能按键)_@MGod吾的博客-CSDN博客_lvgl 按键输入

(24条消息) lvgl使用旋转编码器做为外部输入设备_我来过了.的博客-CSDN博客_lvgl 编码器

(24条消息) ESP32驱动编码器--SIQ-02FVS3 (Vscode + IDF)_请叫我啸鹏的博客-CSDN博客_esp32 编码器

  • 3
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
ESP32 ESP-IDF LVGL 是一种用于嵌入式系统的开发框架,LVGL 则是一个用于创建嵌入式图形界面的开源图形库。关于视频流的处理,可以通过调用 ESP-IDF 中的函数来实现。在这个项目中,作者使用了 JPEG 流封装 AVI 视频的方法,将实时读取的图片写入 AVI 文件,并保存到 SD 卡中。通过调用相关函数,如 `jpeg2avi_start`、`jpeg2avi_add_frame` 和 `jpeg2avi_end`,可以实现将一帧帧的图片构成的视频保存下来。这个方法结合了作者原理的讲解和详细的代码示例,非常值得参考和感谢作者原野追逐的贡献。在这个项目中,ESP32 通过 LVGL 图形库提供的界面,可以实时读取摄像头数据,并将视频流传输到网页上,同时将读取的图片写入 SD 卡中的 AVI 文件中。然而,由于 ESP32 的处理能力有限,同时完成读取摄像头数据、传输到网页、写入 SD 卡这三个功能对其来说是一项挑战。在测试中,视频流的帧率较低,不够流畅。因此,需要进行性能优化或者考虑其他解决方案来改善视频流的流畅度。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [VScode+esp-idf:例程(esp32-web-camera)保存视频到sd卡(附源码)](https://blog.csdn.net/hwd00001/article/details/126679619)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值