单例模式
在软件工程中,单例模式是一种软件设计模式,它将类的实例化限制为一个对象。
维基百科:单例模式
但为什么我们不能直接使用全局变量呢?
我们可以将所有内容都设为公共静态变量,像这样
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()
,我们的代码会 panic!
fn main() {
let serial_1 = unsafe { PERIPHERALS.take_serial() };
// This panics!
// let serial_2 = unsafe { PERIPHERALS.take_serial() };
}
尽管与此结构体交互是 unsafe
的,但是一旦我们有了它包含的 SerialPort
,我们就不再需要使用 unsafe
,或者 PERIPHERALS
结构体了。
这会带来一个小的运行时开销,因为我们必须将 SerialPort
结构体包装在一个 option 中,并且我们需要调用一次 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-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 {
// ...
}
这允许我们在编译时而不是运行时强制执行代码是否应该对硬件进行更改。 需要注意的是,这通常只在一个应用程序中有效,但是对于裸机系统,我们的软件将被编译成一个单独的应用程序,所以这通常不是一个限制。