3. GPIO驱动开发实战:从初始化到中断配置
GPIO,说白了就是芯片的“手脚”。你要让LED亮、检测按键按下、或者跟外设握个手,都得靠它。这一章我们就把GPIO的几种常见玩法过一遍,从初始化到中断,一步不落。
本章核心知识点:GPIO初始化与模式配置、推挽输出 vs 开漏输出、上拉/下拉电阻、中断触发配置(上升沿/下降沿/双边沿)。
3.1 GPIO初始化与模式配置
在Rust嵌入式HAL里,GPIO的初始化通常分两步:先拿到外设的访问权限,再配置引脚的模式。我习惯用 PAC 层拆分外设,然后用 HAL 层封装好的API去操作。
use stm32f4xx_hal::{
prelude::*,
gpio::{GpioExt, Output, PushPull},
pac,
};
let dp = pac::Peripherals::take().unwrap();
let gpioa = dp.GPIOA.split();
let mut led = gpioa.pa5.into_push_pull_output();
你看,into_push_pull_output() 一步到位,把引脚配置成推挽输出模式。如果换成 into_floating_input(),那就是高阻输入。我个人建议,初始化时尽量明确模式,别偷懒用默认值——我在项目里见过有人忘了配置模式,结果引脚电平乱跳,查了半天才发现。
| 模式 | HAL方法 | 典型用途 |
|---|---|---|
| 推挽输出 | into_push_pull_output() |
驱动LED、普通数字信号输出 |
| 开漏输出 | into_open_drain_output() |
I²C总线、多设备共享线 |
| 浮空输入 | into_floating_input() |
外部中断、按键检测(配合外部上拉) |
| 上拉输入 | into_pull_up_input() |
按键检测(省外部电阻) |
| 下拉输入 | into_pull_down_input() |
特定电平检测 |
3.2 推挽输出与开漏输出
这两个概念,很多初学者容易搞混。我简单解释一下:
- 推挽输出:引脚内部有两个MOS管,一个推高电平,一个拉低电平。输出高时直接连VCC,输出低时直接连GND。驱动能力强,但多个推挽输出不能直接连在一起——会打架。
- 开漏输出:只有拉低的能力,没有推高的能力。输出高时引脚处于高阻态,需要外部上拉电阻把电平拉高。多个开漏输出可以“线与”,谁拉低谁赢。
嗯,这里要注意:开漏输出不是“输出开漏”,而是“漏极开路”。我在做I²C驱动时用过它,总线上的SCL和SDA都是开漏结构,配合上拉电阻工作。如果你用推挽输出去接I²C,那通信大概率会出问题——我踩过这个坑。
// 推挽输出:直接驱动LED
let mut led = gpioa.pa5.into_push_pull_output();
led.set_high().unwrap();
// 开漏输出:用于I²C或共享总线
let mut sda = gpiob.pb7.into_open_drain_output();
sda.set_low().unwrap(); // 只能拉低,不能推高
经验之谈:如果你不确定用哪种,先问自己“这个引脚会不会跟其他设备的输出连在一起?”会的话,优先考虑开漏+上拉;不会的话,推挽更简单。
3.3 上拉/下拉电阻配置
上拉和下拉,本质上是给引脚一个确定的默认电平。你想想看,如果引脚悬空,电平是不确定的,外界一点干扰就能让读数乱跳。上拉电阻把电平拉到VCC,下拉电阻拉到GND。
在Rust HAL里,配置上拉/下拉非常直观:
// 上拉输入:按键按下为低电平
let button = gpioa.pa0.into_pull_up_input();
// 下拉输入:按键按下为高电平
let button = gpioa.pa0.into_pull_down_input();
我曾经在一个项目里,按键检测总是误触发。排查了半天,发现是忘了配置内部上拉,引脚浮空,噪声信号被当成按键按下。从那以后,我只要用输入引脚,第一件事就是确认上下拉状态。
| 配置 | 默认电平 | 适用场景 |
|---|---|---|
| 上拉输入 | 高电平 | 按键接地、开漏总线 |
| 下拉输入 | 低电平 | 按键接VCC |
| 浮空输入 | 不确定 | 外部已有上拉/下拉 |
注意:内部上拉/下拉电阻的阻值通常在30kΩ~50kΩ之间,驱动能力有限。如果外部干扰很强,建议外加4.7kΩ或10kΩ的电阻。
3.4 中断触发配置
中断,是嵌入式系统的灵魂。GPIO中断可以检测电平变化,不用CPU一直轮询。Rust HAL里配置中断,一般需要这几步:
- 将引脚配置为输入模式(浮空、上拉或下拉)
- 使能中断线,选择触发边沿
- 编写中断服务函数
use stm32f4xx_hal::{
gpio::{Edge, ExtiPin},
interrupt,
};
// 配置引脚为中断输入
let mut button = gpioa.pa0.into_pull_up_input();
// 使能中断,上升沿触发
button.make_interrupt_source(&mut dp.SYSCFG);
button.trigger_on_edge(&mut dp.EXTI, Edge::RISING);
button.enable_interrupt(&mut dp.EXTI);
// 中断服务函数
#[interrupt]
fn EXTI0_IRQ() {
// 清除中断标志
// 处理按键事件
}
触发边沿有三种:
- 上升沿触发:电平从低变高时触发。适合检测信号开始。
- 下降沿触发:电平从高变低时触发。适合检测按键按下。
- 双边沿触发:电平变化时都触发。适合检测脉冲宽度。
我个人习惯,按键中断用下降沿触发,配合上拉输入。这样按键按下时触发一次,松开时不触发,省得做消抖。不过要注意,机械按键有抖动,硬件上最好加个RC滤波,或者软件里做延时消抖。
避坑指南:我曾经在一个产品里,用了双边沿触发检测编码器,结果中断频率太高,CPU忙不过来。后来改成硬件定时器捕获,才解决问题。中断不是万能的,高频信号还是交给硬件外设吧。
3.5 知识体系总览
这一章的内容,我画了一张图帮你理清关系:
3.6 小结
这一章我们走完了GPIO的完整配置流程。从初始化模式选择,到推挽与开漏的区别,再到上下拉电阻和中断触发,每一步都有实际代码和避坑经验。你想想看,这些配置其实都是围绕一个核心问题:引脚在什么情况下,输出什么电平,或者检测到什么变化。