当前位置: 首页 > news >正文

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 等)都有一个唯一的中断号

当一个中断发生时:

  1. CPU 完成当前指令。

  2. 保存当前上下文(寄存器状态)。

  3. 通过**中断向量表(Interrupt Vector Table)**找到对应中断号的 ISR 地址。

  4. 跳转到 ISR 执行代码。

  5. ISR 执行完毕后,恢复之前保存的上下文。

  6. 返回到被中断的程序继续执行。


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();}}}});
}

代码解释 (新增/修改部分):

  1. 全局状态管理 (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) 是一个令牌,表示你现在处于临界区。

  2. 配置按钮为外部中断源:

    • 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 线的请求。

  3. 使能 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 向量。

  4. loop { cortex_m::asm::wfi(); }:

    • wfi (Wait For Interrupt):这是一个 ARM 指令,让 CPU 进入低功耗睡眠模式,直到下一个中断发生时才被唤醒。这比简单的 loop {} 更省电,是中断驱动程序中的常见做法。

  5. 中断服务程序 (#[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) 或更复杂的外设驱动。

http://www.dtcms.com/a/294127.html

相关文章:

  • webrtc整体架构
  • 重塑优化建模与算法设计:2025年大模型(LLM)在优化领域的应用盘点 - 2
  • Java中IO多路复用技术详解
  • Three.js 材质全解析:从 MeshBasicMaterial 到 MeshStandardMaterial
  • 通缩浪潮中的 “测量防线”:新启航如何用国产 3D 白光干涉仪筑牢半导体成本护城河?
  • 7月23日华为机考真题第二题-200分
  • 从机械操作到智能流程:火语言 RPA 在多场景中的效率提升实践
  • 如何提升AI收录?如何免费增加AI搜索推荐你的网站?有哪些免费好用的检测工具推荐?
  • Kafka使用场景与设计原理
  • 【金融机器学习】第五章:最优投资组合——Bryan Kelly, 修大成(中文翻译)
  • 量化金融简介(附电子书资料)
  • 大规模金融数据相关性并行计算系统设计与实现
  • MySQL金融级数据一致性保障:从原理到实战
  • Web开发基础与RESTful API设计实践指南
  • Linux内核设计与实现 - 第11章 定时器和时间管理
  • static 关键字的 特殊性
  • 【AI智能体】Dify 开发与集成MCP服务实战操作详解
  • Elasticsearch Circuit Breaker 全面解析与最佳实践
  • 【Word Press基础】创建一个动态的自定义区块
  • JS逆向基础( AES 解密密文WordArray和Uint8Array实战②)
  • 【无标题】word 中的中文排序
  • Pycharm2025 安装教程 免费分享 没任何套路
  • PDF转Word的简单方法
  • CSP-J 2021 入门级 第一轮(初赛) 阅读程序(3)
  • Android组件化实现方案深度分析
  • Day 8-zhou R包批量安装小补充!!!
  • java设计模式 -【策略模式】
  • AJAX案例合集
  • flutter使用CupertinoPicker绘制一个传入数据源的省市区选择器
  • 二级建造师学习笔记-2025