Raspberry Pi 4をJTAGデバッグしてみる(FTDI C232HM-DDHSL-0使用)

f:id:hikalium:20210718212859j:plain
JTAGアダプタとシリアルケーブルがつながったRaspberry Pi 4

概要

Raspberry Piでベアメタルプログラミングをするときに、CPUが今実際にどこの命令を実行しているか、メモリ上にどのような値が存在するか…などの情報を確認できると、デバッグが非常に楽になります。それを可能にしてくれるのが、JTAGというインターフェイスです。

今回は、FTDI社製のケーブルを使用して、Raspberry Pi 4のJTAGにアクセスしてみました。前半部分はケーブル固有の話もあるので、他のJTAGケーブルをお持ちの方は、それぞれのマニュアルを参照して適宜読み替えていただく必要があります。一方後半部分は、openocdを使ってJTAG経由でRaspberry Pi 4の情報を取得する一般的な方法について説明しますので、みなさんの環境でも役立つかもしれません。参考にしていただければ幸いです。

主な登場人物

  • Raspberry Pi 4 Model B
    • BCM2711というSoCが載っています。
  • FTDI C232HM-DDHSL-0
    • このケーブルはMPSSE(Multi-Protocol Synchronous Serial Engine) といって、モードを切り替えればJTAG以外にもSPIやI2Cといった通信もできるようです。
    • C232HM-DDHSL-0とC232HM-EDHSL-0の違いは、通信に使う電圧レベルです(3.3Vと5.0V)。Raspberry PiのGPIOは3.3Vなので、間違えないようにしましょう。
  • FTDI TTL-232RG-VREG3V3-WE
    • このケーブルはJTAG通信には関係ないですが、Raspberry PiのUART(シリアルポート)を読むために使っています。ベアメタルプログラミングするなら、基本的にシリアルは生命線になるので、シリアルを読めるケーブルは何らかのものを持っておくとよいと思います。
    • 例によって、これも電圧レベルによって似た品番のものがあるので、他の変換ケーブルやボードを使う際も、3.3Vのものを買うようにしましょう。
    • ちなみにこのケーブルは末端が電線剥き出しでコネクタがついてないので、私はそれをブレッドボードに刺してそこからRasPiに延ばしています。コネクタがついているタイプのものも他の製品だとあるようなので、そっちを買った方が便利かもしれません。

決戦結線の時

というわけで早速ケーブルとRasPiをつないでゆきます。RasPiの電源を落としてから作業しましょう。

落とし穴としては、RasPiのGPIOピンの番号と、物理ピン番号(GPIOヘッダ上の位置)は異なる、というところです。

詳しくはRaspberry Pi 公式のドキュメントに解説があるので、それをチェックしてください。

(以下はあくまでも参考として、鵜呑みにせずに自分で本当に合っているかチェックしながら接続してください!)

JTAG ⇔ C232HM-DDHSL-0

ケーブル自体のデータシートも参考にしてください。

ケーブルの色 GPIOピン番号 物理ピン番号 GPIOヘッダを右上においたときの場所
Brown (TMS) 27 (ARM_TMS) 13 上から7番目の左
Grey (TRST) 22 (ARM_TRST) 15 上から8番目の左
Blue (RTCK) 23 (ARM_RTCK) 16 上から8番目の右
Green (TDO) 24 (ARM_TDO) 18 上から9番目の右
Orange (TCK) 25 (ARM_TCK) 22 上から11番目の右
Yellow (TDI) 26 (ARM_TDI) 37 上から19番目の左=下から2番目の左
Black (GND) GND 39, etc... 一番左下など

RTCKを使わない記事もインターネット上にはありましたが、これをつなぐと通信が格段に安定したのでこのようにしています。

UART ⇔ TTL-232RG-VREG3V3-WE

ケーブル自体のデータシートも参考にしてください。

ケーブルの色 GPIOピン番号 物理ピン番号 GPIOヘッダを右上においたときの場所
Yellow(RXD) 14 8 上から4番目の右
Black(GND) GND 9, etc... 上から5番目の左など
Orange(TXD) 15 10 上から5番目の右

接続イメージ

全部つないでみたときの参考写真を貼っておきました。(写真の向きは、GPIOヘッダを左上にみたときの例なので、上の説明と対応させる場合は、90度首を傾けてみてください。)

シリアル変換ケーブルから伸びている線は、写真上側のブレッドボードでジャンパワイヤに変換されてきているので、上の説明に書かれている色と、写真中の線の色は一致しません。

画面左側、JTAGアダプタから伸びている線の色は、上に書いてある説明と一致します。

f:id:hikalium:20210718213206j:plain
JTAGアダプタとシリアル出力の接続例

ソフトウエアの設定

今回の環境は macOS 11.4ですが、Linuxなどの他の環境でも、似たような設定をすれば動作するはずです。

openocdをインストールする

最新のopenocdをHomebrew経由でインストールします。

brew install openocd --HEAD

私の環境では、以下のようなエラーメッセージが出ました。

$ brew install openocd --HEAD
...
==> Installing open-ocd --HEAD
==> ./bootstrap nosubmodule
==> ./configure --prefix=/usr/local/Cellar/open-ocd/HEAD-cff0e41 --enable-buspir
==> make install
Error: An unexpected error occurred during the `brew link` step
The formula built, but is not symlinked into /usr/local
Cannot link open-ocd
Another version is already linked: /usr/local/Cellar/open-ocd/0.10.0
Error: Cannot link open-ocd
Another version is already linked: /usr/local/Cellar/open-ocd/0.10.0

このエラーは、すでに他のバージョンのopenocdがインストールされている場合に表示されます。この場合、インストールした最新のopenocdにはPATHが通っていない状態になっていますので、パスを直接指定して実行してあげる必要があります。(もちろん、強制的にPATHを通す(link)することも可能ですが、各自の判断にお任せします。)

私の環境では、/usr/local/Cellar/open-ocd/HEAD-cff0e41/bin/openocdにインストールされていましたので、これを直接実行できることを確認します。

$ /usr/local/Cellar/open-ocd/HEAD-cff0e41/bin/openocd --version
Open On-Chip Debugger 0.11.0+dev-00236-gcff0e417d-dirty (2021-06-28-20:42)
Licensed under GNU GPL v2
For bug reports, read
    http://openocd.org/doc/doxygen/bugs.html

バージョン情報が正しく表示されればOKです。

Raspberry Pi 側でJTAGインターフェイスを有効化する

デフォルトでは、JTAGインターフェイスは無効になっており、該当するピンは通常のGPIOポートとして機能するように設定されています。

JTAGを有効化するには、Raspberry PiのSDカード内にある config.txtというファイルの[all]セクションの配下に、以下のような記述を追加します。

enable_jtag_gpio=1

これにより、起動時にRaspberry Piのファームウエアがこの設定ファイルを読み込んだ際に、JTAGを有効化してくれます。

詳しくは、公式のドキュメントを参照してください。

ついでにシリアルポート出力とかも有効化しておく

enable_uart=1

参考: 私のconfig.txtの末尾はこうなっている

他にもいろいろ設定を入れてこんな感じになっています。参考までに。(この記事で解説したことを試すには、上で説明した2つを設定するだけで大丈夫なはずです。)

...
[all]
#dtoverlay=vc4-fkms-v3d
arm_64bit=1
init_uart_clock=48000000
init_uart_baud=115200
enable_uart=1
enable_jtag_gpio=1

openocdの設定

それでは早速openocdを使っていきたいのですが、使うためにはいくつか設定ファイルを作成する必要があります。

インターフェイスの設定

まず、以下のような内容のファイルを作成しc232hm-edhsl-0.cfgという名前で保存します。

adapter driver ftdi
ftdi_vid_pid 0x0403 0x6014
ftdi_device_desc C232HM-DDHSL-0

# ftdi_layout_init <values> <directions>
# initial value:
# 0078 = 0000 0000 0001 1000
# TRST, TMS=1, all others zero
# initial direction:
# 0111 = GPIOL3=RTCK=input, GPIOL2=dontcare=output, GPOL1=SRST=output, GPIOL0=TRST=output
# 1011 = [1=TMS=output, 0=TDO=input, 1=TDI=output, 1=TCK=output]
ftdi_layout_init 0x0018 0x007b

# GPIOL0 is TRST
ftdi_layout_signal nTRST -data 0x0010

(参考にしたブログ記事 のものに比べると、最新のopenocdに合わせて少し修正してあります。)

このファイルには、使用するJTAGアダプタ(今回の場合はFTDI C232HM-DDHSL-0)に固有の設定が記載されています。そのため、別のJTAGアダプタを使用する際は、それに合ったファイルを作成する必要があります。場合によっては標準で提供されていることもあるので、他のアダプタをお使いの方は各自で調べてみてください。

ターゲットの設定

次に、以下のような内容のファイルを作成しraspi4.cfgという名前で保存します。

set _CHIPNAME bcm2711
set _DAP_TAPID 0x4ba00477

adapter speed 1000

transport select jtag
reset_config trst_and_srst

telnet_port 4444

# create tap
jtag newtap auto0 tap -irlen 4 -expected-id $_DAP_TAPID

# create dap
dap create auto0.dap -chain-position auto0.tap

set CTIBASE {0x80420000 0x80520000 0x80620000 0x80720000}
set DBGBASE {0x80410000 0x80510000 0x80610000 0x80710000}

set _cores 4

set _TARGETNAME $_CHIPNAME.a72
set _CTINAME $_CHIPNAME.cti
set _smp_command ""

for {set _core 0} {$_core < $_cores} { incr _core} {
    cti create $_CTINAME.$_core -dap auto0.dap -ap-num 0 -baseaddr [lindex $CTIBASE $_core]

    set _command "target create ${_TARGETNAME}.$_core aarch64 \
                    -dap auto0.dap  -dbgbase [lindex $DBGBASE $_core] \
                    -coreid $_core -cti $_CTINAME.$_core"
    if {$_core != 0} {
        set _smp_command "$_smp_command $_TARGETNAME.$_core"
    } else {
        set _smp_command "target smp $_TARGETNAME.$_core"
    }

    eval $_command
}

eval $_smp_command
targets $_TARGETNAME.0

(こちらも、参考にした記事をベースに、最新のopenocdに合わせて少し修正を加えたものになります。)

このファイルには、デバッグ対象のデバイス(今回の場合はRaspberry Pi 4 / BCM2711)に固有の設定が記載されています。そのため、もし異なるボードをJTAGデバッグしたい場合には、それぞれに合ったファイルを作成して使用する必要があります。

動かしてみる

では、編集済みのconfig.txtが入ったRaspberry Pi OS入りのSDカードをRaspberry Pi 4に挿入し、JTAGケーブルやシリアル変換ケーブルをPCにさして、最後にRaspberry Pi 4に電源をさして起動してみましょう。その後、以下のコマンドを実行してみてください。

/usr/local/Cellar/open-ocd/HEAD-cff0e41/bin/openocd -f c232hm-edhsl-0.cfg -f raspi4.cfg

(最初のopenocdへのパスは、適宜環境に合わせて読みかえてください。)

すべてが正しく動作していれば、以下のような出力が得られ、サーバーが待受状態になります。

$ /usr/local/Cellar/open-ocd/HEAD-cff0e41/bin/openocd -f c232hm-edhsl-0.cfg -f raspi4.cfg
Open On-Chip Debugger 0.11.0+dev-00236-gcff0e417d-dirty (2021-06-28-20:42)
Licensed under GNU GPL v2
For bug reports, read
    http://openocd.org/doc/doxygen/bugs.html
Info : Listening on port 6666 for tcl connections
Info : Listening on port 4444 for telnet connections
Info : clock speed 1000 kHz
Info : JTAG tap: auto0.tap tap/device found: 0x4ba00477 (mfg: 0x23b (ARM Ltd), part: 0xba00, ver: 0x4)
Info : bcm2711.a72.0: hardware has 6 breakpoints, 4 watchpoints
Info : bcm2711.a72.1: hardware has 6 breakpoints, 4 watchpoints
Info : bcm2711.a72.2: hardware has 6 breakpoints, 4 watchpoints
Info : bcm2711.a72.3: hardware has 6 breakpoints, 4 watchpoints
Info : starting gdb server for bcm2711.a72.0 on 3333
Info : Listening on port 3333 for gdb connections

openocdとのやりとり

参考:

上の出力に書かれている通り、telnetなどで4444番ポートにアクセスすると、openocdとおはなしできます。

$ telnet localhost 4444
Trying ::1...
telnet: connect to address ::1: Connection refused
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Open On-Chip Debugger
> 

ここにコマンドを打つと、いろいろ見れます。

targets: 各ターゲット(今回の場合CPUコア)を一覧表示できる

> targets
    TargetName         Type       Endian TapName            State       
--  ------------------ ---------- ------ ------------------ ------------
 0* bcm2711.a72.0      aarch64    little auto0.tap          running
 1  bcm2711.a72.1      aarch64    little auto0.tap          running
 2  bcm2711.a72.2      aarch64    little auto0.tap          running
 3  bcm2711.a72.3      aarch64    little auto0.tap          running

targets bcm2711.a72.1のように、TargetNameを指定すれば、現在選択しているtargetを変更できる。

halt: 実行を停止させる。

> halt
bcm2711.a72.1 cluster 0 core 1 multi core
bcm2711.a72.2 cluster 0 core 2 multi core
bcm2711.a72.2 halted in AArch64 state due to debug-request, current mode: EL2H
cpsr: 0x000003c9 pc: 0x80
MMU: disabled, D-Cache: disabled, I-Cache: disabled
bcm2711.a72.3 cluster 0 core 3 multi core
bcm2711.a72.3 halted in AArch64 state due to debug-request, current mode: EL2H
cpsr: 0x000003c9 pc: 0x80
MMU: disabled, D-Cache: disabled, I-Cache: disabled
bcm2711.a72.0 cluster 0 core 0 multi core
bcm2711.a72.0 halted in AArch64 state due to debug-request, current mode: EL2H
cpsr: 0x600003c9 pc: 0x813d8
MMU: disabled, D-Cache: disabled, I-Cache: disabled
bcm2711.a72.1 halted in AArch64 state due to debug-request, current mode: EL2H
cpsr: 0x000003c9 pc: 0x80
MMU: disabled, D-Cache: disabled, I-Cache: disabled
> targets
    TargetName         Type       Endian TapName            State       
--  ------------------ ---------- ------ ------------------ ------------
 0  bcm2711.a72.0      aarch64    little auto0.tap          halted
 1* bcm2711.a72.1      aarch64    little auto0.tap          halted
 2  bcm2711.a72.2      aarch64    little auto0.tap          halted
 3  bcm2711.a72.3      aarch64    little auto0.tap          halted

reg: レジスタを表示する

> reg                  
===== Aarch64 registers
(0) x0 (/64): 0x000000000000006c (dirty)
(1) x1 (/64): 0x00000000ffffffff
(2) x2 (/64)
...

dirtyと書かれているデータは、openocdがキャッシュしているものなので、もし生の値を反映させたかったら、以下のようにすると強制的に読み書きできる。

reg x0 force

キャッシュ情報とかも見れます

> aarch64 cache_info
L1 I-Cache: linelen 64, associativity 3, nsets 256, cachesize 48 KBytes
L1 D-Cache: linelen 64, associativity 2, nsets 256, cachesize 32 KBytes
L2 D-Cache: linelen 64, associativity 16, nsets 1024, cachesize 1024 KBytes

メモリの読み書きもできちゃいます!たとえば0x80000(raspberry pi 4におけるカーネルのロードアドレス)をみると

>  mdw 0x80000
0x00080000: d53800a1

と書いてありますが、今回起動したイメージの先頭を確認してみると

$ hexdump -C kernel8.img | head -n 1
00000000  a1 00 38 d5 21 04 40 92  42 01 00 58 3f 00 02 eb  |..8.!.@.B..X?...|

たしかに一致していますね!

gdbとあわせて使う

参考: GDB and OpenOCD (OpenOCD User’s Guide)

openocdの起動時のメッセージを注意深く読むと、gdbのサーバーも待ち受けていることがわかります。

Info : starting gdb server for bcm2711.a72.0 on 3333
Info : Listening on port 3333 for gdb connections

ということで、さっそくつないでみましょう。今回使ったgdbのバージョンは以下の通り。

$ gdb --version | head -n 1
GNU gdb (GDB) 9.2

まず、gdbを引数なしで起動します。

gdb

そのあと、gdbのプロンプトに、アーキテクチャとターゲット情報を打ち込んで、リモートデバッグをはじめます。

私が試した例では、config.txtにarm_64bit=1と書いた状態でaarch64のバイナリを実行している状態なので、以下のようにaarch64をターゲットとして指定します。

(gdb) set architecture aarch64
The target architecture is assumed to be aarch64
(gdb) target extended-remote localhost:3333
Remote debugging using localhost:3333
warning: No executable has been specified and target does not support
determining executable automatically.  Try using the "file" command.
0x00000000000813d8 in ?? ()

一方、Raspberry Pi OSは記事執筆現在は32bit(armv7l)で動作しているので、もしそのようなターゲットをデバッグしたい場合は、aarch64の代わりにarmv8-aにするとよいでしょう。

set architecture armv8-a
target extended-remote localhost:3333

もし間違ったarchを指定していたら、以下のようなエラーメッセージが出るので、そのときは別のarchを指定してやり直せばOKです。

warning: Selected architecture armv8-a is not compatible with reported target architecture aarch64
warning: Architecture rejected target-supplied description

さて、これでgdbがつながったので、デバッグし放題です。いつものinfo registersとかもばっちり動きます。

(gdb) info registers
x0             0x0                 0
x1             0x8192d             530733
x2             0x2                 2
x3             0x80264             524900
...

もちろん、今実行しているバイナリが手元にあれば、それを読み込んでgdbデバッグすることもできます。

$ file target/aarch64-unknown-none-softfloat/release/kernel
target/aarch64-unknown-none-softfloat/release/kernel: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, with debug_info, not stripped

$ gdb target/aarch64-unknown-none-softfloat/release/kernel
GNU gdb (GDB) 9.2
...
(gdb) target extended-remote localhost:3333
Remote debugging using localhost:3333
0x00000000000813d8 in kernel::kernel_main ()
(gdb) disas
Dump of assembler code for function _ZN6kernel11kernel_main17h947565f396a80a38E:
   0x0000000000080fd4 <+0>:   sub sp, sp, #0xc0
   0x0000000000080fd8 <+4>:   stp x30, x27, [sp, #112]
(gdb) c
Continuing.

loadコマンドをつかえば、バイナリを動的にメモリ上にロードすることだってできちゃいます。

(gdb) load
Loading section .text, size 0x1674 lma 0x80000
Loading section .rodata, size 0x4c5 lma 0x81678
Loading section .got, size 0x10 lma 0x81b40
Loading section .data, size 0x20 lma 0x81b50
Start address 0x0000000000080000, load size 7017
Transfer rate: 63 KB/sec, 1754 bytes/write.

raspberry piJTAGインターフェイスにはSRSTピンが生えていないため、gdbmonitor resetコマンドを利用してチップをリセットしてロードしたプログラムを再実行させることはできませんが、プログラムカウンタ(pc)を変更することはできるので、代わりに使うといいかもしれません。

(gdb) set $pc=0x80000
(gdb) c

というわけで、とっても便利なJTAGを活用して、皆様も楽しいRasPiベアメタル開発の時間をお過ごしください!

付録: openocdがうまくいかないとき

Error: no device found
Error: unable to open ftdi device with vid 0403, pid 6014, description 'C232HM-DDHSL-0', serial '*' at bus location '*'

→そもそもJTAGアダプタがPCにつながっていない/認識されていない。

Error: JTAG scan chain interrogation failed: all ones
Error: Check JTAG interface, timings, target power, etc.
Error: Trying to use configured scan chain anyway...
Error: auto0.tap: IR capture error; saw 0x0f not 0x01
Warn : Bypassing JTAG setup events due to errors
Error: Invalid ACK (7) in DAP response
Error: JTAG-DP STICKY ERROR

デバッグ対象のデバイスの電源が入っていない/まだ起動直後でJTAGが有効化されてない/配線が間違っている/enable_jtag_gpio=1の設定変更が正しく反映されていない。