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 表示一组寄存器。

1. #[repr(C)]
2. struct SysTick {
3. pub csr: u32,
4. pub rvr: u32,
5. pub cvr: u32,
6. pub calib: u32,
7. }

限定符 #[repr(C)] 告诉Rust编译器像C编译器一样去布局这个结构体。那是非常重要的,因为Rust允许结构体字段被重新排序,而C语言不允许。你可以想象下如果这些字段被编译器悄悄地重新排了序,在调试时会给我们带来多大的麻烦!有了这个限定符,我们就有了与上表对应的四个32位的字段。但当然,这个 struct 本身没什么用处 - 我们需要一个变量。

1. let systick = 0xE000_E010 as *mut SysTick;
2. let time = unsafe { (*systick).cvr };

volatile访问(Volatile Accesses)

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

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

现在的问题是编译器很聪明。如果你往RAM同个地方写两次,一个接着一个,编译器会注意到这个行为,且完全跳过第一个写入操作。在C语言中,我们能标记变量为volatile去确保每个读或写操作按预期发生。在Rust中,我们将访问操作标记为易变的(volatile),而不是将变量标记为volatile。

1. let systick = unsafe { &mut *(0xE000_E010 as *mut SysTick) };
2. let time = unsafe { core::ptr::read_volatile(&mut systick.cvr) };

因此,我们已经修复了我们四个问题中的一个,但是现在我们有了更多的 unsafe 代码!幸运的是,有个第三方的crate可以帮助到我们 - volatile_register

1. use volatile_register::{RW, RO};

3. #[repr(C)]
4. struct SysTick {
5. pub csr: RW<u32>,
6. pub rvr: RW<u32>,
7. pub cvr: RW<u32>,
8. pub calib: RO<u32>,
9. }

11. fn get_systick() -> &'static mut SysTick {
12. unsafe { &mut *(0xE000_E010 as *mut SysTick) }
13. }

15. fn get_time() -> u32 {
16. let systick = get_systick();
17. systick.cvr.read()
18. }

现在通过read和write方法,volatile accesses可以被自动执行。执行写操作仍然是 unsafe 的,但是公平地讲,硬件有一堆可变的状态,对于编译器来说没有方法去知道是否这些写操作是真正安全的,因此默认就这样是个不错的选择。

Rust风格的封装

我们需要把这个struct封装进一个更高抽象的API中,这个API对于我们用户来说,可以安全地被调用。作为驱动的作者,我们亲手验证不安全的代码是否正确,然后为我们的用户提供一个safe的API,因此用户们不必担心它(让他们相信我们不会出错!)。

有可能有这样的例子:

1. use volatile_register::{RW, RO};

3. pub struct SystemTimer {
4. p: &'static mut RegisterBlock
5. }

7. #[repr(C)]
8. struct RegisterBlock {
9. pub csr: RW<u32>,
10. pub rvr: RW<u32>,
11. pub cvr: RW<u32>,
12. pub calib: RO<u32>,
13. }

15. impl SystemTimer {
16. pub fn new() -> SystemTimer {
17. SystemTimer {
18. p: unsafe { &mut *(0xE000_E010 as *mut RegisterBlock) }
19. }
20. }

22. pub fn get_time(&self) -> u32 {
23. self.p.cvr.read()
24. }

26. pub fn set_reload(&mut self, reload_value: u32) {
27. unsafe { self.p.rvr.write(reload_value) }
28. }
29. }

31. pub fn example_usage() -> String {
32. let mut st = SystemTimer::new();
33. st.set_reload(0x00FF_FFFF);
34. format!("Time is now 0x{:08x}", st.get_time())
35. }

现在,这种方法带来的问题是,下列的代码完全可以被编译器接受:

1. fn thread1() {
2. let mut st = SystemTimer::new();
3. st.set_reload(2000);
4. }

6. fn thread2() {
7. let mut st = SystemTimer::new();
8. st.set_reload(1000);
9. }

虽然 set_reload 函数的 &mut self 参数保证了对某个SystemTimer结构体的引用只有一个,但是他们不能阻止用户去创造第二个SystemTimer,其指向同个外设!如果作者足够尽力,他能发现所有这些’重复的’驱动实例,那么按这种方式写的代码将可以工作,但是一旦代码被散播几天,散播到多个模块,驱动,开发者,它会越来越容易犯此类错误。