4. 外部中断按键

任务目标

在STM32F103上配置一个按键作为外部中断输入,当按键按下时触发EXTI中断,在中断服务函数中翻转LED的亮灭状态。通过本例学习Rust嵌入式开发中的中断配置、中断服务函数编写以及共享可变状态的处理方式。

环境与依赖

在 Cargo.toml 中添加以下依赖:

[dependencies]
cortex-m = "0.7"
cortex-m-rt = "0.7"
embedded-hal = "0.2"
stm32f1xx-hal = "0.9"
panic-halt = "0.2"
nb = "1"

硬件连接

  • LED:PA5(通过限流电阻接GND,高电平点亮)
  • 按键:PA0(一端接GND,另一端接PA0,内部上拉)

关键完整代码

#![no_std]
#![no_main]

use panic_halt as _;
use cortex_m::interrupt::{self, Mutex};
use cortex_m_rt::entry;
use core::cell::RefCell;
use stm32f1xx_hal::{
    prelude::*,
    pac,
    gpio::{gpiob::PB5, Output, PushPull},
};

// 共享LED状态:使用Mutex保护,中断与主循环均可访问
static LED: Mutex<RefCell<Option<PB5<Output<PushPull>>>>> = Mutex::new(RefCell::new(None));

#[entry]
fn main() -> ! {
    let dp = pac::Peripherals::take().unwrap();
    let mut rcc = dp.RCC.constrain();
    let mut flash = dp.FLASH.constrain();
    let mut afio = dp.AFIO.constrain(&mut rcc.apb2);

    // 时钟配置:8MHz外部晶振,系统时钟72MHz
    let clocks = rcc.cfgr.use_hse(8.mhz()).sysclk(72.mhz()).freeze(&mut flash.acr);

    // 初始化GPIO
    let mut gpioa = dp.GPIOA.split(&mut rcc.apb2);
    let mut gpiob = dp.GPIOB.split(&mut rcc.apb2);

    // 配置LED引脚(PB5)为推挽输出
    let led = gpiob.pb5.into_push_pull_output(&mut gpiob.crl);
    // 将LED存入全局共享变量
    interrupt::free(|cs| {
        LED.borrow(cs).replace(Some(led));
    });

    // 配置按键引脚(PA0)为浮空输入(外部已接上拉电阻)
    let button = gpioa.pa0.into_floating_input(&mut gpioa.crl);

    // 启用EXTI时钟
    let mut exti = dp.EXTI;
    let mut nvic = dp.NVIC;

    // 配置EXTI0(PA0对应EXTI线0)
    afio.exti_cfg.exti0_select(&mut afio.mapr, 0); // 选择PA0作为EXTI0源
    exti.rising_edge(0);  // 按键按下时产生上升沿(按下接GND,释放后恢复高电平)
    exti.enable(0);

    // 使能EXTI0中断
    nvic.enable(pac::Interrupt::EXTI0);

    loop {
        // 主循环空闲,等待中断
        cortex_m::asm::wfi();
    }
}

// EXTI0中断服务函数
#[interrupt]
fn EXTI0() {
    interrupt::free(|cs| {
        if let Some(led) = LED.borrow(cs).borrow_mut().as_mut() {
            led.toggle();
        }
    });
    // 清除中断标志位
    let dp = unsafe { pac::Peripherals::steal() };
    dp.EXTI.pr.write(|w| w.pr0().set_bit());
}

运行与验证

  1. 编译:在项目根目录执行 cargo build --release,生成固件。
  2. 烧录:使用 cargo flash --release --chip stm32f103c8 或通过ST-Link等工具烧录到开发板。
  3. 验证步骤:
    • 上电后LED保持熄灭状态(初始为低电平);
    • 按下按键并释放,观察LED状态翻转(点亮或熄灭);
    • 每次按下并释放按键,LED状态应切换一次;
    • 连续快速按动按键,LED应跟随每次按键翻转,无卡死或异常。

通过条件:每次按键按下并释放后LED状态翻转一次,且系统运行稳定、无死机现象,即判定本例实现成功。

关键说明

  • 本例使用上升沿触发:按键一端接GND,PA0内部上拉,按下时引脚为低电平,释放时恢复高电平产生上升沿。若硬件连接相反,需改为 exti.falling_edge(0)。
  • 中断服务函数中通过 Mutex<RefCell<Option<...>>> 安全访问共享的LED引脚,避免数据竞争。
  • 中断标志位必须在ISR末尾手动清除,否则中断会持续触发。
  • 若使用PA0以外的引脚,需对应修改EXTI线编号和NVIC中断名(如PA1对应EXTI1)。
外部中断按键流程 主循环 初始化时钟、GPIO、EXTI LED=低电平(灭) 执行 WFI 等待中断 CPU进入低功耗休眠 循环 EXTI0中断服务函数 按键释放 → PA0上升沿 EXTI0中断触发 led.toggle() LED状态翻转(亮↔灭) 清除EXTI0挂起标志 PR0写1清除 返回主循环 按键事件 主循环(低功耗等待) 中断上下文(异步触发)