型安全な操作
直前まで扱っていたレジスタ ODR には、ドキュメントに次のように書かれていました。
ビット 31:16 は予約済みであり、リセット値のまま保持しなければならない
そのため、このレジスタのそれらのビットには書き込んではいけません。そうしないと、まずいことが起こるかもしれません。
さらに、レジスタごとに読み取り/書き込み権限が異なるという事実もあります。書き込み専用のものもあれば、読み書きできるものもあり、当然ながら読み取り専用のものもあります。
最後に、16進数アドレスを直接扱うのはエラーを招きやすいです。無効なメモリアドレスにアクセスしようとすると例外が発生し、そのせいでプログラムの実行が妨げられることは、すでに見たとおりです。
レジスタを「安全」に操作するための API があれば便利ではないでしょうか? 理想を言えば、その API はここで挙げた 3 点、つまり実際のアドレスを直接いじらないこと、読み取り/書き込み権限を守ること、そしてレジスタの予約済み部分の変更を防ぐことをエンコードしているべきです。
そして実際に、そのようなものがあります! aux7::init() は実際には、GPIOE ペリフェラルのレジスタを型安全に操作するための API を提供する値を返します。
覚えているかもしれませんが、あるペリフェラルに関連付けられたレジスタ群はレジスタブロックと呼ばれ、連続したメモリ領域に配置されています。この型安全 API では、各レジスタブロックは struct としてモデル化され、その各フィールドが 1 つのレジスタを表します。各レジスタフィールドは、たとえば u32 上の異なる newtype であり、その読み取り/書き込み権限に応じて read、write、modify のいずれかのメソッドの組み合わせを公開します。最後に、これらのメソッドは u32 のようなプリミティブ値を受け取るのではなく、ビルダーパターンを使って構築でき、レジスタの予約済み部分の変更を防ぐさらに別の newtype を受け取ります。
この API に慣れるいちばんよい方法は、今動いている例をこれに移植することです。
#![no_main]
#![no_std]
#[allow(unused_imports)]
use aux7::{entry, iprintln, ITM, RegisterBlock};
#[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 です。今回はレジスタに 0 以外の値を書き込みたいので、デフォルト値の一部のビットを設定するために bs9 や br9 のようなビルダーメソッドを使います。
このプログラムを実行してみましょう! プログラムをデバッグしている 最中に、いくつか面白いことができます。
gpioe は GPIOE レジスタブロックへの参照です。print gpioe はレジスタブロックのベースアドレスを返します。
$ cargo run
(..)
Breakpoint 1, registers::__cortex_m_rt_main_trampoline () at src/07-registers/src/main.rs:7
7 #[entry]
(gdb) step
registers::__cortex_m_rt_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 = (*mut stm32f3::stm32f303::gpioc::RegisterBlock) 0x48001000
しかし、代わりに print *gpioe を実行すると、レジスタブロックの 全体像 を得られます。つまり、その各レジスタの値が表示されます。
(gdb) print *gpioe
$2 = stm32f3::stm32f303::gpioc::RegisterBlock {
moder: stm32f3::generic::Reg<u32, stm32f3::stm32f303::gpioc::_MODER> {
register: vcell::VolatileCell<u32> {
value: core::cell::UnsafeCell<u32> {
value: 1431633920
}
},
_marker: core::marker::PhantomData<stm32f3::stm32f303::gpioc::_MODER>
},
otyper: stm32f3::generic::Reg<u32, stm32f3::stm32f303::gpioc::_OTYPER> {
register: vcell::VolatileCell<u32> {
value: core::cell::UnsafeCell<u32> {
value: 0
}
},
_marker: core::marker::PhantomData<stm32f3::stm32f303::gpioc::_OTYPER>
},
ospeedr: stm32f3::generic::Reg<u32, stm32f3::stm32f303::gpioc::_OSPEEDR> {
register: vcell::VolatileCell<u32> {
value: core::cell::UnsafeCell<u32> {
value: 0
}
},
_marker: core::marker::PhantomData<stm32f3::stm32f303::gpioc::_OSPEEDR>
},
pupdr: stm32f3::generic::Reg<u32, stm32f3::stm32f303::gpioc::_PUPDR> {
register: vcell::VolatileCell<u32> {
value: core::cell::UnsafeCell<u32> {
value: 0
}
},
_marker: core::marker::PhantomData<stm32f3::stm32f303::gpioc::_PUPDR>
},
idr: stm32f3::generic::Reg<u32, stm32f3::stm32f303::gpioc::_IDR> {
register: vcell::VolatileCell<u32> {
value: core::cell::UnsafeCell<u32> {
value: 204
}
},
_marker: core::marker::PhantomData<stm32f3::stm32f303::gpioc::_IDR>
},
odr: stm32f3::generic::Reg<u32, stm32f3::stm32f303::gpioc::_ODR> {
register: vcell::VolatileCell<u32> {
value: core::cell::UnsafeCell<u32> {
value: 0
}
},
_marker: core::marker::PhantomData<stm32f3::stm32f303::gpioc::_ODR>
},
bsrr: stm32f3::generic::Reg<u32, stm32f3::stm32f303::gpioc::_BSRR> {
register: vcell::VolatileCell<u32> {
value: core::cell::UnsafeCell<u32> {
value: 0
}
},
_marker: core::marker::PhantomData<stm32f3::stm32f303::gpioc::_BSRR>
},
lckr: stm32f3::generic::Reg<u32, stm32f3::stm32f303::gpioc::_LCKR> {
register: vcell::VolatileCell<u32> {
value: core::cell::UnsafeCell<u32> {
value: 0
}
},
_marker: core::marker::PhantomData<stm32f3::stm32f303::gpioc::_LCKR>
},
afrl: stm32f3::generic::Reg<u32, stm32f3::stm32f303::gpioc::_AFRL> {
register: vcell::VolatileCell<u32> {
value: core::cell::UnsafeCell<u32> {
value: 0
}
},
_marker: core::marker::PhantomData<stm32f3::stm32f303::gpioc::_AFRL>
},
afrh: stm32f3::generic::Reg<u32, stm32f3::stm32f303::gpioc::_AFRH> {
register: vcell::VolatileCell<u32> {
value: core::cell::UnsafeCell<u32> {
value: 0
}
},
_marker: core::marker::PhantomData<stm32f3::stm32f303::gpioc::_AFRH>
},
brr: stm32f3::generic::Reg<u32, stm32f3::stm32f303::gpioc::_BRR> {
register: vcell::VolatileCell<u32> {
value: core::cell::UnsafeCell<u32> {
value: 0
}
},
_marker: core::marker::PhantomData<stm32f3::stm32f303::gpioc::_BRR>
}
}
こうした newtype やクロージャは、大きくて肥大化したプログラムを生成しそうに聞こえます。しかし、実際にプログラムを LTO を有効にした release モードでコンパイルすると、write_volatile と 16 進数アドレスを使った「unsafe」版とまったく同じ命令が生成されることが分かります!
cargo objdump を使ってアセンブリコードを release.txt に書き出します:
cargo objdump --bin registers --release -- -d --no-show-raw-insn --print-imm-hex > release.txt
次に、release.txt で main を検索します
0800023e <main>:
800023e: push {r7, lr}
8000240: mov r7, sp
8000242: bl #0x2
8000246: trap
08000248 <registers::__cortex_m_rt_main::h199f1359501d5c71>:
8000248: push {r7, lr}
800024a: mov r7, sp
800024c: bl #0x22
8000250: movw r0, #0x1018
8000254: mov.w r1, #0x200
8000258: movt r0, #0x4800
800025c: str r1, [r0]
800025e: mov.w r1, #0x800
8000262: str r1, [r0]
8000264: mov.w r1, #0x2000000
8000268: str r1, [r0]
800026a: mov.w r1, #0x8000000
800026e: str r1, [r0]
8000270: b #-0x4 <registers::__cortex_m_rt_main::h199f1359501d5c71+0x28>
この一連の作業で最もすばらしい点は、GPIOE API を実装するために、誰もコードを1行も書く必要がなかったことです。すべてのコードは、svd2rust ツールを使って、System View Description (SVD) ファイルから自動生成されました。この SVD ファイルは、実際にはマイクロコントローラベンダーが提供する XML ファイルであり、そのマイクロコントローラのレジスタマップが含まれています。このファイルには、レジスタブロックのレイアウト、ベースアドレス、各レジスタの読み取り/書き込み権限、レジスタのレイアウト、レジスタに予約済みビットがあるかどうか、そしてそのほか多くの有用な情報が含まれています。