3、GPIO基础与LED驱动:GPIO模式配置(推挽、开漏)、点亮板载LED、实现呼吸灯效果、按键输入检测与消抖
各位同学,欢迎来到第三章。
这一章我们开始真正动手了。GPIO,说白了就是芯片的“手脚”。你要让芯片对外输出信号,或者读取外部电平,都得靠它。我当年刚入行时,觉得GPIO太简单了,不就是置1清0嘛。结果第一个项目就栽在开漏输出上,上拉电阻没加,通讯死活不通。嗯,从那以后我再也不敢小看GPIO了。
3.1 GPIO模式:推挽与开漏
先搞清楚两个最常用的输出模式。你想想看,芯片内部其实就是一个开关电路。
推挽输出(Push-Pull)
推挽输出,内部有两个MOS管。一个负责“推”(输出高电平),一个负责“挽”(输出低电平)。输出高电平时,上管导通,下管截止;输出低电平时,上管截止,下管导通。
特点:
- 输出高低电平都有驱动能力
- 输出速度快
- 不能“线与”(多个输出直接连一起会打架)
我个人的习惯是,驱动LED、蜂鸣器这类负载,直接用推挽输出。简单粗暴,效率高。
开漏输出(Open-Drain)
开漏输出,只有下管。输出低电平时,下管导通;输出高电平时,下管截止,引脚处于高阻态。所以,要输出高电平,必须在外部加上拉电阻。
特点:
- 可以“线与”(多个开漏输出直接连一起,实现“与”逻辑)
- 电平转换方便(上拉到不同电压)
- 输出高电平靠外部上拉,速度较慢
核心区别:推挽输出能主动输出高低电平;开漏输出只能主动输出低电平,高电平靠外部上拉。
我在项目中遇到过I2C总线,必须用开漏输出。当时有个同事用了推挽,结果多个设备一通信,总线直接短路。所以,总线类接口,老老实实用开漏。
3.2 点亮板载LED
好,理论讲完,咱们来点实际的。板载LED通常接在GPIO上,低电平点亮或高电平点亮,看原理图。
假设LED接在PA0,低电平点亮。配置步骤如下:
- 使能GPIOA时钟
- 配置PA0为推挽输出
- 设置PA0输出低电平
// 以STM32 HAL库为例
void LED_Init(void)
{
__HAL_RCC_GPIOA_CLK_ENABLE(); // 使能时钟,这一步容易忘
GPIO_InitTypeDef GPIO_InitStruct = {0};
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);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET); // 点亮
}
小技巧:我习惯把LED操作封装成宏,比如 #define LED_ON() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET),这样代码读起来像自然语言。
3.3 实现呼吸灯效果
呼吸灯,说白了就是让LED亮度渐变。怎么实现?用PWM(脉冲宽度调制)。
PWM的原理很简单:快速开关LED,通过调节占空比(高电平时间比例)来控制亮度。人眼有视觉暂留效应,只要频率够高(通常100Hz以上),就看不到闪烁,只看到亮度变化。
实现步骤:
- 配置定时器输出PWM(比如TIM2的CH1)
- 设置PWM频率(比如1kHz)
- 在主循环中逐渐改变占空比
// 呼吸灯主循环
void BreathingLight(void)
{
for(int i = 0; i < 1000; i++) // 渐亮
{
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, i);
HAL_Delay(1);
}
for(int i = 1000; i > 0; i--) // 渐暗
{
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, i);
HAL_Delay(1);
}
}
注意:呼吸效果要平滑,占空比变化步长要小,延时不能太长。我曾经为了省事,步长设了100,结果LED像在“跳霹雳舞”,一点不优雅。
3.4 按键输入检测与消抖
按键,最常用的输入设备。但机械按键有个毛病:按下和松开时,触点会弹跳,产生多个电平跳变。如果不处理,一次按下可能被误判成多次。
硬件消抖
最简单的方法:在按键两端并联一个电容(0.1μF左右),利用电容的充放电来平滑波形。我一般会在按键PCB布局时,把电容紧挨着按键放。
软件消抖
更灵活的方法。核心思想:检测到电平变化后,延时一段时间(通常10-20ms),再读取一次,确认电平是否稳定。
// 按键扫描函数,返回1表示按下
uint8_t Key_Scan(void)
{
static uint8_t last_state = 1; // 上次状态,默认高电平(未按下)
uint8_t current_state;
current_state = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0); // 读取当前电平
if(last_state == 1 && current_state == 0) // 检测下降沿
{
HAL_Delay(20); // 延时消抖
current_state = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0);
if(current_state == 0) // 再次确认按下
{
last_state = 0;
return 1;
}
}
else if(last_state == 0 && current_state == 1) // 检测上升沿(松开)
{
last_state = 1;
}
return 0;
}
避坑指南:我曾经在消抖延时里用了 HAL_Delay(20),结果整个系统响应变慢。后来改用定时器轮询,每10ms扫描一次按键,既消抖又不阻塞主循环。记住,延时函数在实时系统中要慎用。
3.5 综合练习:按键控制呼吸灯
把上面两个知识点结合起来:按一下按键,呼吸灯开始;再按一下,呼吸灯停止。
int main(void)
{
HAL_Init();
SystemClock_Config();
LED_Init();
Key_Init();
PWM_Init();
uint8_t breathing_enable = 0;
while(1)
{
if(Key_Scan()) // 检测到按键按下
{
breathing_enable = !breathing_enable; // 切换状态
}
if(breathing_enable)
{
BreathingLight(); // 执行呼吸效果
}
else
{
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET); // 熄灭LED
}
}
}
这个例子虽然简单,但包含了GPIO输入输出、PWM、状态机的基本思想。你想想看,很多复杂系统,底层无非就是这些东西的组合。
好了,这一章就到这里。下一章我们开始讲中断,那才是真正让MCU“活”起来的关键。到时候见。