初试Rust

寄存器

让我们看一下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 };

易失性(volatile)访问

现在,上述方法存在以下问题:

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

现在的问题是编译器很聪明。如果您对同一块RAM紧挨着进行两次写入,则编译器会注意到这一点,并且会跳过第一次写入。在C语言中,我们可以将变量标记为volatile,以确保每次读取或写入均按预期进行。在Rust中,我们则是将访问本身标记为volatile,而不是变量。

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方法会自动执行易失性(volatile)访问。但是执行写入仍然是unsafe,公平地说,硬件是一堆易变的状态,编译器(todo 难道不应该是volatile_register这个crate么?)无法知道这些写入是否实际上是安全的,因此这是一个很好的默认设置。

Rust封装

我们需要将此struct封装到一个更高层API中,以使我们的用户可以安全地调用它。作为驱动程序开发者,我们手动验证不安全的代码是否正确,然后为用户提供一个安全的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实例,明显它们指向完全相同的外设! 当然如果程序员很努力的避免创建多个实例,则以这种方式编写的代码也可以工作,但是一旦代码分散到不同模块,不同驱动程序,由多个程序员维护,则难免会出现各种错误(API的编写者要能确保自己暴露的API在安全代码中不会被错用)。