第三章 GPIO控制基础:LED指示、按键输入检测、按键消抖处理

各位同学,欢迎来到第三章。这一章我们聊聊GPIO控制。说实话,GPIO是嵌入式开发里最基础、也最容易被忽视的部分。我见过不少工程师,上来就搞复杂的外设,结果连个LED都点不亮,或者按键按下去没反应——嗯,这种坑我自己也踩过。

这一章我们聚焦三个核心点:LED指示、按键输入检测、按键消抖处理。说白了,就是让板子能“看”能“摸”。你想想看,一个理疗仪要是连按键都抖得不行,用户按一下变两下,那体验得多糟糕。

3.1 LED指示:从点亮到呼吸

LED指示是GPIO最基础的应用。我习惯把LED当作“调试神器”——程序跑没跑、中断进没进、状态对不对,看一眼LED就知道了。

3.1.1 硬件连接与GPIO配置

先看硬件。大多数MCU的GPIO驱动能力有限,直接推LED没问题,但记得串个限流电阻。我一般选330Ω到1kΩ,具体看LED的额定电流。

代码示例(以STM32 HAL库为例):

// 初始化LED引脚
void LED_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    
    __HAL_RCC_GPIOB_CLK_ENABLE();   // 使能GPIOB时钟
    
    GPIO_InitStruct.Pin = GPIO_PIN_0;   // PB0接LED
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;  // 推挽输出
    GPIO_InitStruct.Pull = GPIO_NOPULL;   // 无上下拉
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;  // 低速即可
    HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
}

// 点亮LED
#define LED_ON()    HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET)
// 熄灭LED
#define LED_OFF()   HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET)
// 翻转LED
#define LED_TOGGLE() HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0)
小技巧: 我习惯用宏定义封装LED操作,而不是直接写函数。这样代码更简洁,编译后效率也高。另外,注意LED的极性——有些板子是低电平点亮,有些是高电平,别搞反了。

3.1.2 呼吸灯实现

理疗仪上经常需要呼吸灯效果,比如待机状态、工作状态用不同频率的呼吸来指示。实现呼吸灯的核心是PWM,通过调节占空比控制LED亮度。

我曾在项目里用定时器中断模拟PWM,效果也不错。但如果你MCU有硬件PWM,直接用硬件更省心。

// 呼吸灯主循环(伪代码)
void BreathLED(void)
{
    static uint16_t pwm_val = 0;
    static uint8_t direction = 1;  // 1:增亮 0:变暗
    
    if(direction)
    {
        pwm_val++;
        if(pwm_val >= 1000) direction = 0;
    }
    else
    {
        pwm_val--;
        if(pwm_val == 0) direction = 1;
    }
    
    // 设置PWM占空比
    SetPWM(pwm_val);
    delay_ms(2);  // 调整呼吸速度
}
注意: 呼吸灯的频率要合适。太快了像闪烁,太慢了用户等不及。我个人觉得1-2秒一个周期比较舒服。

3.2 按键输入检测:从按下到识别

按键检测看起来简单,但坑不少。你想想看,一个按键按下,机械触点会弹跳好几次,如果不处理,一次按下可能被识别成多次。

3.2.1 硬件连接与GPIO配置

按键一般接GPIO输入,外部上拉或下拉。我习惯用内部上拉,省一个电阻。但要注意,内部上拉电阻值较大(约40kΩ),抗干扰能力弱一些。

// 初始化按键引脚
void Key_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    
    __HAL_RCC_GPIOA_CLK_ENABLE();
    
    GPIO_InitStruct.Pin = GPIO_PIN_0;   // PA0接按键
    GPIO_InitStruct.Mode = GPIO_MODE_INPUT;  // 输入模式
    GPIO_InitStruct.Pull = GPIO_PULLUP;   // 内部上拉
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}

// 读取按键状态
#define KEY_READ()   HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0)
避坑指南: 我曾经在项目里直接用内部上拉,结果在强电磁干扰环境下按键误触发。后来加了外部10kΩ上拉电阻,问题就解决了。所以,如果产品要通过EMC测试,建议用外部上拉。

3.2.2 按键状态机

检测按键不能只读一次,要用状态机。我常用的状态机有四个状态:空闲、按下确认、按下保持、释放确认。

typedef enum {
    KEY_IDLE,       // 空闲
    KEY_PRESSED,    // 按下确认
    KEY_HOLD,       // 按下保持
    KEY_RELEASED    // 释放确认
} KeyState_t;

KeyState_t Key_Scan(void)
{
    static KeyState_t state = KEY_IDLE;
    static uint16_t press_cnt = 0;
    uint8_t key_val = KEY_READ();
    
    switch(state)
    {
        case KEY_IDLE:
            if(key_val == 0)  // 按下(低电平)
            {
                press_cnt = 0;
                state = KEY_PRESSED;
            }
            break;
            
        case KEY_PRESSED:
            if(key_val == 0)
            {
                press_cnt++;
                if(press_cnt >= 5)  // 消抖时间约50ms
                {
                    state = KEY_HOLD;
                    return KEY_PRESSED;  // 返回按下事件
                }
            }
            else
            {
                state = KEY_IDLE;  // 抖动,回到空闲
            }
            break;
            
        case KEY_HOLD:
            if(key_val == 1)  // 释放
            {
                state = KEY_RELEASED;
            }
            break;
            
        case KEY_RELEASED:
            if(key_val == 1)
            {
                state = KEY_IDLE;
                return KEY_RELEASED;  // 返回释放事件
            }
            break;
    }
    
    return KEY_NONE;  // 无事件
}
个人经验: 状态机的好处是能区分短按、长按、双击。我在理疗仪项目里就用这个状态机实现了“短按切换模式,长按开关机”的功能,代码复用性很好。

3.3 按键消抖处理:软件消抖与硬件消抖

消抖是按键处理的核心。说白了,就是要把机械触点的“毛刺”滤掉。方法有两种:软件消抖和硬件消抖。

3.3.1 硬件消抖

硬件消抖最简单——加个RC低通滤波器。电阻和电容组成一个积分电路,把高频抖动滤掉。

典型电路:

  • 电阻R:10kΩ
  • 电容C:0.1μF(104)
  • 时间常数τ = R × C = 1ms

这个时间常数能滤掉大部分机械抖动。我一般选10kΩ+0.1μF,效果不错。

硬件消抖的优点: 不占CPU资源,实时性好。
缺点: 增加BOM成本,PCB面积变大。

3.3.2 软件消抖

软件消抖更灵活,成本为零。核心思想是:检测到电平变化后,延时一段时间再读一次,如果状态一致,就确认按键动作。

常用的软件消抖方法:

  • 延时消抖: 检测到变化后,延时10-20ms再读
  • 连续采样消抖: 连续采样N次,如果N次结果一致,才确认
  • 状态机消抖: 用状态机管理按键状态,如3.2.2节所示

我个人最推荐状态机消抖。为什么呢?因为它不阻塞程序,还能同时处理长按、短按等复杂逻辑。

// 连续采样消抖示例
#define SAMPLE_COUNT  5   // 采样次数

uint8_t Key_Debounce(void)
{
    uint8_t sample[SAMPLE_COUNT];
    uint8_t i;
    uint8_t count_0 = 0, count_1 = 0;
    
    // 连续采样
    for(i = 0; i < SAMPLE_COUNT; i++)
    {
        sample[i] = KEY_READ();
        delay_ms(2);  // 每次采样间隔2ms
    }
    
    // 统计结果
    for(i = 0; i < SAMPLE_COUNT; i++)
    {
        if(sample[i] == 0) count_0++;
        else count_1++;
    }
    
    // 多数决
    if(count_0 > count_1) return 0;  // 按下
    else return 1;  // 释放
}
避坑指南: 我曾经在项目里用delay_ms()做消抖,结果整个程序被卡住了。后来改用定时器轮询+状态机,才解决了问题。记住:不要在中断或主循环里用长延时,除非你的程序只有这一个任务。

3.3.3 硬件消抖 vs 软件消抖:怎么选?

对比项 硬件消抖 软件消抖
成本 增加电阻电容 零成本
CPU占用 需要定时器或轮询
灵活性 固定,改参数需换元件 可调,改代码即可
抗干扰 好(硬件滤波) 一般(依赖软件逻辑)
适用场景 对实时性要求高、环境恶劣 成本敏感、功能复杂

我的建议是:能用软件解决的,就别加硬件。但如果你做的是医疗设备,要通过EMC测试,那还是老老实实加硬件消抖吧——安全第一。

3.4 实战:理疗仪按键与LED联动

最后,我们把这些知识点串起来。假设理疗仪有三个按键:模式切换、强度加、强度减。三个LED分别指示不同模式。

代码框架:

void main(void)
{
    // 初始化
    System_Init();
    LED_Init();
    Key_Init();
    
    while(1)
    {
        // 扫描按键
        KeyEvent_t event = Key_Scan();
        
        // 处理按键事件
        switch(event)
        {
            case KEY_MODE:
                Mode_Switch();
                LED_Update();
                break;
                
            case KEY_PLUS:
                Intensity_Up();
                break;
                
            case KEY_MINUS:
                Intensity_Down();
                break;
                
            default:
                break;
        }
        
        // 呼吸灯效果
        BreathLED();
        
        // 其他任务...
    }
}
经验之谈: 理疗仪的按键响应要快,但也不能太灵敏。我一般把消抖时间设在30-50ms之间。太短容易误触,太长用户觉得“按了没反应”。这个平衡点,需要根据实际按键的机械特性来调。

好了,这一章的内容就到这里。GPIO控制看似简单,但做好并不容易。你想想看,一个按键消抖没处理好,用户按一下变两下,轻则体验差,重则误操作导致安全问题。所以,别小看这些基础内容。

下一章我们聊聊定时器,这是实现PWM、延时、周期性任务的核心。到时候见。