初次尝试
寄存器
让我们来看看“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 };
易失性访问
现在,上面的方法存在几个问题。
- 每次想要访问外围设备时,我们都必须使用 unsafe。
- 我们没有办法指定哪些寄存器是只读的或读写的。
- 程序中任何地方的任何代码都可以通过此结构访问硬件。
- 最重要的是,它实际上不起作用...
现在,问题在于编译器很聪明。如果你连续两次写入同一块 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()
}
现在,易失性访问通过 read
和 write
方法自动执行。执行写入仍然是 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
!如果作者足够勤奋以发现所有这些“重复”的驱动程序实例,则以这种方式编写的代码将起作用,但是一旦代码分布在多个模块、驱动程序、开发人员和几天之后,就更容易犯这类错误。