ARM Debug Interfaceとは何か

西永俊文氏:この節ではARM Debug Interface、略してADIについて紹介していきます。ここからはJTAG共通の話ではなく、ARM独自の仕様についての説明になっていきます。(スライドを示して)この節で説明するのは、要約の図のTAPからデバッグレジスタ等にアクセスするまでの部分です。

ADIはArmのデバイスをデバッグするための規格で、TAPとDAPの通信およびDAPから、デバッグ用のメモリ空間にアクセスするまでの仕組みが定義されています。なお、ADIにはJTAGまたはSWDを使って制御する方法が書かれていますが、今回はJTAGに絞って解説をします。

ADIでは、Debug Port、略してDPの仕様と、Access Port、略してAPの仕様、および各種情報が記録されたROM Tableの仕様が書かれています。DPの説明部には、サポートしているJTAGのパブリック命令とプライベート命令や、プライベート命令の仕様について書かれています。APの説明部には、MEM-APというメモリアクセスポートの仕様が書かれています。各Portについて順番に見ていきましょう。

(スライドを示して)DPはアクセスするAPの選択やデータのやり取りを行うためのポートです。JTAGのDPACCプライベート命令を使ってアクセスします。APはバス等にアクセスするためのポートで、JTAGのAPACCプライベート命令を使ってアクセスします。DPとAPはセットにしてDAPと呼ばれています。

MEM-APは、デバッグ用メモリ空間にアクセスするためのアクセスポートの一種です。各種デバッグ用のレジスタは、デバッグ用メモリ空間にマップされているため、このMEM-APを介してアクセスする必要があります。

DPまたはAPにアクセスするプライベート命令「DPACC」「APACC」

ここからは話をJTAGに戻して、DP、APの制御方法を確認していきます。DPACCとAPACCは、DPとAPを制御するためのプライベート命令です。どちらの命令もDRレジスタ長は35bitで、中のフォーマットも一緒です。送信時のフォーマットは頭から32bitがデータ、そこから2bitがオフセットアドレスの3から2bit目、最後の1bitが読み書きどちらで命令を発行するかのフラグとなっています。

受信時のフォーマットは頭から32bitが返答データ、残りの3bitがACKとなっています。少しややこしいのですが、ACC命令をIRにセットしたあとすぐにDRを読み込んだ時に得られるデータは、前回ACC命令を発行した際に送ったデータに対する返答となっているので注意が必要です。

ACC命令の返答は、次の命令実行時に返ってくるという特徴があるため、DPまたはAP制御用レジスタの読み書き時には、ACC命令を2度発行してから結果を確認する必要があります。例えばレジスタへの書き込みを行う際は、1度目に書き込みフラグ(負論理)をLowにしながらACC命令を発行したあと、DPのRDBUFFというレジスタを読み出してACKを取得します。このDP RDBUFFは読み出しても0が返ってくるだけなので動作に影響はありません。読み込み時もほぼ同じ手順で行います。

次に、ACKの中身を見ていきます。ACKはOK/FAULTとWAITの2種類あります。OK/FAULTは発行した命令が完了したことを示しています。エラーが起きていないかがわからないので、DPのCTRL/STATレジスタを確認する必要があります。WAITは前回発行した命令が完了していないことを示します。

アクセス用の命令がわかったので、ここからは各ポートの持っているレジスタを確認していきます。(スライドを示して)DPの主要なレジスタはこの3種となります。CTRL/STATレジスタは電源制御や転送状態の確認等に使うレジスタです。SELECTはAP自体やAP内のアクセス先バンクを切り替えるレジスタです。RDBUFFは読み出すと0が返ってくるレジスタです。各レジスタをもう少し詳しく見ていきましょう。

レジスタの中身はどうなっているのか

(スライドを示して)CTRL/STATレジスタの中身はこのようになっています。PWRUPREQとPWRUPACKは、システムやデバッグに必要な機能ブロックの電源を制御するビットです。すべてのPWRUPREQビットをHighにしておかないと、APの返答がすべて0になるので注意が必要です。その他のビットは通信状態の確認等に使うビットになりますが、説明は省略します。

DP SELECTレジスタの中身は主に3つに分かれています。APSELはアクセスするAPを選択するフィールドです。APが複数存在するシステムで利用します。APBANKSELはアクセスするAPのバンクアドレスを指定するフィールドです。DPBANKSELは今回使わないので説明を省略します。

ここからはAPレジスタについて説明していきます。CSWはデバッグアクセス許可等を行うレジスタです。DPのCTRL/STATレジスタと同じく、初期設定しないとメモリアクセスがおかしくなるので注意が必要です。TARはアクセス先のメモリアドレスを指定するレジスタです。アクセス先が64bitアドレスの場合は、TARhiとTARloの2つのレジスタに分けてアドレスを書き込みます。

BDはTARで指定したメモリを読み書きするレジスタです。TARの上位28bitしか見ない点に注意してください。

BASEはROMテーブルやデバッグコンポーネントが入ったメモリのベースアドレスを示すレジスタです。IDRはAP識別用IDの入ったレジスタです。これを読むと、そのAPがどんなAPかがわかります。

(スライドを示して)AP CSWレジスタは初期化が必要なので中身を少し見ておきます。初期化の文脈で重要なのは、DbgSwEnableビットです。このビットを立てておかないとAPレジスタの返答がおかしくなるので注意してください。

すでに軽く説明していますが、これを忘れると変なところで返事がこなくなるので再度紹介します。DAPを使ったデバッグを行うには、初期設定としてDPのCTRL/STATレジスタで各種電源を入れるのと、APのCSWレジスタでソフトウェアデバッグの有効化を行う必要があります。忘れずに行ってください。

各ポートにアクセスする手順

ここからは、実際に各ポートにアクセスしてみようと思います。最初の例として、APのIDRレジスタを読み出してみましょう。アクセス手順はDPのSELECTでAPを0、APバンクをFに設定し、APACC命令でAPのIDRレジスタを読み出すだけです。

(スライドを示して)IDR読み出しを行う通信を図示したものがこちらです。矢印の上がデバッガからターゲットへの通信、矢印の下がターゲットからデバッガへの通信です。なお、簡単にするためターゲットからのACKはすべてOKが返ってくるものとします。最初はDPACC命令でAPを0、APバンクをF、DPバンクを0にしてSELECTを発行します。

続けてRDBUFFを読み出して、SELECTのACKを確認します。次に、APACC命令でIDRレジスタを読み出します。アドレスは下位を2bit右シフトしたものになる点に注意します。続いてRDBUFFを読み出して、ACKとIDRレジスタの中身を読み出します。これでIDRの読み出しは完了です。(スライドを示して)先ほど図示した内容をコードにしてみたものがこちらです。

(スライドを示して)実行結果はこちらです。先ほどの手順を実施して、IDRレジスタから値を得られていることが確認できました。

MEM-APを使ったメモリアクセス

次は、MEM-APを使ってメモリマップされているデバッグユニットのMPIDRレジスタにアクセスしてみましょう。基本的な手順は、TARレジスタでメモリアドレスを指定して、BDレジスタで読み書きするだけです。

(スライドを示して)こちらがMPIDRを取得するコードです。TARにアドレスを書き込んで、BDから読み出している様子が見えるかと思います。

(スライドを示して)実行結果はこのようになりました。問題なく読み出せていそうです。うまく読み込めていない時は、前回書き込んだ値や0が返ってくることが多いので、そういう値が返ってきたらバグを疑います。

この節の最後に、ROM Tableの内容について軽く紹介します。ROM TableはAPのメモリにあるコンポーネントの情報が書かれているテーブルです。テーブル自体もメモリにマップされていて、開始アドレスはAPのBASEADDRレジスタで確認できます。

私はまだ行えていませんが、openocdを使うとROMテーブルの中身を簡単に確認できます。デバッグユニットのアドレス等がこのROMテーブルに書かれていることがわかるので、興味のある方は試してみてください。

この節のまとめに入ります。まず、JTAGのTAP以降はDPACCとAPACC命令を使ってDAPとやり取りを行います。デバッグ用のレジスタはメモリマップされていて、MEM-APの仕様に沿ってアクセスする必要があります。アクセス手順の基本は簡単で、TARレジスタを使ってアドレスをセットし、BDレジスタで読み書きを行うだけです。

CPUをデバッグする

ついにCPUデバッグをします。(スライドを示して)この節で説明するのは、要約の図のデバッグレジスタなどを使って実際にデバッグをしていく部分です。

CPUデバッグは、breakpointやレジスタの書き換え等を行う機能です。Armのプロセッサでは、メモリマップされているデバッグユニットのレジスタをMEM-AP経由で制御してデバッグを行います。

CPUデバッグを行うには、各プロセッサコアごとに用意されているデバッグユニットとCTIを利用する必要があります。これらの制御用レジスタはMEM-APのメモリにマップされているので、マップされているアドレスを知る必要があります。

デバッグユニットとCTIのベースアドレスは基本的にROMテーブルに書かれています。ただしRaspberry Pi 3b+はCTIのアドレスがROMに書かれていなかったのでネットで調査しました。

次は、プロセッサからレジスタ値を取得する準備を行っていきます。プロセッサのデバッグを行うには、簡単な初期化を行ったあと、プロセッサをHaltしてデバッグモードにする必要があります。

最初に初期化を行います。デバッグレジスタ制御を行うためにはレジスタアクセスにロックをかける、OS Lockを最初に外しておく必要があります。この作業を忘れると、このあと扱うレジスタ値が書き換わらず、ハマるので気をつけてください。

初期化が完了したので、プロセッサをデバッグモードにしていきます。デバッグモードにするための具体的な作業は、Halting debugの有効化とプロセッサのHaltです。

まずはHalting debugの有効化から見ていきましょう。Halting Debugは、プロセッサにHalt命令が来た時にデバッグモードに入れる機能です。この機能はデバッグユニットのEDSCRレジスタで制御します。

EDSCRはデバッグステートの取得や制御に使うレジスタです。このレジスタのHDEビットを立てると、Halting Debugが有効になります。

次にコアのHalt方法を見ていきます。Cross Trigger Interface、略してCTIは、プロセッサにHalt等のトリガーイベントを送るためのインターフェイスです。ARMv8からはこのCTIがないとHaltが送れないようです。まだあまり詳しくないので詳細は話せません。

CTIを使ったHaltの方法は、ARMv8のアーキテクチャマニュアルに書かれているので、それを参考に実施します。やることはイベントをCTMに流さないようにしたあと、Haltに対応するch0チャンネルでイベント生成するようにして、チャンネルイベントを送るだけです。これでコアにHalt命令が送られ、デバッグステートに移行します。

コアをデバッグステートにしてみる

実際にコアをデバッグステートにしてみましょう。基本的にやることは、HDEを有効化したあと、CTIからコアをHaltするだけです。うまくデバッグステートに入れば、EDSCRレジスタのRWフィールドがデバッグステートを示してくれます。

(スライドを示して)HDEを有効化するコードはこちらです。やっていることはEDSCRを読んでHDEを1にして書き戻すだけです。

CTIを使ってコアをHaltします。手順はマニュアルに書かれていたとおりです。予めCTI自体を有効化するのを忘れないようにしましょう。

(スライドを示して)先ほどのコードを実行した結果がこちらです。HDEが1になってコアがデバッグステートに入っていることがわかります。

デバッグ準備が整ったので、CPUレジスタ値の取得を行います。CPUレジスタへのアクセスは、主にDBGDTRとEDITRの2つのデバッグレジスタを使って行います。

DBGDTRはデバッガとプロセッサの両方から読み書きできるシステムレジスタです。このレジスタを使ってデバッガとプロセッサのデータをやり取りします。デバッガからプロセッサに、プロセッサのレジスタ値をこのDBGDTRレジスタに入れるように命令するには、次に紹介するEDITRレジスタを使います。

EDITRはデバッガからプロセッサの命令のopcodeを書き込むと、プロセッサコアがその命令を実行するレジスタです。デバッガはこのEDITRを使って、プロセッサにレジスタ値をDBGDTRレジスタに入れる命令を実行させることで、レジスタ値を読めるようになります。この仕組みを用いることで、レジスタ値はもちろん、レジスタを経由してプロセッサの見ているメモリ値も取得できます。

レジスタx1の値をデバッガで読み出してみる

実際にプロセッサのx1レジスタ値を読み出してみましょう。手順は、EDITRレジスタを使って、プロセッサにDBGDTRレジスタにx1レジスタの中身を書き込む命令を実行させてから、デバッガからDBGDTRレジスタの中身を読むだけです。

プログラムを動かす前の準備として、レジスタ値が取れているか確認しやすいよう、各レジスタにレジスタ番号を8bitずつ繰り返して入れるコードがRaspberry Piで動くようにしておきます。

話をデバッグ用のコードに戻します。初期化とHaltのコードはフラグの初期化処理を除いて前回とほぼ同じです。

(スライドを示して)こちらが今回のメイン部分になります。x1レジスタ値を、DBGDTRレジスタに入れる命令のopcodeをEDITRに書き込んで実行させます。その後、DBGDTRレジスタを読み出して、x1レジスタ値を取得しています。なお、命令からopcodeへの変換部分は、hikaliumさんが作ってくれたbingenというcrateを使っています。

(スライドを示して)こちらが実行結果です。x1レジスタの中身が取れているのが確認できます。

この節のまとめです。ARMv8のプロセッサコアのデバッグをするには、デバッグユニットとCTIを使ってコアをデバッグステートにする必要があります。デバッガからプロセッサのレジスタ値を取得するのは、EDITRレジスタを使った命令実行と、DBGDTRレジスタを使ったデータの交換を行うことで実現できます。

セッションのまとめ

今日のまとめです。JTAGインターフェイスからTAPまでは、TMS等の信号線を使ってステートマシンを制御しながら、命令やデータをやり取りしています。DAPの制御はDPACCとAPACCというプライベート命令を使って行います。DAPより先は、MEM-APを使ってメモリマップドレジスタを読み書きしながら各ユニットを制御します。

プロセッサのレジスタ値はCTIを使ってコアをHaltしたあと、デバッガとプロセッサ両方から読み書きできるデバッグレジスタに対し、プロセッサのレジスタ値を書き込む命令を実行させてから、デバッガで中身を読み出すと取得できます。

本日の発表は以上です。ありがとうございました。