第4章 GPIO实战:从点灯到按键中断
各位同学,欢迎来到第四章。这一章我们直接上手干活。
GPIO 是嵌入式开发里最基础、也最常用的外设。说白了,它就是芯片的「手脚」——用来输出高低电平,或者读取外部信号。今天我们就围绕 LED 和按键,把 GPIO 的几种典型用法都过一遍。
4.1 LED灯控制:轮询方式
先来个最简单的:让 LED 闪烁。我习惯用轮询方式做演示,因为逻辑最直观。
核心思路:配置 GPIO 为推挽输出,然后在主循环里交替拉高拉低电平。
// 初始化 LED 引脚
void LED_Init(void) {
// 使能 GPIO 时钟
RCC->AHB1ENR |= (1 << 0); // 假设 LED 接在 PA0
// 配置为输出模式
GPIOA->MODER &= ~(0x3 << 0); // 先清零
GPIOA->MODER |= (0x1 << 0); // 设置为输出
// 配置为推挽输出
GPIOA->OTYPER &= ~(1 << 0); // 推挽
// 配置上拉(可选)
GPIOA->PUPDR &= ~(0x3 << 0);
GPIOA->PUPDR |= (0x1 << 0); // 上拉
}
// 主循环
while(1) {
GPIOA->BSRR = (1 << 0); // 置位,LED 亮
delay_ms(500);
GPIOA->BSRR = (1 << 16); // 复位,LED 灭
delay_ms(500);
}
这里有个小细节:BSRR 寄存器写 1 置位,写 1 到高 16 位是复位。我刚开始用的时候老记反,后来干脆记成「低16位置位,高16位复位」——嗯,这样就好记多了。
个人经验:实际项目中,LED 驱动电流一般 5-10mA 就够了。别直接推大电流,加个 330Ω 或 470Ω 限流电阻。我曾经见过有人直接推 20mA,结果 GPIO 口烧了——教训啊。
4.2 按键输入检测:消抖处理
按键检测看着简单,但坑不少。最大的坑就是「抖动」。
你想想看,机械按键按下和松开时,触点会弹跳几次。如果不处理,一次按下可能被误判成好几次。我早期做项目时就吃过这个亏——按一下按键,灯闪了三次,调试了半天才发现是抖动问题。
避坑指南:我曾经在量产产品里只用硬件消抖(RC 滤波),结果不同批次按键的弹跳时间不一样,导致部分产品按键失灵。后来我改成「硬件滤波 + 软件消抖」双保险,再没出过问题。
软件消抖最常用的方法是延时去抖:检测到电平变化后,等 10-20ms 再读一次。如果状态一致,就认为是有效按键。
// 按键消抖函数
uint8_t Key_Scan(void) {
static uint8_t last_state = 1; // 假设上拉,默认高电平
uint8_t current_state;
current_state = (GPIOA->IDR >> 0) & 0x01; // 读取 PA0
if (current_state != last_state) {
// 状态变化,开始消抖
delay_ms(10); // 延时 10ms
current_state = (GPIOA->IDR >> 0) & 0x01;
if (current_state != last_state) {
last_state = current_state;
if (current_state == 0) {
return 1; // 按键按下
}
}
}
return 0; // 无按键
}
这里我用了静态变量保存上次状态。为什么?因为每次调用都要记住上一次的电平,才能判断有没有变化。静态变量正好干这个活。
4.3 中断方式按键检测
轮询方式有个缺点:CPU 得一直跑循环去查按键状态,浪费资源。中断方式就好多了——按键按下时,CPU 才响应,平时该干嘛干嘛。
配置中断的步骤其实不复杂:
- 使能 GPIO 时钟和 SYSCFG 时钟
- 配置 GPIO 为输入模式
- 设置中断触发边沿(上升沿、下降沿或双边沿)
- 使能 NVIC 中断
- 编写中断服务函数
// 配置 PA0 为外部中断
void EXTI_Init(void) {
// 使能时钟
RCC->AHB1ENR |= (1 << 0); // GPIOA 时钟
RCC->APB2ENR |= (1 << 14); // SYSCFG 时钟
// 配置 PA0 为输入
GPIOA->MODER &= ~(0x3 << 0);
// 连接 EXTI0 到 PA0
SYSCFG->EXTICR[0] &= ~(0xF << 0);
SYSCFG->EXTICR[0] |= (0x0 << 0); // 0 表示 PA
// 配置中断触发方式:下降沿触发
EXTI->FTSR |= (1 << 0); // 下降沿
EXTI->RTSR &= ~(1 << 0); // 上升沿不触发
// 取消屏蔽
EXTI->IMR |= (1 << 0);
// 使能 NVIC
NVIC->ISER[0] |= (1 << 6); // EXTI0 中断号 6
}
// 中断服务函数
void EXTI0_IRQHandler(void) {
if (EXTI->PR & (1 << 0)) {
// 清除中断标志
EXTI->PR = (1 << 0);
// 这里加消抖处理
delay_ms(10);
if ((GPIOA->IDR & (1 << 0)) == 0) {
// 按键确实按下,执行操作
LED_Toggle();
}
}
}
注意:中断服务函数里不要做耗时操作。我一般只设置一个标志位,具体处理放到主循环里。否则中断嵌套多了,系统响应会变慢。
4.4 GPIO位带操作技巧
位带操作是 Cortex-M3/M4 内核的一个「隐藏技能」。它允许你像操作普通变量一样,直接读写某个 GPIO 引脚,而不需要读-改-写整个寄存器。
说白了,位带区就是把每个 bit 映射到一个 32 位的地址上。你往这个地址写 1,对应的 bit 就置位;写 0,就清零。效率极高。
位带操作的公式:
位带别名地址 = 位带基址 + (字节偏移 * 32) + (位编号 * 4)
以 STM32F4 为例,GPIOA->ODR 的地址是 0x40020014,我们想操作第 0 位:
// 计算位带别名地址
#define GPIOA_ODR_ADDR (0x40020014)
#define BITBAND_BASE (0x42000000)
#define BIT_OFFSET(addr, bit) ((addr - 0x40000000) * 32 + bit * 4)
#define BITBAND_ADDR(addr, bit) (BITBAND_BASE + BIT_OFFSET(addr, bit))
// 定义 PA0 的位带地址
#define PA0_OUT (*((volatile uint32_t *)BITBAND_ADDR(GPIOA_ODR_ADDR, 0)))
// 使用
PA0_OUT = 1; // LED 亮
PA0_OUT = 0; // LED 灭
我的习惯:位带操作特别适合用在时间敏感的地方,比如模拟时序、快速翻转 IO。但平时写业务逻辑,我还是用标准库或 HAL 库——代码可读性更重要。你想想看,如果满屏都是位带地址,三个月后你自己都看不懂。
位带操作还有个好处:原子操作。在 RTOS 环境下,多个任务同时操作同一个 GPIO 时,位带操作不会被打断。这一点在写驱动时特别有用。
小结
这一章我们讲了四种 GPIO 用法:
- 轮询点灯——最基础,适合入门
- 按键消抖——别小看它,量产产品里翻车最多的就是这里
- 中断检测——效率高,但要注意中断服务函数别写太长
- 位带操作——性能利器,但别滥用
下一章我们会深入定时器,到时候会用到今天讲的 GPIO 中断。嗯,先消化好这些基础,后面才能跑得快。