首次尝试

寄存器

让我们看看“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 中,该 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!如果作者足够勤奋,能够发现所有这些“重复”的驱动程序实例,那么以这种方式编写的代码将可以工作,但一旦代码分散在多个模块、驱动程序、开发人员和天数中,犯这种错误就越来越容易了。