第三章 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)
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); // 调整呼吸速度
}
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)
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,效果不错。
缺点: 增加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; // 释放
}
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();
// 其他任务...
}
}
好了,这一章的内容就到这里。GPIO控制看似简单,但做好并不容易。你想想看,一个按键消抖没处理好,用户按一下变两下,轻则体验差,重则误操作导致安全问题。所以,别小看这些基础内容。
下一章我们聊聊定时器,这是实现PWM、延时、周期性任务的核心。到时候见。