no_std AVR Rust环境下如何正确共享带锁资源引用?
错误根因
alloc不是crates.io上的第三方包,是Rust官方发行的核心库组件,直接在Cargo.toml里加alloc="*"当然找不到对应包。- 你用
cargo build -Z build-std=core,alloc后仍然找不到alloc::sync::Arc,是因为AVR目标平台目前不支持原子指令,而Arc、线程安全Mutex都依赖原子操作实现,alloc在编译到AVR目标时会直接裁剪掉整个sync模块,这条路从底层就走不通。 - AVR是单核、RAM资源普遍只有几KB的单片机,
Arc<Mutex<SPI>>这种依赖堆分配、多所有权的方案本身就不适合这类场景,会引入不必要的开销和内存碎片风险。
持久化build-std配置的方法
不需要每次敲编译参数,在项目根目录创建.cargo/config.toml文件,写入以下配置即可:
[build] # 替换为你实际使用的AVR芯片对应target target = "avr-atmega328p" [unstable] # 共享SPI根本不需要alloc,只编译core即可,省Flash和编译时间 build-std = ["core"]
如果确实需要使用alloc(比如依赖的第三方库要求),把build-std的值改成["core", "alloc"]即可,但注意AVR下alloc::sync模块不可用,不要尝试导入Arc等类型。
推荐实现方案
AVR这类单核嵌入式场景下,共享SPI总线只需要用临界区互斥+运行时借用检查即可,零堆分配、开销极低,完全符合Rust嵌入式最佳实践,有两种常用写法:
方案1:栈上互斥包装(推荐,无全局变量)
不需要静态全局,直接在main函数里创建SPI实例,用avr-device提供的关中断临界区Mutex包裹,再套一层RefCell做运行时借用检查(avr-device的临界区Mutex只提供共享访问权限,要获取外设可变引用必须搭配RefCell,这是单核嵌入式的标准写法,无额外性能开销),两个驱动直接持有互斥包装的共享引用即可:
use avr_hal_generic::avr_device::interrupt::Mutex; use core::cell::RefCell; fn main() { let dp = arduino_hal::Peripherals::take().unwrap(); // 按你的硬件参数初始化SPI外设 let spi = arduino_hal::Spi::new( dp.SPI, arduino_hal::port::mode::Output, arduino_hal::spi::Settings::default(), ); // 栈上创建互斥包装的SPI总线 let spi_bus = Mutex::new(RefCell::new(spi)); // 两个驱动都持有spi_bus的共享引用,生命周期绑定到main,无需克隆、无需堆分配 let device1 = SpiWorkaround::new(&spi_bus, cs1_pin, dc1_pin); let device2 = SpiWorkaround::new(&spi_bus, cs2_pin, dc2_pin); loop { // 正常执行业务逻辑,操作两个设备即可 } }
你只需要在SpiWorkaround的trait实现里,在临界区内获取SPI的可变引用完成传输即可,示例实现:
// 假设你的驱动要求实现的SPI操作trait如下 trait SpiInterface { fn write(&mut self, data: &[u8]); fn transfer(&mut self, buf: &mut [u8]); } impl<'a> SpiInterface for SpiWorkaround<'a> { fn write(&mut self, data: &[u8]) { // 进入关中断临界区,拿SPI的可变引用 avr_hal_generic::avr_device::interrupt::free(|cs| { let mut spi_ref = self.spi_bus.borrow(cs).borrow_mut(); let spi = spi_ref.as_mut().unwrap(); // 传输前拉低当前设备的CS引脚 self.cs.set_low(); spi.write(data); // 传输结束拉高CS self.cs.set_high(); }); } fn transfer(&mut self, buf: &mut [u8]) { avr_hal_generic::avr_device::interrupt::free(|cs| { let mut spi_ref = self.spi_bus.borrow(cs).borrow_mut(); let spi = spi_ref.as_mut().unwrap(); self.cs.set_low(); spi.transfer(buf); self.cs.set_high(); }); } }
方案2:静态全局总线
如果你的驱动不方便携带生命周期引用,可以把SPI总线存到静态全局的Mutex里,初始化时把SPI实例注入即可,逻辑和栈上方案完全一致:
use avr_hal_generic::avr_device::interrupt::{self, Mutex}; use core::cell::RefCell; static SPI_BUS: Mutex<RefCell<Option<Spi>>> = Mutex::new(RefCell::new(None)); fn main() { let dp = arduino_hal::Peripherals::take().unwrap(); let spi = arduino_hal::Spi::new(/* 初始化参数 */); // 初始化时把SPI实例存入全局 interrupt::free(|cs| { SPI_BUS.borrow(cs).replace(Some(spi)); }); // 驱动不需要带生命周期,直接在实现里访问全局SPI_BUS即可 let device1 = SpiWorkaround::new(cs1_pin, dc1_pin); let device2 = SpiWorkaround::new(cs2_pin, dc2_pin); loop { // 业务逻辑 } }
额外说明
如果你用的是标准embedded-hal 1.0版本的trait,不需要自己手搓共享逻辑,可以直接使用embedded-hal-bus库提供的spi::CriticalSectionDevice实现,它就是按照上述临界区互斥逻辑写的现成总线共享封装,no_std兼容、无堆分配,完美适配AVR平台。
内容的提问来源于stack exchange,提问作者Mutant Bob

