LPC11U35にSWDでアクセス(2)

ここでは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への切り替えが必要で、次のようなシーケンスになっている。

  1. HIGH(1)を50回以上出力
  2. 0111100111100111を出力
  3. 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ファースト)。

SWDプロトコル(SWDIO)
SWDプロトコル(SWDIO)

  • 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) { }
}

マイコンボード接続
2つのマイコンボードを接続する

接続したら念のためLPC11U35の電源を入れてリセットボタンを押す。STM32F042にバイナリを書き込めば自動的にスタートしてPCのコンソールに出力が表示される。こういうときNucleoボードにはUSBシリアル機能がついているから便利。ちなみにUARTのボーレートはデフォルトの9600だと遅くてSWDの周波数に間に合わない気がしたので921600に変更した。結果は…

=== START ===
[DP]  DPIDR  = 0x0bb11477

うまくいった。0x0bb11477という値もCortex M0のDPIDRと一致(参考)。

FT2232Dでは全くできなかったアクセスにいとも簡単に成功してしまった。つまりFT2232Dを使っていたときはマイコン(LPC11U35)側に問題はなかったということか。