初次尝试

寄存器

让我们来看看“SysTick”外设——每个 Cortex-M 处理器核心都配备的简单定时器。通常,您会在芯片制造商的数据手册或技术参考手册中查找这些信息,但此示例是所有 ARM Cortex-M 核心通用的,让我们在 ARM 参考手册中查找。我们看到有四个寄存器

偏移量名称描述宽度
0x00SYST_CSR控制和状态寄存器32 位
0x04SYST_RVR重载值寄存器32 位
0x08SYST_CVR当前值寄存器32 位
0x0CSYST_CALIB校准值寄存器32 位

C 语言方法

在 Rust 中,我们可以像在 C 中一样表示寄存器的集合——使用 struct

#[repr(C)]
struct SysTick {
    pub csr: u32,
    pub rvr: u32,
    pub cvr: u32,
    pub calib: u32,
}

限定符 #[repr(C)] 告诉 Rust 编译器像 C 编译器一样布局此结构。这非常重要,因为 Rust 允许结构字段重新排序,而 C 不允许。你可以想象如果这些字段被编译器默默地重新排列,我们必须进行调试!有了这个限定符,我们有了四个 32 位字段,它们对应于上面的表格。当然,这个 struct 本身没有用处——我们需要一个变量。

let systick = 0xE000_E010 as *mut SysTick;
let time = unsafe { (*systick).cvr };

易失性访问

现在,上面的方法存在几个问题。

  1. 每次想要访问外围设备时,我们都必须使用 unsafe。
  2. 我们没有办法指定哪些寄存器是只读的或读写的。
  3. 程序中任何地方的任何代码都可以通过此结构访问硬件。
  4. 最重要的是,它实际上不起作用...

现在,问题在于编译器很聪明。如果你连续两次写入同一块 RAM,编译器可能会注意到这一点,并完全跳过第一次写入。在 C 中,我们可以将变量标记为 volatile 以确保每次读取或写入都按预期发生。在 Rust 中,我们改为将访问标记为易失性,而不是变量。

let systick = unsafe { &mut *(0xE000_E010 as *mut SysTick) };
let time = unsafe { core::ptr::read_volatile(&mut systick.cvr) };

所以,我们解决了四个问题中的一个,但现在我们有了更多的 unsafe 代码!幸运的是,有一个第三方 crate 可以提供帮助 - volatile_register

use volatile_register::{RW, RO};

#[repr(C)]
struct SysTick {
    pub csr: RW<u32>,
    pub rvr: RW<u32>,
    pub cvr: RW<u32>,
    pub calib: RO<u32>,
}

fn get_systick() -> &'static mut SysTick {
    unsafe { &mut *(0xE000_E010 as *mut SysTick) }
}

fn get_time() -> u32 {
    let systick = get_systick();
    systick.cvr.read()
}

现在,易失性访问通过 readwrite 方法自动执行。执行写入仍然是 unsafe 的,但公平地说,硬件是一堆可变状态,并且编译器无法知道这些写入是否真的安全,所以这是一个很好的默认位置。

Rust 封装器

我们需要将这个 struct 封装到一个更高层的 API 中,以便我们的用户可以安全地调用。作为驱动程序作者,我们手动验证 unsafe 代码是否正确,然后为我们的用户提供一个安全的 API,这样他们就不必担心它(前提是他们信任我们能做对!)。

一个例子可能是

use volatile_register::{RW, RO};

pub struct SystemTimer {
    p: &'static mut RegisterBlock
}

#[repr(C)]
struct RegisterBlock {
    pub csr: RW<u32>,
    pub rvr: RW<u32>,
    pub cvr: RW<u32>,
    pub calib: RO<u32>,
}

impl SystemTimer {
    pub fn new() -> SystemTimer {
        SystemTimer {
            p: unsafe { &mut *(0xE000_E010 as *mut RegisterBlock) }
        }
    }

    pub fn get_time(&self) -> u32 {
        self.p.cvr.read()
    }

    pub fn set_reload(&mut self, reload_value: u32) {
        unsafe { self.p.rvr.write(reload_value) }
    }
}

pub fn example_usage() -> String {
    let mut st = SystemTimer::new();
    st.set_reload(0x00FF_FFFF);
    format!("Time is now 0x{:08x}", st.get_time())
}

现在,这种方法的问题在于以下代码对编译器来说是完全可以接受的

fn thread1() {
    let mut st = SystemTimer::new();
    st.set_reload(2000);
}

fn thread2() {
    let mut st = SystemTimer::new();
    st.set_reload(1000);
}

我们 set_reload 函数的 &mut self 参数检查是否没有其他对特定 SystemTimer 结构的引用,但它们不会阻止用户创建指向同一外围设备的第二个 SystemTimer!如果作者足够勤奋以发现所有这些“重复”的驱动程序实例,则以这种方式编写的代码将起作用,但是一旦代码分布在多个模块、驱动程序、开发人员和几天之后,就更容易犯这类错误。