4、GPIO与按键调试:编写GPIO翻转测试程序,用逻辑分析仪抓取按键按下/释放的时序,去抖算法验证
按键调试,听起来简单吧?不就是按下、松开嘛。但说实话,我入行头两年,被按键抖动坑过不止一次。有一次量产前发现设备偶尔会误触发,查了两天才定位到是按键去抖没做好。从那以后,我养成了一个习惯——任何按键逻辑,必须先上逻辑分析仪看波形。
这一章,我们就来干三件事:写一个GPIO翻转的测试程序,用逻辑分析仪抓取按键的完整时序,最后验证去抖算法到底有没有用。
4.1 为什么按键需要去抖?
你想想看,按键本质上是个机械结构。按下的时候,金属触点不是一下子就稳定接触的。它会弹跳几次,就像你扔一个球到地上,它会弹几下才停住。这个弹跳过程,就叫“抖动”。
抖动的持续时间一般在5ms到20ms之间,具体看按键质量。我见过最差的按键,抖了30多ms才稳定下来。如果MCU在抖动期间检测到电平变化就触发动作,那结果就是——按一次,触发了三五次。
核心结论:按键按下和释放时,电平信号不是干净的跳变,而是一串毛刺。去抖的目的,就是滤掉这些毛刺,只认稳定的状态。
4.2 编写GPIO翻转测试程序
我们先写一个简单的测试程序。它的逻辑是:检测到按键按下,就翻转一个LED的GPIO电平。这样我们就能通过LED的亮灭,直观看到按键是否被正确识别。
嗯,这里要注意:先不要加去抖。我们故意不加,就是为了后面对比波形,看看抖动到底有多严重。
// 简单的按键检测,无去抖
#include "stm32f1xx_hal.h"
#define KEY_PIN GPIO_PIN_0
#define KEY_PORT GPIOA
#define LED_PIN GPIO_PIN_5
#define LED_PORT GPIOA
int main(void)
{
HAL_Init();
// 初始化GPIO:PA0为输入(按键),PA5为输出(LED)
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitStruct.Pin = KEY_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP; // 内部上拉,按键按下为低电平
HAL_GPIO_Init(KEY_PORT, &GPIO_InitStruct);
GPIO_InitStruct.Pin = LED_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(LED_PORT, &GPIO_InitStruct);
while (1)
{
// 检测按键是否按下(低电平)
if (HAL_GPIO_ReadPin(KEY_PORT, KEY_PIN) == GPIO_PIN_RESET)
{
// 翻转LED
HAL_GPIO_TogglePin(LED_PORT, LED_PIN);
// 简单延时,防止一次按下翻转多次
HAL_Delay(200);
}
}
}
这个程序有个问题:虽然加了200ms延时,但如果在抖动期间检测到低电平,它还是会翻转。延时只是降低了误触发的概率,并没有从根本上解决问题。
4.3 用逻辑分析仪抓取按键时序
程序写好了,下载到板子上。接下来,拿出逻辑分析仪。我个人习惯用8通道的,便宜好用,采样率10MHz以上就够了。
接线方式:
- 通道0:接按键引脚(PA0)
- 通道1:接LED引脚(PA5)
- GND:接板子共地
设置逻辑分析仪的触发条件为下降沿触发(按键按下是低电平),采样深度设大一点,比如1M个点。然后按下按键,抓取波形。
小技巧:抓取时,手指按下去后稍微停留半秒再松开。这样波形里就能同时看到按下抖动、稳定低电平、释放抖动三段信息。
你会看到什么?我描述一下典型的波形:
| 阶段 | 波形特征 | 持续时间 |
|---|---|---|
| 按下抖动 | 高低电平快速跳变,像锯齿 | 5~20ms |
| 稳定按下 | 稳定的低电平 | 你按多久就多久 |
| 释放抖动 | 又是高低电平快速跳变 | 5~20ms |
| 稳定释放 | 稳定的高电平(上拉) | 直到下次按下 |
我曾经用逻辑分析仪抓过一个劣质按键,按下抖动了35ms,释放抖动了28ms。如果程序里只做了简单的延时,那这个按键基本没法用。
4.4 去抖算法验证
好了,现在我们来加上去抖算法。最常用的方法是延时去抖:检测到电平变化后,等一段时间再读一次,确认状态是否稳定。
// 带去抖的按键检测
#define DEBOUNCE_TIME_MS 20 // 去抖时间,根据实际按键调整
uint8_t ReadKeyDebounced(void)
{
static uint8_t last_state = 1; // 上一次稳定状态,默认高电平
static uint32_t last_time = 0; // 上一次状态变化的时间
uint8_t current_state = HAL_GPIO_ReadPin(KEY_PORT, KEY_PIN);
uint32_t now = HAL_GetTick();
// 如果当前状态和上次稳定状态不同,且持续时间超过去抖时间
if (current_state != last_state)
{
if ((now - last_time) >= DEBOUNCE_TIME_MS)
{
last_state = current_state;
return current_state; // 返回稳定的新状态
}
}
else
{
// 状态没变,更新时间戳
last_time = now;
}
return last_state; // 返回上一次的稳定状态
}
这个算法的核心思想是:不信任瞬间的变化,只信任持续了足够长时间的状态。20ms的去抖时间,基本能覆盖绝大多数按键的抖动周期。
注意:去抖时间不是越长越好。太长会导致按键响应变慢,用户体验差。我一般从10ms开始试,用逻辑分析仪看波形,逐步调整到刚好能滤掉抖动的最小值。
把去抖函数替换到主循环里,再抓一次波形。这次你会看到:LED的翻转和按键按下之间有一个固定的延迟(就是去抖时间),但不会再出现一次按下多次翻转的情况。
4.5 进阶:硬件去抖 vs 软件去抖
说到去抖,其实有两种思路:
- 硬件去抖:在按键引脚上加一个RC低通滤波器(电阻+电容),把抖动的高频分量滤掉。优点是MCU不用做额外处理,缺点是增加了BOM成本。
- 软件去抖:就是我们上面写的延时判断法。优点是零成本,灵活可调,缺点是占用CPU时间(如果用阻塞延时的话)。
我个人更倾向于软件去抖。原因很简单:改代码比改硬件快多了。量产之后发现按键有问题,改个参数重新烧录就行,不用动PCB。
但有一种情况我会用硬件去抖——低功耗场景。如果MCU大部分时间在休眠,靠按键中断唤醒,那软件去抖就不好使了。因为MCU还没醒,抖动已经过去了。这时候在按键引脚上加个RC电路,让硬件帮你把抖动滤掉,MCU醒来直接读到一个干净的电平。
4.6 避坑指南
最后,分享几个我踩过的坑:
- 坑一:逻辑分析仪的采样率设太低。10MHz以下可能抓不到完整的抖动波形,尤其是快速抖动的毛刺。建议至少用10MHz。
- 坑二:去抖时间写死了,没考虑不同按键的差异。同一个板子上,不同按键的抖动时间可能不一样。最好每个按键单独配置去抖参数。
- 坑三:在中断里做去抖。我曾经在GPIO中断里直接调HAL_Delay(),结果系统直接卡死。记住,中断里不要做延时,去抖逻辑应该放在主循环或定时器里。
好了,这一章的内容就到这里。下一章我们会讲定时器与PWM调试,到时候会用到逻辑分析仪测占空比,记得把设备准备好。