最初の試み

レジスタ

SysTick ペリフェラルを見ていきましょう。 SysTick はCortex-Mプロセッサ・コアに搭載されているシンプルなタイマーです。 通常は、チップメーカーのデータシートやテクニカルリファレンスマニュアルでこれらのペリフェラルの情報を調べることができるのですが、この例においては全てのARM Cortex-Mコアで共通のものですので、今回はARMリファレンスマニュアルを見てみましょう。 そこには4つのレジスタが載っています。

オフセット名前説明
0x00SYST_CSR制御およびステータスレジスタ32ビット
0x04SYST_RVRリロード値レジスタ32ビット
0x08SYST_CVR現在値レジスタ32ビット
0x0CSYST_CALIBキャリブレーション値レジスタ32ビット

Cアプローチ

Rustでも、structを使ってCと同じ方法でレジスタの集合を正確に表現することができます。

#[repr(C)]
struct SysTick {
    pub csr: u32,
    pub rvr: u32,
    pub cvr: u32,
    pub calib: u32,
}

#[repr(C)]修飾子はRustコンパイラ対して、この構造体をCコンパイラと同じようにメモリにレイアウトするように指示します。 これはとても重要なことです。なぜならRustでは、Cにおいては行われない構造体のフィールドの並び替えが許されているためです。 コンパイラによって暗黙のうちに構造体のフィールドが並び替えられることにより、デバッグをするはめになることは想像できるでしょう! この修飾子を置くことで、4つの32ビットの各フィールドは上記のテーブルに対応付けられます。 ただしもちろん、このstructはそれ自体では何の役にも立ちません。次のように変数として使う必要があります。

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

volatileアクセス

上記のやり方には、いくつか問題があります。

  1. ペリフェラルにアクセスするためには毎回アンセーフを使わなくてはなりません。
  2. どのレジスタが読み取り専用でどのレジスタが読み書き可能かを指定する方法がありません。
  3. プログラム内のどのコードからでもこの構造体を通してハードウェアにアクセスできてしまいます。
  4. 最も大事なことは、このコードは実際には動作しないということです…

ここで問題となるのは、コンパイラが賢いということです。 同じ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つを直せました。しかし、さらにunafeなコードがあります! 幸いなことに、これに対処できるサードパーティ製のクレート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()
}

これでreadwriteメソッドを通してvolatileアクセスが自動的に行われるようになりました。 書き込みを実行するのはまだunsafeです。しかし、公平を期するために言うと、ハードウェアは変更可能な状態の集まりであるため、それらへの書き込みが実際に安全なのかどうか、をコンパイラが知る方法はないのです。そのため、これは基本姿勢としては悪くないでしょう。

Rustのラッパ

ユーザが安全に呼び出せるように、このstructを高レイヤーのAPIでラップする必要があります。 ドライバの作者として、アンセーフなコードが正しいことを手動で検証し、ユーザがそのドライバを使用する上で心配することがないように安全な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構造体をユーザが作ることは止められません! このように書かれたコードは、作者がこれらの「重複した」ドライバのインスタンスを全て見つけるのに十分に熱心であれば動作するでしょうが、一度コードが複数のモジュール、ドライバ、開発者、そして日に渡って分散すると、この種の間違いがどんどん入り込みやすくなっていきます。