Rust尝鲜

寄存器

让我们看下 'SysTick' 外设 - 一个简单的计时器,它存在于每个Cortex-M处理器内核中。通常你能在芯片厂商的数据手册或者技术参考手册中看到它们,但是下面的例子对所有ARM Cortex-M核心都是通用的,让我们看下ARM参考手册。我们能看到这里有四个寄存器:

| Offset | Name | Description | Width | |--------|-------------|-----------------------------|--------| | 0x00 | SYST_CSR | 控制和状态寄存器 | 32 bits| | 0x04 | SYST_RVR | 重装载值寄存器 | 32 bits| | 0x08 | SYST_CVR | 当前值寄存器 | 32 bits| | 0x0C | SYST_CALIB | 校准值寄存器 | 32 bits|

C语言风格的方法(The C Approach)

在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访问(Volatile Accesses)

现在,上面的方法有一堆问题。

  1. 每次想要访问外设,不得不使用unsafe 。
  2. 无法指定哪个寄存器是只读的或者读写的。
  3. 程序中任何地方的任何一段代码都可以通过这个结构体访问硬件。
  4. 最重要的是,实际上它并不能工作。

现在的问题是编译器很聪明。如果你往RAM同个地方写两次,一个接着一个,编译器会注意到这个行为,且完全跳过第一个写入操作。在C语言中,我们能标记变量为volatile去确保每个读或写操作按所想的那样发生。在Rust中,我们将访问操作标记为易变的(volatile),而不是将变量标记为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 accesses可以被自动执行。执行写操作仍然是 unsafe 的,但是公平地讲,硬件有一堆可变的状态,对于编译器来说没有办法知道是否这些写操作是真正安全的,因此默认就这样是个不错的选择。

Rust风格的封装

我们需要把这个struct封装进一个更高抽象的API中,这个API对于用户来说,可以安全地调用。作为驱动的作者,我们亲手验证不安全的代码是否正确,然后为我们的用户提供一个safe的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,其指向同个外设!如果作者足够努力的话,他能发现所有这些'重复的'驱动实例,那么按这种方式写的代码就可以工作,但是一旦代码被散播一段时间,散播给多个模块,驱动,开发者,它会越来越容易触发此类错误。