You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

no_std AVR Rust环境下如何正确共享带锁资源引用?

AVR no_std环境下共享SPI总线的正确实现方案

错误根因

  • 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 06:24:16