2. GPIO基础与HAL库驱动:GPIO工作原理、HAL_GPIO_Init/WritePin/ReadPin函数详解、GPIO输出驱动LED、GPIO输入读取按键

各位同学,欢迎来到第二章。这一章我们聊点最基础、也最常用的东西——GPIO。说白了,就是芯片的“手脚”,用来对外输出高低电平,或者读取外部信号是高还是低。

我刚开始学STM32的时候,觉得GPIO太简单了,不就是拉高拉低嘛。结果后来做项目,遇到按键抖动、LED不亮、引脚配置冲突……嗯,才发现这里面的门道真不少。今天我就把这些经验掰开了讲给你听。

2.1 GPIO工作原理——从引脚到寄存器

STM32的GPIO,每个引脚背后都有一套完整的电路。我画了个简化的框图,帮你理解信号是怎么从芯片内部走到引脚上的。

核心要点:GPIO的每个引脚,内部都包含输入驱动器、输出驱动器、上下拉电阻、施密特触发器、以及复用功能选择器。你通过配置寄存器,决定用哪一路。

GPIO引脚内部结构框图 CPU/外设总线 输出数据寄存器 输入数据寄存器 复用功能输出 复用功能输入 输出控制 输入控制 上下拉电阻 施密特触发器 GPIO引脚 说明:输出路径(蓝→紫→红→橙→绿)和输入路径(绿→橙→紫→蓝)是分开的,互不干扰。

你看,从CPU总线出来,数据可以走两条路:一条是输出路径,经过输出数据寄存器和输出控制,再到引脚;另一条是输入路径,从引脚进来,经过施密特触发器整形,再到输入数据寄存器。上下拉电阻呢,是帮你把引脚“固定”在一个默认电平上,防止悬空。

我的小经验:做按键输入时,一定要把内部上拉打开。我曾经偷懒没开,结果按键没按下时引脚电平乱跳,程序死活读不对。后来加上内部上拉,世界清净了。

2.2 HAL_GPIO_Init函数详解——配置引脚的第一步

HAL库把底层的寄存器操作都封装好了。你只需要填一个结构体,然后调用 HAL_GPIO_Init 就行。这个函数,我几乎每个项目都会用到。

来看看它的原型:

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

第一个参数指定端口,比如 GPIOA、GPIOB。第二个参数是个结构体,里面装着你想要的所有配置。我列个表,把每个成员都讲清楚:

结构体成员 可选值 说明
Pin GPIO_PIN_0 ~ GPIO_PIN_15,可用 | 组合 指定要配置的引脚编号
Mode GPIO_MODE_INPUT / GPIO_MODE_OUTPUT_PP / GPIO_MODE_OUTPUT_OD / GPIO_MODE_AF_PP / GPIO_MODE_AF_OD / GPIO_MODE_ANALOG / GPIO_MODE_IT_RISING 等 选择输入、输出(推挽/开漏)、复用、模拟或中断模式
Pull GPIO_NOPULL / GPIO_PULLUP / GPIO_PULLDOWN 上下拉配置,输入模式常用
Speed GPIO_SPEED_FREQ_LOW / GPIO_SPEED_FREQ_MEDIUM / GPIO_SPEED_FREQ_HIGH / GPIO_SPEED_FREQ_VERY_HIGH 输出速率,高频通信选高速,普通LED选低速即可
Alternate GPIO_AFx_xxx(如 GPIO_AF1_USART1) 复用功能选择,Mode 为 AF 时才有效

举个例子,配置一个推挽输出的LED引脚:

GPIO_InitTypeDef GPIO_InitStruct = {0};
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);

注意:配置完一定要检查时钟有没有打开。很多人忘了 __HAL_RCC_GPIOA_CLK_ENABLE(),结果引脚怎么配都不工作。我刚开始也犯过这个错,排查了半天才发现是时钟没开。

2.3 HAL_GPIO_WritePin 与 HAL_GPIO_ReadPin——输出与输入的核心

这两个函数,一个写,一个读。简单到不能再简单,但用起来有几个细节要注意。

2.3.1 输出:HAL_GPIO_WritePin

void HAL_GPIO_WritePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState);

PinState 只有两个值:GPIO_PIN_SET(高电平)和 GPIO_PIN_RESET(低电平)。

还有个常用的宏 HAL_GPIO_TogglePin(GPIOx, GPIO_Pin),用来翻转电平。我调试LED的时候特别喜欢用这个,省得每次写两行代码。

2.3.2 输入:HAL_GPIO_ReadPin

GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin);

返回值同样是 GPIO_PIN_SET 或 GPIO_PIN_RESET。注意,读之前要确保引脚配置为输入模式,并且上下拉设置正确。

避坑指南:我曾经在读取按键时,发现读到的电平总是反的。后来一查,原来是外部电路用了上拉,而我内部也配了上拉,两个上拉电阻并联,电平被拉高了。解决办法:外部上拉时,内部配 GPIO_NOPULL。

2.4 GPIO输出驱动LED——实战第一步

好了,理论讲完,咱们来点实战。驱动一个LED,是每个嵌入式工程师的“Hello World”。

电路很简单:LED正极接一个限流电阻(比如220Ω),再接到STM32的某个引脚;LED负极接地。这样引脚输出高电平时,LED亮;输出低电平时,LED灭。

完整代码示例:

// 头文件
#include "stm32f4xx_hal.h"

// 定义LED引脚
#define LED_PIN     GPIO_PIN_5
#define LED_PORT    GPIOA

// 初始化LED
void LED_Init(void)
{
    __HAL_RCC_GPIOA_CLK_ENABLE();

    GPIO_InitTypeDef GPIO_InitStruct = {0};
    GPIO_InitStruct.Pin = LED_PIN;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
    HAL_GPIO_Init(LED_PORT, &GPIO_InitStruct);
}

// 主函数
int main(void)
{
    HAL_Init();
    LED_Init();

    while (1)
    {
        HAL_GPIO_WritePin(LED_PORT, LED_PIN, GPIO_PIN_SET);   // 亮
        HAL_Delay(500);                                        // 延时500ms
        HAL_GPIO_WritePin(LED_PORT, LED_PIN, GPIO_PIN_RESET); // 灭
        HAL_Delay(500);
    }
}

我的习惯:我会把LED相关的宏定义和初始化函数单独放到一个 led.c 文件里。这样主程序看起来干净,以后换引脚也方便。你想想看,如果所有代码都堆在main.c里,项目大了以后维护起来多痛苦。

2.5 GPIO输入读取按键——从按下到响应

按键输入比LED输出稍微复杂一点,因为有个“抖动”的问题。机械按键按下和松开时,电平会跳几下,如果不处理,程序可能一次按下检测到多次。

电路连接:按键一端接GND,另一端接GPIO引脚,同时引脚外部或内部上拉到VCC。这样按键没按下时,引脚读到高电平;按下时,引脚被拉到低电平。

代码示例:

// 定义按键引脚
#define KEY_PIN     GPIO_PIN_0
#define KEY_PORT    GPIOA

// 初始化按键
void KEY_Init(void)
{
    __HAL_RCC_GPIOA_CLK_ENABLE();

    GPIO_InitTypeDef GPIO_InitStruct = {0};
    GPIO_InitStruct.Pin = KEY_PIN;
    GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
    GPIO_InitStruct.Pull = GPIO_PULLUP;  // 内部上拉
    HAL_GPIO_Init(KEY_PORT, &GPIO_InitStruct);
}

// 简单的软件消抖
uint8_t KEY_Read(void)
{
    if (HAL_GPIO_ReadPin(KEY_PORT, KEY_PIN) == GPIO_PIN_RESET)
    {
        HAL_Delay(20);  // 延时20ms,跳过抖动
        if (HAL_GPIO_ReadPin(KEY_PORT, KEY_PIN) == GPIO_PIN_RESET)
        {
            return 1;   // 确认按下
        }
    }
    return 0;
}

// 主函数
int main(void)
{
    HAL_Init();
    LED_Init();
    KEY_Init();

    while (1)
    {
        if (KEY_Read())
        {
            HAL_GPIO_TogglePin(LED_PORT, LED_PIN);  // 按键按下,翻转LED
        }
    }
}

注意:消抖延时20ms是个经验值,不同按键可能不一样。我遇到过一种微动开关,抖动时间长达30ms,20ms不够用。所以实际项目中,最好用示波器看一下按键波形,再确定延时时间。

2.6 本章小结——一张表看清GPIO配置

最后,我把GPIO的几种常用模式整理成一张表,方便你以后查阅:

应用场景 Mode Pull Speed 备注
驱动LED OUTPUT_PP NOPULL LOW 推挽输出,直接驱动
读取按键 INPUT PULLUP — 内部上拉,低电平有效
I2C通信 AF_OD NOPULL HIGH 开漏输出,外部上拉
SPI通信 AF_PP NOPULL VERY_HIGH 推挽复用,高速
模拟输入(ADC) ANALOG NOPULL — 关闭数字功能

嗯,这一章的内容就到这里。GPIO看似简单,但它是所有外设驱动的基础。你把这个搞透了,后面学UART、SPI、I2C都会轻松很多。记住:时钟要开、模式要选对、上下拉要匹配、按键要消抖。做到这四点,GPIO这块基本不会出问题。