rust嵌入式开发零基础入门教程(四)
好的,我们继续 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. 配置 GPIOlet 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) 或更复杂的外设驱动。