Chapter 07 · Rust 嵌入式开发

所有权在裸机中的应用

Send/Sync 的真正含义、如何安全封装静态可变全局、Mutex<RefCell> 共享外设、外设单例模式、move 语义在 ISR 与 main 之间传递数据。

课程进度58%

关键概念

所有权 (Ownership)
Rust 的核心规则:每个值有唯一所有者,所有者离开作用域时值被释放;值可以移动(move,转移所有权)或借用(borrow,临时引用)。在嵌入式里,外设、引脚、缓冲区都是被"拥有"的资源,这套规则让"谁能用这个硬件资源"变得编译期可追踪,从根源避免资源冲突。
Send
标记 trait,表示某类型的值可以安全地"移动"到另一个执行上下文(线程/中断)。大多数类型自动是 Send。裸指针、某些外设句柄不是 Send。在嵌入式里,把一个值从 main 移交给 ISR 使用,就要求它是 Send。
Sync
标记 trait,表示某类型可以安全地被多个上下文同时共享引用(&T 可跨上下文)。static 全局变量要求其类型是 Sync(因为任何代码都能引用它)。裸 u32 不是"可安全并发共享可变"的,所以不能直接做可变全局;而 Mutex<T> 是 Sync,可以。
内部可变性 (Interior Mutability)
通过 &T(不可变引用)也能修改内部数据的机制,由 Cell/RefCell 提供。因为共享的东西通常只能拿到 &T,要修改它就需要内部可变性。Cell 适合 Copy 类型(整体存取),RefCell 适合复杂类型(运行期借用检查)。
critical_section::Mutex
嵌入式专用的 Mutex,与标准库的会阻塞线程的 Mutex 不同:它靠"临界区令牌 CriticalSection"来解锁——只有进入临界区(中断被屏蔽)才能拿到令牌 cs,用 mutex.borrow(cs) 访问内部数据。这保证访问期间不被中断打断,是裸机上 main/ISR 共享数据的标准封装。
Mutex<RefCell<Option<T>>> 惯用法
共享"一个在 init 中才创建的外设"的经典组合。Option 让全局能先声明为 None(因为外设初始化前不存在),init 后 replaceSome(外设)RefCell 提供内部可变;Mutex 保证并发访问在临界区内。ISR 里通过 borrow(cs).borrow_mut() 取出外设操作它。
外设单例 (Peripheral Singleton)
Peripherals::take() 全局只成功一次,是"单例模式"的类型级实现。它保证整个程序里某个外设的所有权唯一,杜绝两处代码各自 take 同一外设导致的配置冲突。这是"所有权即资源管理"思想在硬件上的直接体现——外设不是全局可随便访问的地址,而是有主人的值。
move 闭包与所有权转移
move 关键字让闭包按值捕获环境变量,把所有权移进闭包。在 RTIC 或线程模型里,把资源 move 进某个任务/闭包,就明确了"从此这个资源归它管",别处不能再用。这比 C 里"大家都能访问全局"的模型安全得多。

Send 与 Sync:谁能跨上下文

这两个 trait 是 Rust 并发安全的地基。在嵌入式里,"另一个上下文"通常指中断服务程序。理解它们,就理解了为什么某些代码能编译、某些不能:

Send / Sync 语义 ═══════════════════════════════════════════════════ Send:值可以"搬家"到另一个上下文 main ──move 一个值──▶ ISR (要求 T: Send) Sync:同一个值可以被多处"同时看" main ──&T──┐ ├──▶ 同一个 T (要求 T: Sync) ISR ──&T──┘ static X: T 全局变量 ⇒ 任何代码都能 &X ⇒ 要求 T: Sync ┌────────────────────────────────────────────────┐ │ 裸 u32 → Sync ✓ 但不可变,无法计数 │ │ static mut u32 → 访问需 unsafe,易竞态(禁用) │ │ AtomicU32 → Sync ✓,硬件原子,可并发改 │ │ Mutex<Cell<u32>> → Sync ✓,临界区保护,可并发改 │ └────────────────────────────────────────────────┘
简单计数器优先用原子类型

如果共享的只是一个整数计数器/标志位,用 core::sync::atomic::AtomicU32Ordering::Relaxed 最简单——它靠 Cortex-M 的 LDREX/STREX 硬件原子指令,不需要临界区、开销极小。只有共享的是复杂结构(如整个外设句柄、缓冲区)时,才需要 Mutex<RefCell<_>> 这套重武器。

共享一个外设:Mutex<RefCell<Option>> 惯用法

典型场景:定时器 TIM2 中断里要操作串口 USART1 上报数据,而 USART1 是在 main 的 init 阶段才创建的。这就需要把它放进全局让 ISR 能访问:

#![no_std]
#![no_main]
use panic_halt as _;
use cortex_m_rt::entry;
use core::cell::RefCell;
use critical_section::Mutex;
use stm32f4xx_hal::{pac, prelude::*, serial::Tx, interrupt};

type SharedTx = Mutex<RefCell<Option<Tx<pac::USART1>>>>;

// 初始为 None:外设还没被创建。static 要求 Sync —— 这个类型满足。
static TX: SharedTx = Mutex::new(RefCell::new(None));

#[entry]
fn main() -> ! {
    let dp = pac::Peripherals::take().unwrap();
    let rcc = dp.RCC.constrain();
    let clocks = rcc.cfgr.freeze();

    let gpioa = dp.GPIOA.split();
    let tx_pin = gpioa.pa9.into_alternate();
    let tx = dp.USART1
        .tx(tx_pin, 115200.bps(), &clocks)
        .unwrap();

    // 把创建好的 tx move 进全局:进入临界区,替换 None 为 Some(tx)
    critical_section::with(|cs| {
        TX.borrow(cs).replace(Some(tx));
    });

    // ...配置 TIM2 定时中断并 NVIC 使能...(省略)

    loop {
        cortex_m::asm::wfi();  // 等中断,省电
    }
}

#[interrupt]
fn TIM2() {
    use core::fmt::Write;
    critical_section::with(|cs| {
        // 借出 RefCell,取到 &mut Option<Tx>,若已初始化则写串口
        if let Some(tx) = TX.borrow(cs).borrow_mut().as_mut() {
            let _ = writeln!(tx, "tick");
        }
    });
    // ...清除 TIM2 更新中断标志...
}
RefCell 的借用检查在运行期

RefCellborrow_mut() 会在运行期检查是否已有其他借用,若冲突则 panic。在临界区内单点访问一般没问题,但要避免在持有借用时又触发嵌套访问同一 RefCell 的中断。RTIC 用编译期分析彻底避免了这类运行期 panic——这也是能用 RTIC 就用 RTIC 的理由之一。裸写法则需自己保证不重入。

三种共享数据方案对比

方案适用数据开销失败模式
AtomicU32整数、标志位最低(硬件原子)无(编译期安全)
Mutex<Cell<T>>小的 Copy 类型临界区(短暂关中断)
Mutex<RefCell<T>>外设、复杂结构临界区 + 运行期借用检查重入借用会 panic
RTIC #[shared]任意最小临界区(SRP)无(编译期分析)

move 语义:把资源交给谁

所有权的威力在于让"资源归属"显式化。下面演示把一个 LED 引脚 move 进闭包后,原变量不可再用:

let led = gpioc.pc13.into_push_pull_output();

// move 闭包按值捕获 led,所有权转移进闭包
let mut task = move || {
    let mut led = led;   // led 归这个任务所有
    led.set_high();
};

task();

// led.set_low();  ❌ 编译错误:value moved into closure
//    error[E0382]: borrow of moved value: `led`
// 编译器确保 led 只有一个主人,杜绝两处同时驱动同一引脚
本章小结

Rust 把"谁拥有硬件资源"变成编译期可追踪的所有权。Send 表示值可移动到另一上下文(如 main→ISR),Sync 表示可被多处同时共享引用;static 全局要求类型是 Sync,因此裸 static mut 被禁止(访问需 unsafe 且易竞态)。共享简单整数/标志用 AtomicU32(硬件原子、零临界区);共享外设等复杂结构用 critical_section::Mutex<RefCell<Option<T>>> 惯用法——Option 应对"init 后才创建"、RefCell 提供内部可变、Mutex 靠临界区令牌 cs 保证访问不被中断,ISR 通过 borrow(cs).borrow_mut().as_mut() 操作外设。注意 RefCell 的借用检查在运行期、重入会 panic,而 RTIC 的 #[shared] 用编译期分析彻底避免此类问题。Peripherals::take() 是类型级单例,保证外设所有权唯一。move 闭包让资源归属显式化,move 后原变量不可用,从根源杜绝资源争用。