pico-debugを使って1枚のRaspberry Pi Picoで自分自身をデバッグしてみる

[pico-debug: a built-in CMSIS-DAP debugger for the RP2040](https://blog.adafruit.com/2021/02/05/pico-debug-a-built-in-cmsis-dap-debugger-for-the-rp2040-raspberrypipico-debugging-raspberry_pi/) というブログ記事を見つけました。
Raspberry Pi Pico(以下Pico)には2つのCortex-M0+ CPUが搭載されていますが、[pico-debug](https://github.com/majbthrd/pico-debug/)は、その2つのうち片方のコアでデバッグプローブソフトウェアを動かして、もう片方のコアに対するJTAG(SWD)デバッグ機能を提供するというソフトウェアのようです。
公式の[Getting started with Raspberry Pi Pico](https://datasheets.raspberrypi.org/pico/getting-started-with-pico.pdf)ドキュメントに載っているpicoprobeは2枚のPicoの片方で動かしたデバッグプローブでもう1枚のPicoのデバッグを行うというものですが、1枚で自分自身をデバッグできるというのは面白い。ということで試しに動かしてみました。

…が、結論から先に言うと、gdbでもう片方のコアに繋げられはするのですが様々な制約が山盛りで、少なくとも現状ではPico SDKで作ったアプリを普通にデバッグする目的には使えません。
やったことの記録として記事にはしていますが、特に実用になりそうな情報はないと思いますので「こんなこと出来て面白い」という読み物程度に思っていただければ。**普通にデバッグしたい場合は素直にPicoを2枚揃えた方がずっと手っ取り早くて確実です。:-)**

# 前提条件

- [Raspberry Pi PicoをWSL(Windows Subsystem for Linux)で使う](https://qiita.com/yunkya2/items/e67996301a2d8d2a5609)
- [Raspberry Pi PicoをWSL+OpenOCDでJTAG(SWD)デバッグする](https://qiita.com/yunkya2/items/4e3d89f08b2237ef551f)

以上の記事でWSL上に環境を構築してあることを前提とします。というかドキュメントの推奨環境どおりラズパイ4を使っているのなら、ラズパイ4のGPIOを使ってSWDデバッグをやる方法が載っているのでPicoが1枚でも別に困らないのでした(笑)。

# pico-debugの仕組み

Picoは基板上にDEBUG端子としてSoC RP2040のSWD信号が出ているので、picoprobeではこれをもう1枚のPicoのGPIOに繋いでこれを操作することでデバッグを行っています。
実はこのSWD信号はチップ内のI/Oレジスタからも操作できるようになっていて、[RP2040 Datasheet](https://datasheets.raspberrypi.org/rp2040/rp2040-datasheet.pdf)の **2.21. Syscfg** に説明が載っています。SYSCFG: DBGFORCE Register がそのレジスタです。
pico-debugはこれをソフトウェアから操作することで、SoC内のもう片方のコアをデバッグしています。

picoprobeはUSBを介してのOpenOCDへの接続に独自のプロトコルを使っているようですが、pico-debugはArm社が策定した[CMSIS-DAP](https://arm-software.github.io/CMSIS_5/DAP/html/index.html)というデバッグインターフェース規格のライブラリを使用することで、CMSIS-DAP対応デバッガとして見えるようになっています。

# pico-debugのインストール

[pico-debug](https://github.com/majbthrd/pico-debug/)はgithub上で開発が行われています。
ビルド済みバイナリが[Releases](https://github.com/majbthrd/pico-debug/releases)ページに置いてありますので、*.uf2ファイルを取得して通常のPico SDKアプリと同様にBOOTSELを押しながら立ち上げたPicoに書き込むことで使えるようになります。

注意点として、pico-debugはフラッシュメモリでなくPico内のSRAM上で動作します。リセットするとデバッガが消えるので立ち上げのたびに*.uf2ファイルを書き込む必要があります。

最新版(2021/02/07時点 v10.01)では、SRAMのどこに置くかによって以下の2種類のバイナリがあるようです。

- pico-debug-gimmecache.uf2
  - pico-debug本体はSRAMの末尾の方(0x2003C000～0x2003FFFF)に置かれます。デバッグ対象アプリケーションはこの領域を使えません。
- pico-debug-maxram.uf2
  - RP2040はフラッシュメモリからの直接実行(XIP)用キャッシュとして16kBのSRAMを持っているのですが、このキャッシュを無効化してそこにpico-debug本体を置きます。メインSRAMはすべてデバッグ対象アプリケーションから使えるようになりますが、XIPキャッシュが無効化されるため、フラッシュ上のプログラムの実行がかなり遅くなるはずです。

# OpenOCD修正

pico-debugが立ち上がったので、接続先のPCからOpenOCDを使って接続しますが、ここで一点問題が。
Raspberry Pi公式githubにある[OpenOCDのソースコード](https://github.com/raspberrypi/openocd)には、オリジナルのOpenOCDから

1. デバッガからPicoのフラッシュメモリに書き込めるようにするためのドライバ追加
2. picoprobe対応追加
3. RP2040のCore0, Core1に接続できるようにするための修正

などのような変更が行われています。この中で3.の変更によって、OpenOCDコンフィグレーションファイルのswd newdapコマンドに新たなオプション(`-dp-id`, `-instance-id`)が追加されているのですが、その影響で逆に既存のデバッガから既存のターゲットに接続できなくなってしまいました。

pico-debugを使ってデバッグする場合、OpenOCDから見たinterfaceはpicoprobeではなくCMSIS-DAPになりますが、この問題のせいで、pico-debug利用時にはRaspberry Pi公式OpenOCDは使えません。
Pi公式でないオリジナルのOpenOCDを使うか、私が https://github.com/yunkya2/openocd-win64 で修正したOpenOCDはこの問題を回避してあるのでこれを使う必要があります。

# RP2040用cfgファイル作成

更に、OpenOCDからPicoに繋ぐために使用する、ターゲット用のコンフィグレーションファイルにも問題が。
通常のデバッグ用には、Pi公式OpenOCDに入っている`rp2040.cfg`を使えば良いのですが、このファイルはCore0,Core1の両方に繋ぎに行く設定なのでpico-debugで使えない他、Pi公式パッチで追加されたオプションを使用しているので、CMSIS-DAPでは使用することができません。

というわけで、既存のターゲット用のコンフィグファイルとrp2040.cfgを見比べながら、見様見まねで「RP2040のCMSIS-DAP接続用コンフィグファイル」を作ってみました。

```rp2040-cmsis-dap.cfg
source [find target/swj-dp.tcl]
source [find mem_helper.tcl]

set _CHIPNAME rp2040
set _CPUTAPID 0x01002927
set _ENDIAN little

transport select swd
adapter speed 5000

swj_newdap $_CHIPNAME.core0 cpu -expected-id $_CPUTAPID

# Give OpenOCD SRAM1 (64k) to use for e.g. flash programming bounce buffers (should avoid algo stack etc)
# Don't require save/restore, because this isn't something we'd do whilst a user app is running
set _WORKSIZE 0x10000
set _WORKBASE 0x20010000

#core 0
set _TARGETNAME_0 $_CHIPNAME.core0
dap create $_TARGETNAME_0.dap -chain-position $_CHIPNAME.core0.cpu
target create $_TARGETNAME_0 cortex_m -endian $_ENDIAN -coreid 0 -dap $_TARGETNAME_0.dap -rtos hwthread
$_TARGETNAME_0 configure -work-area-phys $_WORKBASE -work-area-size $_WORKSIZE -work-area-backup 0
cortex_m reset_config sysresetreq

set _FLASHNAME $_CHIPNAME.flash
set _FLASHSIZE 0x200000
set _FLASHBASE 0x10000000
#          name        driver        base, size in bytes, chip_width, bus_width, target used to access
flash bank $_FLASHNAME rp2040_flash $_FLASHBASE $_FLASHSIZE    1 32 $_TARGETNAME_0

# srst is not fitted so use SYSRESETREQ to perform a soft reset
reset_config srst_nogate

gdb_flash_program enable
gdb_memory_map enable
```

- OpenOCDは自前パッチ済み
- インターフェースとしてCMSIS-DAPを指定
- デバッグターゲットとして上記RP2040用ファイルを指定

ここまでやって、ようやくOpenOCDを使ってpico-debugに繋がるようになりました。

```
$ openocd.exe -c 'bindto 0.0.0.0' -f interface/cmsis-dap.cfg -f rp2040-cmsis-dap.cfg
Open On-Chip Debugger 0.11.0-rc2+dev-gabbf03b09 (2021-01-30-21:57)
Licensed under GNU GPL v2
For bug reports, read
        http://openocd.org/doc/doxygen/bugs.html
Info : auto-selecting first available session transport "swd". To override use
'transport select <transport>'.
Warn : Transport "swd" was already selected
Info : Hardware thread awareness created
Info : RP2040 Flash Bank Command
Info : Listening on port 6666 for tcl connections
Info : Listening on port 4444 for telnet connections
Info : CMSIS-DAP: SWD  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
Info : rp2040.core0: hardware has 4 breakpoints, 2 watchpoints
Info : starting gdb server for rp2040.core0 on 3333
Info : Listening on port 3333 for gdb connections

```

早速gdb-multiarchでこのOpenOCDに繋いでみると…

```
$ gdb-pico
GNU gdb (Ubuntu 8.1.1-0ubuntu1) 8.1.1
Copyright (C) 2018 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 "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://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".
Remote debugging using 172.xx.xx.xx:3333
warning: No executable has been specified and target does not support
determining executable automatically.  Try using the "file" command.
0x20003d7e in ?? ()
(gdb) x/32x 0
0x0:    0x20041f00      0x000000ef      0x00000035      0x00000031
0x10:   0x0101754d      0x00c8007a      0x0000001d      0x88022300
0x20:   0xd003429a      0x30048843      0xd1f74291      0x47701c18
0x30:   0xe7fdbf30      0xf00046f4      0x4874f805      0x60012100
0x40:   0x46e76041      0x21004872      0x600143c9      0x47706041
0x50:   0xd24340e4      0x000020ed      0x20294328      0x30323032
0x60:   0x73615220      0x72656270      0x50207972      0x72542069
0x70:   0x6e696461      0x744c2067      0x33500064      0x33520191
(gdb)
```

gdbが動きました! メモリreadなどもできているようです。

# サンプルアプリのload(失敗)

それではサンプルのLチカ(blink.elf)をロードしてみましょう。
`cmake -DCMAKE_BUILD_TYPE=Debug`でデバッグ情報入りでビルドしたELFファイルをgdbから読み込ませてみると…

```
$ gdb-pico blink.elf
GNU gdb (Ubuntu 8.1.1-0ubuntu1) 8.1.1
Copyright (C) 2018 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 "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://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 blink.elf...done.
Remote debugging using 172.xx.xx.xx:3333
(gdb) monitor reset init
target halted due to debug-request, current mode: Thread
xPSR: 0xf1000000 pc: 0x000000ee msp: 0x20041f00
(gdb) load
Loading section .boot2, size 0x100 lma 0x10000000
Loading section .text, size 0x4758 lma 0x10000100
Loading section .rodata, size 0xca0 lma 0x10004858
Loading section .binary_info, size 0x1c lma 0x100054f8
Loading section .data, size 0x9dc lma 0x10005514
Start address 0x10000104, load size 24304
Transfer rate: 8 KB/sec, 4050 bytes/write.
(gdb) b main
Breakpoint 1 at 0x1000038c: file pico/pico-examples/blink/blink.c, line 9.
(gdb) c
Continuing.
Note: automatically using hardware breakpoints for read-only addresses.
error writing data:

Program stopped.
```

フラッシュにバイナリをロードしてmainにブレークポイントを置いて実行開始までやってみたところ、OpenOCDの接続が切れてデバッガも止まってしまいました。
プログラム実行中もpico-debugは動き続けてUSBからPCに接続し続けなければならないですが、SDKアプリ起動時の初期化でハードウェアをリセットしてしまうため、その時点でpico-debugも止まってしまったわけです。考えて見れば当たり前ですが…。

# SDKを修正してなんとかmain()までたどり着くが…

アプリ起動時に周辺ハードウェアを初期状態に戻すのはSDKとしてはまったく当たり前の処理なのですが、デバッガまで初期化されてしまってはたまらない。やむを得ず、SDKのソースコードを変更して初期化処理を行わないようにしてみます。**この時点で既に実用性ゼロです(苦笑)。**

アプリ起動時の初期化処理はPico SDKの [pico-sdk/blob/master/src/rp2_common/pico_runtime/runtime.c](https://github.com/raspberrypi/pico-sdk/blob/master/src/rp2_common/pico_runtime/runtime.c) の、`runtime_init()`で行っています。ここを変更してリセットとクロック初期化処理をつぶします。

```diff
diff --git a/src/rp2_common/pico_runtime/runtime.c b/src/rp2_common/pico_runtime/runtime.c
index 5a5627b..97864d1 100644
--- a/src/rp2_common/pico_runtime/runtime.c
+++ b/src/rp2_common/pico_runtime/runtime.c
@@ -59,6 +59,7 @@ void runtime_init(void) {
     // Reset all peripherals to put system into a known state,
     // - except for QSPI pads and the XIP IO bank, as this is fatal if running from flash
     // - and the PLLs, as this is fatal if clock muxing has not been reset on this boot
+#if 0
     reset_block(~(
             RESETS_RESET_IO_QSPI_BITS |
             RESETS_RESET_PADS_QSPI_BITS |
@@ -77,6 +78,7 @@ void runtime_init(void) {
             RESETS_RESET_UART1_BITS |
             RESETS_RESET_USBCTRL_BITS
     ));
+#endif

     // pre-init runs really early since we need it even for memcpy and divide!
     // (basically anything in aeabi that uses bootrom)
@@ -95,10 +97,10 @@ void runtime_init(void) {

     // After calling preinit we have enough runtime to do the exciting maths
     // in clocks_init
-    clocks_init();
+//    clocks_init();

     // Peripheral clocks should now all be running
-    unreset_block_wait(RESETS_RESET_BITS);
+//    unreset_block_wait(RESETS_RESET_BITS);

 #if !PICO_IE_26_29_UNCHANGED_ON_RESET
     // after resetting BANK0 we should disable IE on 26-29
```

実行してみると、無事main()までたどり着きました。

```
$ gdb-pico blink.elf
GNU gdb (Ubuntu 8.1.1-0ubuntu1) 8.1.1
Copyright (C) 2018 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 "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://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 blink.elf...done.
Remote debugging using 172.xx.xx.xx:3333
0x2003bf0a in ?? ()
(gdb) monitor reset init
target halted due to debug-request, current mode: Thread
xPSR: 0xf1000000 pc: 0x000000ee msp: 0x20041f00
(gdb) load
Loading section .boot2, size 0x100 lma 0x10000000
Loading section .text, size 0x43f0 lma 0x10000100
Loading section .rodata, size 0xb50 lma 0x100044f0
Loading section .binary_info, size 0x1c lma 0x10005040
Loading section .data, size 0x9dc lma 0x1000505c
Start address 0x10000104, load size 23096
Transfer rate: 7 KB/sec, 3849 bytes/write.
(gdb) b main
Breakpoint 1 at 0x1000038c: file pico/pico-examples/blink/blink.c, line 9.
(gdb) c
Continuing.
Note: automatically using hardware breakpoints for read-only addresses.

Breakpoint 1, main () at pico/pico-examples/blink/blink.c:9
9       int main() {
(gdb) n
11          gpio_init(LED_PIN);
(gdb)
12          gpio_set_dir(LED_PIN, GPIO_OUT);
(gdb)
14              gpio_put(LED_PIN, 1);
(gdb)
15              sleep_ms(250);
(gdb)
16              gpio_put(LED_PIN, 0);
(gdb)
17              sleep_ms(250);
(gdb)
14              gpio_put(LED_PIN, 1);
(gdb)
15              sleep_ms(250);
(gdb)
16              gpio_put(LED_PIN, 0);
```

コードのステップ実行もちゃんと動作して、デバッガとしてはきちんと機能しているようです。
ですが、GPIOを操作してもLEDが点灯しない…やっぱりハードウェアの初期化をすっ飛ばしたからダメか。

# というわけで

散々苦労した割には実用性皆無という、非常に残念な結果になってしまいました。
「自分自身のSWDに繋いでデバッグできると面白い」というただその一点のみでやってみましたが、冒頭にも書きましたが実用するには普通にPico 2枚使った方が確実で手っ取り早いです。

pico-debug自体もバージョンアップが進んでいるようで[^1]、今後出来ることが増える可能性もありますが、「初期化時に自分自身がリセットされないようにしないといけない」という制約だけはどうしようもないので実用で使うのは難しそうですね。

[^1]: 最初のバージョンではデバッガがCore0、デバッグ対象がCore1で動いていたため、SDKのCPU番号チェックを外したり、フラッシュへのloadが出来なくてプログラムを全部SRAMに置くよう設定変更したりと更に手間がかかっていました。この部分は若干改善されました。