好的,我们继续 Rust 嵌入式开发的旅程!在前面的部分,我们已经成功地在真实硬件上点亮了 LED。现在,我们将深入了解嵌入式开发中至关重要的概念:中断(Interrupts),以及如何利用它们来响应外部事件,比如按下一个按钮。
10. 理解中断 (Interrupts)
在嵌入式系统中,微控制器不会一直忙于执行你的 loop
循环中的代码。它需要能够响应外部事件,例如用户按下按钮、传感器检测到变化、或者定时器达到预设值。这时,中断就派上用场了。
10.1 什么是中断?
中断是一种硬件机制,当特定事件发生时,它会暂时中止当前正在执行的程序,转而去执行一段特殊设计的代码,这段代码称为中断服务程序(Interrupt Service Routine, ISR)或中断处理程序(Interrupt Handler)。ISR 执行完毕后,程序会返回到它被中断的地方继续执行。
这就像你正在看书,突然电话响了。你放下书(保存当前状态),接电话(执行 ISR),电话打完后,你拿起书(恢复之前状态),继续阅读。
10.2 为什么需要中断?
-
实时响应: 能够立即响应重要的外部事件,而不是等待主程序循环到检查该事件的代码。
-
效率: 微控制器不需要不断地“轮询”或检查每个外设的状态。它只在事件发生时才被“唤醒”并处理。
-
并发性(假): 虽然不是真正的多任务操作系统,但中断让微控制器看起来像在同时处理多个任务,提高了系统的响应性和效率。
10.3 Cortex-M 中的中断
ARM Cortex-M 处理器有一个内置的嵌套向量中断控制器 (Nested Vectored Interrupt Controller, NVIC),它负责管理所有的中断请求。每个中断源(如 GPIO 引脚、定时器、UART 等)都有一个唯一的中断号。
当一个中断发生时:
-
CPU 完成当前指令。
-
保存当前上下文(寄存器状态)。
-
通过**中断向量表(Interrupt Vector Table)**找到对应中断号的 ISR 地址。
-
跳转到 ISR 执行代码。
-
ISR 执行完毕后,恢复之前保存的上下文。
-
返回到被中断的程序继续执行。
11. 编写一个按钮控制 LED 的程序
现在我们来编写一个程序:当用户按下开发板上的一个按钮时,LED 的状态会切换(如果亮着就熄灭,如果熄灭就点亮)。
11.1 确定按钮和 LED 引脚
-
STM32 Nucleo-64 系列 (如 F401RE/F411RE):
-
板载绿色 LED 通常连接到 PA5 引脚(我们上节课用过的)。
-
板载用户按钮 (USER Button) 通常连接到 PC13 引脚。这个按钮是低电平有效的,即按下时引脚电平变为低。
-
-
其他板子: 请务必查阅你的开发板原理图或用户手册,确认 LED 和按钮连接的 GPIO 引脚,以及按钮的电平有效性。
11.2 修改 Cargo.toml
(无需额外修改,沿用上一节的配置)
由于我们使用的是 stm32f4xx-hal
,并且只使用了 GPIO 和中断相关的基本功能,所以 Cargo.toml
的配置可以沿用上一节的。如果你使用的是其他系列的芯片,请确保 Cargo.toml
中的 HAL 库和 features
与你的芯片匹配。
11.3 编写 src/main.rs
现在,打开 src/main.rs
文件,并将其内容替换为以下代码。这个程序会相对复杂一些,因为它涉及到了中断的配置。
Rust
#![no_std]
#![no_main]
use panic_halt as _; // panic 时停止 CPU
// 导入核心运行时和中断相关宏
use cortex_m_rt::{entry, exception};
// 导入外设访问和HAL库
use stm32f4xx_hal::{
gpio::{Edge, ExtiPin, Input, Gpiob, PinState}, // 导入 GPIO 相关类型,如 ExtiPin 用于外部中断
pac::{self, interrupt}, // 导入 PAC 外设和 interrupt 宏
prelude::*, // 导入常用的 trait
};
// 静态可变变量来存储 LED 和按钮的状态
// WARNING: 在嵌入式 Rust 中,全局可变变量的使用需要特别小心,
// 必须用 `cortex_m::interrupt::Mutex` 或 `static mut` 配合 unsafe 块保护。
// 这里我们用 Mutex,并在中断处理函数中安全访问。
use cortex_m::interrupt::Mutex;
use core::cell::RefCell;
// 定义全局的 Mutex 来持有 LED 和按钮的 Pin 实例
// RefCell 允许在运行时可变借用,而 Mutex 提供中断安全访问
static G_LED: Mutex<RefCell<Option<stm32f4xx_hal::gpio::PA5<stm32f4xx_hal::gpio::Output<stm32f4xx_hal::gpio::PushPull>>>>> =
Mutex::new(RefCell::new(None));
static G_BUTTON: Mutex<RefCell<Option<stm32f4xx_hal::gpio::PC13<stm32f4xx_hal::gpio::Input>>>> =
Mutex::new(RefCell::new(None));
#[entry]
fn main() -> ! {
// 1. 获取对外设的访问权限
let dp = pac::Peripherals::take().unwrap();
let cp = cortex_m::Peripherals::take().unwrap(); // 内核外设,用于NVIC
// 2. 配置时钟
let rcc = dp.RCC.constrain();
let clocks = rcc.cfgr.use_hse(8.MHz()).sysclk(84.MHz()).freeze();
// 3. 配置 GPIO
let gpioa = dp.GPIOA.split();
let gpioc = dp.GPIOC.split();
// 配置 LED (PA5) 为推挽输出
let mut led = gpioa.pa5.into_push_pull_output();
led.set_low(); // 初始状态:熄灭
// 配置用户按钮 (PC13) 为输入模式,并启用内部上拉电阻 (可选,但通常推荐)
// 按钮通常是低电平有效,所以我们需要检测下降沿
let mut button = gpioc.pc13.into_pull_up_input();
// 4. 配置外部中断 (EXTI)
// 获取 EXTI 和 SYSCFG 外设的访问权限,它们用于配置外部中断
let mut syscfg = dp.SYSCFG.constrain();
let mut exti = dp.EXTI;
// 将 PC13 引脚配置为外部中断源,监听下降沿 (按钮按下)
// 注意:`listen` 方法会启用该引脚的 EXTI 中断请求
button.make_interrupt_source(&mut syscfg);
button.trigger_on_edge(&mut exti, Edge::FALLING); // 监听下降沿 (按下按钮)
button.enable_interrupt(&mut exti); // 启用该引脚的 EXTI 中断
// 5. 将 LED 和 Button 的 Pin 实例存入全局 Mutex
// 这样可以在中断服务程序中安全地访问它们
cortex_m::interrupt::free(|cs| { // cs 是 CriticalSection,提供原子访问
*G_LED.borrow(cs).borrow_mut() = Some(led);
*G_BUTTON.borrow(cs).borrow_mut() = Some(button);
});
// 6. 启用 NVIC 中的 EXTI15_10 中断
// PC13 属于 EXTI_LINE13,它由 EXTI15_10 中断向量处理
unsafe {
cp.NVIC.set_priority(interrupt::EXTI15_10, 1); // 设置中断优先级 (数字越小优先级越高)
cortex_m::peripheral::NVIC::unmask(interrupt::EXTI15_10); // 启用中断
}
// 7. 主循环 (空闲)
loop {
// 在中断驱动的程序中,主循环通常是空闲的,等待中断发生
cortex_m::asm::wfi(); // Wait For Interrupt: 进入低功耗模式,等待中断唤醒
}
}
// 8. 定义中断服务程序 (ISR)
// `#[interrupt]` 宏将这个函数注册为 EXTI15_10 的中断处理程序
#[interrupt]
fn EXTI15_10() {
cortex_m::interrupt::free(|cs| { // 进入临界区,防止中断重入或数据竞争
// 获取全局的 LED 和 Button 实例
let mut led = G_LED.borrow(cs).borrow_mut();
let mut button = G_BUTTON.borrow(cs).borrow_mut();
// 确保 LED 和 Button 实例已经被初始化 (Some())
if let (Some(led_pin), Some(button_pin)) = (led.as_mut(), button.as_mut()) {
// 检查是不是 PC13 触发的中断(因为 EXTI15_10 也会处理其他引脚的中断)
if button_pin.check_interrupt() {
// 清除 PC13 对应的中断标志位,非常重要!否则中断会不断触发
button_pin.clear_interrupt_pending_bit();
// 切换 LED 的状态
if led_pin.get_state() == PinState::High {
led_pin.set_low();
} else {
led_pin.set_high();
}
}
}
});
}
代码解释 (新增/修改部分):
-
全局状态管理 (
Mutex<RefCell<Option<...>>>
):-
在中断服务程序 (ISR) 中直接访问主函数中创建的变量是受限的。为了让 ISR 能够修改 LED 和按钮的状态,我们需要将它们的
Pin
实例存储在全局可变静态变量中。 -
static G_LED: Mutex<RefCell<Option<...>>>
: 这种复杂的类型组合是 Rust 嵌入式中安全访问全局可变状态的惯用模式:-
static
: 声明为静态变量,程序启动时创建,生命周期贯穿整个程序。 -
Option<T>
: 因为这些变量在main
函数启动前是None
,直到main
函数中进行初始化时才变为Some(T)
。 -
RefCell<T>
: 提供了内部可变性。通常,不可变引用不能修改数据,但RefCell
允许你在持有不可变引用的同时进行可变借用(运行时检查)。 -
cortex_m::interrupt::Mutex<T>
: 这是关键!它是一个中断安全的互斥锁。在裸机嵌入式中,它通过禁用中断来实现临界区,确保在访问被保护的数据时不会被中断打断,从而避免数据竞争。cortex_m::interrupt::free(|cs| { ... })
是进入临界区的方式,cs
(CriticalSection) 是一个令牌,表示你现在处于临界区。
-
-
-
配置按钮为外部中断源:
-
use stm32f4xx_hal::{gpio::{Edge, ExtiPin, Input, Gpiob, PinState}, ...};
: 导入了ExtiPin
trait 和Edge
枚举,用于配置外部中断。 -
let mut button = gpioc.pc13.into_pull_up_input();
: 配置 PC13 为上拉输入模式。上拉电阻确保按钮未按下时引脚处于高电平。 -
let mut syscfg = dp.SYSCFG.constrain();
:SYSCFG
外设用于将 GPIO 引脚映射到外部中断线。 -
let mut exti = dp.EXTI;
:EXTI
外设是外部中断控制器。 -
button.make_interrupt_source(&mut syscfg);
: 将 PC13 映射到 EXTI 外部中断线。 -
button.trigger_on_edge(&mut exti, Edge::FALLING);
: 配置 EXTI,使其在引脚电平从高到低变化时触发(即按钮按下)。 -
button.enable_interrupt(&mut exti);
: 启用该 EXTI 线的请求。
-
-
使能 NVIC 中的中断:
-
unsafe { cp.NVIC.set_priority(interrupt::EXTI15_10, 1); ... }
: 这是告诉处理器允许EXTI15_10
中断发生。-
EXTI15_10
: 这是一个枚举值,代表处理 EXTI 线 10-15 的中断向量。因为 PC13 是 EXTI 线 13,所以它由这个向量处理。 -
set_priority
: 设置中断优先级。数字越小优先级越高。 -
unmask
: 启用特定中断。unsafe
块是必要的,因为直接操作 NVIC 是低级操作,可能导致未定义行为,Rust 要求你明确承认这种潜在风险。
-
-
注意: 对于 STM32F4,EXTI 线 0-4 都有独立的向量,而 EXTI 5-9 和 10-15 是共享的。PC13 属于
EXTI15_10
向量。
-
-
loop { cortex_m::asm::wfi(); }
:-
wfi
(Wait For Interrupt):这是一个 ARM 指令,让 CPU 进入低功耗睡眠模式,直到下一个中断发生时才被唤醒。这比简单的loop {}
更省电,是中断驱动程序中的常见做法。
-
-
中断服务程序 (
#[interrupt] fn EXTI15_10()
):-
#[interrupt]
: 宏,将此函数标记为中断处理程序,并将其名称与中断向量表中的EXTI15_10
入口关联起来。 -
cortex_m::interrupt::free(|cs| { ... });
: 进入临界区,保证中断处理程序中的代码是原子执行的,不会被其他中断打断。 -
button_pin.check_interrupt()
: 检查当前中断是否是由button_pin
触发的(因为EXTI15_10
可能会处理多个引脚的中断)。 -
button_pin.clear_interrupt_pending_bit();
: 极其重要! 每次中断发生后,你必须清除该中断的挂起位(Pending Bit)。如果不清除,中断控制器会认为中断仍然是活跃的,并会立即再次触发中断,导致程序卡死在 ISR 中。 -
led_pin.get_state() == PinState::High
: 读取 LED 当前状态,然后切换。
-
11.4 构建、烧录和运行
保存 src/main.rs
文件,在项目根目录下,运行:
Bash
cargo build --release
如果编译成功,则通过 probe-run
烧录并运行:
Bash
cargo run --release
观察结果: 你的开发板上的绿色 LED 应该最初是熄灭的。当你按下用户按钮 (通常是蓝色按钮) 时,LED 就会切换状态。再按一次,再次切换。
恭喜你!
你已经成功地在 Rust 嵌入式程序中实现了中断处理,并用一个按钮来控制 LED 的状态!这比简单的 LED 闪烁更进了一步,因为它展示了如何让微控制器响应外部世界的事件。
下一步可以尝试:
-
调试中断: 使用 VS Code 的调试器,在
EXTI15_10
中断函数中设置断点,观察中断如何被触发,以及程序如何进入和退出 ISR。 -
添加防抖(Debouncing): 机械按钮在按下和松开时会产生短暂的电压抖动(弹跳),这会导致一次按键被识别为多次中断。在实际应用中,你需要实现软件防抖(例如,在检测到第一次按下后,短暂地忽略后续的相同中断,或者使用定时器来确认按键状态稳定)。这是嵌入式开发中的一个常见挑战。
-
探索其他中断源: 尝试配置定时器中断,让 LED 自动闪烁,而无需在
main
循环中使用delay
。
在后续的教程中,我们将探讨更高级的主题,例如定时器、串行通信 (UART/SPI/I2C) 或更复杂的外设驱动。