UARTドライバを書いてみましょう

QEMUの'virt' マシンにはPL011というUARTがあるので、それに対するドライバを書いてみましょう。

const FLAG_REGISTER_OFFSET: usize = 0x18;
const FR_BUSY: u8 = 1 << 3;
const FR_TXFF: u8 = 1 << 5;

/// PL011 UARTの最小ドライバ。
#[derive(Debug)]
pub struct Uart {
    base_address: *mut u8,
}

impl Uart {
    /// 指定されたベースアドレスに存在する
    /// PL011 デバイス用の UART ドライバの新しいインスタンスを作成します。
    ///
    /// # 安全性
    ///
    /// 指定されたベースアドレスは PL011 デバイスの 8 つの MMIO 制御レジスタを指していなければなりません。
    /// これらはデバイスメモリとしてプロセスのアドレス空間に
    /// マッピングされ、他のエイリアスはありません。
    pub unsafe fn new(base_address: *mut u8) -> Self {
        Self { base_address }
    }

    /// UART に 1 バイトを書き込みます。
    pub fn write_byte(&self, byte: u8) {
        // TX バッファに空きができるまで待機します。
        while self.read_flag_register() & FR_TXFF != 0 {}

        // SAFETY: We know that the base address points to the control
        // registers of a PL011 device which is appropriately mapped.
        unsafe {
            // TX バッファに書き込みます。
            self.base_address.write_volatile(byte);
        }

        // UART がビジーでなくなるまで待機します。
        while self.read_flag_register() & FR_BUSY != 0 {}
    }

    fn read_flag_register(&self) -> u8 {
        // SAFETY: We know that the base address points to the control
        // registers of a PL011 device which is appropriately mapped.
        unsafe { self.base_address.add(FLAG_REGISTER_OFFSET).read_volatile() }
    }
}
  • Uart::newがアンセーフでその他のメソッドがセーフであるということに注目してください。これは、Uart::newの安全性要求が満たされている(すなわち特定のUARTに対して一つしかドライバのインスタンスが存在せず、そのアドレス空間に対してエイリアスが全く存在しない)ことをその呼び出し元が保証する限り、それ以降は必要な事前条件が満たされていると想定することができwrite_byteを常に安全に呼び出すことができるようになることが理由です。
  • 逆に(newをセーフにして、write_byte をアンセーフに)することもできましたが、そうするとwrite_byteの全呼び出し箇所において安全性を考慮しなければならなくなり、利便性が低下します
  • これはアンセーフなコードに対してセーフなラッパーを構築する場合の共通パターンです:健全性に関する証明に関する労力を多数の場所から少数の場所に集約します。