Rust的首次尝试

寄存器

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

OffsetNameDescriptionWidth
0x00SYST_CSR控制和状态寄存器32 bits
0x04SYST_RVR重装载值寄存器32 bits
0x08SYST_CVR当前值寄存器32 bits
0x0CSYST_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};
  2. #[repr(C)]
  3. struct SysTick {
  4. pub csr: RW<u32>,
  5. pub rvr: RW<u32>,
  6. pub cvr: RW<u32>,
  7. pub calib: RO<u32>,
  8. }
  9. fn get_systick() -> &'static mut SysTick {
  10. unsafe { &mut *(0xE000_E010 as *mut SysTick) }
  11. }
  12. fn get_time() -> u32 {
  13. let systick = get_systick();
  14. systick.cvr.read()
  15. }

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

Rust风格的封装

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

有可能有这样的例子:

  1. use volatile_register::{RW, RO};
  2. pub struct SystemTimer {
  3. p: &'static mut RegisterBlock
  4. }
  5. #[repr(C)]
  6. struct RegisterBlock {
  7. pub csr: RW<u32>,
  8. pub rvr: RW<u32>,
  9. pub cvr: RW<u32>,
  10. pub calib: RO<u32>,
  11. }
  12. impl SystemTimer {
  13. pub fn new() -> SystemTimer {
  14. SystemTimer {
  15. p: unsafe { &mut *(0xE000_E010 as *mut RegisterBlock) }
  16. }
  17. }
  18. pub fn get_time(&self) -> u32 {
  19. self.p.cvr.read()
  20. }
  21. pub fn set_reload(&mut self, reload_value: u32) {
  22. unsafe { self.p.rvr.write(reload_value) }
  23. }
  24. }
  25. pub fn example_usage() -> String {
  26. let mut st = SystemTimer::new();
  27. st.set_reload(0x00FF_FFFF);
  28. format!("Time is now 0x{:08x}", st.get_time())
  29. }

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

  1. fn thread1() {
  2. let mut st = SystemTimer::new();
  3. st.set_reload(2000);
  4. }
  5. fn thread2() {
  6. let mut st = SystemTimer::new();
  7. st.set_reload(1000);
  8. }

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