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里配置中断,一般需要这几步:

  1. 将引脚配置为输入模式(浮空、上拉或下拉)
  2. 使能中断线,选择触发边沿
  3. 编写中断服务函数
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 知识体系总览

这一章的内容,我画了一张图帮你理清关系:

GPIO驱动开发知识体系 GPIO配置 模式配置 输出类型 上下拉电阻 中断触发 推挽输出 开漏输出 浮空输入 上拉输入 下拉输入 上升沿 下降沿 双边沿 💡 核心原则 • 输出引脚:明确推挽还是开漏,考虑总线冲突 • 输入引脚:配置上下拉,避免浮空 • 中断引脚:选择合适边沿,注意消抖

3.6 小结

这一章我们走完了GPIO的完整配置流程。从初始化模式选择,到推挽与开漏的区别,再到上下拉电阻和中断触发,每一步都有实际代码和避坑经验。你想想看,这些配置其实都是围绕一个核心问题:引脚在什么情况下,输出什么电平,或者检测到什么变化。