Raspberry Pi 5のほとんどのIOはPCIeで接続されたRP1というチップを経由して行われるので、私はbaremetalではPCIeコントローラーのドライバを書かないとRP1経由でIO制御ができないと思っていました。
そのため先日なんとかPCIeコントローラーのドライバを書き上げたのですが、実はfirmwareがPCIeコントローラーを初期化したあとリセットせずにkernelを立ち上げてくれる機能がfirmwareにあることを後で知りました。
By default, the PCIe X4 interface is reset before loading the kernel so that the PCIe RC is in a clean state. For bare metal / bringup you can add pciex4_reset=0 to config.txt because writing a bare metal PCIe RC driver probably isn't everyone's cup of tea :) https://forums.raspberrypi.com/viewtopic.php?p=2159255#p2159255 より引用
つまり、このオプションを使えばPCIeドライバを書かなくてもRP1のペリフェラルが使えます。
今回はこのオプションを活用し、baremetalでRP1を使ってGPIO PINのUART端子からシリアル出力を行ってみました。 動きはしたものの完動はせずという状況ですが、原因がわからないので一旦ここまで動きましたという共有をしたいと思います。
(PCIeコントローラーの解説はまた後日)
やるべきこと
- RP1のメモリとCPUメモリのメモリマップの確認
- どこを読み書きして設定するか調べる
- GPIOの設定
- UARTの信号をGPIOから出力可能にする
- UARTの設定
- UARTの信号をペリフェラルから出せるようにする
RP1のメモリとCPUメモリのメモリマップ
PCI/PCIeではデバイスのメモリ空間をCPUのメモリ空間にマップしたあと、CPUがマップされたメモリにアクセスすることでデバイスの制御等を行います。 RP1はPCIe経由でCPUとつながっているので、一般的なPCIeデバイスと同様に特定のCPUメモリを読み書きすると、RP1のMMIO経由でRP1のペリフェラルを制御できます。
Pi5のCPUのメモリ空間は64bitで、RP1のプロセッサメモリ空間は32bitです。 これらのアドレスの対応は以下のようになっています。
CPU | RP1 |
---|---|
0x1f_0000_0000 | 0x4000_0000 |
このマッピングの理由はPCIeコントローラーの設定とPCIのBAR設定によるものなのですが、今回は解説を省略します。
RP1のメモリマップは rp1-peripherals.pdfの 2.3.1. PCIe and 40-bit to peripheral address mapping
の Table 2. Peripheral Address Map
に示されています。
今回扱うペリフェラルはuart0(0x40030000) と iobank0(0x400d0000) の2つです。
また、先程のアドレスの対応表より、Pi5のCPUから uart0(0x1f_0003_0000) と iobank0(0x1f_000d_0000) にアクセスすると、MMIO経由で各種ペリフェラルの設定を行えるとわかります。
GPIO端子の設定
UARTの信号は以下のようにGPIOを経由してPi5のGPIO端子に出力されています。
SoC内部[UART peripheral(PL011) -> GPIO(iobank) -> GPIO(padsbank)]-> Pi5のGPIO端子
GPIOの設定が未設定だとUARTの信号をGPIOでフィルターしてしまい、外部から我々が観測できない状態になってしまいます。 これを防ぐために適切な設定を行います。
GPIOの設定でやることは以下の2つです。
- GPIOのFunctionをUARTに設定する
- GPIOのOutputをEnableにする
RP1のGPIOはRP2040(Raspberry Pi Pico)とほぼ同じなので、Picoを使ったことがある人なら簡単に行えるかもしれません。
GPIOのFunctionをUARTに設定する
RP1のGPIO Functionは iobank0(0x1f_000d_0000) の GPIOx_CTRLレジスタののFUNCSELで設定します。
Raspberry PiのGPIO端子でUARTが出ているのは GPIO 14と15です。 これはそのままRP1のGPIO番号と対応しています。
GPIO Functionと機能の対応は rp1-peripherals.pdf の Table 4. GPIO function selection
にかかれています。
これによると GPIO14と15はFunction a4をセットするとUART0のTX/RXにつながるようです。
よって、FUNCSELには4を設定すればよいです。
一緒に念の為 OEOVER と OUTOVER も0にセットしておきましょう。
GPIOのOutputをEnableにする
RP1のGPIOで出力を有効にするには、さらにpadsbank0(0x1f_000f_0000)の制御が必要です。
これもGPIOごとに1つレジスタが割り当てられていて、レジスタの7bit目にあるOD(Output Disable)を0にすると出力できるようになります。
UART(PL011)
みんな大好きARM社のUARTペリフェラルです。
とりあえず出力するだけの初期化手順は以下になります。
- CRレジスタのUARTENビットに0を書き込んでUARTをDISABLEDする
- LCR_HレジスタでFIFOを無効化
- FLAGレジスタのBUSYが0になるのを待つ
- 各種入出力の設定
- IMSCレジスタで割り込みを全マスク
- 0xffff_ffffを書き込めばOK
- IBRDとFBRDレジスタでbaudrate生成用clock divisorを設定
- CRレジスタのUARTENビットに0を書き込んでUARTをENABLEにする
IBRD/FBRDレジスタ設定に必要なF_UARTCLKパラメーターはRP1のマニュアルにかかれています。
UART interfaces have an independent baud clock (clk_uart), typically 48MHz. 2.5.5. Other Peripheral clocks, Raspberry Pi RP1 Peripherals
よって、以下のように計算すればIBRDとFBRDの設定値が求まります。
let baudrate: u64 = 115200; let peripheral_clock_hz:u64 = 54 * 1000 * 1000; // 54MHz // 最初に1000倍して小数点以下3桁分を整数で計算する let div_x1000:u64 = peripheral_clock_hz * 1000 / 16 / baudrate; let ibrd:u32 = (div_x1000 / 1000) as u32; let fbrd:u32 = (((div_x1000 % 1000) * 64 + 500) / 1000) as u32;
その他設定方法の詳細はPL011のマニュアルをご確認ください。
以上を行ったコードはこちら
初期化後の入出力方法は、FRレジスタで各種FLAGを確認しながらDRレジスタを読み書きするだけです。
バイナリの作成
Rustを使っていれば cargo objcopy
で簡単にバイナリが生成できます。
詳しくは以下のリンカスクリプトとMakefileを参照してください。
- https://github.com/tnishinaga/pi5_hack/blob/develop/baremetal/XX_easy_rp1_uart/ldscript.lds
- https://github.com/tnishinaga/pi5_hack/blob/develop/baremetal/XX_easy_rp1_uart/Makefile
SDカードへのファイル書き込み
以下のファイルをFAT32でフォーマットしたSDカードに入れてください。
- https://github.com/raspberrypi/firmware/blob/master/boot/bcm2712-rpi-5-b.dtb
- https://github.com/tnishinaga/pi5_hack/blob/develop/baremetal/XX_easy_rp1_uart/config.txt
- ビルドした jtag_kernel.bin
あとはPi 5にSDカードを入れて電源をONにすればGPIO端子からUARTの出力が出てくるはずなのですが、なぜかシリアル出力が確認できません。 同じコードをSWD経由で読み込むとシリアル出力が確認できるのでレジスタの設定ミスもなさそうですし、SDカードから起動した場合のみpanicしているなどの状況も確認できていません。
多分なにかを見逃している気がするので、気がついた方は教えていただけると嬉しいです。