型安全な操作

前回の最後に取り扱ったODRレジスタは、ドキュメント内で次のように書かれています。

ビット16:31 予約済み, リセット値を保持しなければなりません

レジスタのこれらのビットには書き込んではいけないようです。そうでなければ、悪いことが起こるでしょう。

レジスタは、異なる読み書きのパーミッションを持っている、という事実もあります。 書き込み専用のものもあれば、読み書き可能なものもあり、読み込み専用のものもあるはずです。

最後に、16進数のアドレスを直接扱うことは、間違いを犯しやすいです。 既に不正なメモリアドレスへのアクセスが、プログラムの実行を中断する例外の原因になることを実験しました。

「安全」な方法でレジスタを操作できるAPIがあると、良いと思いませんか?理想的には、そのAPIは、これまでに述べた3つの点をエンコードするべきです。 実際のアドレスを取り扱わない、読み/書きのパーミッションを守る、レジスタの予約済み部分を修正できないようにする。

やりましょう!実はaux7::init()は、GPIOEペリフェラルのレジスタを操作する、型安全なAPIを提供する値を返しています。

覚えているかもしれませんが、ペリフェラルに関連するレジスタのグループは、レジスタブロックと呼ばれており、連続したメモリ領域に位置しています。 この型安全なAPIでは、各レジスタブロックは、各フィールドがレジスタを表現するstructとしてモデル化されています。 各レジスタのフィールドは、例えばu32の、異なる新しい型で、次のメソッドの組み合わせを提供します。 読み/書きのパーミッションに応じたreadwrite、または、modifyです。 最後に、これらのメソッドは、u32のようなプリミティブな値を受け取りません。代わりに、ビルダーパターンを使って構築された、別の新しい型を受け付けます。 このことにより、レジスタの予約済み部分を修正できないようにしています。

このAPIに慣れるための最善の方法は、プログラムを次のように移植することです。

#![no_main]
#![no_std]

#[allow(unused_imports)]
use aux7::{entry, iprint, iprintln};

#[entry]
fn main() -> ! {
    let gpioe = aux7::init().1;

    // 北のLEDを点灯
    gpioe.bsrr.write(|w| w.bs9().set_bit());

    // 東のLEDを点灯
    gpioe.bsrr.write(|w| w.bs11().set_bit());

    // 北のLEDのを消灯
    gpioe.bsrr.write(|w| w.br9().set_bit());

    // 東のLEDを消灯
    gpioe.bsrr.write(|w| w.br11().set_bit());

    loop {}
}

最初に気がつくことは、魔法のアドレスがないことです。代わりに、より人間が理解しやすい方法を使っています。 例えば、gpioe.bsrrは、GPIOEレジスタブロックのBSRRレジスタを意味しています。

そして、writeメソッドは、クロージャを引数に取ります。アイデンティティクロージャ(|w| w)を使った場合、 このメソッドは、レジスタにデフォルト(リセット)値を設定します。デフォルト値は、マイクロコントローラが電源オン / リセットされた直後の値です。 BSRRレジスタでは、デフォルト値は0x0です。 レジスタにゼロでない値を書き込みたいので、デフォルト値のいくつかのビットを設定するために、bs9br9のようなビルダーメソッドを使用します。

このプログラムを実行してみましょう!プログラムをデバッグしている間にいくつかのおもしろいことができます。

gpioeは、GPIOEレジスタブロックへの参照です。print gpioeは、レジスタブロックのベースアドレスを返します。

$ cargo run
Breakpoint 3, main () at src/07-registers/src/main.rs:9
9           let gpioe = aux7::init().1;

(gdb) next
12          gpioe.bsrr.write(|w| w.bs9().set_bit());

(gdb) print gpioe
$1 = (stm32f30x::gpioc::RegisterBlock *) 0x48001000

しかし、代わりにprint *gpioeを実行すると、レジスタブロックの全貌を得ることができます。 レジスタの各値が表示されます。

(gdb) print *gpioe
$2 = stm32f30x::gpioc::RegisterBlock {
  moder: stm32f30x::gpioc::MODER {
    register: vcell::VolatileCell<u32> {
      value: core::cell::UnsafeCell<u32> {
        value: 0x55550000
      }
    }
  },
  otyper: stm32f30x::gpioc::OTYPER {
    register: vcell::VolatileCell<u32> {
      value: core::cell::UnsafeCell<u32> {
        value: 0x0
      }
    }
  },
  ospeedr: stm32f30x::gpioc::OSPEEDR {
    register: vcell::VolatileCell<u32> {
      value: core::cell::UnsafeCell<u32> {
        value: 0x0
      }
    }
  },
  pupdr: stm32f30x::gpioc::PUPDR {
    register: vcell::VolatileCell<u32> {
      value: core::cell::UnsafeCell<u32> {
        value: 0x0
      }
    }
  },
  idr: stm32f30x::gpioc::IDR {
    register: vcell::VolatileCell<u32> {
      value: core::cell::UnsafeCell<u32> {
        value: 0xcc
      }
    }
  },
  odr: stm32f30x::gpioc::ODR {
    register: vcell::VolatileCell<u32> {
      value: core::cell::UnsafeCell<u32> {
        value: 0x0
      }
    }
  },
  bsrr: stm32f30x::gpioc::BSRR {
    register: vcell::VolatileCell<u32> {
      value: core::cell::UnsafeCell<u32> {
        value: 0x0
      }
    }
  },
  lckr: stm32f30x::gpioc::LCKR {
    register: vcell::VolatileCell<u32> {
      value: core::cell::UnsafeCell<u32> {
        value: 0x0
      }
    }
  },
  afrl: stm32f30x::gpioc::AFRL {
    register: vcell::VolatileCell<u32> {
      value: core::cell::UnsafeCell<u32> {
        value: 0x0
      }
    }
  },
  afrh: stm32f30x::gpioc::AFRH {
    register: vcell::VolatileCell<u32> {
      value: core::cell::UnsafeCell<u32> {
        value: 0x0
      }
    }
  },
  brr: stm32f30x::gpioc::BRR {
    register: vcell::VolatileCell<u32> {
      value: core::cell::UnsafeCell<u32> {
        value: 0x0
      }
    }
  }
}

全てのこれらの新しい型とクロージャは、大きく肥大化したプログラムを生成するように見えます。 しかし、実際にこのプログラムをLTOを有効化してリリースモードでコンパイルすると、 write_volatileと16進数アドレスを使った「unsafe」版と全く同じ命令が生成されることがわかります。

$ cargo objdump --bin registers --release -- -d -no-show-raw-insn -print-imm-hex
registers:      file format ELF32-arm-little

Disassembly of section .text:
main:
 8000188:       bl      #0x22
 800018c:       movw    r0, #0x1018
 8000190:       mov.w   r1, #0x200
 8000194:       movt    r0, #0x4800
 8000198:       str     r1, [r0]
 800019a:       mov.w   r1, #0x800
 800019e:       str     r1, [r0]
 80001a0:       mov.w   r1, #0x2000000
 80001a4:       str     r1, [r0]
 80001a6:       mov.w   r1, #0x8000000
 80001aa:       str     r1, [r0]
 80001ac:       b       #-0x4 <main+0x24>

最も良い点は、GPIOE APIを実装するために、1行もコードを書く必要がなかったことです。 全ては、svd2rustツールを使って、System View Description (SVD)ファイルから自動生成されています。 SVDファイルは、実のところ、マイクロコントローラのベンダが提供しているXMLファイルです。このファイルは、マイクロコントローラのレジスタマップを含んでいます。 このファイルは、レジスタブロックのレイアウトやベースアドレス、書くレジスタの読み/書きのパーミッション、レジスタのレイアウト、 レジスタが予約済みのビットを持っているかどうか、などの有用な情報を含んでいます。