LPC11U35にSWDでアクセス(2)
- LPC11U35にSWDでアクセス(1)
- LPC11U35にSWDでアクセス(2)
- LPC11U35にSWDでアクセス(3)
- LPC11U35にSWDでアクセス(4)
ここではSWDプロトコルでSWD-DPのDPIDRレジスタを読み出すところまで実装する。
SWDとは
SWD(Serial Wire Debug)はデバッグに使われるインターフェイスで、2線しか使わないのがウリ。1つはSWCLKというクロック信号、もう1つはSWDIOという双方向データ信号となっている。SWCLKはホスト(デバッガ)側が出力し、SWDIOは状況によってどちらかが出力し、もう片方はハイインピーダンスにする。SWDIOへの出力は、ホスト側の場合はSWCLKの立ち下がりエッジで、デバイス側は立ち上がりエッジで切り替える。そしてどちらもSWCLKの立ち上がりエッジでSWDIOを読む。タイミング要件を見るとクロック周波数は最低1kHz、最高50MHzのようだ。
SWDプロトコル
それではプログラムを書いていこう。プログラムを実行するのはSTM32F042 Nucleoボード、開発環境はMbedを使用。
入出力
まずはSWCLKとSWDIOのためのGPIOを設定する。SWCLKはホスト側が出すのでDigitalOut、SWDIOは双方向なのでDigitalInOut。
#include "mbed.h" DigitalOut swclk(A2); DigitalInOut swdio(A3); int main() { while (1) { } }
次にビットの読み書きをする関数を作る。
#define SWD_CLK_HALF_PERIOD 250 // microseconds void swd_data_out(int d) { swdio.output(); swdio = d; wait_us(SWD_CLK_HALF_PERIOD); swclk = 1; wait_us(SWD_CLK_HALF_PERIOD); swclk = 0; } int swd_data_in(void) { swdio.input(); wait_us(SWD_CLK_HALF_PERIOD); int d = swdio; // sample SWDIO just before posedge of SWCLK swclk = 1; wait_us(SWD_CLK_HALF_PERIOD); swclk = 0; return d; }
ここで注意するのは、SWDIOを読み取るタイミング。本来はSWCLKの立ち上がりエッジだが、これはデバイス側出力の切り替わりタイミングと同じ。マイコンのGPIOで読み取る場合、SWCLKをHIGHにしてから読んでも間に合わず、その前にデバイス側の出力が変化してしまう。そのためSWCLKをHIGHにする直前に読んでいる。
JTAGからの切り替えシーケンス
SWDはJTAGから派生したようなインターフェイスで、JTAG用のピンの一部を使う形になっている。そのため、SWDを使う場合は最初にJTAGからSWDへの切り替えが必要で、次のようなシーケンスになっている。
- HIGH(1)を50回以上出力
- 0111100111100111を出力
- HIGH(1)を50回以上出力
この後出力をLOW(0)にするとSWDのアイドル状態となって、続くSWDプロトコルを受けつけてくれる。
以上を関数にしたのがこちら。
void switch_jtag_to_swd(void) { for (int i = 0; i < 50; i++) swd_data_out(1); swd_data_out(0); swd_data_out(1); swd_data_out(1); swd_data_out(1); swd_data_out(1); swd_data_out(0); swd_data_out(0); swd_data_out(1); swd_data_out(1); swd_data_out(1); swd_data_out(1); swd_data_out(0); swd_data_out(0); swd_data_out(1); swd_data_out(1); swd_data_out(1); for (int i = 0; i < 50; i++) swd_data_out(1); // idle swd_data_out(0); swd_data_out(0); }
DPレジスタへアクセス
SWDへの切り替えが完了すれば、DP(デバッグポート)のレジスタにアクセスできるようになる。SWDではこのレジスタへの読み書きを通じて内部のAP(アクセスポート)、さらにはその奥にあるCPUやバスにアクセスできる。
レジスタアクセスのプロトコルは次の通り。複数ビットのデータは下位ビットから送る(LSBファースト)。
- Start: 常に1
- APnDP: APアクセスなら1、DPアクセスなら0
- RnW: 読み込みなら1、書き込みなら0
- A: レジスタのアドレス。下位2ビットは0固定なので使わない
- Parity(1つ目): パリティビット。APnDP、RnW、Aの合わせて4ビットの中に1が奇数個あれば1、偶数個あれば0を送る
- Stop: 常に0
- Park: 常に1
- Trn: ホストとデバイスの入出力方向を切り替えるために、どちらも出力をしないサイクルを挿入する
- ACK: デバイスからの応答。1なら「OK」、2なら「WAIT」、4なら「FAULT」。OK以外が返ってきたらアクセスは中断(Trnを入れて終了)
- WDATA: レジスタに書き込むデータ。ホストが出力する
- RDATA: レジスタのデータ。デバイスが出力する
- Parity(2つ目): パリティビット。WDATAまたはRDATAの32ビットの中に1が奇数個あれば1、偶数個あれば0にする
これを関数にする。Trnの部分はswd_data_in()を使う。パリティの計算にはXOR演算(^
)が便利。
#define SWD_OK 1 #define SWD_WAIT 2 #define SWD_FAULT 4 #define SWD_PARITY (-1) int swd_reg; int swd_reg_access(int ap, int read, uint32_t address, uint32_t value) { int a2 = address >> 2 & 0x1; int a3 = address >> 3 & 0x1; // start(1) + APnDP + RnW + A + parity + stop(0) + park(1) swd_data_out(1); swd_data_out(ap); swd_data_out(read); swd_data_out(a2); swd_data_out(a3); swd_data_out(ap ^ read ^ a2 ^ a3); swd_data_out(0); swd_data_out(1); // trn + ack swd_data_in(); int ack0 = swd_data_in(); int ack1 = swd_data_in(); int ack2 = swd_data_in(); int ack = ack0 | ack1 << 1 | ack2 << 2; // read: data + parity + trn // write: trn + data + parity if (read) { if (ack == SWD_OK) { // data swd_reg = 0; int parity = 0; for (int i = 0; i < 32; i++) { int b = swd_data_in(); swd_reg |= b << i; parity ^= b; } // parity int p = swd_data_in(); if (p != parity) ack = SWD_PARITY; } // trn swd_data_in(); } else { // trn swd_data_in(); if (ack == SWD_OK) { // data int parity = 0; for (int i = 0; i < 32; i++) { swd_data_out(value & 1); parity ^= value & 1; value >>= 1; } // parity swd_data_out(parity); } } if (ack != SWD_OK) { printf("access failed (%2s %c 0x%08x): %d\n", ap ? "AP" : "DP", read ? 'R' : 'W', address, ack); } return ack; } int read_dp_reg(uint32_t address, const char *name = NULL) { int ack = swd_reg_access(0, 1, address, 0); if (name != NULL && ack == SWD_OK) printf("[DP] %6s = 0x%08x\n", name, swd_reg); return ack; } int write_dp_reg(uint32_t address, uint32_t value, const char *name = NULL) { int ack = swd_reg_access(0, 0, address, value); if (name != NULL && ack == SWD_OK) printf("[DP] %6s <- 0x%08x\n", name, value); return ack; }
DPIDRを読む
いよいよSWDでLPC11U35にアクセスする。LPC11U35のSWDピンはPIO0_10(SWCLK)とPIO0_15(SWDIO)なので、ここにSTM32F042のピンを接続する。まずはDPIDR(アドレス0x00)を読み出してみよう。
// DP register address #define DP_DPIDR 0x0 #define DP_ABORT 0x0 #define DP_CTRL 0x4 #define DP_STAT 0x4 #define DP_SELECT 0x8 #define DP_RDBUFF 0xc int main() { puts("\n=== START ==="); switch_jtag_to_swd(); read_dp_reg(DP_DPIDR); while (1) { } }
接続したら念のためLPC11U35の電源を入れてリセットボタンを押す。STM32F042にバイナリを書き込めば自動的にスタートしてPCのコンソールに出力が表示される。こういうときNucleoボードにはUSBシリアル機能がついているから便利。ちなみにUARTのボーレートはデフォルトの9600だと遅くてSWDの周波数に間に合わない気がしたので921600に変更した。結果は…
=== START === [DP] DPIDR = 0x0bb11477
うまくいった。0x0bb11477という値もCortex M0のDPIDRと一致(参考)。
FT2232Dでは全くできなかったアクセスにいとも簡単に成功してしまった。つまりFT2232Dを使っていたときはマイコン(LPC11U35)側に問題はなかったということか。
- LPC11U35にSWDでアクセス(1)
- LPC11U35にSWDでアクセス(2)
- LPC11U35にSWDでアクセス(3)
- LPC11U35にSWDでアクセス(4)