3、GPIO基础:LED控制、按键输入、中断处理、PWM调光

好,咱们进入第三章。这一章可以说是嵌入式开发的「手脚」——GPIO。你想想看,一个自动售货机,要亮灯、要检测按键、要响应投币、要调节屏幕背光,哪个离得开GPIO?说白了,GPIO就是芯片和外界打交道的桥梁。

我个人习惯,每接触一个新平台,第一件事就是点灯。为什么?因为点灯能最快验证你的开发环境、编译链、下载工具是否全部打通。要是灯都不亮,后面写再多代码也是白搭。

3.1 LED控制——从点灯到闪烁

先看最简单的:控制一个LED亮灭。在嵌入式里,这通常就是写一个寄存器,或者调用一个库函数。

以STM32为例,我们通常这样初始化一个GPIO输出:

// 初始化PB0为推挽输出
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOB_CLK_ENABLE();   // 开启GPIOB时钟
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(GPIOB, &GPIO_InitStruct);

// 点亮LED
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET);
// 熄灭LED
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET);

嗯,这里要注意:时钟必须先使能。我在项目中遇到过新手死活点不亮灯,查了半天发现是忘了开时钟。STM32的外设默认时钟是关闭的,你得手动打开它。

小技巧: 调试时可以用示波器或逻辑分析仪抓一下GPIO引脚波形。如果波形有,灯不亮,那多半是硬件问题——比如限流电阻焊错了。

3.2 按键输入——去抖是门手艺

按键输入看起来简单,读个电平而已。但实际项目中,按键抖动是个大坑。

为什么会抖动?机械按键在按下和释放的瞬间,触点会弹跳几次,持续几毫秒到十几毫秒。如果你直接读电平,一次按下可能被误判成多次。

我曾经在一个售货机项目里,用户投币后按键选商品,结果因为没做去抖,一次按键触发了两次选择,多扣了钱。嗯,从那以后我再也不敢省去抖了。

去抖有两种常见做法:

  1. 硬件去抖:加RC滤波电路,简单粗暴。
  2. 软件去抖:检测到电平变化后,延时10-20ms再读一次。

软件去抖代码示例:

uint8_t Read_Key(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
{
    if(HAL_GPIO_ReadPin(GPIOx, GPIO_Pin) == GPIO_PIN_RESET) // 检测到按下
    {
        HAL_Delay(20);  // 延时20ms,避开抖动
        if(HAL_GPIO_ReadPin(GPIOx, GPIO_Pin) == GPIO_PIN_RESET) // 再次确认
        {
            return 1;   // 确实按下了
        }
    }
    return 0;
}
注意: HAL_Delay() 在中断里调用会出问题。如果你在中断服务函数里做按键检测,别用延时,改用状态机或定时器轮询。

3.3 中断处理——别让CPU空等

轮询按键有个缺点:CPU得一直问「你按了吗?你按了吗?」。这在低功耗设备里简直是灾难。更好的方式是中断——让按键主动通知CPU。

配置一个GPIO外部中断,通常需要这几步:

  • 配置GPIO为输入模式
  • 配置中断触发边沿(上升沿、下降沿或双边沿)
  • 使能中断线
  • 编写中断服务函数

以STM32为例:

// 配置PA0为外部中断,下降沿触发
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;  // 下降沿中断
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

// 使能中断线
HAL_NVIC_SetPriority(EXTI0_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(EXTI0_IRQn);

// 中断服务函数
void EXTI0_IRQHandler(void)
{
    HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);
}

// 回调函数
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
    if(GPIO_Pin == GPIO_PIN_0)
    {
        // 处理按键事件
        // 注意:这里要快进快出,别做耗时操作
    }
}

我个人习惯,中断服务函数里只设标志位,真正的处理放到主循环里做。为什么?因为中断里做太多事,会影响其他中断的响应,甚至导致系统卡死。

核心原则: 中断服务函数要「短小精悍」,就像你去便利店买个水,拿了就走,别在里面逛超市。

3.4 PWM调光——让LED呼吸起来

前面我们只能让LED亮或灭。但自动售货机的屏幕背光、氛围灯,需要调节亮度。这时候就要用到PWM(脉冲宽度调制)

PWM的原理很简单:以固定频率切换高低电平,通过改变高电平占整个周期的比例(占空比)来调节平均电压。人眼有视觉暂留效应,只要频率够高(通常1kHz以上),看到的就是稳定的亮度。

用定时器输出PWM:

// 配置TIM2的通道1输出PWM,频率1kHz
TIM_HandleTypeDef htim2;
TIM_OC_InitTypeDef sConfigOC = {0};

htim2.Instance = TIM2;
htim2.Init.Prescaler = 71;      // 72MHz / 72 = 1MHz
htim2.Init.Period = 999;        // 1MHz / 1000 = 1kHz
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
HAL_TIM_PWM_Init(&htim2);

sConfigOC.OCMode = TIM_OCMODE_PWM1;
sConfigOC.Pulse = 500;          // 占空比50%
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_1);

HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);

改变占空比,只需要修改 htim2.Instance->CCR1 的值。比如从0到999渐变,就能实现呼吸灯效果。

我在项目中遇到过一个问题:PWM频率选太低,LED会有可见的闪烁;选太高,开关损耗增大,驱动芯片发热。一般LED调光用1kHz-5kHz比较合适,电机调速用50Hz-100Hz。

避坑指南: 我曾经在一个产品里用PWM控制风扇,频率设成了20kHz,结果风扇发出刺耳的啸叫声。后来查资料才知道,人耳对20kHz以下的声音敏感,电机线圈会振动发声。把频率调到25kHz以上,问题就解决了。

小结

这一章我们聊了GPIO的四个基本操作:

功能 核心要点 常见坑
LED控制 推挽输出、时钟使能 忘了开时钟
按键输入 上拉/下拉、软件去抖 抖动误触发
中断处理 边沿触发、快进快出 中断里做耗时操作
PWM调光 占空比、频率选择 频率不合适导致闪烁或啸叫

嗯,这些基础打牢了,后面做售货机的电机控制、传感器读取、通信协议,都会顺手很多。下一章我们聊聊定时器,那玩意儿比GPIO更有意思。