2. GPIO基础与HAL库驱动:GPIO工作原理、HAL_GPIO_Init/WritePin/ReadPin函数详解、GPIO输出驱动LED、GPIO输入读取按键
各位同学,欢迎来到第二章。这一章我们聊点最基础、也最常用的东西——GPIO。说白了,就是芯片的“手脚”,用来对外输出高低电平,或者读取外部信号是高还是低。
我刚开始学STM32的时候,觉得GPIO太简单了,不就是拉高拉低嘛。结果后来做项目,遇到按键抖动、LED不亮、引脚配置冲突……嗯,才发现这里面的门道真不少。今天我就把这些经验掰开了讲给你听。
2.1 GPIO工作原理——从引脚到寄存器
STM32的GPIO,每个引脚背后都有一套完整的电路。我画了个简化的框图,帮你理解信号是怎么从芯片内部走到引脚上的。
核心要点: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这块基本不会出问题。