GPIO 输入输出:从概念到实战
各位同学,欢迎来到第二章。上一章我们搭好了开发环境,这一章咱们来点实在的——让芯片跟外界“说话”。
GPIO,全称 General-Purpose Input/Output,通用输入输出口。说白了,就是芯片上那些可以编程控制的引脚。你可以让它输出高电平点亮 LED,也可以读取它上面的电平判断按键是否按下。
我刚开始学 STM32 时,觉得 GPIO 不就是个开关嘛,有啥好学的?后来做项目才发现,恰恰是这个最简单的外设,坑最多。嗯,咱们今天就把这些坑一个个填上。
2.1 GPIO 基本概念
STM32 的 GPIO 可不是简单的“0 或 1”。它内部有一套完整的电路结构,支持多种工作模式。我习惯把这几种模式分成三大类:
| 模式分类 | 具体模式 | 典型用途 |
|---|---|---|
| 输入模式 | 浮空输入、上拉输入、下拉输入、模拟输入 | 读取按键、检测传感器电平、ADC 采样 |
| 输出模式 | 推挽输出、开漏输出 | 驱动 LED、控制继电器、I2C 通信 |
| 复用功能模式 | 复用推挽输出、复用开漏输出 | USART、SPI、定时器通道等外设引脚 |
这里有个关键点:推挽输出能主动输出高低电平,驱动能力强;开漏输出只能拉低,拉高要靠外部上拉电阻,常用于多设备共线的总线。
为什么会这样设计?你想想看,如果所有引脚都只能输出固定电平,那 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 模式下有效 |
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。就这么简单。
但读取按键时有个大坑——机械抖动。按键按下和松开的瞬间,电平会跳变多次。直接读的话,一次按下可能被识别成几十次。
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);
}
}
2.3.3 核心逻辑流程图
下面这张图展示了按键检测的完整流程:
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 开漏模式不会。
2.4.4 中断模式配置后没写中断服务函数
如果你把引脚配成 GPIO_MODE_IT_RISING 等中断模式,却没在 stm32f1xx_it.c 里实现对应的 HAL_GPIO_EXTI_Callback,中断来了程序会跑飞。
嗯,GPIO 的内容就这么多。看似简单,但它是所有外设的基础。你把这章吃透了,后面学 USART、SPI、I2C 时会发现——它们的底层也是 GPIO 的复用功能模式。