rust-raspberrypi-OS-tutorialsの行間を埋める
趣旨
趣味開発で汎用OSをRustで書くための事前勉強として,rust-embeddedが提供するrust-raspberrypi-OS-tutorialsをやります.(現在進行中)
このチュートリアルは特に難しい環境構築も必要なく素晴らしいチュートリアルだと思うのですが,無駄に長くならないように各ステップで最低限の情報(概要・そのステップで実装されたことの目ぼしい内容)くらいしか書いていません.従って実装内容を全て理解するにはARMやラズパイの仕様,Rustのcore
などの情報を自分で適宜回収してくる必要があります(個人的には好きなスタイルのチュートリアルです).
このスクラップには,せっかくなので調べた内容のメモを日本語で残します.あくまで自分が実装に納得できるまでに必要だった情報の断片のみなので,間違っていたり不足している可能性があることにご注意ください(電気電子や情報の学部レベルの内容は前提知識として端折っていることも多いかもしれません).
準備
自分はチュートリアルとは異なり一つのレポジトリでコードを書き換えながら進めたので一部変なことをしているかもしれません.(レポジトリ:den-taku/raspi-os)
用意すべきハードウェア類
- Raspberry Pi 4 Model B またはRaspberry Pi 3 Model B
- USB-シリアル変換アダプタ(6章以降)
- JTAGデバッガ ARM-USB-TINY-H(8章)
環境:
MacBook Pro (Monterey 12.1)
rustc 1.64.0-nightly (f9cba6374 2022-07-31)
Raspberry Pi 4 Model B 8GB
bundle install --path .vendor/bundle --without development
(最初はrust-toolchaiin.toml
を追加し忘れていたので以下も実行しました.多分不要)
rustup target add aarch64-unknown-none-softfloat
rustup component add llvm-tools-preview
記憶にあまりないですがエラーが出てdocker/rustembedded-osdev-utils
のDockerfileのビルドもしたかもしれません.
00
bsp
: board support package
01
ld: linker script
ブートプロセス:[ref: Raspberry Pi 3 Model Bのブート周り]
- SDカードに
bootcode.bin
,start.elf
,config.txt
,kernel.img
を置いておく - 電源ON
- GPUが起動
- CPUがSoS内ROMから第1段ブートローダを実行
- 第1段BLは第2段BL(
bootcode.bin
)をキャッシュにロードしそこにジャンプ - 第2段BLがSDRAM(Synchronous Dynamic RAM)を有効化
- 第2段BLがGPUファームウェア(
start.elf
)をSDRAMにロード - GPUファームウェアにジャンプ
- GPUFWが
config.txt
を読みCPUの設定を行う - GPUFWが
kernel.img
を読みSDRAMにロード - GPUFWがCPUのリセット信号を有効化
- CPUがSDRAMに置かれた
kernel.img
を実行
実際にRaspberry Pi Imagerで焼いたSDカードを覗いてみた
$ ls /Volumes/boot
COPYING.linux bootcode.bin issue.txt
LICENCE.broadcom cmdline.txt kernel8.img
bcm2710-rpi-2-b.dtb config.txt overlays
bcm2710-rpi-3-b-plus.dtb firstrun.sh start.elf
bcm2710-rpi-3-b.dtb fixup.dat start4.elf
bcm2710-rpi-cm3.dtb fixup4.dat start4cd.elf
bcm2710-rpi-zero-2-w.dtb fixup4cd.dat start4db.elf
bcm2710-rpi-zero-2.dtb fixup4db.dat start4x.elf
bcm2711-rpi-4-b.dtb fixup4x.dat start_cd.elf
bcm2711-rpi-400.dtb fixup_cd.dat start_db.elf
bcm2711-rpi-cm4.dtb fixup_db.dat start_x.elf
bcm2711-rpi-cm4s.dtb fixup_x.dat
02
linker script
-
ENTRY
:entry addressを決める(ref 3.4.1 Setting the Entry Point) -
PHDRS
:プログラムヘッダを指定してロードの仕方を決める(ref 3.8 PHDRS Command) -
SECTIONS
(必須):メモリ配置を定める(ref 3.6 SECTIONS Command)-
.text
:プログラム -
.rodata
:定数値 -
.data
:初期値ありグローバル変数 -
.bss
:初期値なし(0で初期化)グローバル変数 -
.got
:共有ライブラリ関数
-
03
0x3F20_1000
:UARTのbase address (Rapberry Pi 3)
ちなみにRaspberry Pi 4でCPU側から見た時のUARTのbase addressは0xFE20_1000
04
UnsafeCell
,Cell
,RefCell
を久々に復習したくらいでした
05
MacだとDEV_SERIAL=/dev/tty.usbserial-0001
でUARTと接続できるはず.
(自分は手元にあったmicroUSBに変換するやつで代用したので DEV_SERIAL=/dev/tty.usbserial-AU01PR2F
でしたが)
レジスタの読み書きにはtock-registersを使用
これは簡単にいうとregister_structs!
でレジスタのアドレスを指定し,register_bitfields!
で各レジスタのbitの割り当てを書けば洗練されたインターフェイスで値の読み書きができる素晴らしいクレート.
Raspberry Pi 4のプロセッサはbcm2711
(仕様PDF:bcm2711/bcm2711-peripherals.pdf)
基本的にはこの仕様の該当箇所を読めばデバイスドライバが何をやっているかは理解できるはずです.
Device Drivers
GPIO
PL011Uart
GPIO
仕様の65ページから
概要
- GPUから見たベースアドレスが
0x7E20_0000
→CPUからは0xFE20_0000
(P.5 Figure1の対応関係より) - Function Select Regs
- GPFSEL1レジスタ(Offset: 0x04)
- フィールド
- [15:7]:FSEL15
- [14:12]:FSEL14
- 値
-
0b000
:Input -
0b001
:Output -
0b100
:alternate function 0
-
- フィールド
- GPFSEL1レジスタ(Offset: 0x04)
- Pin Set & Clear Regs
- CPIO_PUP_PDN_CNTRL_REG0レジスタ(Offset: 0xE4)
- フィールド
- [31:30]:GPIO_PUP_PDN_CTRL15
- [29:28]:GPIO_PUP_PDN_CTRL14
- 値
-
0b00
:No registor -
0b01
:Pull up registor -
0b10
:Pull down registor
-
- フィールド
- CPIO_PUP_PDN_CNTRL_REG0レジスタ(Offset: 0xE4)
- Alternative Function Assignments
- GPIO14::ALT0:TXD0
- GPIO15::ALT0:RXD0
PL011Uart
(ARM UART)
仕様の144ページから
概要
- PL01 UART
- 非同期,FIFOを利用可能
- Input:
RXD
(,nCTS
)- GPIO14, ALT0
- Output:
TXD
(,nRTS
)- GPIO15, ALT0
- GPUから見たベースアドレスが
0x7E20_1000
→CPUからは0xFE20_1000
(P.5 Figure1の対応関係より)(UART0)- Data Register(Offset: 0x00)
- [7:0]:データ
- [11:8]:エラーフラグ
- Flag register(Offset: 0x18)
- [7]:送信FIFOがEmpty
- [5]:送信FIFOがFull
- [4]:受信FIFOがEmpty
- [3]:Busy(送信中)
- Integer Baud rate divisor(Offset: 0x24)
- [15:0]:値
- Fractional Baud rate divisor(Offset: 0x28)
- [5:0]:値
- Line Control register(Offset: 0x2c)
- [6:5]:ワード長
-
0b00
:5bit,0b01
:6bit,0b10
:7bit,0b11
:8bit
-
- [4]:FIFOを有効化
- [6:5]:ワード長
- Control register(Offset: 0x30)
- 設定方法
- [0] := 0
- 送信終了を待つ
- FIFOをflush
- 設定後する
- [0] := 1
- [9]:受信を可能に
- [8]:送信を可能に
- [0]:UARTを可能に
- 設定方法
- Interrupt Clear Register(Offset: 0x44)
- [10:0]:*IC
- Data Register(Offset: 0x00)
06
chainloader
- 自身(
chainloader
)が0x8_0000
にロードされ実行開始 -
chainloader
を0x200_0000
スタートの場所にコピー -
0x200_0000
スタートのプログラム中の_start_rust
にジャンプ -
chainloader
はUART経由で\x03\x03\x03
を送る -
\x03\x03\x03
を受け取ったMiniload
はkernel
のバイナリを送信し始める -
chainloader
がkernel
を0x8_0000
にロード -
0x8_0000
にジャンプしてkernel
を実行する
自分はこれも上書きしていくスタイルで進めたので,以後もロードに使うこの段階でブランチを切りました
07
これらはARMが決めているレジスタで,cortex-a
クレートがいい感じに提供してくれている
-
CNTPCT_EL0
:Counter-timer Physical Count register -
CNTFRQ_EL0
:Counter-timer Frequency register
// asmで ADL_REL x1 ARCH_TIMER_COUNTER_FREQUENCYのようにaddressにアクセスできる
#[no_mangle]
static ARCH_TIMER_COUNTER_FREQUENCY: NonZeroU32 = NonZeroU32::MIN;
timerのread_volatile
はアセンブリで書き換える前の値で固定されないため
GenericTimerCounterValue
は名前通りプログラムカウンタが何回進む分くらいの時間かを意味する.
// core::time::Durationの定義
pub struct Duration {
secs: u64,
nanos: u32
}
Barrier Instructions(out of orderが好ましくない場合の命令)
-
DMB
:data memory barrier,これより前のメモリアクセスが全て完了するまではメモリアクセスを行わない -
DSB
:data synchronization barrier,これより前のメモリアクセスが全て完了するまでは待機 -
ISB
:instruction synchronization barrier,Write bufferのパイプラインをフラッシュし命令を再フェッチ-
CNTPCT_EL0
を読む前にISB
を挟む理由:out of order実行で想定よりずっと前の実行結果がキャッシュに残っていると時刻が不正確になるので,直前でフラッシュしてその後命令を実行し直すようにすることで本当に欲しかった時刻のカウンタの値を読むことができる.DMB
やDSB
ではそれ以前のメモリアクセスが完了してしまうとout of orderの対象になってしまうのでこの場合は不適.
-
08
必要なハードを購入していなかったので断念.
特に後半影響なさそうな気がするのでスキップ.
09
進める前に読むべき資料:Programmer's Guide for ARMv8-A(pdf)のChapter 3(計9ページ程度)
privilege lebels
Typically used for | AArch64 | RIST-V | x86 |
---|---|---|---|
Userspace applications | EL0 | U/VU | Ring 3 |
OS Kernel | EL1 | S/VS | Ring 0 |
Hypervisor | EL2 | HS | Ring-1 |
Low-Level Firmware | EL3 | M |
Normal:
- Gest OS kernels at level
EL1
- Hypervisor runs at
EL2
(always non-secure)
Secure:
- Firemware runs at boot at lebel
EL0
- Trusted OS at lebel
EL1
例外発生時に自動でモードが切り替わることがあるが,
当然権限が小さい方にしか移動しない.
-
CurrentEL
:現在のELの値が保存されたレジスタ- [3:2]の値:レジスタとして読んだ値:EL
- 0b00:0x0:
EL0
- 0b01:0x4:
EL0
- 0b10:0x8:
EL0
- 0b11:0xC:
EL0
- 0b00:0x0:
- [3:2]の値:レジスタとして読んだ値:EL
-
CNTHCTL
:タイマー関係の設定-
EL1PCEN
:[10],1
でtrapされないように設定 -
EL1PCTEN
:[10],1
でtrapされないように設定
-