目录
一、前言
之前的工作中,我们已经实现了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);
这几个函数的交互方式大致如下:
![](https://img-blog.csdnimg.cn/9e4b856aea8e4164bcccf0e5a830cc63.png)
简单来说,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 编码器
此外,也有通过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 编码器