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

📅 发布时间:2026/8/17 21:11:47
ESP32 GPIO中断与防抖:按键/脉冲采集实战
ESP32 GPIO 中断与防抖按键/脉冲采集实战先说结论硬件一个 RC 滤波 软件一个防抖算法基本能搞定 99% 的按键场景。如果做脉冲计数霍尔传感器、编码器得用 GPIO 中断 定时器配合。按键检测看起来是入门级的问题——GPIO 配输入读电平完事。但实际做产品时一个按键可能踩 5 个坑抖动、误触发、中断嵌套、ISR 阻塞、低功耗冲突。一、GPIO 中断配置ESP-IDF 的 GPIO 中断配置#includedriver/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(1ULLBUTTON_GPIO),.modeGPIO_MODE_INPUT,.pull_up_enGPIO_PULLUP_ENABLE,.pull_down_enGPIO_PULLDOWN_DISABLE,.intr_typeGPIO_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_time0;while(1){if(xQueueReceive(gpio_evt_queue,gpio_num,portMAX_DELAY)){uint32_tnowesp_timer_get_time()/1000;// ms// 软件防抖50ms内不处理if(now-last_time50)continue;last_timenow;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Ω 充电电压缓慢上升。如果抖动持续时间 1msGPIO 电平不会到达阈值。RC 滤波的效果上升时间约 2.2τ 2.2ms0→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_tstateBTN_IDLE;uint32_tstate_time0;voidbutton_state_machine(uint8_tpin_level,uint32_tnow){switch(state){caseBTN_IDLE:if(pin_level0){// 检测到下降沿stateBTN_DEBOUNCE;state_timenow;}break;caseBTN_DEBOUNCE:if(now-state_time20){// 20ms后确认if(pin_level0){stateBTN_PRESSED;state_timenow;printf(Short press\n);}else{stateBTN_IDLE;}}break;caseBTN_PRESSED:if(now-state_time2000){// 2秒长按stateBTN_LONG_PRESS;printf(Long press\n);}if(pin_level1){// 松开stateBTN_IDLE;}break;caseBTN_LONG_PRESS:if(pin_level1){stateBTN_IDLE;}break;}}// 在定时器中调用每5msvoidbutton_timer_callback(void*arg){uint8_tlevelgpio_get_level(BUTTON_GPIO);uint32_tnowesp_timer_get_time()/1000;button_state_machine(level,now);}算法 3Shift Register抗噪声强#defineDEBOUNCE_COUNT5// 连续5次相同才算稳定uint8_tshift_reg0xFF;// 初始为高uint8_tstable_level1;voidbutton_sample(void){uint8_tcurrentgpio_get_level(BUTTON_GPIO);// 移位寄存器新值进入最低位shift_reg(shift_reg1)|current;// 检查是否全0或全1if((shift_reg((1DEBOUNCE_COUNT)-1))0){if(stable_level!0){stable_level0;printf(Button pressed\n);}}elseif((shift_reg((1DEBOUNCE_COUNT)-1))((1DEBOUNCE_COUNT)-1)){if(stable_level!1){stable_level1;printf(Button released\n);}}}// 每5ms采样一次esp_timer_handle_ttimer;esp_timer_create_args_ttimer_cfg{.callbackbutton_sample,.namebutton_timer,};esp_timer_create(timer_cfg,timer);esp_timer_start_periodic(timer,5000);// 5ms四、脉冲计数做流量计或编码器时GPIO 脉冲频率可能很高10kHz。软件防抖扛不住要用硬件计数器。方案 1PCNT 外设ESP32 有 PCNTPulse Counter硬件8 个通道每个通道 16 位计数器#includedriver/pcnt.hpcnt_config_tpcnt_cfg{.pulse_gpio_numGPIO_NUM_4,// 脉冲输入.ctrl_gpio_numPCNT_PIN_NOT_USED,// 不用控制引脚.channelPCNT_CHANNEL_0,.unitPCNT_UNIT_0,.pos_modePCNT_COUNT_INC,// 上升沿计数.neg_modePCNT_COUNT_DIS,// 下降沿不计数.lctrl_modePCNT_MODE_KEEP,.hctrl_modePCNT_MODE_KEEP,.counter_h_lim10000,// 上限.counter_l_lim0,};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_tcount0;pcnt_get_counter_value(PCNT_UNIT_0,count);printf(Pulse count: %d\n,count);PCNT 最高计数频率约 40MHzAPB 时钟/2但实际受滤波值限制。100 的滤波值对应最高 ~80kHz。方案 2外部中断 硬件定时器频率不高1kHz时用中断计数volatileuint32_tpulse_count0;voidIRAM_ATTRpulse_isr_handler(void*arg){pulse_count;}voidpulse_counter_task(void*arg){uint32_tlast_count0;while(1){vTaskDelay(pdMS_TO_TICKS(1000));uint32_tcurrentpulse_count;uint32_tfreqcurrent-last_count;last_countcurrent;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占用延迟轮询10ms50Hz1%10ms轮询1ms500Hz5%1msGPIO中断防抖100Hz2%20-50msGPIO中断无防抖5kHz15%1μsPCNT硬件计数80kHz0%0μs七、踩坑清单ISR 里不能调 printf中断上下文没有堆栈空间给 stdio。用队列发到任务再打印IRAM_ATTR中断函数必须放内部 RAM。不加IRAM_ATTRFlash cache miss 时会崩溃嵌套中断ESP32 中断优先级 0-7高优先级可以打断低优先级。如果两个按键中断优先级一样不会嵌套上拉电阻ESP32 内部上拉约 45kΩ太弱。机械按键建议外接 10kΩ 上拉INPUT_PULLUP vs INPUT_PULLDOWN按键接 GND 用上拉接 VCC 用下拉。搞反了会一直触发ESP32-C3/C6 差异ESP32-C3 的 GPIO 中断分配方式和 ESP32 不同每个核心有自己的中断PCNT 溢出16 位计数器最多 65535。超过会溢出回 0。高频脉冲要定期清零或用 PCNT 中断在溢出前读取按键长按误触发长按 2 秒和连续按 2 次容易混。用状态机区分长按是持续低电平超过阈值双击是两次短按间隔 300ms