第四章:GPIO模块设计:输入输出模式配置、上下拉电阻、中断触发方式、实战:按键扫描与LED控制
各位同学,欢迎来到第四章。这一章我们聊聊GPIO。
GPIO,通用输入输出口,听起来简单吧?不就是拉高拉低嘛。但说实话,我见过太多项目栽在GPIO上。不是按键误触发,就是LED亮度不对,甚至烧引脚。嗯,这一章我们就把GPIO吃透。
4.1 GPIO输入输出模式配置
先问个问题:你写GPIO_Init()的时候,真的知道芯片内部发生了什么吗?
我个人习惯,拿到一个新MCU,第一件事就是看GPIO结构框图。你会发现,每个引脚背后都有一堆MOS管、触发器和电阻网络。
4.1.1 输出模式
输出模式分两种:推挽输出和开漏输出。
- 推挽输出:内部两个MOS管交替导通。输出高电平时,上管导通拉高;输出低电平时,下管导通拉低。驱动能力强,适合LED、蜂鸣器。
- 开漏输出:只有下管,上管没有。输出低电平靠内部拉低,输出高电平必须外部上拉。我常用在I2C总线上,因为多个设备可以共线。
避坑指南:我曾经在项目里用推挽输出驱动一个5V的继电器模块,结果MCU引脚直接冒烟了。为什么?因为推挽输出高电平是3.3V,继电器模块内部有上拉到5V,两个电源直接短路了。后来我改成开漏输出加外部NPN三极管驱动,问题解决。
4.1.2 输入模式
输入模式也有讲究。常见的有:浮空输入、上拉输入、下拉输入。
| 模式 | 内部状态 | 适用场景 |
|---|---|---|
| 浮空输入 | 无上下拉,高阻态 | 外部已有上下拉电阻 |
| 上拉输入 | 内部接VDD | 按键检测(默认高电平) |
| 下拉输入 | 内部接GND | 按键检测(默认低电平) |
你想想看,如果按键一端接GND,另一端接GPIO。用上拉输入,按键按下时读到低电平,松开时读到高电平。逻辑清晰,代码好写。
4.2 上下拉电阻
上下拉电阻,说白了就是给不确定的电平一个确定的归宿。
我记得有一次调试一个温湿度传感器,数据线总是跳变。示波器一看,浮空输入状态下,引脚电平在1.2V到2.8V之间乱飘。这就是典型的「引脚悬空」问题。
我的经验:内部上下拉电阻一般在30kΩ~50kΩ之间,驱动能力有限。如果外部干扰强,建议外加4.7kΩ或10kΩ的电阻。尤其是长线传输时,内部上拉根本拉不住。
上下拉电阻的选择原则:
- 上拉电阻越小,驱动能力越强,但静态功耗越大
- 下拉电阻同理
- 按键场景:10kΩ上拉是黄金值
- I2C场景:4.7kΩ是常用值
4.3 中断触发方式
GPIO中断,是嵌入式系统的灵魂。没有中断,你只能轮询,CPU累死,功耗也高。
中断触发方式有四种:
- 上升沿触发:电平从低变高时触发
- 下降沿触发:电平从高变低时触发
- 双边沿触发:变化就触发
- 电平触发:保持某个电平就持续触发
注意:电平触发容易产生重复中断。我曾经在做一个门禁系统时,用了高电平触发,结果门一开,中断一直进,CPU直接卡死在中断服务函数里。后来改成下降沿触发,只触发一次,完美解决。
实际项目中,按键用下降沿触发最常见。但要注意硬件抖动。机械按键按下时,电平会弹跳几次,持续5~20ms。如果不做处理,一次按键会触发多次中断。
4.4 实战:按键扫描与LED控制
好了,理论讲完了,我们动手写代码。这个实战我做了不下十次,每次都有新感悟。
4.4.1 硬件连接
- 按键:PA0,外部上拉10kΩ,按下接地
- LED:PB0,推挽输出,串联220Ω电阻到GND
4.4.2 代码实现
先看初始化部分:
void GPIO_Init(void)
{
// 使能GPIO时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN;
// PA0: 上拉输入
GPIOA->CRL &= ~(0x0F << 0); // 清除配置
GPIOA->CRL |= (0x08 << 0); // 上拉输入模式
GPIOA->ODR |= (1 << 0); // 使能内部上拉
// PB0: 推挽输出,50MHz
GPIOB->CRL &= ~(0x0F << 0);
GPIOB->CRL |= (0x03 << 0); // 推挽输出,50MHz
}
按键扫描,我习惯用状态机加延时消抖:
uint8_t Key_Scan(void)
{
static uint8_t last_state = 1; // 上次状态,默认高电平
static uint16_t count = 0; // 消抖计数器
uint8_t current_state = (GPIOA->IDR & (1 << 0)) ? 1 : 0;
uint8_t key_event = 0;
if(current_state != last_state)
{
count++;
if(count >= 5) // 约5ms消抖
{
last_state = current_state;
if(current_state == 0) // 按下
{
key_event = 1; // 有效按键
}
count = 0;
}
}
else
{
count = 0;
}
return key_event;
}
主循环控制LED:
int main(void)
{
GPIO_Init();
uint8_t led_state = 0;
while(1)
{
if(Key_Scan())
{
led_state = !led_state;
if(led_state)
GPIOB->BSRR = (1 << 0); // 点亮
else
GPIOB->BRR = (1 << 0); // 熄灭
}
}
}
避坑指南:我曾经在按键消抖里用了delay_ms(20),结果按键响应特别迟钝。后来改成状态机加计数器,不阻塞CPU,响应快多了。记住:中断里别用延时,主循环里也别用长延时。
4.5 总结与扩展
这一章我们讲了GPIO的输入输出配置、上下拉电阻、中断触发方式,还做了按键和LED的实战。
最后说一句:GPIO看似简单,但它是嵌入式系统的基石。我见过很多老工程师,写GPIO代码时依然会看数据手册的电气特性表。为什么?因为不同负载下,GPIO的驱动能力、上升时间、下降时间都不一样。
下一章我们会讲定时器,到时候你会发现,GPIO和定时器配合起来,能玩出更多花样。
课后思考:如果按键接在中断引脚上,怎么在中断服务函数里实现消抖?提示:可以用定时器延时再检测电平。