公開日: 2023年10月26日

GDB dashboard で Raspberry Pi Pico の L チカをデバッグする (後編)

前回に引き続き、GDB と GDB dashboard を使って Pico の L チカプログラムをデバッグしていきます。最後におまけで、GDB に標準添付されている「TUI モード」も紹介します。今回は Pico に搭載されている RP2040 マイコンの内部にも言及するので、必要に応じて下記 URL のデータシートを参照してください。

[1]RP2040 Datasheet
https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf

よく使う GDB コマンド

前回は GDB の基本的な使い方をやりました。ここではもう少し進んだ使い方を見ていきます。よく使いそうなコマンドを下の表にまとめました。

コマンド名省略形説明備考
load

ELF ファイルをフラッシュメモリにロードする

continue

c

実行開始または停止しているプログラムを再開する

Ctrl-c

強制停止

break

b

ブレークポイントを設定

(*1)

enable

en

ブレークポイント、ウォッチポイントを有効化

(*1)

disable

dis

ブレークポイント、ウォッチポイントを無効化

(*1)

delete

d

ブレークポイント、ウォッチポイントを削除

(*1)

watch

wa

ウォッチポイントを設定

(*2)

rwatch

rw

ウォッチポイントを設定

(*2)

awatch

aw

ウォッチポイントを設定

(*2)

info breakpoints

i b

ブレークポイント,ウォッチポイントの一覧を表示

backtrace

bt

ブレークした位置から関数の呼び出し元をたどる

dump memory

dump mem

メモリの中身をファイルにダンプする

(*3)

next

n

1 行実行 (ステップオーバー)

step

s

1 行実行 (ステップイン)

finish

fin

関数の末尾まで実行

quit

q

GDB 終了

(*1) ブレークポイントについて
ソースファイル名、関数名、行番号等を組み合わせてブレークポイントを置く位置を指定できます。if文を使ってブレークが成立する条件を指定することもできます。次の例では上から順にdelay_ms関数の先頭、main.c にあるdelay_ms関数の先頭、main.c の 10 行目、main.c の 10 行目 (cntの値が 50 以外だった場合のみブレーク) にブレークを張ります。

(gdb) b delay_ms
(gdb) b main.c:delay_ms
(gdb) b main.c:10
(gdb) b main.c:10 if cnt != 50

info breakpointsコマンドでブレークポイントの一覧を表示します。ここに表示されるブレークポイントの番号を指定して、そのブレークポイントを有効化・無効化・削除することもできます。

(gdb) i b
Num     Type           Disp Enb Address    What
1       breakpoint     keep y   0x100001f0 in delay_ms at ...
2       breakpoint     keep y   0x100001f0 in delay_ms at ...
3       breakpoint     keep y   0x100001fe in delay_ms at ...
4       breakpoint     keep y   0x100001fe in delay_ms at ...
        stop only if cnt != 50
(gdb) dis 1
(gdb) d 2 3
(gdb) i b
Num     Type           Disp Enb Address    What
1       breakpoint     keep n   0x100001f0 in delay_ms at ...
4       breakpoint     keep y   0x100001fe in delay_ms at ...
        stop only if cnt != 50

(*2) ウォッチポイントについて
指定した変数や番地を監視して、その場所への読み書きが発生した場合にブレークします。watchは書き込み、rwatchは読み出し、awatchは読み書きどちらか、というように使い分けます。メモリ番地を指定するときは先頭に「*」をつけます。次の例では上から順に 0xD000001C 番地 (GPIO_OUT_XOR レジスタ) への書き込み、0xE000E010 番地 (SYST_CSR レジスタ) からの読み出し、0x40028000 番地 (PLL の CS レジスタ) への読み書きが発生した場合にブレークします。

(gdb) wa *0xd000001c
(gdb) rw *0xe000e010
(gdb) aw *0x40028000

例えば 1 番目の 0xD000001C 番地の例では、サンプルプログラムのmain関数内で LED の表示反転を行うたびにout_w関数を呼んでこのレジスタに書き込みを行います。よってout_w関数の末尾 (stdlib.h 18 行目) で何度でもブレークします。その際、printコマンドで引数adrの中身を表示すると 0xD000001C になっているので、確かに SYST_CSR レジスタへの書き込みでブレークしたことがわかります。

(gdb) print/x adr
$1 = 0xd000001c

ウォッチポイントの一覧表示・有効化・無効化・削除は、ブレークポイントとまったく同じコマンドで実行することができます。

(*3) メモリダンプについて
メモリをダンプしてバイナリデータとしてファイルに保存します。次の例では 0x40008000 ~ 0x400080C7 番地の 200 バイト分を mem.bin に保存します。

(gdb) dump mem mem.bin 0x40008000 0x400080C8

実際にデバッグしてみる

それでは実際にデバッグしてみましょう。この L チカプログラムは Cortex-M0+ の SysTick タイマを使って 500 ミリ秒ごとに LED の出力を反転させています。具体的には、SYST_RVR というレジスタに 1249999 という値1) をセットしておくと、10 ミリ秒ごとに SYST_CSR レジスタの COUNTFLAG ビットが立つので、それを 50 回カウントしてから LED の出力を反転させる、ということをやっています。

  1. RP2040 のシステムクロック周波数は 125 MHz なので、10 ミリ秒に相当するクロックパルス数は (125e6 / 1e3) × 10 = 1250000 パルスとなります。ここから 1 を引いた値を SYST_RVR レジスタにセットします。

そこでデバッグの一例として、SYST_RVR レジスタに正しく 1249999 がセットされていることを確認してみましょう。何かのまちがいで「1197492064」のようなとんでもない値がセットされていると、点滅しているんだかいないんだかわからないくらいスローな L チカになってしまいます (別のマイコンボードでだけど実際にやらかした)。まず、すでに説明した手順で GDB を起動し、必要に応じてプログラムをロードします。

% cd ~/pico/repo/interface_trykernel/build_part2/sect_3/
% pgdb build/blink.elf
(gdb) load

プログラムがスタートして最初にやってくるのが reset_hdr.c にあるReset_Handler()です。ここにブレークを張ってプログラムをスタート (continue) してみます。

(gdb) b Reset_Handler
(gdb) c

ブレークポイントが正しく設置されていれば、図 1 のように reset_hdr.c の 192 行目、Reset_Handler()の先頭でブレークします。このように、ブレークするたびに GDB dashboard がプログラムの内部状態を表示してくれます。何かの拍子に表示されなかったとしても dashboardコマンドを引数なしで実行すれば表示されます。

図 1
図 1

ここで、SYST_RVR レジスタの中身を GDB dashboard で確認できるように設定します。Reset_Handler()の先頭でブレークしている今の状態からプログラムを再開すると、init_systim関数が呼ばれ、その中で SYST_RVR レジスタにクロックカウンタのリロード値をセットしています。その直前の位置にブレークポイントを置きましょう。reset_hdr.c の 181 行目です。

(gdb) b reset_hdr.c:181

次に、SYST_RVR レジスタの中身を表示させるようにします。それには dashboard memory watchコマンドを使います。このレジスタの番地とサイズを RP2040 のデータシートで確認すると、それぞれ 0xE000E014 番地、4 バイトであることがわかります ([1] 2.4.8 節)。そこで、

(gdb) dashboard memory watch 0xe000e014 4

としてもいいですし、あるいは SysTick 関係のレジスタをまとめて

(gdb) dashboard memory watch 0xe000e010 16

としてもおもしろいです。ここは後者でいきましょう。continueコマンドでプログラムを再開すると、reset_hdr.c の 181 行目、今まさに SYST_RVR に値を書き込もうとする直前でブレークします (図 2)。

図 2
図 2

nextコマンドで 1 行進めて、SYST_RVR レジスタに値を書き込みます (図 3)。Memory モジュールの表示を見ると、0xE000E014 番地からの 4 バイトが「cf 12 13 00」になっています。バイトオーダーがリトルエンディアンであることに注意して、数値としては 0x001312CF、10 進数になおすと 1249999 であり、無事に意図したとおりの値が書き込まれていることがわかりました。

図 3
図 3

問題なさそうなので、実際に 1 秒周期で点滅してもらいましょう。ここのブレークポイントを削除するか無効にして、プログラムを再開します。

(gdb) dis 2
(gdb) c

プログラムを止めるときは Ctrl-c キーです。

GDB コマンドをスクリプトに書く

ところで、毎回上のようにブレークポイントを設置したりメモリの番地を指示したりするのは面倒くさいです。GDB ではそのようなコマンドをスクリプトに書いておいて一括して実行したり、いくつかのコマンドをまとめてひとつの新しいコマンドとして定義したりすることができます。

スクリプトを読み込む方法はいくつかあるのですが、内容からしてプロジェクト固有の記述になるので、前回紹介した-xオプションをつけて GDB を起動する方法が良いと思います。というわけで前回作った .gdbinit.pico に次のような感じで追記します。

interface_trykernel/build_part2/sect_3/.gdbinit.pico (改)
target remote localhost:3333
monitor reset init

break reset_hdr.c:181
dashboard memory watch 0xE000E010 0x10
dashboard memory watch 0xD0000000 0x30

define start
    break Reset_Handler
    continue
end
1,2 行目:初期設定 (前回から変更なし)
4 行目:reset_hdr.c の 181 行目にブレークポイントを設定する。
5 行目:メモリの 0xE000E010 番地から 16 バイト を Memory モジュールに表示する。
6 行目:必要に応じて他の番地も追加する。
8~11 行目:新しいコマンドを定義してstartと名づける。GDB を起動してstartコマンドを実行すると、Reset_Handler()の先頭にブレークを張ったうえでプログラムを開始する (その結果Reset_Handler()の先頭でブレークして待機状態になる)。

前回と同様、GDB の起動時に-xオプションでこのファイルを読み込ませます。GDB が起動してプロンプトが表示される頃には、ブレークポイントと Memory モジュールの表示設定は済んでいる状態です。必要に応じてloadを行ったのち、新しく定義したstartコマンドを実行すると、Reset_Handler()の先頭でブレークして待機します。これであとはデバッグを開始するばかり、という状態です。

% cd ~/pico/repo/interface_trykernel/build_part2/sect_3/
% arm-none-eabi-gdb -x .gdbinit.pico build/blink.elf
(gdb) load
(gdb) start
(gdb)

おまけ: GDB の TUI モード

「わざわざ GDB dashboard をダウンロードしてきて設定やらなにやらは面倒くさい」というときは、GDB dashboard ほど便利ではないにしろ、GDB が標準で具備する TUI モードという選択肢もあります (図 4) 。layoutコマンドで表示を切り替えなければならない点が若干不便ですが、ブレークするたびにレジスタや変数の内容を勝手に表示してくれます。レスポンスは GDB dashboard よりも速い気がします (個人の感想です)。

図 4
図 4

TUI モードで起動するにはarm-none-eabi-gdbコマンドに-tuiオプションをつけます。

% cd ~/pico/repo/interface_trykernel/build_part2/sect_3/
% arm-none-eabi-gdb -tui build/blink.elf

TUI モードでは、上で紹介したコマンドに加えて、次のコマンドが有用です。

コマンド名省略形説明備考
layout next

la n

表示切り替え (ソース/アセンブリ/レジスタ)

display

disp

ブレークするたびに変数・メモリ等の値を表示

(*4)

(*4) displayコマンドについて
指定した変数やメモリの値をブレークするたびに表示してくれます。メモリ番地を指定するときは先頭に「*」をつけます。また、オプションに/dをつけると 10 進数、/xをつけると 16 進数で表示します。

(gdb) disp cnt
(gdb) disp /x *0xe000e014
(gdb) next
1: cnt = 50
2: /x *0xe000e014 = 0x1312cf

外観の設定は ~/.gdbinit に書きます。例えば図 4 でソースコードを取り囲んでいる太い枠線の色は、次のようにして変えることができます。

~/.gdbinit
set style tui-active-border foreground color

colorの部分にはnoneblackredgreenyellowbluemagentacyanwhiteのいずれかが入ります。これらが実際にどのような色になるかは、使用する端末エミュレータのカラースキームに依存します。その他のカスタマイズ可能な項目についてはhelp set stylehelp set style tui-active-borderなどのコマンドで調べてください。ちなみに図 4 は次のようにスタイルを何も適用していない素の状態です。

set style enabled off
Raspberry Pi Pico 実験室