Raspberry Pi Picoのリセットからmain()までを追う

githubにはRaspberry Pi Pico(以下Pico)のブートROMを含めたすべてのソースコードが公開されています。
そこでgithub上のコードを参照しながら、電源が投入されてCPUが動作を開始してからPico SDKを使ってユーザが書いたアプリケーションのmain()が呼び出されるまでの流れを追ってみます。

# ブートROM

Picoに搭載されているSoC、RP2040のアドレス 0x00000000 からにはブートROMが置かれています。これはRP2040内に埋め込まれていて書き換えることはできません。

### リセット

- ソースコード
    - https://github.com/raspberrypi/pico-bootrom/blob/master/bootrom/bootrom_rt0.S

PicoのCPUであるArm Cortex-M0+は、リセットされるとメモリの 0x00000000 からに置かれているベクタテーブルを読み出すところから動作を開始します。
ベクタテーブルの最初の2ワードには、以下の値を置くことになっています。

- 0x00000000 : スタックポインタ(MSP)の初期値
- 0x00000004 : プログラムカウンタ(PC)の初期値

CPUはこれらの値を読み出し、PCの初期値として与えられた `_start` から実行を開始します。`_start` には以下のような処理が書かれています。

- 自分自身のCPU番号を調べて、Core 1なら`wait_for_vector`に飛ぶ
    - RP2040には2つのCortex-M0+が搭載されていますが、これらはリセットされると2つとも同時に動き始めます。
    - このままだと都合が悪いので、まず最初に自身がCore 0かCore 1かを調べて、Core 1なら後でCore 0から起動の指示があるまで待ちに入ります。
    - Pico SDKの[pico_multicore API](https://raspberrypi.github.io/pico-sdk-doxygen/group__pico__multicore.html)などが使われるまで、Core 1はずっとこの状態です。以降のコード実行はCore 0のみが行います。
- Core0だった場合はC言語で書かれたブート処理である、ブートROM内の`main()`へ飛ぶ

### ブートROMのmain()

- ソースコード
    - https://github.com/raspberrypi/pico-bootrom/blob/master/bootrom/bootrom_main.c

Picoに搭載されているフラッシュメモリから起動するか、USBマスストレージデバイスとして起動してフラッシュ書き込みモードに移行するかの処理を行います。

- 起動に必要な最低限の周辺回路のリセット
- フラッシュメモリが接続されているかを確認
    - Pico基板上のBOOTSELボタンはこの接続を回路上で切って、強制的に「フラッシュメモリが接続されていない状態」にします。これによりUSB起動モードに移行できるようにしています。
- フラッシュメモリが存在したら`_flash_boot()`へ
- フラッシュメモリが存在しなかったら`_usb_boot()`へ
    - USBデバイスとして起動するのに必要な初期化を行って、フラッシュ書き込みモードに移行します。
 
`_flash_boot()`では以下のような処理を行います。

- フラッシュメモリ読み出しのための初期化
    - Picoで使用されているのはSPI接続のNOR Flashなので、SPI読み出しのための設定を行います。
- 最初の256バイトをSRAMに読む(boot2)
    - SRAMの末尾256バイトに読み込まれます。
- 正しく読めてチェックサムが合っていたらboot2の先頭アドレスへジャンプ
- 読めなければなにもしないで戻る
    - `_usb_boot()`によるUSB起動へ移行します。

# Boot Stage2 (フラッシュメモリ上)

ブートROMによってSRAMに読み込まれた、フラッシュメモリの先頭256バイトに置かれているBoot Stage2という2段階目のブートローダを実行します。
これはPico SDKでアプリをビルドするとバイナリの先頭に必ず置かれるようになっていて、アプリをフラッシュに書き込むことでフラッシュメモリ先頭に書き込まれます。

### フラッシュメモリのモード切り替え

- ソースコード
    - https://github.com/raspberrypi/pico-sdk/blob/master/src/rp2_common/boot_stage2/boot2_w25q080.S

フラッシュメモリのアクセスモードを最も高速にアクセスできるように切り替えて、システムをXIPモードという、CPUのメモリアドレス0x10000000～にフラッシュの内容が見えるようなモード[^1]に切り替えます。

[^1]: フラッシュメモリ自体はSPIインターフェースでSoCに接続されているので、このままではCPUのアドレス空間としては見えません。そこで、SoCの機能で「CPUが特定のアドレス領域をアクセスしたら、対応する領域をSPIフラッシュから読みだしてSRAMキャッシュメモリ上に展開し、それをCPUに見せることで、あたかもフラッシュメモリの内容がそのまま配置されているように見せかける」といった動作を行います。

フラッシュメモリのモード切り替えのコードがフラッシュメモリ自身にあるのは、このモード切り替えの方法がフラッシュメモリの品種によって微妙に異なるからです。
特定のフラッシュメモリに特化したコードをRP2040のROMの中に入れてしまうとそのメモリしか繋げなくなってしまうので、そうしたコードをフラッシュメモリ側に置いて、ROMからはどのフラッシュメモリでも共通な(その代わり遅い)アクセスコマンドでBoot Stage2のみをSRAMに読みだして実行することで、異なるフラッシュメモリが繋がれた場合もBoot Stage2の差し替えで対応できるようにしています。

Picoの基板上に搭載されているフラッシュメモリは[W25Q16JV](https://www.winbond.com/hq/product/code-storage-flash-memory/serial-nor-flash/?__locale=ja&partNo=W25Q16JV)というもので、上記ソースコードはこのメモリに対応したものになっています。
Boot Stage2の[ソースコードのあるフォルダ](https://github.com/raspberrypi/pico-sdk/tree/master/src/rp2_common/boot_stage2)には他にもいくつかのコードが置かれていて、他のフラッシュメモリが使われる場合も考慮しているようです。

### SDKへジャンプ

- ソースコード
    - https://github.com/raspberrypi/pico-sdk/blob/master/src/rp2_common/boot_stage2/asminclude/boot2_helpers/exit_from_boot2.S

モード切り替えによって、フラッシュメモリの内容がCPUのメモリアドレス 0x10000000～ から見えるようになります。Boot Stage2の直後、0x10000100 を新たなベクタテーブルとして設定し、MSPとPCの値を読んで実行を開始します。

# SDK

メモリアドレス 0x10000100～ には、ユーザが作成するアプリとそれを実行できるようにするためのランタイムライブラリが置かれます。
ランタイムライブラリがユーザアプリを起動するために必要な周辺の初期化とクロック設定を行い、最終的にユーザの書いた`main()`を呼び出します。


### SDKスタートアップ

- ソースコード
    - https://github.com/raspberrypi/pico-sdk/blob/master/src/rp2_common/pico_standard_link/crt0.S

Boot Stage2の直後に置かれるベクタテーブルとSDKのスタートアップ処理が書かれています。
スタートアップ処理のある`_reset_handler`から以下を実行します。

- CPU番号を調べて、Core 1ならブートROMに戻る
    - デバッガなどでROMを通らずにここに飛んできたケースを想定しているものと思われます。
- dataセクションの初期値コピー、bssセクションのクリア
    - 以降のC言語で書かれたコードが実行できるようにするための準備を行います。
- `runtime_init()`、`main()`、`exit()`を呼ぶ
    - これら、C言語で書かれた処理を実行します。

### SDK初期化

- ソースコード
    - https://github.com/raspberrypi/pico-sdk/blob/master/src/rp2_common/pico_runtime/runtime.c

SDKスタートアップから最初に呼び出される`runtime_init()`の処理です。
SDKを使って書かれたアプリを実行できるようにするための、以下の初期化を行います。

- 周辺デバイスのリセット
- preinit_array呼び出し
    - 特定のセクションに置かれた関数ポインタを通して追加の初期化コードを実行します。
    - 現状のSDKではこれを使っているコードはなさそうです。
- クロック初期化
    - CPU自身と全周辺デバイスを通常のクロック周波数で動作するように初期化します。
    - リセット直後は、CPUはRP2040内のリングオシレータ(ROSC)というクロック源で低速(6.5MHz程度)で動作していますが、ここで外部接続されたクリスタル(XOSC)とSoC内のPLLを初期化することで、通常の動作周波数である125MHzで動作するようになります。 
- ベクタテーブルの再初期化
    - SDK初期化時に設定されているベクタテーブルはフラッシュメモリ上にあって書き換えができないため、SRAMに内容をコピーしてこちらを使うように再設定します。
    - これにより、SDKアプリから割り込みハンドラを登録できるようになります。
- init_array呼び出し
    - C++のグローバルコンストラクタは`main()`より先に実行されるようになっています。コンパイル時にこうしたコンストラクタへの関数ポインタがinit_arrayセクションに置かれるようになっているため、ここにポインタがあればその関数を呼び出します。

### main()

- ソースコード
    - ユーザ自身が記述するmain()です。

ようやくたどり着きました。ここからがユーザがSDKを使って作成したアプリの`main()`関数の処理になります。