最初の試み
レジスタ
「SysTick」ペリフェラルを見てみましょう。これは、すべての Cortex-M プロセッサコアに備わっているシンプルなタイマーです。通常、これらはチップメーカーのデータシートや テクニカルリファレンスマニュアル で調べることになりますが、この例はすべての ARM Cortex-M コアに共通なので、ARM reference manual を見てみましょう。4 つのレジスタがあることが分かります。
| オフセット | 名前 | 説明 | 幅 |
|---|---|---|---|
| 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)] は、この構造体を C コンパイラと同じように配置するよう Rust コンパイラに指示します。これは非常に重要です。C ではできませんが、Rust では構造体のフィールドが並べ替えられる可能性があるからです。もしこれらのフィールドがコンパイラによって黙って並べ替えられていたら、どれだけデバッグすることになるか想像できますよね! この属性を付ければ、上の表に対応する 4 つの 32 ビットフィールドが得られます。しかし当然ながら、この struct だけでは役に立ちません。変数が必要です。
let systick = 0xE000_E010 as *mut SysTick;
let time = unsafe { (*systick).cvr };
volatile アクセス
さて、上のアプローチにはいくつか問題があります。
- ペリフェラルにアクセスしたいたびに
unsafeを使わなければなりません。 - どのレジスタが読み取り専用で、どれが読み書き可能かを指定する方法がありません。
- プログラム中のどのコード片からでも、この構造体を通じてハードウェアにアクセスできてしまいます。
- そして最も重要なのは、実際には動作しないということです…
問題は、コンパイラが賢いことです。同じ RAM 領域に対して続けて 2 回書き込みを行うと、コンパイラはそれに気付き、最初の書き込みを完全に省いてしまうことがあります。C では、すべての読み書きが意図どおりに行われるよう、変数を volatile としてマークできます。Rust では代わりに、変数ではなく アクセス を volatile として扱います。
let systick = unsafe { &mut *(0xE000_E010 as *mut SysTick) };
let time = unsafe { core::ptr::read_volatile(&mut systick.cvr) };
これで 4 つの問題のうち 1 つは解決しましたが、今度は unsafe コードがさらに増えてしまいました! 幸い、これを助けてくれるサードパーティのクレートがあります。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()
}
これで、volatile アクセスは 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 構造体に対してほかの参照が存在しないことは保証しますが、ユーザーがまったく同じペリフェラルを指す 2 つ目の SystemTimer を作成することまでは防げません! この書き方のコードでも、作者がこうした「重複した」ドライバインスタンスをすべて見つけ出せるほど注意深ければ動作します。しかし、コードが複数のモジュール、ドライバ、開発者、そして日数にまたがって広がると、この種のミスはますます起こしやすくなります。