4. GPIO与基础外设:GPIO配置、按键输入(去抖处理)、LED指示、蜂鸣器控制
做嵌入式开发,GPIO 是最基础的东西。但说实话,我见过不少项目栽在 GPIO 上——不是配置错了,就是没处理好抖动。窗帘电机这种产品,按键和指示灯天天跟用户打交道,搞不好体验就很糟糕。
这一章,咱们就把 GPIO 的配置、按键去抖、LED 指示、蜂鸣器控制这些基本功,一次性讲透。
4.1 GPIO 配置:别小看这步
GPIO 配置看似简单,其实坑不少。我习惯把 GPIO 分为三类:输入、输出、复用功能。窗帘电机里,按键是输入,LED 和蜂鸣器是输出,PWM 控制电机则用复用功能。
配置时,有几点要注意:
- 上拉/下拉电阻:按键输入一般用内部上拉,省一颗电阻。但要注意,内部上拉电阻值通常在 40kΩ 左右,功耗比外部 10kΩ 大一点。电池供电的产品,我建议用外部上拉。
- 输出速度:LED 和蜂鸣器这种低频信号,用低速档就够了。高速档会增加 EMI 和功耗。我见过有人把 LED 配成 50MHz 速度,结果辐射超标,得不偿失。
- 初始状态:上电瞬间,GPIO 默认可能是浮空输入。如果直接驱动蜂鸣器,可能会“嘀”一声。我一般会在初始化函数里,先把所有输出引脚拉到确定电平,再配置模式。
核心原则:GPIO 配置要“先定电平,再定模式”。这样能避免上电瞬间的毛刺。
下面是一个典型的 GPIO 初始化代码(以 STM32 为例):
void GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
// 使能时钟
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
// LED 输出 - PA0
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// 按键输入 - PB1,内部上拉
GPIO_InitStruct.Pin = GPIO_PIN_1;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
// 蜂鸣器输出 - PB2,初始低电平
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_2, GPIO_PIN_RESET);
GPIO_InitStruct.Pin = GPIO_PIN_2;
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);
}
小技巧:我习惯把 GPIO 初始化放在一个函数里,按“先时钟、再写电平、后配置模式”的顺序写。这样逻辑清晰,也方便后期维护。
4.2 按键输入与去抖处理
按键去抖,是每个嵌入式工程师的必修课。机械按键按下和松开时,触点会弹跳几毫秒到几十毫秒。如果不处理,一次按键可能被识别成多次。
去抖方法有两种:硬件去抖和软件去抖。
4.2.1 硬件去抖
硬件去抖就是在按键引脚上加一个 RC 低通滤波器。R 取 10kΩ,C 取 0.1μF,时间常数 τ = 1ms 左右。这样能滤掉大部分弹跳。
但硬件去抖会增加 BOM 成本。量产产品里,除非按键特别重要(比如急停按钮),否则我一般用软件去抖。
4.2.2 软件去抖
软件去抖的核心思路:检测到电平变化后,延迟一段时间再读一次。如果两次结果一致,就认为是有效按键。
我常用的去抖流程:
- 检测到按键按下(电平从高变低)
- 启动一个 20ms 的定时器
- 20ms 后再次读取按键电平
- 如果还是低电平,确认按键按下
- 如果变成高电平,认为是抖动,忽略
为什么要 20ms?这是我踩过坑总结出来的。太短(比如 5ms)去不干净,太长(比如 50ms)用户会觉得按键反应慢。20ms 是个折中值,大部分机械按键都能覆盖。
注意:不要用 HAL_Delay(20) 这种阻塞延时去抖。它会卡死整个系统,电机都转不了。要用状态机或者定时器轮询的方式。
下面是一个非阻塞去抖的代码示例:
typedef enum {
KEY_IDLE,
KEY_PRESS_DETECTED,
KEY_PRESS_CONFIRMED,
KEY_RELEASE_DETECTED
} KeyState_t;
KeyState_t keyState = KEY_IDLE;
uint32_t keyTimer = 0;
void Key_Scan(void)
{
uint8_t keyLevel = HAL_GPIO_ReadPin(KEY_GPIO_PORT, KEY_PIN);
switch (keyState) {
case KEY_IDLE:
if (keyLevel == 0) { // 检测到按下
keyState = KEY_PRESS_DETECTED;
keyTimer = HAL_GetTick();
}
break;
case KEY_PRESS_DETECTED:
if (HAL_GetTick() - keyTimer > 20) {
if (keyLevel == 0) {
keyState = KEY_PRESS_CONFIRMED;
// 执行按键动作
Key_Action();
} else {
keyState = KEY_IDLE; // 抖动,忽略
}
}
break;
case KEY_PRESS_CONFIRMED:
if (keyLevel == 1) { // 检测到松开
keyState = KEY_RELEASE_DETECTED;
keyTimer = HAL_GetTick();
}
break;
case KEY_RELEASE_DETECTED:
if (HAL_GetTick() - keyTimer > 20) {
if (keyLevel == 1) {
keyState = KEY_IDLE; // 松开确认
} else {
keyState = KEY_PRESS_CONFIRMED; // 抖动,回到按下状态
}
}
break;
default:
keyState = KEY_IDLE;
break;
}
}
经验之谈:我曾经在一个项目中,按键去抖时间设成了 5ms。结果用户反馈“按一下有时会触发两次”。后来改成 20ms,问题就解决了。别小看这 20ms,它能让用户体验上一个台阶。
4.3 LED 指示
LED 指示在窗帘电机里很常见:电源指示灯、运行状态灯、故障报警灯。控制方式很简单,就是 GPIO 输出高低电平。
但有几个细节要注意:
- 限流电阻:LED 不能直接接 GPIO,要串一个电阻。一般 5V 系统用 330Ω,3.3V 系统用 220Ω。电流控制在 5-10mA 就够了,太亮反而刺眼。
- 低电平驱动 vs 高电平驱动:有些 MCU 的 GPIO 灌电流能力比拉电流强。如果 LED 阳极接 VCC,阴极接 GPIO,用低电平点亮,这样驱动能力更好。
- 呼吸灯效果:用 PWM 控制 LED 亮度,可以实现呼吸灯。频率设到 100Hz 以上,人眼就感觉不到闪烁了。
下面是一个简单的 LED 控制函数:
void LED_Control(LED_t led, uint8_t state)
{
switch (led) {
case LED_POWER:
HAL_GPIO_WritePin(LED_POWER_PORT, LED_POWER_PIN, state);
break;
case LED_RUN:
HAL_GPIO_WritePin(LED_RUN_PORT, LED_RUN_PIN, state);
break;
case LED_ERROR:
HAL_GPIO_WritePin(LED_ERROR_PORT, LED_ERROR_PIN, state);
break;
default:
break;
}
}
建议:把 LED 控制封装成函数,不要直接操作 GPIO。这样后期换引脚或者换 MCU,只需要改底层,上层逻辑不用动。
4.4 蜂鸣器控制
蜂鸣器分两种:有源蜂鸣器和无源蜂鸣器。
| 类型 | 驱动方式 | 频率控制 | 适用场景 |
|---|---|---|---|
| 有源蜂鸣器 | 直流电平 | 固定频率 | 简单提示音 |
| 无源蜂鸣器 | PWM 方波 | 可调频率 | 音乐、多音调 |
窗帘电机里,我一般用有源蜂鸣器。成本低,驱动简单。只需要一个 GPIO 输出高电平,蜂鸣器就响。但要注意驱动电流——MCU 的 GPIO 通常只能输出几毫安,而蜂鸣器可能需要 30-50mA。所以要用三极管或 MOS 管驱动。
驱动电路很简单:GPIO 接三极管基极,蜂鸣器接集电极,发射极接地。GPIO 输出高电平,三极管导通,蜂鸣器响。
软件控制也很直接:
void Buzzer_On(void)
{
HAL_GPIO_WritePin(BUZZER_PORT, BUZZER_PIN, GPIO_PIN_SET);
}
void Buzzer_Off(void)
{
HAL_GPIO_WritePin(BUZZER_PORT, BUZZER_PIN, GPIO_PIN_RESET);
}
// 短促提示音:响 100ms,停 100ms
void Buzzer_Beep(void)
{
Buzzer_On();
HAL_Delay(100);
Buzzer_Off();
HAL_Delay(100);
}
避坑指南:我曾经在一个产品里,蜂鸣器直接接在 GPIO 上,没加驱动管。结果 GPIO 被拉低,MCU 直接复位。后来加了 2N2222 三极管,问题解决。记住:蜂鸣器是感性负载,启动电流大,一定要用驱动管。
4.5 综合示例:按键控制 LED 和蜂鸣器
把上面几个知识点串起来,做一个简单的功能:按一下按键,LED 翻转,蜂鸣器响一声。
void System_Process(void)
{
Key_Scan(); // 非阻塞按键扫描
if (keyState == KEY_PRESS_CONFIRMED) {
// 翻转 LED
static uint8_t ledState = 0;
ledState = !ledState;
LED_Control(LED_RUN, ledState);
// 蜂鸣器响一声
Buzzer_Beep();
// 重置按键状态
keyState = KEY_IDLE;
}
}
这个例子虽然简单,但涵盖了 GPIO 配置、按键去抖、LED 控制、蜂鸣器驱动四个知识点。实际项目中,你只需要在这个基础上扩展——比如长按、双击、组合键等。
最后说一句:GPIO 是嵌入式开发的“手脚”,看似简单,但细节决定成败。我见过太多项目因为 GPIO 配置不当导致的问题。希望这一章能帮你少走弯路。