2. GPIO与按键中断基础
各位同学,咱们今天来聊聊嵌入式开发里最基础、也最常用的两个外设——GPIO和外部中断。说白了,就是怎么让芯片“看”到外部世界的变化,再“动”起来。
我刚开始学STM32的时候,觉得GPIO不就是个高低电平嘛,有啥好学的?结果第一个项目就栽在按键抖动上,LED死活不按我预想的亮灭。嗯,从那以后,我再也不敢小看这些基础操作了。
2.1 GPIO输出驱动LED——让芯片“说话”
GPIO,全称General Purpose Input Output,通用输入输出口。你可以把它想象成芯片的“手脚”——用来对外输出信号,或者读取外部信号。
驱动LED,是GPIO输出最典型的应用。我们以STM32F103系列为例,看看具体怎么配置。
2.1.1 GPIO模式选择
STM32的GPIO有好几种模式,我列个表,大家一看就明白:
| 模式 | 说明 | 典型应用 |
|---|---|---|
| 推挽输出 | 可以输出高电平和低电平,驱动能力强 | 驱动LED、蜂鸣器 |
| 开漏输出 | 只能输出低电平,高电平需要外部上拉 | I2C总线、多路信号线与 |
| 浮空输入 | 引脚电平不确定,容易受干扰 | 基本不用 |
| 上拉输入 | 默认高电平,外部拉低时读取到低电平 | 按键检测(按下为低) |
| 下拉输入 | 默认低电平,外部拉高时读取到高电平 | 按键检测(按下为高) |
| 模拟输入 | 用于ADC采集模拟信号 | 电压测量、传感器 |
驱动LED,我们选推挽输出。为什么?因为推挽输出既能灌电流也能拉电流,驱动LED最合适。
2.1.2 代码实战:点亮LED
直接上代码,我习惯用寄存器操作,因为这样能让你真正理解底层在干什么。当然,HAL库也可以,但咱们做裸机调度,寄存器更直接。
// 开启GPIOB时钟(挂在APB2总线上)
RCC->APB2ENR |= (1 << 3); // 位3对应GPIOB
// 配置PB0为推挽输出,50MHz速度
GPIOB->CRL &= ~(0x0F << 0); // 先清除配置位
GPIOB->CRL |= (0x03 << 0); // 0x03 = 推挽输出,50MHz
// 点亮LED(低电平点亮)
GPIOB->BRR = (1 << 0); // 复位PB0,输出低电平
// 熄灭LED
GPIOB->BSRR = (1 << 0); // 置位PB0,输出高电平
2.2 外部中断EXTI配置——让芯片“听”到外部信号
光会输出还不够,芯片还得能“听”到外部事件。比如按键按下、传感器触发,这些都需要中断来响应。
外部中断EXTI,就是专门干这个的。它可以把GPIO引脚上的电平变化,转化成中断请求,让CPU暂停当前任务,去处理这个事件。
2.2.1 EXTI的触发方式
EXTI支持三种触发方式:
- 上升沿触发: 信号从低变高时触发
- 下降沿触发: 信号从高变低时触发
- 双边沿触发: 上升沿和下降沿都触发
按键中断,我一般用下降沿触发。为什么?因为按键按下时,电平从高变低,下降沿正好对应这个动作。当然,如果你需要同时检测按下和松开,那就用双边沿。
2.2.2 配置步骤
配置EXTI,我总结了四个步骤,按顺序来,不容易出错:
- 配置GPIO为输入模式——告诉芯片这个引脚是用来读的
- 配置EXTI线——把GPIO引脚映射到对应的EXTI线
- 配置触发方式——选择上升沿、下降沿还是双边沿
- 配置NVIC——使能中断通道,设置优先级
来看代码:
// 1. 配置PA0为输入模式(上拉输入)
RCC->APB2ENR |= (1 << 2); // 开启GPIOA时钟
GPIOA->CRL &= ~(0x0F << 0); // 清除配置
GPIOA->CRL |= (0x08 << 0); // 0x08 = 上拉输入
// 2. 配置EXTI线0(PA0对应EXTI0)
AFIO->EXTICR[0] &= ~(0x0F << 0); // 清除配置
AFIO->EXTICR[0] |= (0x00 << 0); // 0x00 = PA口
// 3. 配置下降沿触发
EXTI->FTSR |= (1 << 0); // 下降沿触发寄存器
// 4. 使能EXTI线0
EXTI->IMR |= (1 << 0); // 中断屏蔽寄存器
// 5. 配置NVIC
NVIC->ISER[0] |= (1 << 6); // EXTI0中断通道号是6
// 开启AFIO时钟
RCC->APB2ENR |= (1 << 0); // 位0对应AFIO
2.3 中断服务函数编写规范——别在中断里“磨洋工”
中断服务函数,也就是ISR(Interrupt Service Routine),是中断处理的核心。但很多新手容易犯一个错误——在中断里写太多代码。
我给大家一个原则:中断服务函数要短、平、快。为什么?因为中断是在打断主程序执行的,你在中断里待得越久,主程序就被耽误得越久。如果中断里再嵌套中断,那系统响应就乱套了。
2.3.1 ISR的基本结构
一个规范的ISR,应该包含以下部分:
- 清除中断标志位——告诉硬件“我已经处理完了”
- 处理紧急事件——比如读取数据、设置标志位
- 退出中断——返回主程序
来看一个按键中断的ISR示例:
void EXTI0_IRQHandler(void)
{
// 1. 清除中断标志位(必须!)
if (EXTI->PR & (1 << 0))
{
EXTI->PR = (1 << 0); // 写1清除
}
// 2. 处理紧急事件——设置一个标志位
// 不要在中断里做复杂操作,比如延时、打印
g_button_pressed = 1;
// 3. 退出中断(硬件自动完成,不需要手动写)
}
2.3.2 避坑指南——我踩过的那些坑
做嵌入式这么多年,我在中断上栽过不少跟头。分享几个典型的:
- 忘记清除标志位: 中断标志位不清除,中断会反复触发,系统直接卡死。我曾经因为这个,在客户现场调试到凌晨两点。
- 在中断里用printf: printf是阻塞的,而且耗时很长。在中断里用printf,相当于把整个系统按了暂停键。我建议用串口DMA或者环形缓冲区。
- 中断里调用HAL_Delay: HAL_Delay依赖SysTick中断,而SysTick中断优先级可能比当前中断低,导致死锁。这个坑我踩过两次才长记性。
2.4 本章知识体系
为了让大家更直观地理解GPIO和中断的关系,我画了一张图:
2.5 小结
这一章,我们讲了GPIO输出驱动LED、外部中断EXTI的配置,以及中断服务函数的编写规范。说白了,就是让芯片既能“动”(输出),又能“听”(中断)。
我个人觉得,这两块是嵌入式开发的“基本功”。你想想看,如果连GPIO和中断都搞不定,那后面更复杂的任务调度、多任务管理,根本无从谈起。