ESP-IDF学习笔记-驱动SIQ-02FVS3编码器

SIQ-02FVS3是一款小体积的拨轮滚轮旋转编码器开关,同时具有按钮和编码器的功能且体积较小,但是比较昂贵。本文记录使用ESP-IDF驱动该编码器开关的过程。

参考文章

数据手册

ESP32驱动编码器–SIQ-02FVS3 (Vscode + IDF)

IDF通用定时器使用

硬件与环境

  1. 硬件连接:A——IO2 | S——IO10 | C——GND | B——IO3
  2. 软件环境:CLion+ESP-IDFv5.1.1

编码器电路结构与原理

本次使用的编码器各引脚内部电路结构如下:

image-20240814150941250

可见,当编码器工作时,A,B,S端会与C端连接,由于此时C端接地,因此我们需要将A,B,S端的引脚拉高到高电平才能正常使用。

当编码器旋转时,输出波形如下:
image-20240814151801395

可见,当旋转方向不同时,AB两端上升沿到来的先后不同。当从CW方向转动的时候,A的波形上升沿比B波形的上升沿快,具体快多少,这里数据手册给出24±3°,当从CCW方向转动的这个时候恰好相反,B的相位上升沿快于A的上升沿。这样可以通过捕获上升沿的顺序来判断编码器的方向。

程序思路

编码器读取

根据以上原理,我们可以使用以下方法读取编码器:

  1. 检测A上升沿,在A上升时检测B的状态,或相反。
  2. 检测AB上升沿,判断先后顺序。
  3. 检测A的边缘,通过连续两次边缘时AB的状态来判断方向。

在STM32中,有定时器有专门的模式来读取编码器,但是ESP32没有,因此考虑使用GPIO中断来实现。但是引入中断会出现一个问题,由于没有硬件滤波,很容易出现毛刺多次触发中断,还好ESP32内置硬件毛刺滤波器,可以过滤毛刺。

为减少毛刺的干扰,这这里使用方法三进行读取。将A端口设置为双边缘中断触发,利用状态机,检测连续的两个边缘的AB电平,这样就可以判断一些不正常的输出。这样可以在较简单代码下去除一些毛刺。

可能的正常结果如下:

  1. A1=1,B1=0,A2=0,B2=1——CW方向
  2. A1=1,B1=1,A2=0,B2=0——CCW方向
  3. A1=0,B1=1,A2=1,B2=0——CW方向
  4. A1=0,B1=0,A2=1,B2=1——CCW方向
  5. 其余情况为无效情况

按键实现

对编码器按键实现长按短按检测,这里考虑使用一个按键状态机。

设置一个定时器,每100ms读取一次按键并进行状态转移,状态转移图如下:

image-20240815102429267

  1. 按下时触发,可在进入KEY_DOWN时执行。
  2. 松开时触发,可在从KEY_DOWN到IDLE时进行。
  3. 长按,可在进入KET_LONG时进行。

代码实现

使用的数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#define ENCODER_A_PIN 2
#define ENCODER_B_PIN 3
#define ENCODER_S_PIN 10

typedef enum {
ENCODER_NONE=0, /* no event */
ENCODER_CW, /* revolve in CW dir */
ENCODER_CCW, /* revolve in CCW dir */
ENCODER_SW_DOWN, /* bottom is pressed */
ENCODER_SW_LONG /* bottom is long pressed */
}Encoder_Event;

typedef enum{
KEY_STATUS_IDLE, /* IDLE */
KEY_STATUS_PRESSED, /* detect press,wait to confirm */
KEY_STATUS_DOWN, /* Key down confirmed */
KEY_STATUS_LONG /* Key long down */
}KEY_Status;

static uint8_t key_count=0;//conut to 10 before next status
static KEY_Status key_status=KEY_STATUS_IDLE;
static gptimer_handle_t key_timer;
static QueueHandle_t encoder_gpio_event_queue=NULL;//Use for msg from ISR
static uint8_t edgecount=0,as1,as2,bs1,bs2;//statues of A and B in two check
static gpio_glitch_filter_handle_t gpiofilter_S,gpiofilter_A,gpiofilter_B;

中断函数

需要两个中断函数,一个是给编码器判断方向使用的GPIO中断,一个是给按键使用的定时器中断。

GPIO中断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* the dir interrupt function
* @param arg
*/
static void IRAM_ATTR gpio_isr_handler(void* arg)
{
// uint32_t gpio_num = (uint32_t) arg;//not use
Encoder_Event msg;//result msg

if(edgecount==0){//first edge
bs1= gpio_get_level(ENCODER_B_PIN);
as1= gpio_get_level(ENCODER_A_PIN);
edgecount=1;
}
else{
bs2= gpio_get_level(ENCODER_B_PIN);
as2= gpio_get_level(ENCODER_A_PIN);

if((as1==1 && bs1==0 && as2==0 && bs2==1) || (as1==0 && bs1==1 && as2==1 && bs2==0)){
msg=ENCODER_CW;
xQueueOverwriteFromISR(encoder_gpio_event_queue,&msg,NULL);
}
else if((as1==1 && bs1==1 && as2==0 && bs2==0) || (as1==0 && bs1==0 && as2==1 && bs2==1)){
msg=ENCODER_CCW;
xQueueOverwriteFromISR(encoder_gpio_event_queue,&msg,NULL);
}
edgecount=0;
}
}

该中断使用IRAM_ATTR放到RAM中,加快执行速度。通过edgecount变量判断捕获到第几个上升沿。判断后通过xQueueOverwriteFromISR()函数向事件队列覆写传输Encoder_Event类型的数据。关于FreeRTOS队列的用法这里不细说了,网上有很多教程,这里需要注意:1.中断中使用FromISR后缀的函数;2.xQueueOverwrite只能对长度为1的队列使用。

按键定时器中断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
static bool key_timer_cb(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *user_ctx)
{
BaseType_t high_task_awoken = pdFALSE;
uint8_t key_read;
Encoder_Event msg;

key_read= !gpio_get_level(ENCODER_S_PIN);//active low
switch (key_status) {
case KEY_STATUS_IDLE:
if(key_read){
key_status=KEY_STATUS_PRESSED;
}
else {
key_status = KEY_STATUS_IDLE;
}
break;
case KEY_STATUS_PRESSED:
if(key_read){
key_status=KEY_STATUS_DOWN;
}else{
key_status=KEY_STATUS_IDLE;
}
break;
case KEY_STATUS_DOWN:
if(key_read){
key_count++;
if(key_count==10){//active key long down event
key_count=0;
key_status=KEY_STATUS_LONG;
msg=ENCODER_SW_LONG;
xQueueOverwriteFromISR(encoder_gpio_event_queue,&msg,&high_task_awoken);
} else{
key_status=KEY_STATUS_DOWN;
}
} else{//active key down event when realise
key_status=KEY_STATUS_IDLE;
msg=ENCODER_SW_DOWN;
xQueueOverwriteFromISR(encoder_gpio_event_queue,&msg,&high_task_awoken);
}
break;
case KEY_STATUS_LONG:
if(key_read){
key_status=KEY_STATUS_LONG;
} else{
key_status=KEY_STATUS_IDLE;
}
break;
default:
break;
}

return high_task_awoken==pdTRUE;
}

该中断最主要是其中用switch语句实现的状态机。利用100ms一次的中断频率读取GPIO输入,同时做到了按键消抖、短按以及长按监测。最后将结果通过队列发送到读取函数。

初始化函数

初始化使用到的外设较多,代码比较长。

输入滤波器

1
2
3
4
5
6
7
8
9
10
11
12
13
gpio_pin_glitch_filter_config_t filter;

/*create gpio filter */
filter.clk_src=GLITCH_FILTER_CLK_SRC_DEFAULT;
filter.gpio_num=ENCODER_S_PIN;
ret|= gpio_new_pin_glitch_filter(&filter,&gpiofilter_S);
ret|= gpio_glitch_filter_enable(gpiofilter_S);
filter.gpio_num=ENCODER_A_PIN;
ret|= gpio_new_pin_glitch_filter(&filter,&gpiofilter_A);
ret|= gpio_glitch_filter_enable(gpiofilter_A);
filter.gpio_num=ENCODER_B_PIN;
ret|= gpio_new_pin_glitch_filter(&filter,&gpiofilter_B);
ret|= gpio_glitch_filter_enable(gpiofilter_B);

ESP32C3自带引脚毛刺滤波,滤除时间小于两个采样时钟的毛刺,使用起来比较简单,配置方法见以上代码。

引脚初始化

1
2
3
4
5
6
7
8
9
10
11
12
/*SW and B just input pull up,no interrupt */
config.pin_bit_mask=(1ULL<<ENCODER_S_PIN)|(1ULL<<ENCODER_B_PIN);
config.mode=GPIO_MODE_INPUT;
config.intr_type=GPIO_INTR_DISABLE;
config.pull_down_en=GPIO_PULLDOWN_DISABLE;
config.pull_up_en=GPIO_PULLUP_ENABLE;
ret|= gpio_config(&config);

/* input A have interrupt */
config.pin_bit_mask=(1ULL<<ENCODER_A_PIN);
config.intr_type=GPIO_INTR_ANYEDGE;//double edge
ret|= gpio_config(&config);

引脚初始化没什么好说的,主要注意设置A输入中断类型为双边沿且三个引脚全部上拉

添加GPIO中断函数

1
2
3
/*install isr,not use gpio_isr_register(),for it will register a single handler for all pins*/
ret|= gpio_install_isr_service(0);
ret|= gpio_isr_handler_add(ENCODER_A_PIN,gpio_isr_handler,(void*)ENCODER_A_PIN);

好有一个gpio_isr_register()方法添加,不过那会将所有GPIO给一个中断,这里中断资源还充足,不需要使用该方法。

创建通用定时器中断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*create timer and add interrupt*/
gptimer_config_t timer= {
.clk_src = GPTIMER_CLK_SRC_DEFAULT,
.direction=GPTIMER_COUNT_UP,
.resolution_hz=10000,//10000 Hz,1 tick =0.1 ms,must beyond (80MHz/65535=)1221Hz.
};
ret|= gptimer_new_timer(&timer,&key_timer);
gptimer_alarm_config_t alarm_config={
.reload_count=0,//restart when alarm
.alarm_count=1000,//100ms in 10000Hz
.flags.auto_reload_on_alarm=true,//enable auto-reload
};
ret|= gptimer_set_alarm_action(key_timer,&alarm_config);
gptimer_event_callbacks_t cbs={
.on_alarm= key_timer_cb,//key status manager
};
ret|= gptimer_register_event_callbacks(key_timer,&cbs,NULL);
ret|= gptimer_enable(key_timer);
ret|= gptimer_start(key_timer);

这里就是简单定时器使用方法,但是要注意频率的设置。由于分频器为16位,最大分频数为65535,因此根据ESP32C3的主频设置,会有一个最低的计数频率,不要低于该值

创建事件队列

考虑到任务同步,这里使用一个队列来传递信息。

1
2
/*create the msg queue*/
encoder_gpio_event_queue= xQueueCreate(1,sizeof(Encoder_Event));//length is 1,each element is Encoder_Event

要注意长度为1,以能够使用Overwrite覆写函数,提高实时性。

反初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void encoder_deinit(void)
{
//del filter
gpio_glitch_filter_disable(gpiofilter_S);
gpio_del_glitch_filter(gpiofilter_S);
gpio_glitch_filter_disable(gpiofilter_A);
gpio_del_glitch_filter(gpiofilter_A);
gpio_glitch_filter_disable(gpiofilter_B);
gpio_del_glitch_filter(gpiofilter_B);
//del timer
gptimer_stop(key_timer);
gptimer_disable(key_timer);
gptimer_del_timer(key_timer);
//del isr
gpio_isr_handler_remove(ENCODER_A_PIN);
gpio_uninstall_isr_service();
//del queue
vQueueDelete(encoder_gpio_event_queue);
}

读取函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* read encoder
* @param timeout time to wait,0 for no blocking
* @return
*/
Encoder_Event encoder_read(uint8_t timeout)
{
Encoder_Event ret;
if(pdTRUE== xQueueReceive(encoder_gpio_event_queue,&ret,timeout/portTICK_PERIOD_MS)){
return ret;
} else{//no msg in queue
return ENCODER_NONE;
}
}

直接调用队列读取,其中timeout可以为0,表示队列为空立刻返回,实现无阻塞。