ESP32 GPIO中断与防抖:按键/脉冲采集实战
2026/9/6 17:30:18 网站建设 项目流程

ESP32 GPIO 中断与防抖:按键/脉冲采集实战

先说结论

硬件一个 RC 滤波 + 软件一个防抖算法,基本能搞定 99% 的按键场景。如果做脉冲计数(霍尔传感器、编码器),得用 GPIO 中断 + 定时器配合。

按键检测看起来是入门级的问题——GPIO 配输入,读电平,完事。但实际做产品时,一个按键可能踩 5 个坑:抖动、误触发、中断嵌套、ISR 阻塞、低功耗冲突


一、GPIO 中断配置

ESP-IDF 的 GPIO 中断配置:

#include"driver/gpio.h"#defineBUTTON_GPIOGPIO_NUM_0// 中断回调staticvoidIRAM_ATTRbutton_isr_handler(void*arg){// 这里在中断上下文中运行!// 不能用printf、malloc、vTaskDelay// 只能用xQueueSendFromISR、xEventGroupSetBitsFromISR等uint32_tgpio_num=(uint32_t)arg;xQueueSendFromISR(gpio_evt_queue,&gpio_num,NULL);}voidbutton_init(void){// 配置GPIO为输入gpio_config_tio_conf={.pin_bit_mask=(1ULL<<BUTTON_GPIO),.mode=GPIO_MODE_INPUT,.pull_up_en=GPIO_PULLUP_ENABLE,.pull_down_en=GPIO_PULLDOWN_DISABLE,.intr_type=GPIO_INTR_NEGEDGE,// 下降沿触发};gpio_config(&io_conf);// 安装中断服务gpio_install_isr_service(0);gpio_isr_handler_add(BUTTON_GPIO,button_isr_handler,(void*)BUTTON_GPIO);}// 中断处理任务voidbutton_task(void*arg){uint32_tgpio_num;uint32_tlast_time=0;while(1){if(xQueueReceive(gpio_evt_queue,&gpio_num,portMAX_DELAY)){uint32_tnow=esp_timer_get_time()/1000;// ms// 软件防抖:50ms内不处理if(now-last_time<50)continue;last_time=now;printf("Button pressed (gpio %d)\n",gpio_num);// 在这里做实际处理}}}

关键点:

  • IRAM_ATTR保证中断函数在内部 RAM 运行(不从 Flash 加载)
  • xQueueSendFromISR把事件发给任务,在中断中安全
  • 50ms 防抖过滤掉按键抖动

二、硬件防抖

RC 滤波电路:

VCC → 10kΩ → GPIO → 按键 → GND | 0.1μF | GND

时间常数 τ = R × C = 10kΩ × 0.1μF = 1ms。按键按下时,电容通过 10kΩ 充电,电压缓慢上升。如果抖动持续时间 <1ms,GPIO 电平不会到达阈值。

RC 滤波的效果:

  • 上升时间约 2.2τ = 2.2ms(0→63%→稳态的 90%)
  • 抖动幅度大幅减少
  • 但响应有 1-2ms 延迟

三、软件防抖算法

算法 1:延时确认(最简单)

voidbutton_isr_handler(void*arg){uint32_tgpio_num=(uint32_t)arg;// 发送到队列,在任务里处理xQueueSendFromISR(gpio_evt_queue,&gpio_num,NULL);}voidbutton_task(void*arg){uint32_tgpio_num;while(1){xQueueReceive(gpio_evt_queue,&gpio_num,portMAX_DELAY);vTaskDelay(pdMS_TO_TICKS(20));// 等20ms// 再检查GPIO状态if(gpio_get_level(gpio_num)==0){printf("Button pressed\n");}}}

算法 2:状态机(适合长按检测)

typedefenum{BTN_IDLE,BTN_PRESSED,BTN_DEBOUNCE,BTN_LONG_PRESS,}button_state_t;button_state_tstate=BTN_IDLE;uint32_tstate_time=0;voidbutton_state_machine(uint8_tpin_level,uint32_tnow){switch(state){caseBTN_IDLE:if(pin_level==0){// 检测到下降沿state=BTN_DEBOUNCE;state_time=now;}break;caseBTN_DEBOUNCE:if(now-state_time>20){// 20ms后确认if(pin_level==0){state=BTN_PRESSED;state_time=now;printf("Short press\n");}else{state=BTN_IDLE;}}break;caseBTN_PRESSED:if(now-state_time>2000){// 2秒长按state=BTN_LONG_PRESS;printf("Long press\n");}if(pin_level==1){// 松开state=BTN_IDLE;}break;caseBTN_LONG_PRESS:if(pin_level==1){state=BTN_IDLE;}break;}}// 在定时器中调用(每5ms)voidbutton_timer_callback(void*arg){uint8_tlevel=gpio_get_level(BUTTON_GPIO);uint32_tnow=esp_timer_get_time()/1000;button_state_machine(level,now);}

算法 3:Shift Register(抗噪声强)

#defineDEBOUNCE_COUNT5// 连续5次相同才算稳定uint8_tshift_reg=0xFF;// 初始为高uint8_tstable_level=1;voidbutton_sample(void){uint8_tcurrent=gpio_get_level(BUTTON_GPIO);// 移位寄存器:新值进入最低位shift_reg=(shift_reg<<1)|current;// 检查是否全0或全1if((shift_reg&((1<<DEBOUNCE_COUNT)-1))==0){if(stable_level!=0){stable_level=0;printf("Button pressed\n");}}elseif((shift_reg&((1<<DEBOUNCE_COUNT)-1))==((1<<DEBOUNCE_COUNT)-1)){if(stable_level!=1){stable_level=1;printf("Button released\n");}}}// 每5ms采样一次esp_timer_handle_ttimer;esp_timer_create_args_ttimer_cfg={.callback=button_sample,.name="button_timer",};esp_timer_create(&timer_cfg,&timer);esp_timer_start_periodic(timer,5000);// 5ms

四、脉冲计数

做流量计或编码器时,GPIO 脉冲频率可能很高(10kHz+)。软件防抖扛不住,要用硬件计数器。

方案 1:PCNT 外设

ESP32 有 PCNT(Pulse Counter)硬件,8 个通道,每个通道 16 位计数器:

#include"driver/pcnt.h"pcnt_config_tpcnt_cfg={.pulse_gpio_num=GPIO_NUM_4,// 脉冲输入.ctrl_gpio_num=PCNT_PIN_NOT_USED,// 不用控制引脚.channel=PCNT_CHANNEL_0,.unit=PCNT_UNIT_0,.pos_mode=PCNT_COUNT_INC,// 上升沿计数.neg_mode=PCNT_COUNT_DIS,// 下降沿不计数.lctrl_mode=PCNT_MODE_KEEP,.hctrl_mode=PCNT_MODE_KEEP,.counter_h_lim=10000,// 上限.counter_l_lim=0,};pcnt_unit_config(&pcnt_cfg);pcnt_set_filter_value(PCNT_UNIT_0,100);// 滤波100个APB时钟(~1.2μs)pcnt_filter_enable(PCNT_UNIT_0);pcnt_counter_clear(PCNT_UNIT_0);pcnt_counter_resume(PCNT_UNIT_0);// 读取计数值int16_tcount=0;pcnt_get_counter_value(PCNT_UNIT_0,&count);printf("Pulse count: %d\n",count);

PCNT 最高计数频率约 40MHz(APB 时钟/2),但实际受滤波值限制。100 的滤波值对应最高 ~80kHz。

方案 2:外部中断 + 硬件定时器

频率不高(<1kHz)时用中断计数:

volatileuint32_tpulse_count=0;voidIRAM_ATTRpulse_isr_handler(void*arg){pulse_count++;}voidpulse_counter_task(void*arg){uint32_tlast_count=0;while(1){vTaskDelay(pdMS_TO_TICKS(1000));uint32_tcurrent=pulse_count;uint32_tfreq=current-last_count;last_count=current;printf("Frequency: %d Hz\n",freq);}}

五、低功耗场景

DeepSleep 时 GPIO 中断唤醒:

// 配置GPIO为唤醒源esp_sleep_enable_ext0_wakeup(GPIO_NUM_0,0);// 下降沿唤醒// 进入DeepSleepesp_deep_sleep_start();

但 DeepSleep 期间计数器清零,不适合脉冲计数。用ULP 协处理器可以在 DeepSleep 期间做简单计数。


六、实测数据

ESP32 @240MHz:

方案最大频率CPU占用延迟
轮询(10ms)50Hz<1%10ms
轮询(1ms)500Hz5%1ms
GPIO中断+防抖100Hz2%20-50ms
GPIO中断无防抖5kHz15%1μs
PCNT硬件计数80kHz0%0μs

七、踩坑清单

  1. ISR 里不能调 printf:中断上下文没有堆栈空间给 stdio。用队列发到任务再打印
  2. IRAM_ATTR:中断函数必须放内部 RAM。不加IRAM_ATTR,Flash cache miss 时会崩溃
  3. 嵌套中断:ESP32 中断优先级 0-7,高优先级可以打断低优先级。如果两个按键中断优先级一样,不会嵌套
  4. 上拉电阻:ESP32 内部上拉约 45kΩ,太弱。机械按键建议外接 10kΩ 上拉
  5. INPUT_PULLUP vs INPUT_PULLDOWN:按键接 GND 用上拉,接 VCC 用下拉。搞反了会一直触发
  6. ESP32-C3/C6 差异:ESP32-C3 的 GPIO 中断分配方式和 ESP32 不同,每个核心有自己的中断
  7. PCNT 溢出:16 位计数器最多 65535。超过会溢出回 0。高频脉冲要定期清零或用 PCNT 中断在溢出前读取
  8. 按键长按误触发:长按 2 秒和连续按 2 次容易混。用状态机区分:长按是"持续低电平超过阈值",双击是"两次短按间隔 <300ms"

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询