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