4、深入GPIO:按键输入与中断
按键,是嵌入式世界里最朴素的人机交互方式。你按下它,系统响应——听起来简单,但实际做起来,坑不少。我刚开始用Rust写STM32的时候,就栽在按键消抖上。今天咱们把这部分彻底理清楚。
4.1 按键消抖原理
机械按键的本质是什么?是金属弹片的物理接触。按下和释放的瞬间,弹片会反弹,产生一连串的毛刺信号。这就是抖动。
抖动的典型特征:
- 持续时间:5~20ms
- 频率:不固定,随机性高
- 后果:一次按下被误判为多次
消抖的思路无非两种:硬件消抖和软件消抖。硬件上,加个RC低通滤波器,把毛刺滤掉。软件上,更灵活——检测到电平变化后,延时10~20ms再读一次。如果电平稳定,才算有效。
我的经验: 软件消抖用延时函数是最直接的办法。但注意,在中断里别用阻塞延时,否则整个系统都卡住了。我一般用状态机+定时器轮询,或者用RTOS的延时。
4.2 配置GPIO为输入模式
在STM32的HAL里,配置GPIO输入很简单。但在Rust的embedded-hal抽象下,我们需要用更类型安全的方式来做。
以STM32F4为例,用stm32f4xx-hal库配置PA0为输入:
use stm32f4xx_hal::{
gpio::GpioExt,
gpio::gpioa::Parts,
gpio::Input,
gpio::PullUp,
pac,
};
let dp = pac::Peripherals::take().unwrap();
let gpioa = dp.GPIOA.split();
let button = gpioa.pa0.into_pull_up_input();
这里的关键点:
into_pull_up_input()启用内部上拉电阻,默认高电平- 按键另一端接地,按下时引脚被拉低
- 类型系统保证了引脚模式在编译期确定,不会出现运行时配置错误
| 配置项 | 说明 | 常用值 |
|---|---|---|
| 引脚模式 | 输入/输出/复用 | Input |
| 上拉/下拉 | 内部电阻 | PullUp / PullDown |
| 触发方式 | 中断触发边沿 | 上升沿/下降沿/双边沿 |
注意: 如果外部电路已经接了上拉电阻,就别再启用内部上拉。两个上拉并联会改变分压,导致电平判断不准。我曾经因为这个排查了半天。
4.3 外部中断(EXTI)配置
轮询按键太浪费CPU了。更好的做法是用外部中断——按键按下时,CPU暂停当前任务,跳转到中断服务函数处理。
EXTI的配置流程:
- 选择GPIO引脚连接到EXTI线
- 配置触发边沿(上升沿、下降沿或双边沿)
- 使能中断线
- 编写中断服务函数
在Rust中,用stm32f4xx-hal配置EXTI:
use stm32f4xx_hal::{
interrupt,
gpio::ExtiPin,
pac::{self, EXTI},
};
// 配置PA0为外部中断,下降沿触发
let mut button = gpioa.pa0.into_pull_up_input();
button.make_interrupt_source(&mut dp.SYSCFG);
button.trigger_on_edge(&mut dp.EXTI, Edge::Falling);
button.enable_interrupt(&mut dp.EXTI);
这里trigger_on_edge指定了触发边沿。按键按下时,电平从高变低,所以用下降沿。如果按键接法相反(按下接高电平),就用上升沿。
EXTI线映射规则:
- PA0、PB0、PC0... 都映射到EXTI0线
- PA1、PB1、PC1... 映射到EXTI1线
- 同一时刻,一条EXTI线只能连接一个引脚
4.4 在Rust中处理中断回调
Rust的中断处理跟C不太一样。C里你直接写中断函数名,Rust里需要用#[interrupt]属性宏来声明。
基本结构:
use core::cell::RefCell;
use cortex_m::interrupt::Mutex;
static BUTTON_PRESSED: Mutex<RefCell<bool>> = Mutex::new(RefCell::new(false));
#[interrupt]
fn EXTI0() {
// 清除中断标志
// 设置按键状态
cortex_m::interrupt::free(|cs| {
*BUTTON_PRESSED.borrow(cs).borrow_mut() = true;
});
}
这里有几个要点:
Mutex+RefCell是Rust在中断中共享数据的标准做法cortex_m::interrupt::free保证临界区安全- 中断函数名必须与中断向量表中的名称一致(如
EXTI0)
我的习惯: 中断里只做最轻量的事——设置标志位、清除中断标志。真正的业务逻辑放到主循环里处理。这样中断响应快,也不容易出问题。
完整的按键中断示例:
use stm32f4xx_hal::{
gpio::{ExtiPin, GpioExt, Input, PullUp},
pac,
prelude::*,
};
fn main() -> ! {
let dp = pac::Peripherals::take().unwrap();
let cp = cortex_m::Peripherals::take().unwrap();
let gpioa = dp.GPIOA.split();
let mut button = gpioa.pa0.into_pull_up_input();
button.make_interrupt_source(&mut dp.SYSCFG);
button.trigger_on_edge(&mut dp.EXTI, Edge::Falling);
button.enable_interrupt(&mut dp.EXTI);
// 使能全局中断
unsafe { cortex_m::interrupt::enable(); }
loop {
// 主循环处理按键事件
cortex_m::interrupt::free(|cs| {
if *BUTTON_PRESSED.borrow(cs).borrow() {
// 处理按键逻辑
// ...
*BUTTON_PRESSED.borrow(cs).borrow_mut() = false;
}
});
}
}
嗯,这里要注意一点:unsafe块是因为使能全局中断在Rust里被认为是不安全操作。但实际用起来,只要确保中断服务函数正确,就没问题。
4.5 本章知识体系
下面这张图把按键输入与中断的完整流程串起来了:
总结一下: 按键输入看似简单,但消抖、配置、中断处理每个环节都有细节。用Rust写的好处是,类型系统帮你挡住了很多低级错误。比如引脚模式配置错了,编译期就报错,不会等到运行时才发现。