初试Rust
寄存器
让我们看一下SysTick外设(所有Cortex-M处理器都有的简单计时器)。通常您会在芯片制造商的《技术参考手册》中查找这些信息,但是此示例对于所有ARM Cortex-M内核都是通用的,因此也可以在ARM参考手册中查到,我们看到有四个寄存器:
| 偏移 | 名称 | 描述 | 位宽 |
|---|---|---|---|
| 0x00 | SYST_CSR | 控制和状态寄存器 | 32位 |
| 0x04 | SYST_RVR | 重新加载值寄存器 | 32位 |
| 0x08 | SYST_CVR | 当前值寄存器 | 32位 |
| 0x0C | SYST_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)访问
现在,上述方法存在以下问题:
- 每次访问外设时,我们都必须使用unsafe关键字。
- 我们无法指定哪些寄存器是只读或读写寄存器。
- 程序中任何地方的任何代码段都可以通过这种结构访问硬件。
- 最重要的是,它实际上不起作用...
现在的问题是编译器很聪明。如果您对同一块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()
}
现在,通过read和write方法会自动执行易失性(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在安全代码中不会被错用)。