Open12

rust-raspberrypi-OS-tutorialsの行間を埋める

DenTakuDenTaku

趣旨

趣味開発で汎用OSをRustで書くための事前勉強として,rust-embeddedが提供するrust-raspberrypi-OS-tutorialsをやります.(現在進行中)

このチュートリアルは特に難しい環境構築も必要なく素晴らしいチュートリアルだと思うのですが,無駄に長くならないように各ステップで最低限の情報(概要・そのステップで実装されたことの目ぼしい内容)くらいしか書いていません.従って実装内容を全て理解するにはARMやラズパイの仕様,Rustのcoreなどの情報を自分で適宜回収してくる必要があります(個人的には好きなスタイルのチュートリアルです).

このスクラップには,せっかくなので調べた内容のメモを日本語で残します.あくまで自分が実装に納得できるまでに必要だった情報の断片のみなので,間違っていたり不足している可能性があることにご注意ください(電気電子や情報の学部レベルの内容は前提知識として端折っていることも多いかもしれません).

DenTakuDenTaku

準備

自分はチュートリアルとは異なり一つのレポジトリでコードを書き換えながら進めたので一部変なことをしているかもしれません.(レポジトリ: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のビルドもしたかもしれません.

DenTakuDenTaku

01

Raspberry Pi Documentation

ld: linker script

ブートプロセス:[ref: Raspberry Pi 3 Model Bのブート周り]

  1. SDカードにbootcode.binstart.elfconfig.txtkernel.imgを置いておく
  2. 電源ON
  3. GPUが起動
  4. CPUがSoS内ROMから第1段ブートローダを実行
  5. 第1段BLは第2段BL(bootcode.bin)をキャッシュにロードしそこにジャンプ
  6. 第2段BLがSDRAM(Synchronous Dynamic RAM)を有効化
  7. 第2段BLがGPUファームウェア(start.elf)をSDRAMにロード
  8. GPUファームウェアにジャンプ
  9. GPUFWがconfig.txtを読みCPUの設定を行う
  10. GPUFWがkernel.imgを読みSDRAMにロード
  11. GPUFWがCPUのリセット信号を有効化
  12. 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
DenTakuDenTaku

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:共有ライブラリ関数
DenTakuDenTaku

03

0x3F20_1000:UARTのbase address (Rapberry Pi 3)
ちなみにRaspberry Pi 4でCPU側から見た時のUARTのbase addressは0xFE20_1000

DenTakuDenTaku

04

UnsafeCellCellRefCellを久々に復習したくらいでした

DenTakuDenTaku

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
  • 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
  • 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を有効化
    • Control register(Offset: 0x30)
      • 設定方法
        1. [0] := 0
        2. 送信終了を待つ
        3. FIFOをflush
        4. 設定後する
        5. [0] := 1
      • [9]:受信を可能に
      • [8]:送信を可能に
      • [0]:UARTを可能に
    • Interrupt Clear Register(Offset: 0x44)
      • [10:0]:*IC
DenTakuDenTaku

06

chainloader

  1. 自身(chainloader)が0x8_0000にロードされ実行開始
  2. chainloader0x200_0000スタートの場所にコピー
  3. 0x200_0000スタートのプログラム中の_start_rustにジャンプ
  4. chainloaderはUART経由で\x03\x03\x03を送る
  5. \x03\x03\x03を受け取ったMiniloadkernelのバイナリを送信し始める
  6. chainloaderkernel0x8_0000にロード
  7. 0x8_0000にジャンプしてkernelを実行する

自分はこれも上書きしていくスタイルで進めたので,以後もロードに使うこの段階でブランチを切りました

DenTakuDenTaku

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実行で想定よりずっと前の実行結果がキャッシュに残っていると時刻が不正確になるので,直前でフラッシュしてその後命令を実行し直すようにすることで本当に欲しかった時刻のカウンタの値を読むことができる.DMBDSBではそれ以前のメモリアクセスが完了してしまうとout of orderの対象になってしまうのでこの場合は不適.
DenTakuDenTaku

08

必要なハードを購入していなかったので断念.
特に後半影響なさそうな気がするのでスキップ.

DenTakuDenTaku

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
  • CNTHCTL:タイマー関係の設定
    • EL1PCEN:[10],1でtrapされないように設定
    • EL1PCTEN:[10],1でtrapされないように設定