セカンド・ステージ・ブート・ローダのソースコードを読んでみる
前回および前々回の記事により、Raspberry Pi Pico (以下 Pico) セカンド・ステージ・ブート・ローダのソースコードが boot2_w25q080.S であることがわかりました。今回は、このソースコードを読むのに役立ちそうな前提知識などに触れつつ、実際にソースコードを読んでみます。
なお、いつものことですが、まだ不明点がいろいろあり、内容的には不完全です。きりがないのでこのへんで適当にまとめております。あしからず…
目次
セカンド・ステージ・ブート・ローダとは
前回前々回、「そもそもセカンド・ステージ・ブート・ローダとは何ぞや?」について書くのを忘れていました。ということで、ここに簡単にまとめておきます。
ひとことでいうなら、「フラッシュ・メモリからユーザープログラムを実行できるように準備を調えてくれるプログラム」ということになるかと思います。Pico に搭載されている外部フラッシュ・メモリはシリアル・フラッシュ・メモリであり、SPI (の一種である Quad-SPI) で RP2040 に接続されています。パラレル・フラッシュ・メモリのように直接バスに接続されているわけではないので、そのままではフラッシュ・メモリからプログラムを実行することができません。
そこで、システムの起動時に Quad-SPI を使って通信しながらフラッシュ・メモリの初期設定などを行い、あたかもメモリ空間に直接マップされているかのように見せかける機能 (この機能のことを eXecute In Place: XIP といいます) を有効にするためのプログラムが必要になります。それがセカンド・ステージ・ブート・ローダというわけです。
セカンド・ステージ・ブート・ローダは Raspberry Pi 公式の Pico SDK の一部として提供されており、ソースコードも公開されています (まさにこれから読もうとしているところ)。ちなみに前回前々回も触れましたが、μT-Kernel 3.0 BSP も Pico SDK のセカンド・ステージ・ブート・ローダを借用しています。
Pico のブートシーケンス
Pico のブートシーケンスをもう少し見ていきます。詳しく知りたい方は [1] 2.8.1 節や [2] などを参照してください。
まず Cortex-M0+ では、メモリ空間の 0x00000000 番地にスタック・ポインタの初期値を、その次の 0x00000004 番地にプログラム・カウンタの初期値を格納することに決まっています [3]。つまりリセットがかかると、0x00000004 番地に書かれた値が指す先にあるプログラムが実行されます。Cortex-M0+ の仕様で決まっているのはここまでです。
で、RP2040 ではそのあとどうなるのかというと、Bootrom というプログラムが起動します。0x00000000 番地からの 16 KB は内蔵 ROM となっており、ここに工場出荷時点ですでに Bootrom が書き込まれていて ([1] 2.8 節)、ユーザーが書き換えることはできません。RP2040 を搭載したシステムでは例外なく Bootrom が起動します。ちなみに Bootrom のソースコードも公開されています。
で、起動した Bootrom は、フラッシュ・メモリの先頭から 256 バイト分のデータを SRAM にコピーします。256 バイトのうち末尾の 4 バイトは CRC を付加することに決められていて、無事に CRC チェックをパスすると、コピーしたコードが SRAM から実行されます。もしやりたいことが 252 バイト + CRC に収まる範囲でできるんだったら、無理に Pico SDK とかセカンド・ステージ・ブート・ローダとかを使う必要はなく、自分のプログラムをフラッシュ・メモリの先頭に仕込んでおけば、それが SRAM 上で実行されるわけです。実際、そういうやり方でセカンド・ステージ・ブート・ローダの代わりに L チカを動かしたりして遊んでいる方も居られます [4]。でもたいていは、もっと大規模なプログラムをフラッシュ・メモリ上で動かしたいはずですので、Pico SDK や μT-Kernel 3.0 BSP では Bootrom からセカンド・ステージ・ブート・ローダが起動するようになっています。つまりフラッシュ・メモリの先頭 256 バイトにはセカンド・ステージ・ブート・ローダが配置されています。
ところで、なんでわざわざ 2 段階に分かれているんですかね…?推測するに Bootrom で全部やってしまうと、外部フラッシュ・メモリが部品変更になったときに、Bootrom (内蔵 ROM) まで書き換えが必要になってしまうからだと思いますが、違っていたらすみません。ちなみに Bootrom のほうは標準的な SPI コマンドだけを使用することで、汎用性を確保しているようです ([1] 2.8.1.2 節)。
で、ここからがセカンド・ステージ・ブート・ローダの出番です。
予備知識を得る
セカンド・ステージ・ブート・ローダのソースコードを読むのに役立つかもしれない予備知識をまとめておきます。ポイントは、フラッシュ・メモリからコードを直接実行するしくみである XIP と、XIP に必要な高速かつ連続的なリードを可能にする Quad-SPI かなと思います。これらについては、Pico 向けの資料ではありませんが Microchip 社の資料がとてもわかりやすいので、先に一読されるとよいと思います ([5] 2 節および 3 節)。目にやさしい日本語版です。
これを踏まえて RP2040 データシート [1] 2.8.1.3 節を読んでみます。適当に要約&補足します:
- セカンド・ステージ・ブート・ローダは、XIP のパフォーマンス (インターフェースの幅であるとか、クロック周波数だとか、連続リードモードを有効にするだとか) が最適となるように SSI と外部フラッシュ・メモリを設定する。
- その他の初期設定は、セカンド・ステージが終わったあとに XIP 経由でフラッシュ・メモリから実行されるプログラムでやればいい。
- SSI を設定してからでないと、XIP によるフラッシュ・メモリへのアクセスは不可能であり、プログラムをフラッシュ・メモリ上で実行することもできない。
- よってセカンド・ステージ・ブート・ローダは SRAM にコピーされ、SRAM から実行される必要がある。
SSI というのは Synchronous Serial Interface の略で、SPI 等を使って外部デバイスと通信するためのペリフェラルです ([1] 4.10 節)。セカンド・ステージ・ブート・ローダは、XIP を有効にすることで外部フラッシュ・メモリがメモリ空間にマップされているかのようにお膳立てしてくれますが、それ自体がフラッシュ・メモリに置かれているので、そのままでは実行できません (「にわとりが先か、たまごが先か」みたいな)。そのためまずは SSI を設定のうえ、SPI 通信でセカンド・ステージ・ブート・ローダを SRAM にコピーし、そこで実行してくださいよということです。
実際のソースコード (pico-sdk/src/rp2040/boot_stage2/boot2_w25q080.S) の冒頭には、もう少し具体的な処理内容がコメントで書かれています。ざっくり要約&補足すると:
- W25Q16JV (Pico に搭載されている外部フラッシュ・メモリ) のステータス・レジスタ 2 (SR2) の Quad Enable (QE) ビットをチェックし、立っていなければ立てる。
- そのうえで最初に 1 回だけリードコマンド (0xEB) を送信すれば、その後は指令コードなしで連続リードが可能になる。
0xEB は W25Q16JV のリードコマンドのひとつです。リードコマンドの中では最速であり、XIP 向きのコマンドと言えます。W25Q16JV には 0xEB をはじめとする Fast 系のリードコマンドと、0x03 などの普通のリードコマンドがあります。
「連続リード」というのは、ひとたび指令コードとアドレスを送ったら、あとはこちらからやめと言うまで (たぶんチップセレクト信号 (/CS) をネゲートするまで) どんどんデータを送りつけてくるモードのことです。普通のリードの場合は「指令コード、アドレス、データ、指令コード、アドレス、データ、...」という感じに、ちまちまと指令コードを送ります。
実は普通のリードコマンドでも連続リードは可能なんですが、クロック周波数的には Fast 系のほうが高速に動作します ([6] 9.2.7 節および 10.6 節)。で、Fast 系の中でも 0xEA は Quad-SPI で動作するので、W25Q16JV のリードコマンドの中では 0xEA が最速であり XIP に最適ということになります。
とりあえずこのくらいを頭の片隅に置いて、ソースコードを読んでみます。
ソースコードを読む
ここからは、2025 年 6 月現在の最新リリース版である 2.1.1 のソースコードをベースに読んでいきます。
全体的なフローは次のようになっています。
- 諸々のマクロ定義と前処理
- 信号線の電気的な設定
- SSI の基本設定
- QE ビットを立てる
- ダミーリード
- SSI 再設定 (XIP を有効化)
- 終了処理
1. 諸々のマクロ定義と前処理
冒頭 34 ~ 83 行目はフラッシュ・メモリの設定に関するマクロを定義しています。
PICO_FLASH_SPI_CLKDIV
SSI:BAUDR レジスタにセットする値の定義。詳しくは [1] 4.10.4 節を参照。
FRAME_FORMAT
SSI:CTRLR0 レジスタの SPI_FRF にセットする値の定義。値は 0x2 (Quad-SPI frame format) を指定している ([1] p.600)。
CMD_READ
連続リードを有効にするために最初に送るリードコマンドの定義。ここはもちろん 0xEB を指定する。
MODE_CONTINUOUS_READ
モードビットの定義。連続リードモードに入るには、
CMD_READ
で指定したコマンドに続けて 24 ビットのアドレスと、8 ビットの「モードビット」を送信する ([6] 9.2.11 節)。 値は 0xA0 を指定しているが、W25Q16JV のデータシートに 0xFx を指定せよと書かれている ([6] 9.1.3 節) のと合致しない。理由は不明だが、とにかく RP2040 では 0xA0 を送信することになっている ([1] 4.10.3 節などを参照)。ADDR_L
SSI:SPI_CTRLR0 レジスタの ADDR_L にセットする値の定義。連続リードモードでのアドレス長が 32 ビットとなるように指定する ([1] p.592, 607)。具体的には
8
。WAIT_CYCLES
連続リードモードに入る際に、デバイス側 (フラッシュ・メモリ) がデータ送信を開始するまでに要するクロックサイクル数 ([1] 4.10.10.4.2 節および [6] 9.2.11 節)。値は 4 を指定している。
PROGRAM_STATUS_REG
QE ビットを立てるために W25Q16JV のステータス・レジスタ 2 に書き込みを行う処理を有効にするかどうか、の設定と思われる。デフォルトは有効。有効化されている場合はこのあとの「4. QE ビットを立てる」の処理が行われる。
CMD_WRITE_*
,CMD_READ_*
W25Q16JV に送信する諸々のコマンドの定義。
SREG_DATA
W25Q16JV のステータス・レジスタ 2 の QE ビット ([6] p.14)。
そのあと、89 行目で pico_default_asm_setup マクロを呼んでいます。ここはまあ本稿にはあまり関係のないところですが、実体は src/rp2040/pico_platform/include/pico/asm_helper.S で定義されているので、興味があれば読んでみてください。いくつかの疑似命令からなっています。疑似命令については GNU Binutils のマニュアルを参照してください ([7] 9.4.4 節)。
で、100 行目から始まる _stage2_boot がセカンド・ステージ・ブート・ローダのメインルーチンになります。その次の行で LR (リンク・レジスタ) をスタックにプッシュしています。すぐ上のコメントに書かれているように、セカンド・ステージ・ブート・ローダが Bootrom から起動された場合、LR には 0 (NULL) がセットされます。その場合、セカンド・ステージ・ブート・ローダが終了したあとは 0x10000100 番地 (つまりフラッシュ・メモリ上でセカンド・ステージ・ブート・ローダが置かれている領域のすぐ後ろ) に置かれているユーザー・アプリケーション用のベクタ・テーブルに基づいて、ユーザー・アプリケーションが起動します (より詳しくは「終了処理」のところでやります)。
2. 信号線の電気的な設定
103 ~ 116 行目は Quad-SPI 通信に用いる 6 本の信号線 (QSPI_SCLK、QSPI_SS、QSPI_SD0 ~ QSPI_SD3) の電気的な設定をしています。駆動能力 (どれだけの電流を流すことができるか)、スルー・レート、シュミットトリガの有無などを設定していると思われますが、本稿にとって興味あるところではなさそうなので飛ばします。詳細は [1] 2.19.4 節などを参照してください。
3. SSI の基本設定
118 ~ 136 行目が SSI の基本設定です。次の工程で SPI 通信を使って W25Q16JV からレジスタの値を読み出さなきゃならないので、そのための設定をしています。まず 118 行目で汎用レジスタ R3 に SSI 関連レジスタのベースアドレス 0x18000000 をセットしています。
以降、SSI 関連レジスタの番地は、このベースアドレスからのオフセットで指定されます。なお、ここで使われている LDR は CPU への命令ではなくアセンブラへの疑似命令です ([7] 9.4.5 節)。
120 ~ 122 行目で、SSI の設定変更をするために SSI を一旦無効化しています。具体的には、SSIENR レジスタに 0 を書き込むことで無効にしています ([1] p.601)。
// Disable SSI to allow further config
movs r1, #0
str r1, [r3, #SSI_SSIENR_OFFSET]
出典: raspberrypi/pico-sdk (GitHub)なお、[Rn, offset]
という記法は、ベースレジスタRn
の値にoffset
の値を加算または減算した結果をアドレスとして、メモリにアクセスすることを意味します ([8] A6.5 節)。
その後、124 ~ 126 行目でボーレートを設定し (具体的には、上のほうで定義しておいた PICO_FLASH_SPI_CLKDIV の値を SSI:BAUDR レジスタに書き込む)、128 ~ 136 行目で受信データのサンプリング遅延を設定しています (具体的には、遅延のためのクロックサイクル数 1 を SSI:RX_SAMPLE_DLY レジスタに書き込む)。受信データのサンプリング遅延については [1] 4.10.9.1.1 節を参照してください。
4. QE ビットを立てる
次にやることは W25Q16JV 側の Quad-SPI を有効にすることです。そのために、ステータス・レジスタ 2 (SR2) の QE ビットを立てます。大まかなフローは次のとおり:
まず 143 ~ 148 行目で SPI のデータ転送に関する設定を行います。具体的には、SSI:CTRLR0 レジスタを次の表のように設定します:
ビット | 名称 | 説明 | 設定値 | 意味 |
---|---|---|---|---|
20:16 | DFS_32 | 32 ビット転送モードでのデータフレームサイズ | 7 | 8 ビット |
9:8 | TMOD | 転送モード | 0x0 | 送受信 |
32 ビット転送モードでのデータフレームサイズは、設定値が n ならデータフレームサイズは (n + 1) クロック / フレームとなります。SPI では 1 クロックにつき 1 ビットということなので [9]、ここでのデータフレームサイズは 8 ビットとなります。32 ビット転送モードについては [1] 4.10.6 節などを参照してください。
転送モードは「送受信モード」「送信オンリーモード」「受信オンリーモード」「EEPROM リードモード」の 4 つがあり、設定値が 0 の場合は送受信モードとなります ([1] 4.10.8 節)。
このあと W25Q16JV のレジスタを読んだり書いたりするので、8 ビットのデータを送受信できるように設定しているのだと思います。ちなみに、表にはありませんがビット 22:21 の SPI_FRF は SPI 通信が Dual か、Quad か、ノーマル (単線) かを指定する項目で、ここではデフォルトの 0、つまりノーマルな SPI 通信を使うように設定されます。
その後、150 ~ 152 行目で SSI を有効化して、SPI で通信できる状態にします。
154 ~ 159 行目、Read Status Register-2 コマンド (0x35) で SR2 を汎用レジスタに読み出し、SREG_DATA の値 (0x02、QE ビットだけが立っている状態) と比較します。その結果、QE ビットが立っていたらskip_sreg_programming
のラベルがあるところまで処理をスキップします。
// Check whether SR needs updating
movs r0, #CMD_READ_STATUS2
bl read_flash_sreg
movs r2, #SREG_DATA
cmp r0, r2
beq skip_sreg_programming
出典: raspberrypi/pico-sdk (GitHub)read_flash_sreg() は src/rp2040/boot_stage2/asminclude/boot2_helpers/read_flash_sreg.S で定義されているサブルーチンで、R0 で渡されたリードコマンドを送信し、返ってきたデータをまた R0 に格納します (付録 B 参照)。
161 ~ 187 行目は、QE ビットが立っていなかった場合の処理です。
ステータス・レジスタに書き込みを行いたいので、まず 162、163 行目で Write Enable コマンド (0x06) を送信します ([6] 9.2.1 節)。次の 166 行目で送信用 FIFO が空かつビジー状態ではなくなるのを待ちます (付録 B 参照)。167 行目は受信データを読み捨てする処理ですが、なんで Write Enable コマンドの直後にあるのかはよくわかりません。念のためここで受信 FIFO を空にしておきたいという意味かなと思います。
// Send write enable command
movs r1, #CMD_WRITE_ENABLE
str r1, [r3, #SSI_DR0_OFFSET]
// Poll for completion and discard RX
bl wait_ssi_ready
ldr r1, [r3, #SSI_DR0_OFFSET]
出典: raspberrypi/pico-sdk (GitHub)wait_ssi_ready() については付録 A を参照してください。
次の 169 ~ 174 行目で実際にステータス・レジスタへの書き込みを行っています。コマンドは Write Status Register-1 (0x01) を使用し、SR1 と SR2 をまとめて書き込みます ([6] 9.2.5 節)。R0 には SR1 用のデータ「0」をセットしています。0 で構わないのかどうかはよくわかりません。R2 には SR2 用のデータとして QE ビットを立てた「0x02」が 157 行目ですでにセットされています。
// Send status write command followed by data bytes
movs r1, #CMD_WRITE_STATUS
str r1, [r3, #SSI_DR0_OFFSET]
movs r0, #0
str r0, [r3, #SSI_DR0_OFFSET]
str r2, [r3, #SSI_DR0_OFFSET]
出典: raspberrypi/pico-sdk (GitHub)ところで、ここでは SR2 だけに書き込みたいところ、Write Status Register-1 コマンド (0x01) を使って SR1、SR2 をまとめて書き込んでいるのはちょっと不思議です。Write Status Register-2 コマンド (0x31) を使えばいいのでは?理由はたぶんですが、Winbond (W25Q16JV のメーカー) 製の過去製品との互換性を保つためと思われます ([6] p.25 参照)。
181 ~ 187 行目で、SR1 を読み出して BUSY ビットをチェックし、W25Q16JV のビジー状態が解消されるのを待ちます。既出の read_flash_sreg() をここでも使っています。個人的には「仮に BUSY が解消されなかったら永久ループになるのでは?」とか「SR2 の QE が立ったか確認しなくてもいいのかな?」と思わなくもないですが、あまり深く考えないことにします。
で、ようやくskip_sreg_programming
のラベルがあるところまで来ました (189 行目)。このパートの最後に、次の工程に備えて SSI をまた無効にしています (191 ~ 193 行目)。
5. ダミーリード
196 行目からのコメントにあるとおり、連続リードを有効にするためにダミーリードを行います。大まかなフローは次のとおり:
ダミーリードは、Fast Read Quad I/O コマンド (0xEA) と、それに続く 32 ビットデータ (リードアドレスとモードビットからなる) を送ることで行います。
最初はしばらくレジスタの設定が続きます。まず、203 ~ 211 行目で SSI:CTRLR0 レジスタを次のように設定しています。
ビット | 名称 | 説明 | 設定値 | 意味 |
---|---|---|---|---|
22:21 | SPI_FRF | SPI フレームフォーマット | 0x2 | Quad-SPI |
20:16 | DFS_32 | 32 ビット転送モードでのデータフレームサイズ | 31 | 32 ビット |
9:8 | TMOD | 転送モード | 0x3 | EEPROM リード |
SPI フレームフォーマットが Quad-SPI、データフレームサイズが 32 ビット、転送モードが EEPROM リードモードとなるように設定されています。EEPROM リードモードでは、SSI:CTRLR1 レジスタの NDF (Number of data frames) で設定した値に 1 を足した数だけデータフレームを受信するまで転送が継続するモードです ([1] 4.10.8.4 節)。
というわけで、213、214 行目を見ると SSI:CTRLR1 レジスタの NDF に 0 をセットしています。ダミーリードなので、こちらから送った 0xEA コマンドに対してデータフレームが 1 個返ってきたら転送が終わるような設定になっています。返ってきたデータは読み捨てです。中身が何かは気にしません。
216 ~ 226 行目は SSI:SPI_CTRLR0 レジスタです。
ビット | 名称 | 説明 | 設定値 | 意味 |
---|---|---|---|---|
15:11 | WAIT_CYCLES | ウェイトサイクル数 | 4 | 本文参照 |
9:8 | INST_L | 命令長 | 0x2 | 8 ビット |
5:2 | ADDR_L | アドレス長 | 0x8 | 32 ビット |
9:8 | TRANS_TYPE | 送信タイプ | 0x1 | 本文参照 |
ウェイトサイクル数は 69 行目で定義しておいた値をセットします。命令長は 8 ビット、アドレス長は 32 ビットとなるように設定します (アドレス長については [1] p.592 の Table 578 を参照)。送信タイプには、命令とアドレスを送信する際のフォーマットを指定します。上の表のように 0x1 を指定すると、命令に対してはノーマル (単線) な SPI が、アドレスに対しては SSI:CTRLR0 レジスタの SPI_FRF で指定したフォーマットが使われます。今の場合は 204 行目で指定しておいた Quad-SPI ですね。
これでレジスタの設定が済んだので、228、229 行目で SSI を再度有効化し、231 ~ 234 行目で 0xEA コマンドと、アドレス&モードビット (0x000000A0) を送信しています。ダミーリードだからアドレスは何でもよいということで、0 をセットしています。モードビットに関しては、上のほうでも書きましたが、なぜか W25Q16JV のデータシートで指定されている 0xFx ではなく 0xA0 を送っています。
237 行目で送信完了するまで待ったら、このパートは終わりです。
6. SSI 再設定 (XIP を有効化)
ここまでで Quad-SPI による連続リードが可能な状態になりました。残るは XIP によるフラッシュ・メモリへのアクセスが Quad-SPI 転送に変換されるよう SSI を設定することです。上にも書いたとおり、XIP モードでは読み出しの際にいちいちリードコマンドを送ったりしないので、そのように設定します。
具体的には、前工程でも登場した SSI:SPI_CTRLR0 レジスタを次のように書き換えます (251 ~ 263 行目)。
ビット | 名称 | 説明 | 設定値 | 意味 |
---|---|---|---|---|
31:24 | XIP_CMD | XIP モードにおけるコマンド | 0xA0 | 本文参照 |
15:11 | WAIT_CYCLES | ウェイトサイクル数 | 4 (変更なし) | ― |
9:8 | INST_L | 命令長 | 0 | コマンドなし |
5:2 | ADDR_L | アドレス長 | 0x8 (変更なし) | 32 ビット |
9:8 | TRANS_TYPE | 送信タイプ | 0x2 | 本文参照 |
ウェイトサイクル数とアドレス長は前工程の設定を引き継ぎます。XIP モードでも、アドレス 24 ビット + モードビット 8 ビットで 32 ビットであることに変わりはありません。
変わったのは XIP_CMD、INST_L、TRANS_TYPE の 3 つです。XIP_CMD には、INST_L が 8 ビットのときは XIP モードにおいて使用するリードコマンドを指定します。デフォルトは 0x03 (Read Data コマンド) です。ところが上の表のように INST_L が 0 に設定されているときは、アドレスの後ろに付加するモードビットを指定します ([1] p.607)。よってここでは INST_L が 0、XIP_CMD が 0xA0 となっています。
送信タイプ 0x2 は、命令、アドレスともに SSI:CTRLR0 レジスタの SPI_FRF で指定したフォーマットで送信することを意味します。つまりここから先はコマンドもアドレスも Quad-SPI で送信されます。
7. 終了処理
セカンド・ステージ・ブート・ローダの本来の役目はこれで終わりですが、272 行目にある終了処理も重要だと思いますので、最後の力を振りしぼってやっておきます。
現状を確認しておくと、図 4 (a) のようになっています。
- フラッシュ・メモリが XIP によって 0x10000000 番地からのメモリ空間にマップされた。
- フラッシュ・メモリの先頭 256 バイトにはセカンド・ステージ・ブート・ローダが置かれている。
- その後ろにはユーザー・アプリケーションが置かれている (そうなるようにリンカ・スクリプトを書いてビルドする)。
- SRAM の末尾には Bootrom によってコピーされたセカンド・ステージ・ブート・ローダがあって、現在実行中。
ユーザーアプリケーションの先頭には、Cortex-M0+ の仕様に則ってベクタ・テーブルがあり、最初のエントリがメイン・スタック・ポインタ (MSP) の初期値、次のエントリがリセット・ハンドラへのポインタになっています。
セカンド・ステージ・ブート・ローダは、この 2 つの値を読み出して MSP を初期化し、リセットハンドラへ分岐して終了します (図 4 (b))。
ソースコードは boot2_w25q080.S の 272 行目でインクルードしている exit_from_boot2.S にあります。
最初に、boot2_w25q080.S の 101 行目でスタックに積んでおいた LR の値を R0 に pop し、値が 0 であればその後の処理を継続します (16 ~ 18 行目)。セカンド・ステージ・ブート・ローダを Bootrom から起動した場合はそうなります。0 以外だった場合は (どういう場合にそうなるのかよくわかりませんが)、その値が指す番地に分岐して終了します (19 行目)。
で、21 ~ 23 行目で、VTOR レジスタ ([1] p.86) にユーザー・アプリケーションのベクタ・テーブルのアドレス (0x10000100) をセットしています。これにより、以降はユーザー・アプリケーションが用意したベクタ・テーブルが使用されます。
ldr r0, =(XIP_BASE + 0x100)
ldr r1, =(PPB_BASE + M0PLUS_VTOR_OFFSET)
str r0, [r1]
出典: raspberrypi/pico-sdk (GitHub)24 行目で、0x10000100 から 2 ワード分の値を読み出し、それぞれ R0、R1 に格納します。つまり R0 には MSP が、R1 にはユーザー・アプリケーションのリセット・ハンドラのアドレスが格納されている状態です。25 行目で MSP を初期化し、26 行目でリセット・ハンドラへ分岐して終わります。
ldmia は ldm と同じ機能を持つ疑似命令です ([8] A6.7.25)。上の場合は、R0 に格納されているアドレス (0x10000100) から 2 ワード分を読み出して、それぞれ R0 と R1 に格納しています。
msr は汎用レジスタから特殊レジスタへ値をコピーする命令です ([8] A6.7.43)
おわりに
セカンド・ステージ・ブート・ローダが何をやっているのかを理解したくて、前々々々々々回くらいから細々とやってきましたが、ようやくひと区切りです。その間には Pico 2 が登場し、Pico SDK は 2.0 にメジャーバージョンアップし、ソースツリーの構造が変わって、セカンド・ステージ・ブート・ローダのソースファイルの場所も変わってしまいました (涙)。まあめげずにコツコツやっていこうと思います。
付録 A: wait_ssi_ready()
wait_ssi_ready() は SPI 通信において RP2040 側で送信完了を待つためのサブルーチンです。ソースファイルは wait_ssi_ready.S です。
やっていることは、まず 15 ~ 19 行目で SSI:SR レジスタ ([1] p.603) の TFE ビットにて送信 FIFO が空になったかを確認し、空なら次へ進みます。空でなければ、空になるまでチェックを繰り返します。
1:
ldr r1, [r3, #SSI_SR_OFFSET]
movs r0, #SSI_SR_TFE_BITS
tst r1, r0
beq 1b
出典: raspberrypi/pico-sdk (GitHub)次に、20 ~ 22 行目で同じく SSI:SR レジスタの BUSY ビットにて SSI がビジーであるかを確認し、ビジーでなければ終了します。ビジーであれば、ビジーでなくなるまでチェックを繰り返します。
movs r0, #SSI_SR_BUSY_BITS
tst r1, r0
bne 1b
出典: raspberrypi/pico-sdk (GitHub)付録 B: read_flash_sreg()
read_flash_sreg() は Read Status Register コマンドを使ってフラッシュ・メモリのステータス・レジスタを読み出すためのサブルーチンです。ソースファイルは read_flash_sreg.S です。
入力として、使用するコマンド (0x05、0x15、または 0x35) を R0 に、SSI 関連レジスタのベースアドレス(0x18000000) を R3 に受け取ります。出力として、ステータス・レジスタから読み出した値を R0 に格納します。
19 ~ 21 行目で早速 Read Status Register コマンドを送信します。なぜか同じコマンドを 2 回送っています。
str r0, [r3, #SSI_DR0_OFFSET]
// Dummy byte:
str r0, [r3, #SSI_DR0_OFFSET]
出典: raspberrypi/pico-sdk (GitHub)で、23 行目で送信完了待ちしたあと、25、26 行目で受信データを R0 に受け取っていますが、コマンドを 2 回送信したので、受信も 2 回行っています。1 回目は読み捨てです。2 度読みして一致するかを確認しようとしたんでしょうか?
// Discard first byte and combine the next two
ldr r0, [r3, #SSI_DR0_OFFSET]
ldr r0, [r3, #SSI_DR0_OFFSET]
出典: raspberrypi/pico-sdk (GitHub)とにかくこうして R0 にステータス・レジスタの値を取得することができました。
参考資料
[1] | RP2040 データシート Rel. 1.8 https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf |
[2] | CQ 出版『インターフェース』2023 年 7 月号 p.49, 129, pp.132-135 |
[3] | Cortex-M0+ Devices Generic User Guide https://developer.arm.com/documentation/dui0662/b/The-Cortex-M0--Processor/Exception-model/Vector-table |
[4] | dwelch67/raspberrypi-pico - Raspberry Pi pico baremetal examples https://github.com/dwelch67/raspberrypi-pico |
[5] | Cortex-M7 MCU への QSPI メモリによる XIP (Execution-InPlace)機能の MPLAB Harmony® v3 を使った実装 https://ww1.microchip.com/downloads/jp/Appnotes/00003443A_JP.pdf |
[6] | W25Q16JV データシート Rev.H https://www.winbond.com/hq/product/code-storage-flash-memory/serial-nor-flash/?__locale=ja&partNo=W25Q16JV |
[7] | Using as https://sourceware.org/binutils/docs/as.html |
[8] | ARMv6-M Architecture Reference Manual Rev.E https://developer.arm.com/documentation/ddi0419/e/ |
[9] | Quad SPIって何? https://edn.itmedia.co.jp/edn/articles/1511/18/news018_2.html |
広告