Raspberry Pi pico のブートストラップ

CQ出版インターフェース誌7月号特集「ラズパイPicoで1500行 ゼロから作るOS」から、ブートストラップの流れを整理してみた。

電源オンからの起動

Raspberry Pi pico の電源が入ると、以下が起こる。

  • アドレス 0x0000 0000 の32ビットデータがスタックポインタにセットされる。
  • アドレス 0x0000 0004 の32ビットデータがプログラムカウンタにセットされる。

当然、この領域はROMに格納されているので、ROM内のプログラムが実行されることになる。ちなみに、アドレス空間上でROMの領域は、0x0000 0000 から 0x0000 3FFF の16KiB である。

さて、最初に実行されるブートローダ(ファーストステージ)はROMに格納されており、そのソースコードは以下で公開されている。

https://github.com/raspberrypi/pico-bootrom/blob/master/bootrom/bootrom_rt0.S

_start で始まる部分がそれで、コメントに「Entry point for both cores」とあるように、Cortex-M0+ の2つのコアとも同時にスタートされる。その後の流れは以下のようである。

  • CPUコアのIDが0以外の場合はスリープする(みたいだけど追っかけきれてない)
  • NMI を設定する
  • フラッシュメモリの先頭からの256バイトを読み込んで、SRAMの末尾256バイトにコピーする
  • チェックサムを計算して一致すれば、読み込んだプログラムにジャンプする

ARMの命令に馴染みがないのでアヤフヤだが、だいたい合っていると思う。

pico-sdk の src/rp2040/hardware_regs/include/hardware/regs/addressmap.h にメモリマップが定義されている。それによると、SRAM の領域は 0x2000 0000 から 0x2004 2000 の264KiBとなっているので、0x2004 1F00 からの256バイトにセカンドステージのプログラムが格納され、実行されることになる。

ブートストラップのセカンドステージ

フラッシュメモリの先頭256バイトがセカンドステージのプログラムとなるため、256バイト以内であれば何を記述してもよい。ただし、最後の4バイトはチェックサムに使うので、実質252バイトとなる。

Try Kernel では pico-sdk のセカンドステージのプログラムをそのまま使用しており、そのコードは以下にある。

https://github.com/raspberrypi/pico-sdk/blob/26653ea81e340cacee55025d110c3e014a252a87/src/rp2_common/boot_stage2/boot2_w25q080.S

boot_stage2ディレクトリには、SPI Flash の種類にあわせてソースファイルが分かれているが、回路図によると Raspberry Pi pico のフラッシュメモリのチップは W25Q16JV なので、そのチップをサポートしている上記が該当する。

このセカンドステージのプログラムでは、まずSPIの設定を行ってから、フラッシュメモリをアドレス空間(XIP_BASE=0x1000 0000)にマッピングする。フラッシュメモリのアドレス 0x0100 以降(アドレス空間上では 0x1000 0100以降)をベクタテーブルとしてVTORレジスタに設定し、最後にベクタテーブル先頭のスタックポインタをSPレジスタに設定するとともに、リセットベクタに設定されている直後のアドレス(つまりリセットハンドラ)へジャンプする。

https://github.com/raspberrypi/pico-sdk/blob/master/src/rp2_common/boot_stage2/asminclude/boot2_helpers/exit_from_boot2.S

以上がブートストラップのセカンドステージの流れである。

リセットハンドラの実行

Try Kernel (の第2部第3章)のリセットハンドラでは以下を行っている。

  • PendSV 例外とSysTick 例外の優先度設定
  • クロックの初期化
  • 内蔵周辺装置の初期化
  • メモリの初期化
  • システムタイマの初期化
  • main関数の実行

PendSV とはソフトウェア例外のことだそうで、タスクの切り替え等に用いるのが一般的なようである。これは最低レベルに設定し、逆に SysTick は最高レベルに設定する。

内蔵周辺装置の初期化としてはGPIOとUART0を有効化し、ボード上のLEDに接続されているGPIO25の初期設定を行っている。

メモリの初期化では、.rodata セクションの内容を .data セクションにコピーし、.bss セクションをゼロクリアしている。

クロックやタイマ関係はかなりの設定が必要みたい。よくわかっていないので省略。

ということで、Raspberry Pi pico のブートストラップの流れがだいたいつかめた。アセンブリ言語を真面目に読むのは 8086 以来かなぁ。ちゃんとARMの勉強もしようっと。

ELF バイナリの逆アセンブル

arm-none-eabi-objdump を使うと、ビルドしたELFバイナリを逆アセンブルできるが、セカンドステージ部分がthumbコードとして解釈されず困った。強制的にthumbコードに逆アセンブルする方法は以下の通り。

/usr/bin/arm-none-eabi-objdump -D -m arm -M force-thumb <elfファイル名>

ベクターテーブルもthumbコードになってしまうのが難点。