单例

在软件工程中,单例模式是一种软件设计模式,它限制将一个类实例化为一个对象。

维基百科:单例模式

但是为什么我们不能只使用全局变量呢?

我们可以将所有内容都设为公共静态,例如这样

static mut THE_SERIAL_PORT: SerialPort = SerialPort;

fn main() {
    let _ = unsafe {
        THE_SERIAL_PORT.read_speed();
    };
}

但这存在一些问题。它是一个可变的全局变量,而在 Rust 中,这些变量始终与不安全的交互相关联。这些变量在整个程序中也可见,这意味着借用检查器无法帮助您跟踪这些变量的引用和所有权。

我们如何在 Rust 中做到这一点?

与其将外设简单地设为全局变量,不如考虑创建一个结构(在本例中称为 PERIPHERALS),其中包含每个外设的 Option<T>

struct Peripherals {
    serial: Option<SerialPort>,
}
impl Peripherals {
    fn take_serial(&mut self) -> SerialPort {
        let p = replace(&mut self.serial, None);
        p.unwrap()
    }
}
static mut PERIPHERALS: Peripherals = Peripherals {
    serial: Some(SerialPort),
};

此结构允许我们获取外设的单个实例。如果我们尝试多次调用 take_serial(),我们的代码将出现恐慌!

fn main() {
    let serial_1 = unsafe { PERIPHERALS.take_serial() };
    // This panics!
    // let serial_2 = unsafe { PERIPHERALS.take_serial() };
}

虽然与该结构的交互是 unsafe 的,但一旦我们拥有它包含的 SerialPort,我们就不再需要使用 unsafePERIPHERALS 结构。

这会产生少量的运行时开销,因为我们必须将 SerialPort 结构包装在选项中,并且需要调用 take_serial() 一次,但是这种少量的前期成本使我们能够在程序的其余部分中利用借用检查器。

现有库支持

虽然我们在上面创建了自己的 Peripherals 结构,但对于您的代码来说,这样做并非必要。cortex_m crate 包含一个名为 singleton!() 的宏,它将为您执行此操作。

use cortex_m::singleton;

fn main() {
    // OK if `main` is executed only once
    let x: &'static mut bool =
        singleton!(: bool = false).unwrap();
}

cortex_m 文档

此外,如果您使用 cortex-m-rtic,定义和获取这些外设的整个过程将为您抽象化,您将获得一个 Peripherals 结构,其中包含所有定义项的非 Option<T> 版本。

// cortex-m-rtic v0.5.x
#[rtic::app(device = lm3s6965, peripherals = true)]
const APP: () = {
    #[init]
    fn init(cx: init::Context) {
        static mut X: u32 = 0;
         
        // Cortex-M peripherals
        let core: cortex_m::Peripherals = cx.core;
        
        // Device specific peripherals
        let device: lm3s6965::Peripherals = cx.device;
    }
}

但是为什么呢?

但是这些单例如何对我们的 Rust 代码的工作方式产生明显的影响呢?

impl SerialPort {
    const SER_PORT_SPEED_REG: *mut u32 = 0x4000_1000 as _;

    fn read_speed(
        &self // <------ This is really, really important
    ) -> u32 {
        unsafe {
            ptr::read_volatile(Self::SER_PORT_SPEED_REG)
        }
    }
}

这里有两个重要的因素在起作用

  • 因为我们使用的是单例,所以只有一种或一个地方可以获取 SerialPort 结构
  • 要调用 read_speed() 方法,我们必须拥有 SerialPort 结构的所有权或引用

这两个因素加在一起意味着,只有在满足借用检查器的条件下才能访问硬件,这意味着在任何时候我们都不会对同一硬件拥有多个可变引用!

fn main() {
    // missing reference to `self`! Won't work.
    // SerialPort::read_speed();

    let serial_1 = unsafe { PERIPHERALS.take_serial() };

    // you can only read what you have access to
    let _ = serial_1.read_speed();
}

将您的硬件视为数据

此外,由于某些引用是可变的,而另一些引用是不可变的,因此可以查看函数或方法是否可能修改硬件的状态。例如,

这允许更改硬件设置

fn setup_spi_port(
    spi: &mut SpiPort,
    cs_pin: &mut GpioPin
) -> Result<()> {
    // ...
}

这不是

fn read_button(gpio: &GpioPin) -> bool {
    // ...
}

这使我们能够在编译时而不是在运行时强制执行代码是否应该或不应该对硬件进行更改。需要注意的是,这通常只适用于一个应用程序,但对于裸机系统,我们的软件将被编译成一个应用程序,因此这通常不是限制。