GPIO 输入输出:从概念到实战

各位同学,欢迎来到第二章。上一章我们搭好了开发环境,这一章咱们来点实在的——让芯片跟外界“说话”。

GPIO,全称 General-Purpose Input/Output,通用输入输出口。说白了,就是芯片上那些可以编程控制的引脚。你可以让它输出高电平点亮 LED,也可以读取它上面的电平判断按键是否按下。

我刚开始学 STM32 时,觉得 GPIO 不就是个开关嘛,有啥好学的?后来做项目才发现,恰恰是这个最简单的外设,坑最多。嗯,咱们今天就把这些坑一个个填上。

2.1 GPIO 基本概念

STM32 的 GPIO 可不是简单的“0 或 1”。它内部有一套完整的电路结构,支持多种工作模式。我习惯把这几种模式分成三大类:

模式分类 具体模式 典型用途
输入模式 浮空输入、上拉输入、下拉输入、模拟输入 读取按键、检测传感器电平、ADC 采样
输出模式 推挽输出、开漏输出 驱动 LED、控制继电器、I2C 通信
复用功能模式 复用推挽输出、复用开漏输出 USART、SPI、定时器通道等外设引脚

这里有个关键点:推挽输出能主动输出高低电平,驱动能力强;开漏输出只能拉低,拉高要靠外部上拉电阻,常用于多设备共线的总线。

我的习惯:驱动 LED 一律用推挽输出,读取按键用上拉输入(内部上拉电阻省掉外部元件)。复用功能模式直接交给 HAL 库的 MX 配置工具去设,别手写。

为什么会这样设计?你想想看,如果所有引脚都只能输出固定电平,那 I2C 这种“线与”逻辑就没法工作了。开漏模式就是为这种场景准备的。

2.2 HAL 库 GPIO 驱动 API 详解

HAL 库把 GPIO 的操作封装成了几个核心函数。咱们一个一个看。

2.2.1 初始化:HAL_GPIO_Init

这个函数负责配置引脚的工作模式、上下拉、输出速度等。原型如下:

void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init);

第一个参数指定端口(GPIOA、GPIOB 等),第二个参数是一个结构体指针,里面填配置项。

结构体 GPIO_InitTypeDef 的成员:

成员 可选值 说明
Pin GPIO_PIN_0 ~ GPIO_PIN_15 要配置的引脚,可用 | 组合多个
Mode GPIO_MODE_INPUT / OUTPUT_PP / OUTPUT_OD / AF_PP / AF_OD / ANALOG / IT_RISING / IT_FALLING / IT_RISING_FALLING 工作模式,中断模式也在这里
Pull GPIO_NOPULL / GPIO_PULLUP / GPIO_PULLDOWN 上下拉配置
Speed GPIO_SPEED_FREQ_LOW / MEDIUM / HIGH / VERY_HIGH 输出速度,低速省电,高速防信号畸变
Alternate GPIO_AFx_xxx 复用功能选择,仅在 AF 模式下有效
我曾经踩过的坑:配置多个引脚时,Pin 成员用 | 合并,但 Mode、Pull、Speed 是共用的。如果你把 PA0 设成输入、PA1 设成输出,必须分两次调用 HAL_GPIO_Init。一次调用会把所有引脚设成同一模式。

2.2.2 输出操作

写电平就两个函数:

// 设置指定引脚为高电平
void HAL_GPIO_WritePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState);

// 翻转指定引脚电平
void HAL_GPIO_TogglePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin);

PinState 取 GPIO_PIN_SET(高电平)或 GPIO_PIN_RESET(低电平)。

我个人习惯用 TogglePin 做 LED 闪烁,省去维护状态变量的麻烦。

2.2.3 输入操作

GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin);

返回 GPIO_PIN_SET 或 GPIO_PIN_RESET。就这么简单。

但读取按键时有个大坑——机械抖动。按键按下和松开的瞬间,电平会跳变多次。直接读的话,一次按下可能被识别成几十次。

消抖方法:检测到电平变化后,延时 10~20ms 再读一次。如果电平稳定,才认为按键有效。HAL 库没有现成的消抖函数,需要自己写。

2.3 按键输入与 LED 输出实战

理论说完了,咱们直接上手。下面是一个完整的按键控制 LED 示例。

2.3.1 硬件连接

  • LED:PA5 推挽输出,阳极接引脚,阴极串联 330Ω 电阻到 GND
  • 按键:PC13 上拉输入,按键一端接引脚,另一端接 GND

按键按下时,PC13 读到低电平;松开时,内部上拉拉到高电平。

2.3.2 代码实现

// main.c 关键部分

// 初始化函数(由 MX 生成,这里只展示核心配置)
static void MX_GPIO_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};

    // 使能 GPIO 时钟
    __HAL_RCC_GPIOA_CLK_ENABLE();
    __HAL_RCC_GPIOC_CLK_ENABLE();

    // 配置 PA5 为推挽输出
    GPIO_InitStruct.Pin = GPIO_PIN_5;
    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);

    // 配置 PC13 为上拉输入
    GPIO_InitStruct.Pin = GPIO_PIN_13;
    GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
    GPIO_InitStruct.Pull = GPIO_PULLUP;
    HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);
}

// 主循环
int main(void)
{
    HAL_Init();
    MX_GPIO_Init();

    // 按键状态记录,用于检测边沿
    uint8_t lastButtonState = 1;  // 默认高电平(松开)
    uint8_t currentButtonState;

    while (1)
    {
        currentButtonState = HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13);

        // 检测下降沿:按键按下
        if (lastButtonState == 1 && currentButtonState == 0)
        {
            // 消抖延时
            HAL_Delay(20);

            // 再次确认
            if (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13) == 0)
            {
                // 翻转 LED
                HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
            }
        }

        lastButtonState = currentButtonState;

        // 主循环适当延时,降低 CPU 占用
        HAL_Delay(10);
    }
}
注意:HAL_Delay 依赖 SysTick 中断。如果你在中断服务函数里调用它,会死锁。按键消抖建议用定时器或状态机,别在中断里延时。

2.3.3 核心逻辑流程图

下面这张图展示了按键检测的完整流程:

按键检测流程图 按键检测与 LED 控制流程 系统初始化 读取 PC13 电平 检测到下降沿? 否 是 延时 20ms 消抖 再次读取确认 翻转 PA5 LED 关键点 边沿检测 + 消抖 避免误触发

2.4 避坑指南与经验总结

做 GPIO 驱动,有几个问题我几乎每次都会被问到。这里统一说一下。

2.4.1 引脚未初始化就操作

很多人写完 HAL_GPIO_WritePin 发现没反应,一查——忘了调用 HAL_GPIO_Init。默认状态下,引脚是浮空输入模式,写电平无效。

2.4.2 输出速度选太高

驱动 LED 用 GPIO_SPEED_FREQ_LOW 就够了。选 VERY_HIGH 会增加功耗和电磁干扰。我见过有人用高速模式驱动一个继电器,结果板子上的无线模块直接罢工。

2.4.3 开漏输出忘加上拉电阻

开漏输出只能拉低,不能拉高。如果你忘了在外部加上拉电阻,引脚永远读不到高电平。I2C 总线自带上拉,但普通 GPIO 开漏模式不会。

我曾经犯过的错:用开漏模式驱动 LED,LED 正极接 3.3V,负极接引脚。结果 LED 微亮但不完全熄灭。因为开漏输出高电平时引脚是高阻态,LED 通过负载电阻微弱导通。正确做法是用推挽输出。

2.4.4 中断模式配置后没写中断服务函数

如果你把引脚配成 GPIO_MODE_IT_RISING 等中断模式,却没在 stm32f1xx_it.c 里实现对应的 HAL_GPIO_EXTI_Callback,中断来了程序会跑飞。

嗯,GPIO 的内容就这么多。看似简单,但它是所有外设的基础。你把这章吃透了,后面学 USART、SPI、I2C 时会发现——它们的底层也是 GPIO 的复用功能模式。