2、GPIO外设驱动开发:掌握GPIO的输入输出模式,编写按键控制LED的驱动程序
好,咱们正式开始动手写代码了。这一章是嵌入式开发里最基础、也最绕不开的一环——GPIO。说白了,GPIO就是芯片的“手脚”,你要让它感知外部世界(输入),或者去控制外部设备(输出)。
我个人习惯,每接触一个新MCU,第一个例程一定是点灯和读按键。这俩搞定了,时钟、引脚、寄存器这些基本功就算过了关。今天咱们就拿STM32来走一遍完整流程。
2.1 GPIO是什么?
GPIO,全称General Purpose Input Output,通用输入输出口。每个GPIO引脚可以独立配置成输入或输出模式。STM32的GPIO比传统51单片机复杂不少,但功能也更强大。
我刚开始从51转STM32时,最不适应的就是GPIO还得先开时钟。51单片机一上电就能直接操作P1口,但STM32不行——你得先告诉芯片:“我要用这个引脚了,请给它供电。”这个动作就是使能GPIO时钟。
| 特性 | 传统51单片机 | STM32 |
|---|---|---|
| 时钟控制 | 默认开启 | 需手动使能 |
| 输出驱动能力 | 固定 | 可配置(2/8/50MHz) |
| 上下拉电阻 | 需外接 | 内置可配置 |
| 复用功能 | 无 | 可映射到外设 |
2.2 GPIO的8种工作模式
STM32的GPIO有8种模式,但别被吓到。实际开发中常用的就4种:推挽输出、浮空输入、上拉输入、复用推挽输出。剩下的几种,你遇到再查也来得及。
核心记忆法:
- 输出场景:控制LED、蜂鸣器、继电器 → 推挽输出
- 输入场景:读取按键、检测电平 → 浮空输入或上拉/下拉输入
- 外设场景:USART、SPI、I2C等 → 复用推挽输出
这里有个坑,我踩过好几次。配置成开漏输出时,如果不外接上拉电阻,输出高电平是拉不上去的。有一次调试I2C总线,波形死活不对,折腾了半天才发现是忘了开内部上拉。嗯,从那以后我每次配开漏模式都会检查上拉电阻。
2.3 按键控制LED的硬件连接
咱们用最经典的电路:
- LED:正极接VCC(3.3V),负极接GPIO引脚。这样引脚输出低电平时LED亮,输出高电平时LED灭。
- 按键:一端接GND,另一端接GPIO引脚,并开启内部上拉电阻。按键按下时引脚读到低电平,松开时读到高电平。
为什么LED用低电平点亮?
因为STM32的GPIO驱动能力有限,灌电流(吸入电流)能力比拉电流(输出电流)强。低电平点亮时,电流从VCC经过LED流入引脚,引脚工作在灌电流模式,更稳定。
2.4 寄存器级驱动代码
先看寄存器版本。虽然现在HAL库很流行,但我建议你至少看一遍寄存器操作。为什么?因为只有理解了寄存器,你才能真正明白HAL库背后在干什么。
// 按键控制LED - 寄存器版本
// 硬件:LED接PA0,按键接PA1
// 1. 使能GPIOA时钟
RCC->APB2ENR |= (1 << 2); // RCC_APB2ENR的bit2控制GPIOA时钟
// 2. 配置PA0为推挽输出,50MHz
GPIOA->CRL &= ~(0x0F << 0); // 先清零CNF0和MODE0
GPIOA->CRL |= (0x0B << 0); // MODE0=11(50MHz), CNF0=00(推挽输出)
// 3. 配置PA1为上拉输入
GPIOA->CRL &= ~(0x0F << 4); // 先清零CNF1和MODE1
GPIOA->CRL |= (0x08 << 4); // MODE1=00(输入), CNF1=10(上拉/下拉)
GPIOA->ODR |= (1 << 1); // 开启上拉电阻
// 4. 主循环:读取按键,控制LED
while(1)
{
if((GPIOA->IDR & (1 << 1)) == 0) // 按键按下(低电平)
{
GPIOA->BSRR = (1 << 0); // 复位PA0,LED亮
}
else
{
GPIOA->BRR = (1 << 0); // 置位PA0,LED灭
}
}
注意:BSRR和BRR是STM32特有的“原子操作”寄存器。用ODR操作单个引脚时,如果被中断打断,可能产生“读-改-写”冲突。BSRR和BRR是只写寄存器,不会出现这个问题。
2.5 HAL库版本驱动代码
再看HAL库版本。实际项目中我大部分时候用HAL库,因为可移植性好,而且CubeMX生成的代码结构清晰。
// 按键控制LED - HAL库版本
// 硬件:LED接PA0,按键接PA1
// 1. 初始化GPIO
void MX_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
// 使能GPIOA时钟
__HAL_RCC_GPIOA_CLK_ENABLE();
// 配置PA0为推挽输出
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// 配置PA1为上拉输入
GPIO_InitStruct.Pin = GPIO_PIN_1;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}
// 2. 主循环
int main(void)
{
HAL_Init();
MX_GPIO_Init();
while(1)
{
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET)
{
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET); // LED亮
}
else
{
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET); // LED灭
}
}
}
2.6 按键消抖处理
上面那个代码有个问题——按键会抖动。机械按键按下和松开的瞬间,电平会跳变几次,持续约5-20ms。如果不做消抖,一次按键可能被误判成多次。
我见过一个同事的产品,用户按一次菜单键,界面跳了三层。最后查出来就是没做消抖。最简单的消抖方法就是延时+多次采样。
// 按键消抖函数
uint8_t Key_Scan(void)
{
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET)
{
HAL_Delay(10); // 延时10ms,避开抖动期
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET)
{
// 等待按键松开
while(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET);
return 1; // 返回按键有效
}
}
return 0; // 无按键
}
进阶建议:在RTOS环境下,不要用HAL_Delay()做消抖,会阻塞任务。可以用定时器轮询或状态机方式实现非阻塞消抖。这个咱们后面讲RTOS移植时会详细说。
2.7 GPIO驱动知识体系
下面这张图把GPIO驱动开发的核心脉络梳理了一下。你写代码时脑子里要有这个结构,才不会漏掉配置步骤。
2.8 常见问题与避坑指南
- 引脚没反应?先检查时钟有没有使能。我见过太多人卡在这一步。
- LED微亮?可能是引脚配置成了开漏输出,或者上下拉电阻配置错了。
- 按键误触发?大概率是没做消抖,或者消抖时间太短。
- 程序跑飞?检查BSRR和BRR的位操作是否正确。BSRR的低16位是置位,高16位是复位,别搞反了。
我曾经犯过的错:有一次把按键引脚配置成了推挽输出,结果按键按下时相当于VCC对GND短路,芯片瞬间发烫。从那以后,我每次配置输入引脚都会再三确认模式。
好了,这一章的内容就到这儿。GPIO看似简单,但它是所有外设驱动的基础。你把这章吃透了,后面学USART、SPI、I2C都会轻松很多。动手把代码跑一遍,遇到问题再回来看这张知识体系图,思路会清晰很多。