单例

在软件工程中,单例模式是一个软件设计模式,其限制了一个类到一个对象的实例化。

Wikipedia: Singleton Pattern

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

可以像这样,我们可以使每个东西都变成公共静态的(public static):

  1. static mut THE_SERIAL_PORT: SerialPort = SerialPort;
  2. fn main() {
  3. let _ = unsafe {
  4. THE_SERIAL_PORT.read_speed();
  5. };
  6. }

但是这个带来了一些问题。它是一个可变的全局变量,在Rust,与这些变量交互总是unsafe的。这些变量在你所有的程序之间也是可见的,意味着借用检查器不能帮你跟踪这些变量的引用和所有权。

我们在Rust中要怎么做?

与其只是让我们的外设变成一个全局变量,我们不如决定创造一个全局变量,在这个例子里其被叫做 PERIPHERALS,这个全局变量对于我们的每个外设,它都有一个与之对应的 Option<T>

  1. struct Peripherals {
  2. serial: Option<SerialPort>,
  3. }
  4. impl Peripherals {
  5. fn take_serial(&mut self) -> SerialPort {
  6. let p = replace(&mut self.serial, None);
  7. p.unwrap()
  8. }
  9. }
  10. static mut PERIPHERALS: Peripherals = Peripherals {
  11. serial: Some(SerialPort),
  12. };

这个结构体允许我们获得外设的一个实例。如果我们尝试调用take_serial()获得多个实例,我们的代码将会抛出运行时恐慌(panic)!

  1. fn main() {
  2. let serial_1 = unsafe { PERIPHERALS.take_serial() };
  3. // 这里造成运行时恐慌!
  4. // let serial_2 = unsafe { PERIPHERALS.take_serial() };
  5. }

虽然与这个结构体交互是unsafe,然而一旦我们获得了它包含的 SerialPort,我们将不再需要使用unsafe,或者PERIPHERALS结构体。

这个带来了少量的运行时开销,因为我们必须打包 SerialPort 结构体进一个option中,且我们将需要调用一次 take_serial(),但是这种少量的前期成本,能使我们在接下来的程序中使用借用检查器(borrow checker) 。

已存在的库支持

虽然我们在上面创造了我们自己的 Peripherals 结构体,但这并不是必须的。cortex_m crate 包含一个被叫做 singleton!() 的宏,其将为你完成这个任务。

  1. #[macro_use(singleton)]
  2. extern crate cortex_m;
  3. fn main() {
  4. // OK 如果 `main` 只被执行一次
  5. let x: &'static mut bool =
  6. singleton!(: bool = false).unwrap();
  7. }

cortex_m docs

另外,如果你使用 cortex-m-rtic,它将获取和定义这些外设的整个过程抽象了出来,你将获得一个Peripherals结构体,其包含了一个所有你定义了的项的非 Option<T> 的版本。

  1. // cortex-m-rtic v0.5.x
  2. #[rtic::app(device = lm3s6965, peripherals = true)]
  3. const APP: () = {
  4. #[init]
  5. fn init(cx: init::Context) {
  6. static mut X: u32 = 0;
  7. // Cortex-M外设
  8. let core: cortex_m::Peripherals = cx.core;
  9. // 设备特定的外设
  10. let device: lm3s6965::Peripherals = cx.device;
  11. }
  12. }

但是为什么?

但是这些单例模式是如何使我们的Rust代码在工作方式上产生很大不同的?

  1. impl SerialPort {
  2. const SER_PORT_SPEED_REG: *mut u32 = 0x4000_1000 as _;
  3. fn read_speed(
  4. &self // <------ 这个真的真的很重要
  5. ) -> u32 {
  6. unsafe {
  7. ptr::read_volatile(Self::SER_PORT_SPEED_REG)
  8. }
  9. }
  10. }

这里有两个重要因素:

  • 因为我们正在使用一个单例模式,所以我们只有一种方法或者地方去获得一个 SerialPort 结构体。
  • 为了调用 read_speed() 方法,我们必须拥有一个 SerialPort 结构体的所有权或者一个引用。

这两个因素放在一起意味着,只有当我们满足了借用检查器的条件时,我们才有可能访问硬件,也意味着我们在任何时候不可能有多个对同一个硬件的可变引用(&mut)!

  1. fn main() {
  2. // 缺少对`self`的引用!将不会工作。
  3. // SerialPort::read_speed();
  4. let serial_1 = unsafe { PERIPHERALS.take_serial() };
  5. // 你只能读取你有权访问的内容
  6. let _ = serial_1.read_speed();
  7. }

像数据一样对待你的硬件

另外,因为一些引用是可变的,一些是不可变的,观察一个函数或者方法是否会潜在修改硬件的状态变得可能了。比如,

这个函数允许改变硬件的配置:

  1. fn setup_spi_port(
  2. spi: &mut SpiPort,
  3. cs_pin: &mut GpioPin
  4. ) -> Result<()> {
  5. // ...
  6. }

这个不行:

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

这允许我们在编译时而不是运行时强制代码是否应该或者不应该对硬件进行改变,这通常在只有一个应用的情况下起作用,但是对于裸机系统来说,我们的软件将被编译进一个单一应用中,因此它通常是不受限的。