ESP-IDF学习笔记-驱动SIQ-02FVS3编码器
8月 13
ESP32学习笔记
ESP-IDF , ESP32 , 编码器
字数统计: 1.9k(字)
阅读时长: 8(分)
ESP-IDF学习笔记-驱动SIQ-02FVS3编码器 SIQ-02FVS3是一款小体积的拨轮滚轮旋转编码器开关,同时具有按钮和编码器的功能且体积较小,但是比较昂贵。本文记录使用ESP-IDF驱动该编码器开关的过程。
参考文章 数据手册
ESP32驱动编码器–SIQ-02FVS3 (Vscode + IDF)
IDF通用定时器使用
硬件与环境
硬件连接:A——IO2 | S——IO10 | C——GND | B——IO3
软件环境:CLion+ESP-IDFv5.1.1
编码器电路结构与原理 本次使用的编码器各引脚内部电路结构如下:
可见,当编码器工作时,A,B,S端会与C端连接,由于此时C端接地,因此我们需要将A,B,S端的引脚拉高到高电平 才能正常使用。
当编码器旋转时,输出波形如下:
可见,当旋转方向不同时,AB两端上升沿到来的先后不同。当从CW方向转动的时候,A的波形上升沿比B波形的上升沿快,具体快多少,这里数据手册给出24±3°,当从CCW方向转动的这个时候恰好相反,B的相位上升沿快于A的上升沿。这样可以通过捕获上升沿的顺序来判断编码器的方向。
程序思路 编码器读取 根据以上原理,我们可以使用以下方法读取编码器:
检测A上升沿,在A上升时检测B的状态,或相反。
检测AB上升沿,判断先后顺序。
检测A的边缘,通过连续两次边缘时AB的状态来判断方向。
在STM32中,有定时器有专门的模式来读取编码器,但是ESP32没有,因此考虑使用GPIO中断来实现。但是引入中断会出现一个问题,由于没有硬件滤波,很容易出现毛刺多次触发中断,还好ESP32内置硬件毛刺滤波器 ,可以过滤毛刺。
为减少毛刺的干扰,这这里使用方法三进行读取。将A端口设置为双边缘中断触发,利用状态机,检测连续的两个边缘的AB电平,这样就可以判断一些不正常的输出。这样可以在较简单代码下去除一些毛刺。
可能的正常结果如下:
A1=1,B1=0,A2=0,B2=1——CW方向
A1=1,B1=1,A2=0,B2=0——CCW方向
A1=0,B1=1,A2=1,B2=0——CW方向
A1=0,B1=0,A2=1,B2=1——CCW方向
其余情况为无效情况
按键实现 对编码器按键实现长按短按检测,这里考虑使用一个按键状态机。
设置一个定时器,每100ms读取一次按键并进行状态转移,状态转移图如下:
按下时触发,可在进入KEY_DOWN时执行。
松开时触发,可在从KEY_DOWN到IDLE时进行。
长按,可在进入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 , ENCODER_CW, ENCODER_CCW, ENCODER_SW_DOWN, ENCODER_SW_LONG }Encoder_Event; typedef enum { KEY_STATUS_IDLE, KEY_STATUS_PRESSED, KEY_STATUS_DOWN, KEY_STATUS_LONG }KEY_Status; static uint8_t key_count=0 ;static KEY_Status key_status=KEY_STATUS_IDLE;static gptimer_handle_t key_timer;static QueueHandle_t encoder_gpio_event_queue=NULL ;static uint8_t edgecount=0 ,as1,as2,bs1,bs2;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 static void IRAM_ATTR gpio_isr_handler (void * arg) { Encoder_Event msg; if (edgecount==0 ){ 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); 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 ){ 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 { 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;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 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); config.pin_bit_mask=(1ULL <<ENCODER_A_PIN); config.intr_type=GPIO_INTR_ANYEDGE; ret|= gpio_config(&config);
引脚初始化没什么好说的,主要注意设置A输入中断类型为双边沿且三个引脚全部上拉 。
添加GPIO中断函数
1 2 3 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 gptimer_config_t timer= { .clk_src = GPTIMER_CLK_SRC_DEFAULT, .direction=GPTIMER_COUNT_UP, .resolution_hz=10000 , }; ret|= gptimer_new_timer(&timer,&key_timer); gptimer_alarm_config_t alarm_config={ .reload_count=0 , .alarm_count=1000 , .flags.auto_reload_on_alarm=true , }; ret|= gptimer_set_alarm_action(key_timer,&alarm_config); gptimer_event_callbacks_t cbs={ .on_alarm= key_timer_cb, }; ret|= gptimer_register_event_callbacks(key_timer,&cbs,NULL ); ret|= gptimer_enable(key_timer); ret|= gptimer_start(key_timer);
这里就是简单定时器使用方法,但是要注意频率的设置。由于分频器为16位,最大分频数为65535,因此根据ESP32C3的主频设置,会有一个最低的计数频率,不要低于该值 。
创建事件队列
考虑到任务同步,这里使用一个队列来传递信息。
1 2 encoder_gpio_event_queue= xQueueCreate(1 ,sizeof (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 ) { 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); gptimer_stop(key_timer); gptimer_disable(key_timer); gptimer_del_timer(key_timer); gpio_isr_handler_remove(ENCODER_A_PIN); gpio_uninstall_isr_service(); vQueueDelete(encoder_gpio_event_queue); }
读取函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 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 { return ENCODER_NONE; } }
直接调用队列读取,其中timeout可以为0,表示队列为空立刻返回,实现无阻塞。