所有权在裸机中的应用
Send/Sync 的真正含义、如何安全封装静态可变全局、Mutex<RefCell> 共享外设、外设单例模式、move 语义在 ISR 与 main 之间传递数据。
Send/Sync 的真正含义、如何安全封装静态可变全局、Mutex<RefCell> 共享外设、外设单例模式、move 语义在 ISR 与 main 之间传递数据。
static 全局变量要求其类型是 Sync(因为任何代码都能引用它)。裸 u32 不是"可安全并发共享可变"的,所以不能直接做可变全局;而 Mutex<T> 是 Sync,可以。&T(不可变引用)也能修改内部数据的机制,由 Cell/RefCell 提供。因为共享的东西通常只能拿到 &T,要修改它就需要内部可变性。Cell 适合 Copy 类型(整体存取),RefCell 适合复杂类型(运行期借用检查)。CriticalSection"来解锁——只有进入临界区(中断被屏蔽)才能拿到令牌 cs,用 mutex.borrow(cs) 访问内部数据。这保证访问期间不被中断打断,是裸机上 main/ISR 共享数据的标准封装。Option 让全局能先声明为 None(因为外设初始化前不存在),init 后 replace 成 Some(外设);RefCell 提供内部可变;Mutex 保证并发访问在临界区内。ISR 里通过 borrow(cs).borrow_mut() 取出外设操作它。Peripherals::take() 全局只成功一次,是"单例模式"的类型级实现。它保证整个程序里某个外设的所有权唯一,杜绝两处代码各自 take 同一外设导致的配置冲突。这是"所有权即资源管理"思想在硬件上的直接体现——外设不是全局可随便访问的地址,而是有主人的值。move 关键字让闭包按值捕获环境变量,把所有权移进闭包。在 RTIC 或线程模型里,把资源 move 进某个任务/闭包,就明确了"从此这个资源归它管",别处不能再用。这比 C 里"大家都能访问全局"的模型安全得多。这两个 trait 是 Rust 并发安全的地基。在嵌入式里,"另一个上下文"通常指中断服务程序。理解它们,就理解了为什么某些代码能编译、某些不能:
如果共享的只是一个整数计数器/标志位,用 core::sync::atomic::AtomicU32 配 Ordering::Relaxed 最简单——它靠 Cortex-M 的 LDREX/STREX 硬件原子指令,不需要临界区、开销极小。只有共享的是复杂结构(如整个外设句柄、缓冲区)时,才需要 Mutex<RefCell<_>> 这套重武器。
典型场景:定时器 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 的 borrow_mut() 会在运行期检查是否已有其他借用,若冲突则 panic。在临界区内单点访问一般没问题,但要避免在持有借用时又触发嵌套访问同一 RefCell 的中断。RTIC 用编译期分析彻底避免了这类运行期 panic——这也是能用 RTIC 就用 RTIC 的理由之一。裸写法则需自己保证不重入。
| 方案 | 适用数据 | 开销 | 失败模式 |
|---|---|---|---|
AtomicU32 等 | 整数、标志位 | 最低(硬件原子) | 无(编译期安全) |
Mutex<Cell<T>> | 小的 Copy 类型 | 临界区(短暂关中断) | 无 |
Mutex<RefCell<T>> | 外设、复杂结构 | 临界区 + 运行期借用检查 | 重入借用会 panic |
| RTIC #[shared] | 任意 | 最小临界区(SRP) | 无(编译期分析) |
所有权的威力在于让"资源归属"显式化。下面演示把一个 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 后原变量不可用,从根源杜绝资源争用。