RP2040(Raspberry Pi Pico) - ブートの仕組みとブートローダ

Written on 2024-06-02

長くなりすぎたので分割された、前の記事の後半。本記事では自作ブートローダを実装するときに固有の技術について説明する。前半では、RP2040のRustでのテンプレートプロジェクトやboot2 bootloaderについて解説している。

実質的に、開発メモ(NOTE.md)の要約である。開発メモなのでまとまっておらず読みにくいが、細かい点はそちらも参照してほしい。

  • Cargoでのマルチターゲットプロジェクトの構成
  • boot2-ram-memcpy(Flashメモリから内蔵SRAMにコピーして動作するboot2プログラム)
  • イメージ操作ツール(自作のイメージ操作ツール、それをブートローダ、ブートされる組み込みアプリケーション、イメージ操作用のコマンドラインツールで兼用する)
  • アドレスを指定したメモリアドレスからの直読み
  • OpenOCD + GDB でデバッグ

汎用的な内容は別記事にまとめた

作りたいブートローダ

マイコン内蔵製品のファームウエアの場合、ファームウエアのアップデートが必要になることが多い。そのような場合、ブートローダとアプリケーションに分割し、通常時はブートローダがアプリケーションを起動し、アップデートが必要な場合はブートローダがアプリケーションを書き換える、という構成をとることが多い。

今回作ろうとするブートローダは、アプリケーションの起動をサポートすること、アプリケーションのアップデートをサポートすることを目標としたい。

以下の説明では、今回作るブートローダの名称として”boot-k”と呼称する。

  • プロジェクトの名称として”boot-k”
  • ブートローダのディレクトリ名は”bootloader”だが、それを呼ぶ時は”boot-k”と呼ぶことにする
  • “boot-k”から実行されるアプリケーションとして、LEDが点滅する”app-blinky”。ディレクトリ名としても”app-blinky”
  • アプリケーションのイメージヘッダをハンドルするライブラリとして”blxlib”。ディレクトリ名としても”blxlib”
  • イメージヘッダを操作するコマンドラインツールとして”bintool”。ディレクトリ名としても”bintool”

Cargoでのマルチターゲット・ワークスペースの構成

このようなことをしたい場合、複数のコンポーネントが連携して、メモリマップやインターフェイスの情報を共有しながら動作する。Rustのプロジェクト管理ツールであるCargoはワークスペースという機能でそれをサポートしている。

ワークスペースの作成

本ブートローダ・システムを実現するためには、ブートローダ、アプリケーションの2つのプロジェクトが必要となってくる。 Cargoのワークスペースの機能をつかい、2つのプロジェクトを1つのワークスペースで管理する。

https://doc.rust-jp.rs/book-ja/ch14-03-cargo-workspaces.html

$ mkdir boot-k
$ cd boot-k
$ code .

ルートのCargo.toml[workspace]members に子プロジェクトを指定する。edition 2021 を使うために resolver = "2" も指定しておく。

[workspace]
members = [
    "bootloader",
    "app-blinky",
]

resolver = "2"

その後、コマンドラインで次を実行すれば bootloader, app-blinky という子プロジェクトが生成される。

この時点で cargo buildcargo clippy などが双方のプロジェクトに対して実行できる。cargo run はどちらのプロジェクトを実行すればよいのかを、ワークスペースの Cargo.tomldefault-run で指定しなければならない。

❯ cargo new --bin bootloader
warning: compiling this new package may not work due to invalid workspace configuration

failed to load manifest for workspace member `/.../boot-k/app-blinky`

Caused by:
  failed to read `/.../boot-k/app-blinky/Cargo.toml`

Caused by:
  No such file or directory (os error 2)
     Created binary (application) `bootloader` package

❯ cargo new --bin app-blinky

     Created binary (application) `app-blinky` package

❯ cargo build               
   Compiling app-blinky v0.1.0 (/.../boot-k/app-blinky)
   Compiling bootloader v0.1.0 (/.../boot-k/bootloader)
    Finished dev [unoptimized + debuginfo] target(s) in 0.52s

❯ cargo test 
   Compiling bootloader v0.1.0 (/.../boot-k/bootloader)
   Compiling app-blinky v0.1.0 (/.../boot-k/app-blinky)
    Finished test [unoptimized + debuginfo] target(s) in 0.08s
     Running unittests src/main.rs (target/debug/deps/app_blinky-72fb7b958e84668f)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running unittests src/main.rs (target/debug/deps/bootloader-2419ad1a1251e783)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

❯ cargo clippy
    Checking bootloader v0.1.0 (/.../boot-k/bootloader)
    Checking app-blinky v0.1.0 (/.../boot-k/app-blinky)
    Finished dev [unoptimized + debuginfo] target(s) in 0.06s

同様に、ライブラリであるblxlibと、コマンドラインツールであるbintoolを作成する。

blxlibはアプリケーションのイメージヘッダのためのlibクレートである。イメージヘッダの構造体とそれを操作する関数たちが定義されている。ブートローダはイメージを操作するためにヘッダの情報が必要だし、アプリケーションも自分自身の情報管理のためにヘッダの情報が必要だ。これらはターゲットであるRP2040上で動作する。

blxlibの中にはCRC32を計算するサブルーチンがある。そのような計算サブルーチンについてはRust標準のテストフレームワーク(mod tests)を使ってテストが構成されている。それはホスト上でcargo testで実行できる。アーキテクチャ依存性を無視すれば、ターゲットで動くコードも、このようにホスト上でテスト可能となる。

❯ cargo test                                   
    Finished test [optimized + debuginfo] target(s) in 0.09s
     Running unittests src/lib.rs (.../boot-k/target/debug/deps/blxlib-12ebb4783387e3a1)

running 2 tests
test crc32::tests::test_crc32 ... ok
test image_header::tests::test_calc_crc32 ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests blxlib

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

また、ホストで動作するイメージ操作のためのコマンドラインツールであるbintoolも作成する。これはホスト(LinuxなりMac)で動作するコマンドラインツールであり、binクレートである。このツールもイメージヘッダの情報が必要であり、同じイメージヘッダのライブラリblxlibを使用する。

Rustは、ターゲットのアーキテクチャに依存する部分が、うまく分離されているので、組み込みターゲットで動くコードでもホストで動くコードでも、このように共用可能となる。イメージヘッダを読み込む”bootloader”(ターゲット上で動作)、イメージヘッダを提供する”app-blinky”(ターゲット上で動作)、イメージヘッダを作成する”bintool”(ホスト上で動作)で、共通のblxlibのコードが使える。

また、練習プロジェクトなど、ビルド対象としないサブディレクトリはexcludeに指定しておく。

[workspace]
members = [
    "bootloader",
    "app-blinky",
    "bintool",
    "blxlib",
]

exclude = [
    "rp2040-project-template",
    "rp2040-boot2",
    "boot-mem",
]

具体的な様子は、プロジェクトのソースコードを参照してほしい。

メモリマップの設計

RP2040のメモリ構成は次のとおりだ。

  • 0x0000_0000 から 16KB(0x400): 内蔵ROM
  • 0x1000_0000 から 2MB(0x20_0000): 外付けFlashがマップされている
  • 0x2000_0000 から 256KB(0x4_0000): 内蔵SRAM
  • 0x4000_0000 移行: ペリフェラルレジスタ

通常のプロジェクト・テンプレートどおりにビルドすると、次のようなメモリマップになる。詳細は前編参照。

address size コンテンツ
0x0000_0000 16K(0x400) Internal ROM
     
0x1000_0000 0x100 .boot2
0x1000_0100 0xc0 .vector_table
0x1000_01c0   .text
     
0x2000_0000 256K(0x4_0000) Internal SRAM
     
0x4000_0000   Peripherals

ブートローダの機能を考慮した場合、次のような構成になる。

Address size Physical project Segment Address size Alias
0x0000_0000 16K( 0x400) Internal ROM     0x0000_0000   ROM_BASE
               
0x1000_0000 2048K(0x20_0000) QSPI Flash(XIP) boot-k .boot2 0x1000_0000 0x100(256B) total 0x2_0000(128KB)
        .vector_table 0x1000_0100 0x0c0(192B)  
        .text 0x1000_01c0    
          0x1002_0000    
      app-blinky .image_header 0x1002_0000 0x100(256B) total 0xe_0000(896KB)
        .vector_table 0x1002_0100 0x0c0(192B)  
        .text 0x1002_01c0    
          0x1010_0000    
      app_update .image_header 0x1010_0000 0x100(256B) total 0xe_0000(896KB)
        .vector_table 0x1010_0100 0x0c0(192B)  
        .text 0x1010_01c0    
          0x101e_0000    
      swap   0x101e_0000 0x2_0000(128KB)  
          0x1020_0000   QSPI_END
               
0x2000_0000 256K( 0x4_0000) SRAM boot-k   0x2000_0000 0x2_0000(128KB) copied boot-k
          0x2002_0000 0x3_2000 work RAM
          0x2004_2000   SRAM_END
               
0x4000_0000   APB Peripherals     0x4000_0000    
0x5000_0000   AHB-Lite Peripherals     0x5000_0000    
0xd000_0000   IOPORT Registers     0xd000_0000    
0xe000_0000   Cortex-M0+ internal registers     0xe000_0000    
               
  • 先頭 0x0000_0000から16KBは内蔵ROM。これは固定。
  • 0x1000_0000から2MB(0x20_0000)は外付けFlash。
    • 外付けFlashの先頭はboot-k。
      • boot-kの先頭0x100(256B)はboot2️。
      • その次の0xC0(192B)はboot-kのvector_table
      • その後にboot-kの実行コード。全体として0x2_0000(128KB)を割り当てる。
    • その後にアプリケーションコード。
      • アプリケーションの先頭は自分で定義したimage_header。0x100(256B)。
      • その次の0xC0(192B)はapp-blinkyのvector_table
      • その後にapp-blinkyの実行コード。全体として0xe_0000(896KB)を割り当てる。
    • その後にアプリケーションコードのアップデート領域
      • この領域にアップデートコードを書き込むと。本来のアプリケーション領域にコピーされる。
    • 残りの外付けFlashの領域 0x2_0000(128KB)はイメージ交換などの作業領域とする。
  • 0x2000_0000から256KB(0x4_0000)は内蔵SRAM。
    • ただし、先頭128KB(0x2_0000)は、boot-kがコピーされて動作する領域とする。
    • それ以降の領域はboot-k、app-blinky のRAM領域(スタック+ヒープ)。boot-kとapp-blinkyは同時には動作しないので、領域を共用して相互に破壊しても問題ない。

このようなメモリマップを設計して、それに向けてコードをビルドしていく。具体的には、リンカスクリプトでセグメントを作成して、固定アドレスに割り当て、コンパイラはそのセグメントに対してコードを出力すれば良い。

ただし、ブートローダの場合、boot2のコードは0x1000_0000から始まるアドレスに書き込まれ、そのアドレスで動作する。しかし、割り込みベクタとテキスト(実行されるコード)は0x1000_01c0から始まるアドレスに書き込まれるが、0x2000_0000から始まるRAMのアドレスにコピーされ、そこで実行される。

コード生成の方針として、リンカスクリプトで次のように指定する。つまりbootは0x1000_0000から0x100バイト、コードは0x2000_0000から0x1_ff00バイト(boot2の分だけ小さくなる)、RAMは0x2002_0000から0x2_0000バイト(textがコピーされるぶんだけスタック+ヒープの割当ては減る)というようにする。

    BOOT2 : ORIGIN = 0x10000000, LENGTH = 0x100
    FLASH : ORIGIN = 0x20000000, LENGTH = 0x1ff00
    RAM   : ORIGIN = 0x20020000, LENGTH = 0x20040000-0x20020000

この状態で、コンパイラの出力(ELF形式)は、.boot2セグメントが0x1000_0000から、コード領域は0x2000_0000から配置されたときに正常に実行されるように内部のアドレスが設定されている。

その後、objcopyで、.boot2とコード領域(.vector_table、.text, .rodata)をバイナリ形式で別々に出力し、catで結合する。

ELF形式は、セグメント情報、アドレス情報などを持っているが、バイナリ形式はメモリ領域をmemcpyなどでベタにコピーしてきた形式をしており、先頭アドレスの情報は別途与えなければならない。

arm-none-eabi-objcopy --only-section=".boot2" \
    -O binary ${target_dir}/bootloader ${target_dir}/boot2.bin
arm-none-eabi-objcopy --only-section=".vector_table" \
    --only-section=".text" --only-section=".rodata" \
    -O binary ${target_dir}/bootloader ${target_dir}/bootloader.bin

cat ${target_dir}/bootloader.bin >> ${target_dir}/boot2.bin

このバイナリ・イメージをprobe-rsで0x1000_0000から始まるアドレスにベタッと書き込めば良い。

probe-rs download --chip RP2040 --protocol swd --format bin --base-address 0x10000000 --skip 0 ${target_dir}/boot2.bin
probe-rs reset --chip RP2040 --protocol swd

すると、.boot2は0x1000_0000で実行可能なコードであり、0x1000_0000に書き込まれ、コード領域は0x2000_0000で実行可能なコードであり、0x1000_0100から書き込まれる。実行が開始されるとboot2がコード領域を0x2000_0000にコピーして、コードは正常に実行される。

ややこしいので整理すると、次のようなことを行っている。

コンパイラの出力

  • 0x1000_0000..0x1000_0100 : boot2(BOOT_LOADER_RAM_MEMCPY)
  • 0x2000_0000.. : bootloader

objdumpで切り出す

  • 0x1000_0000..0x1000_0100 : > boot2.bin
  • 0x2000_0000.. : > bootloader.bin

結合する。binファイルは先頭からのオフセットのみで、アドレスを持たない

  • 0x0000..0x0100 : < boot2.bin
  • 0x0100.. : < bootloader.bin

書き込まれるアドレス(--base-address=0x1000_0000)

  • 0x1000_0000..0x1000_0100 : < boot2
  • 0x1000_0100.. : < bootloader

boot2がSRAMにコピー(コンパイラの出力が再現されている)

  • 0x1000_0000..0x1000_0100 : < boot2
  • 0x2000_0000.. : < bootloader

リロケータブル・コード

どのアドレスにコピーしても動作するコード(マシン語)をPosition Independent Code(リロケータブル・コード)という。GCCの場合は-fPICをつければそのようなコードが生成される。Rustの場合は、.cargo/config.toml[rustflats]"-C", "relocation-model=pic"と書けば良い。

ただし、実際に試してみると、ビルドエラーになる。原因はcortex-m-rtがPICに対応していない。

cortex-m-rtlink.x.gotセクションを定義しており(https://github.com/rust-embedded/cortex-m-rt/blob/master/link.x.in#L176)、.gotセクションがあるとリンクエラーとなる(https://github.com/rust-embedded/cortex-m-rt/blob/master/link.x.in#L242)。

実際に調べてみたところ、関連クレートには.gotを使っているものはいないみたいだ。

というわけで、cortex-m-rtを使わずにリロケータブル・コードを書くか、最初からRAMにコピーされる前提で位置固定のコードを書くか、ということになる。

boot2 と boot-k

なぜboot-kをRAMにコピーしてから実行するのか、という理由は、boot-kから外付けフラッシュを書き込みたいから。

RP2040では外付けフラッシュをコードメモリとして扱うことができ、そのアドレスで実行できる。XIP(eXecute In Place)という。ただし、XIPをONにしていると外付けFlashは読み出し専用となってしまう。ブートローダとして、アプリケーション・イメージのアップデートなどフラッシュの書き換えを行いたい場合、外付けフラッシュをXIPに設定せず、そのままのQSPIメモリとして扱う。ブートローダ自身のコードはSRAMにコピーして、その上で実行する。

フラッシュの書き換えを行う場合でも、メモリにマップされていないので、memcpy()などではなく、QSPI FlashのコマンドをSSIペリフェラルに発行する必要がある。

もともとの boot2_w25q080 の動作

  • 内蔵ROMの初期化ルーチン
  • 外付けFlashの先頭領域にある boot2_w25q080 を実行
    • QSPIの有効化
    • XIPを有効化
    • boot-kにジャンプ
  • boot-kが実行される(Flash上で動作している、この時点ではXIPが有効)

もともとの boot2_ram_memcpy の動作

  • 内蔵ROMの初期化ルーチン
  • 外付けFlashの先頭領域にある boot2_ram_memcpy を実行
    • XIPを有効化
    • Flash領域からRAMにboot-kをコピー
    • XIPを無効化
    • boot-kにジャンプ
  • boot-kが実行される(RAM上で動作している、この時点ではXIPが無効)

ここで問題となるのが、boot2_ram_memcpy からboot-kが起動された時にはXIPが無効化されていることだ。

boot2_w25q080 からboot-kが起動されたときはXIPが有効化されているのでapp-blinkyのヘッダをチェックするときに、アドレスを指定してメモリの内容を直接読むことができた。しかし boot2_ram_memcpy からboot-kが起動されたときはXIPが無効化されているのでapp-blinkyのヘッダをメモリアドレス経由で読み出すことができない。

これに気づかずに、ずいぶんとデバッグにハマってしまった。

こういうふうにしたい。そのために、rp2040-boot2 をローカルツリーにクローンして、XIPを無効化しないように修正してビルド&リンクする。

  • 内蔵ROMの初期化ルーチン
  • 外付けFlashの先頭領域にある boot2_ram_memcpyを実行
    • XIPを有効化
    • Flash領域からRAMにboot-kをコピー
    • boot-kにジャンプ(この時点ではXIPが有効)
  • boot-kの実行(RAM上で動作している)
    • app-blinkyのヘッダをチェック(XIPが有効なのでアプリのヘッダをメモリアドレス経由で読める)
      • アップデートが不要であれば app-blinkyにジャンプ
      • アップデートする必要があれば
        • XIPを無効化
        • アップデート領域からapp-blinky領域にコピー
        • XIPを有効化
        • app-blinkyにジャンプ
  • app-blinkyの実行(Flash上で動作している、この時点ではXIPが有効)

イメージヘッダ操作ツール

“app-blinky”にはイメージヘッダが付加される。bootloaderはイメージヘッダを読み込み、必要な情報を得る。


#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct ImageHeader {
    pub header_magic: u32,  // 4
    pub header_length: u16, // +2 = 6
    pub hv_major: u8,       // +1 = 7
    pub hv_minor: u8,       // +1 = 8

    pub iv_major: u8,  // +1 = 9
    pub iv_minor: u8,  // +1 = 10
    pub iv_patch: u16, // +2 = 12
    pub iv_build: u32, // +4 = 16

    pub image_length: u32,    // +4 = 20
    pub signature: [u8; 128], // +128 = 148

    pub payload_crc: u32,   // +4 = 152
    pub padding: [u8; 100], // +100 = 252

    pub crc32: u32, // +4 = 256
}
  • マジックナンバー
  • ヘッダ長さ
  • バージョン情報
  • イメージのサイズ
  • 署名
  • CRC

通常のビルドの後プロセスとして”app-blinky”にイメージヘッダを付加する必要がある。イメージヘッダを操作するツール(イメージの長さを求め、CRCを計算し、ヘッダを付加する)も自作する必要がある。

同一のソースコードから、bootloaderに組み込むためのイメージ操作ライブラリ(ターゲット環境: ARM/Thumb2)で動作する、と、イメージヘッダを操作するツール(ホスト環境: x64/Linuxだったりaarch64/MacOSだったり)の両方のバイナリが生成される。

このライブラリは“blxlib”と呼んでいる。

Rust組み込みの#[cfg(test)]も実装されていて、ホスト環境でcargo testとすれば、論理的なテストが実施できる。

❯ cargo test   
   Compiling blxlib v0.1.0 (.../boot-k/blxlib)
    Finished `test` profile [optimized + debuginfo] target(s) in 5.17s
     Running unittests src/lib.rs (.../boot-k/target/debug/deps/blxlib-bd447e91ed9b4b41)

running 2 tests
test crc32::tests::test_crc32 ... ok
test image_header::tests::test_calc_crc32 ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests blxlib

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

アドレスを指定したメモリアドレスからの直読み

bootloaderからイメージヘッダを読み込む場合、特定のアドレスから、イメージヘッダに相当する部分を読み込んでパースすることになる。

Rustの場合、core::ptr::slice_from_raw_parts()を使う。

  • 第一引数は*const u8にキャストした先頭アドレスの即値(32ビット整数)
  • 第二引数はusizeにキャストしたメモリの長さ
  • 返り値は*const [u8]というスライス。
  • core::ptr::slice_from_raw_parts()自体はunsafe不要。
  • ただし、戻り値の*const [u8]の参照外しをしてスライスを得るときにはunsafeが必要。

次の例は、メモリアドレス(start_address+image_header::HEADER_LENGTH:ヘッダの終わり)からimage_lengthだけスライスとして読み出して、crc32を計算する例。


pub fn crc32(buf: &[u8]) -> u32 {
    let seed = 0u32;
    let mut out = !seed;
    let mut i = 0usize;
    while i < buf.len() {
        out = (out >> 8) ^ TABLE[((out & 0xff) ^ (buf[i] as u32)) as usize];
        i += 1;
    }
    !out
}


    let slice = core::ptr::slice_from_raw_parts(
        (start_address as usize + image_header::HEADER_LENGTH as usize) as *const u8,
        ih.image_length as usize,
    );
    let payload_crc = crc32::crc32(unsafe { &*slice });

次の例は、スライスとして読み出すのではなく、直接ImageHeaderという構造体にマップして読み出す場合。

pub fn load_from_addr(addr: u32) -> ImageHeader {
    unsafe { ptr::read_volatile(addr as *const ImageHeader) }
}

OpenOCD + GDB でデバッグ

  • OpenOCDは brewでインストール。
  • デバッグアダプタは、汎用のinterface/cmsis-dap.cfgを使えばPicoProbeを駆動することができる。
  • adapter speed 5000も追加。
  • ターゲットはtarget/rp2040.cfgを指定。
❯ sudo openocd -f interface/cmsis-dap.cfg -f target/rp2040.cfg -c "adapter speed 5000"
Password:
Open On-Chip Debugger 0.12.0
Licensed under GNU GPL v2
For bug reports, read
        http://openocd.org/doc/doxygen/bugs.html
adapter speed: 5000 kHz

Info : Listening on port 6666 for tcl connections
Info : Listening on port 4444 for telnet connections
Info : Using CMSIS-DAPv2 interface with VID:PID=0x2e8a:0x000c, serial=E660D4A0A781212F
Info : CMSIS-DAP: SWD supported
Info : CMSIS-DAP: Atomic commands supported
Info : CMSIS-DAP: Test domain timer supported
Info : CMSIS-DAP: FW Version = 2.0.0
Info : CMSIS-DAP: Interface Initialised (SWD)
Info : SWCLK/TCK = 0 SWDIO/TMS = 0 TDI = 0 TDO = 0 nTRST = 0 nRESET = 0
Info : CMSIS-DAP: Interface ready
Info : clock speed 5000 kHz
Info : SWD DPIDR 0x0bc12477, DLPIDR 0x00000001
Info : SWD DPIDR 0x0bc12477, DLPIDR 0x10000001
Info : [rp2040.core0] Cortex-M0+ r0p1 processor detected
Info : [rp2040.core0] target has 4 breakpoints, 2 watchpoints
Info : [rp2040.core1] Cortex-M0+ r0p1 processor detected
Info : [rp2040.core1] target has 4 breakpoints, 2 watchpoints
Info : starting gdb server for rp2040.core0 on 3333
Info : Listening on port 3333 for gdb connections
Info : starting gdb server for rp2040.core1 on 3334
Info : Listening on port 3334 for gdb connections

gdbで接続する場合、ポート3333が core0, 3334が core1となる。

❯ arm-none-eabi-gdb ../target/thumbv6m-none-eabi/debug/bootloader
GNU gdb (GDB) 14.1
Copyright (C) 2023 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "--host=aarch64-apple-darwin22.6.0 --target=arm-none-eabi".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ../target/thumbv6m-none-eabi/debug/bootloader...
(gdb) target remote localhost:3333
Remote debugging using localhost:3333
<signal handler called>
(gdb) monitor reset init
[rp2040.core0] halted due to debug-request, current mode: Thread
xPSR: 0xf1000000 pc: 0x000000ee msp: 0x20041f00
[rp2040.core1] halted due to debug-request, current mode: Thread
xPSR: 0xf1000000 pc: 0x000000ee msp: 0x20041f00
(gdb) disp $pc
1: $pc = (void (*)()) 0xfffffffe
(gdb) continue
Continuing.
[rp2040.core0] clearing lockup after double fault

Program received signal SIGINT, Interrupt.
<signal handler called>
1: $pc = (void (*)()) 0xfffffffe
(gdb)