お前ほんまどハマりしてばっかりやな
##概要
ゴールデンウィークで時間あるので、前から興味あったけど手をつけられていなかった「Rustでベアメタルプログラミング・組込みOS作成」に手をつけました。
以下のサイトにすごく細かく手順が書いてあったので、それをみながら勉強し、実装を進めました。
Rustで始める自作組込みOS入門
ある程度実装が進んでからそういえばLチカしてへんなと思い、レジスタを叩くLチカに挑戦しました。
マニュアルを読む練習を兼ねて手順をググらず、またボードのクレートも使わず車輪の再開発的にやろうとしたんですが、結果どハマりして無限に時間を溶かしました。
最終的には解決しLチカできたので、こんなアホな時間の溶かし方をする人が減るよう、間違った手順で出た挙動、およびその直し方をまとめます。
##環境・使用したマニュアル
上述の通り、以下のサイトを見て実装を進めました。
Rustで始める自作組込みOS入門
このサイトの「4.割り込み制御」までは実装が完了していました。
(Systick内でLEDをON/OFFすればblinkできるだろうと考えたのがこの試みのきっかけです)
開発に使用した環境はだいたい以下の通りです。
- パソコン:MacOS Mojave 10.14.6
- 使用言語:Rust
- cargo 1.45.0-nightly (f534844c2 2020-05-06)
- rustc 1.45.0-nightly (7ebd87a7a 2020-05-08)
- rustup 1.21.1 (7832b2ebe 2019-12-20)
- ボード:STM32 NUCLEO-F446RE
- エディタ:VSCode
- デバッグ環境: VSCode(Cortex-Debugプラグイン導入)
- 背景知識:やや組込み初心者(基礎の基礎は知ってるものの基礎以上はわからない)
上サイトではNUCLEO-F429ZIボードが使われていましたが、そのボードを持っていませんでした。
STM32の他のマイコンでも大丈夫だろうと書いてあったこともあり、家にあったNUCLEO-F446REボードを使いました。¥
このため、参考にしたマニュアル類がサイトに書いてあるものとは異なりました。
サイトにないもので使用したものは以下のものです。
##コード実装&失敗
###実装
マニュアルを辿り、
- LEDはGPIOのA5ピンに接続されている
- GPIOAのベースアドレスは0x4002_0000にマッピングされている
- GPIAの入出力切り替えはオフセット0x00のレジスタに書き込んで行う
- outputポートからの出力値の設定はオフセット0x04のレジスタに書き込んで行う
ということが判明しました。
これをもとに、だいたい以下のような感じにコードを書きました。(解説サイトと重複しているところは省略します)
まず、ledを操作するモジュールをled.rs
として新たに作成します。
use core::ptr::{write_volatile};
const GPIO_A_ADDR : usize = 0x4002_0000;
const GPIO_MODER_A_DEFAULT: usize = 0xA800_0000;
const GPIO_MODER_OFFSET : usize = 0x00;
const GPIO_ODR_OFFSET: usize = 0x14;
const LED_PIN : usize = 5;
pub fn init() {
unsafe{
let writingValue : u32 = (GPIO_MODER_A_DEFAULT | (0x01 << (LED_PIN * 2))) as u32;
write_volatile((GPIO_A_ADDR + GPIO_MODER_OFFSET) as *mut u32, writingValue);
}
}
pub fn turn_on() {
unsafe {
let reg_value : u32 = read_volatile((GPIO_A_ADDR + GPIO_ODR_OFFSET) as *mut u32);
let writing_value : u32 = reg_value | (0x1 << (LED_PIN));
write_volatile(
(GPIO_A_ADDR + GPIO_ODR_OFFSET) as *mut u32,
writing_value
);
}
}
ボード依存のライブラリは使用しないと縛ったので、アドレス類は全てconstで定義して利用しました。
init()
メソッドでポートAのMODERレジスタのうち、5ピンの入出力設定に対応する位置に値を書いてA5ピンをOUTPUTに設定します。
turn_on()
およびturn_off()
では、アウトデータレジスタであるODRレジスタのうち、5番ピンに対応するところに値を書いています。
※このコードは誤っており、このままやと5番ピン以外のレジスタ値を全部0で書き潰してしまいます。noobかな?
後述するコードはこの点も修正しています。
あとは、Reset関数の中でled::init()
とled::turn_on()
を呼び出します
...
#[no_mangle]
pub unsafe extern "C" fn Reset() -> ! {
...
hprintln!("Hello World").unwrap();
systick::init();
led::init();
led::turn_on();
loop {}
}
...
###失敗・発生した現象
これでLEDが点灯はするやろー(ヘラヘラ)と思ってたのですが、LEDはうんともすんとも言いません。
仕方がないのでGPIOA
のレジスタに関し、書き込む値や書き込んだ後のレジスタの値を読み出してデバッグしてみました。
なおマニュアルより、
-
GPIO_MODER
は、0xa800_0000、GPIO_ODR_ADDR
は0x0000_0000で初期化される -
GPIO_MODER
には0xa800_0400が書き込まれないといけない
ということはわかっていました。
デバッグの結果、
- 書き込もうとしている値は確かに0x0000_0400になっている
- 書き込む前に
GPIO_MODER
のアドレスから読み出す値は確かに0xa800_0000になっている - 書き込んだ後も
GPIO_MODER
から読み出す値が0xa800_0000のままになっている GPIO_ODR
のアドレスを読み出しても0xa800_0000が返ってくる
となりました。
4.が一番不自然だったので、他のアドレスにもアクセスして値を確かめました。すると
- GPIOAのアドレス範囲(0x4002_0000-0x4002_03FF)のどのアドレスから読み出しても0xa800_0000が返ってくる
- GPIOBのアドレス範囲(0x4002_0400-0x4002_0800)のどのアドレスから読み出しても0x0000_0280が返ってくる
(0x0000_0280はGPIOBのGPIO_MODER
の初期値です)
うーん、なんでなんやろうとマニュアルとにらめっこしながら無限時間を溶かしいろいろ試しました
原因・解決
マニュアルだけから解決するんだ!と意気込んでましたが、諦めてググりました。
以下のブログ様の内容がそのものズバリでした。
STM32のレジスタを叩く。 #1 ~GPIO編~ - エビデイエビナイ電子工作
ブログ様によると
STM32では省電力のため、ペリフェラルへのクロック供給がデフォルトで無効になっています。
なるほどそうだったのか。電源入れたら全部準備できてると思うなよってことか
メモリ読み書きで操作しているのですっかり忘れていたのですが、あくまで「ペリフェラルの操作インターフェースがメモリ空間にマッピングされている」のであり実際にメモリがあってそこに読み書きしているわけではない(であってる??)ので
ペリフェラルが動作していなければ想定する値を返すわけがないということですね
(当たり前かも知れませんがすっかり抜け落ちていました)
というわけでled.rs
のプログラムを以下のように修正しました。
...
const RCC_ADDR : usize = 0x4002_3800;
const RCC_AHB1ENR_OFFSET : usize = 0x30;
pub fn init() {
hprintln!("led init").unwrap();
unsafe{
write_volatile((RCC_ADDR + RCC_AHB1ENR_OFFSET) as *mut u32, 1);
let writingValue : u32 = (GPIO_MODER_A_DEFAULT | (0x01 << (LED_PIN * 2))) as u32;
write_volatile((GPIO_A_ADDR + GPIO_MODER_OFFSET) as *mut u32, writingValue);
...
GPIOAへのクロック供給を行うよう、RCC_AHB1ENR
への書き込みを行っています。
このコードを実行したところ無事LEDが光りました。めでたしめでたし。