第四章:GPIO模块设计:输入输出模式配置、上下拉电阻、中断触发方式、实战:按键扫描与LED控制

各位同学,欢迎来到第四章。这一章我们聊聊GPIO。

GPIO,通用输入输出口,听起来简单吧?不就是拉高拉低嘛。但说实话,我见过太多项目栽在GPIO上。不是按键误触发,就是LED亮度不对,甚至烧引脚。嗯,这一章我们就把GPIO吃透。

4.1 GPIO输入输出模式配置

先问个问题:你写GPIO_Init()的时候,真的知道芯片内部发生了什么吗?

我个人习惯,拿到一个新MCU,第一件事就是看GPIO结构框图。你会发现,每个引脚背后都有一堆MOS管、触发器和电阻网络。

4.1.1 输出模式

输出模式分两种:推挽输出和开漏输出。

  • 推挽输出:内部两个MOS管交替导通。输出高电平时,上管导通拉高;输出低电平时,下管导通拉低。驱动能力强,适合LED、蜂鸣器。
  • 开漏输出:只有下管,上管没有。输出低电平靠内部拉低,输出高电平必须外部上拉。我常用在I2C总线上,因为多个设备可以共线。

避坑指南:我曾经在项目里用推挽输出驱动一个5V的继电器模块,结果MCU引脚直接冒烟了。为什么?因为推挽输出高电平是3.3V,继电器模块内部有上拉到5V,两个电源直接短路了。后来我改成开漏输出加外部NPN三极管驱动,问题解决。

4.1.2 输入模式

输入模式也有讲究。常见的有:浮空输入、上拉输入、下拉输入。

模式 内部状态 适用场景
浮空输入 无上下拉,高阻态 外部已有上下拉电阻
上拉输入 内部接VDD 按键检测(默认高电平)
下拉输入 内部接GND 按键检测(默认低电平)

你想想看,如果按键一端接GND,另一端接GPIO。用上拉输入,按键按下时读到低电平,松开时读到高电平。逻辑清晰,代码好写。

4.2 上下拉电阻

上下拉电阻,说白了就是给不确定的电平一个确定的归宿。

我记得有一次调试一个温湿度传感器,数据线总是跳变。示波器一看,浮空输入状态下,引脚电平在1.2V到2.8V之间乱飘。这就是典型的「引脚悬空」问题。

我的经验:内部上下拉电阻一般在30kΩ~50kΩ之间,驱动能力有限。如果外部干扰强,建议外加4.7kΩ或10kΩ的电阻。尤其是长线传输时,内部上拉根本拉不住。

上下拉电阻的选择原则:

  • 上拉电阻越小,驱动能力越强,但静态功耗越大
  • 下拉电阻同理
  • 按键场景:10kΩ上拉是黄金值
  • I2C场景:4.7kΩ是常用值

4.3 中断触发方式

GPIO中断,是嵌入式系统的灵魂。没有中断,你只能轮询,CPU累死,功耗也高。

中断触发方式有四种:

  1. 上升沿触发:电平从低变高时触发
  2. 下降沿触发:电平从高变低时触发
  3. 双边沿触发:变化就触发
  4. 电平触发:保持某个电平就持续触发

注意:电平触发容易产生重复中断。我曾经在做一个门禁系统时,用了高电平触发,结果门一开,中断一直进,CPU直接卡死在中断服务函数里。后来改成下降沿触发,只触发一次,完美解决。

实际项目中,按键用下降沿触发最常见。但要注意硬件抖动。机械按键按下时,电平会弹跳几次,持续5~20ms。如果不做处理,一次按键会触发多次中断。

4.4 实战:按键扫描与LED控制

好了,理论讲完了,我们动手写代码。这个实战我做了不下十次,每次都有新感悟。

4.4.1 硬件连接

  • 按键:PA0,外部上拉10kΩ,按下接地
  • LED:PB0,推挽输出,串联220Ω电阻到GND

4.4.2 代码实现

先看初始化部分:

void GPIO_Init(void)
{
    // 使能GPIO时钟
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN;
    
    // PA0: 上拉输入
    GPIOA->CRL &= ~(0x0F << 0);  // 清除配置
    GPIOA->CRL |= (0x08 << 0);   // 上拉输入模式
    GPIOA->ODR |= (1 << 0);      // 使能内部上拉
    
    // PB0: 推挽输出,50MHz
    GPIOB->CRL &= ~(0x0F << 0);
    GPIOB->CRL |= (0x03 << 0);   // 推挽输出,50MHz
}

按键扫描,我习惯用状态机加延时消抖:

uint8_t Key_Scan(void)
{
    static uint8_t last_state = 1;  // 上次状态,默认高电平
    static uint16_t count = 0;      // 消抖计数器
    uint8_t current_state = (GPIOA->IDR & (1 << 0)) ? 1 : 0;
    uint8_t key_event = 0;
    
    if(current_state != last_state)
    {
        count++;
        if(count >= 5)  // 约5ms消抖
        {
            last_state = current_state;
            if(current_state == 0)  // 按下
            {
                key_event = 1;  // 有效按键
            }
            count = 0;
        }
    }
    else
    {
        count = 0;
    }
    
    return key_event;
}

主循环控制LED:

int main(void)
{
    GPIO_Init();
    uint8_t led_state = 0;
    
    while(1)
    {
        if(Key_Scan())
        {
            led_state = !led_state;
            if(led_state)
                GPIOB->BSRR = (1 << 0);   // 点亮
            else
                GPIOB->BRR = (1 << 0);    // 熄灭
        }
    }
}

避坑指南:我曾经在按键消抖里用了delay_ms(20),结果按键响应特别迟钝。后来改成状态机加计数器,不阻塞CPU,响应快多了。记住:中断里别用延时,主循环里也别用长延时。

4.5 总结与扩展

这一章我们讲了GPIO的输入输出配置、上下拉电阻、中断触发方式,还做了按键和LED的实战。

最后说一句:GPIO看似简单,但它是嵌入式系统的基石。我见过很多老工程师,写GPIO代码时依然会看数据手册的电气特性表。为什么?因为不同负载下,GPIO的驱动能力、上升时间、下降时间都不一样。

下一章我们会讲定时器,到时候你会发现,GPIO和定时器配合起来,能玩出更多花样。

课后思考:如果按键接在中断引脚上,怎么在中断服务函数里实现消抖?提示:可以用定时器延时再检测电平。