LPC11U35にSWDでアクセス(4)

前回はMEM-APを使ってAHBバスにアクセスした。RAMを好き放題できるようになったので、いよいよLPC11U35のIAP機能を使ってFlashをいじる。

IAP(In-Application Programming)は通常のマイコン動作中に内蔵Flashを書き換えられる機能。リセット時にブートローダーから(通常動作に入る前に)起動されるISP(In-System Programming)とは別。もちろんSWDデバッグ中も有効で、実際SWDデバッガーからプログラミングするときはIAPを使用するらしい。これを利用してFlashに書き込んでしまったCRPコードを消去する。

前回まではARMマイコンならだいたい通じる内容だったが、今回はLPC11U35限定の話になる。詳細はLPC11U35のユーザーマニュアルを参照してほしい。

IAPコマンドの呼び出し

IAP機能はIAPコマンドを呼び出して利用する。マニュアルによると、パラメータを格納する構造体と結果を受け取る構造体を用意して、それらへのポインタを引数にして特定のアドレスにあるIAPの関数を呼べばいいらしい。しかし、問題はそれをどうやってCPUに実行させるのか。普通はCPUのレジスタを直に操作すればいいはずだが、方法がすぐにわからなかったので別の手段を試してみた。

  1. IAPの関数を呼ぶ関数(ここではiap_call())を書いてコンパイル
  2. コンパイル結果からiap_call()の部分を抜き出して、バイナリコードを得る
  3. 空いているRAM領域に2のコードをSWDで書き込む
  4. タイマー割り込みハンドラに3で書き込んだ関数のアドレスを指定

これでタイマーの割り込みが発生すれば1で書いた関数が実行される。幸い、この時LPC11U35で動いているプログラムはmbed-os-example-blinkyとほぼ同じもので、ウェイト動作にタイマーを利用している(thread_sleep_for())。アセンブラがわかる人なら1や2の作業はいらないのかもしれない。

IAPでFlashのデータを消去する関数の準備

まずは関数を書く。一応、別ファイルに書いてインライン展開されにくくしておく(効果があるのかは不明)。

#include <stdint.h>

typedef void (*IAP)(uint32_t parameter_table_address, uint32_t result_table_address);

void iap_call(void)
{
    static const IAP iap = (IAP)0x1fff1ff1;
    iap(0x10000ee0, 0x10000ef0);
    *(volatile unsigned long *)0x0000008c = 0x00005321; // restore timer IRQ handler
}

0x1FFF1FF0番地にあるIAP関数をiapとして定義している(iap_call()1行目)。アドレスの最下位ビットが1なのはIAP関数がThumbコードであることを示す。次にこれに引数を与えて呼び出す(iap_call()2行目)。0x10000EE0番地と0x10000EF0番地はどちらも使っていない領域なので、そこをIAP関数のパラメータと結果の受け渡しに使うことにした。最後にタイマーの割り込みハンドラを元に戻して終了(iap_call()3行目)。0x00005321というのは先ほどMEM-APでメモリを読み出したときの0x8C番地の値。

これをmain()の中で呼び出すようにしてコンパイルする。そして、<プログラム名>.mapファイル(.binや.elfなどと同じフォルダにできる)をテキストエディタで開いてiap_callがある場所を探す。

    0x08004104   0x00000020   Code   RO            3    .text.iap_call      BUILD/NUCLEO_F042K6/ARMC6/iap_call.o

上記は一例。この場合、0x08004104番地から0x20 = 32バイトの部分にiap_call()が配置されていることがわかる。(本当はちゃんとLPC11U35をターゲットにしてコンパイルすべきだろうが、同じCortex M0なのでSTM32F042でも多分大丈夫だろう…。)

次に、実際のバイナリファイル(<プログラム名>.bin)をバイナリエディタで開いて.mapファイルで得られた番地のデータを見る。STM32F042の場合Flashは0x08000000番地からスタートなので.binファイルでは0x00004104番地を見る。

バイナリエディタの画面
.binファイルをバイナリエディタで開いて確認

これでiap_call()のバイナリコードがわかった。

IAPコマンドを実行

いよいよIAPコマンドの実行だ。まずは用意したiap_call()のバイナリコードをメモリの空いている場所に書き込む。ここでは0x10000E00番地を使うことにした。

    puts("write IAP call code");
    write_ap_reg(AP_TAR, 0x10000e00);
    write_ap_reg(AP_DRW, 0x4804b580);
    write_ap_reg(AP_DRW, 0x31104601);
    write_ap_reg(AP_DRW, 0x47904a03);
    write_ap_reg(AP_DRW, 0x4903208c);
    write_ap_reg(AP_DRW, 0xbd806001);
    write_ap_reg(AP_DRW, 0x10000ee0);
    write_ap_reg(AP_DRW, 0x1fff1ff1);
    write_ap_reg(AP_DRW, 0x00005321);

次に、0x2FC番地を含むセクター(セクター0)に対して書き込みや削除ができるようにするためのIAPコマンド(のパラメータ)を準備する。アドレス(0x10000EE0)はiap_call()の定義に合わせる。

    puts("prepare sector for write operation");
    // prepare sector 0 (0x00000000-0x00000fff) for write operation
    write_ap_reg(AP_TAR, 0x10000ee0);
    write_ap_reg(AP_DRW, 50);       // command code
    write_ap_reg(AP_DRW, 0);        // start sector number
    write_ap_reg(AP_DRW, 0);        // end sector number

そして実行。タイマー割り込みを先ほどIAPコマンドパラメータを書き込んだアドレス(0x10000E00)に設定する。最下位ビットを1にするのを忘れずに(Thumbコードのため)。その後はタイマー割り込みが発生するのを待つ。

    // invoke
    write_ap_reg(AP_TAR, 0x0000008c); // CT32B0 (32-bit timer0)
    write_ap_reg(AP_DRW, 0x10000e01);
    // wait
    for (int i = 0; i < 1000; i++)
        swd_data_out(0);

リターンコードは0x10000EF0番地に入る(iap_call()の定義を参照)ので、それを読む。

    // check return code
    write_ap_reg(AP_TAR, 0x10000ef0);
    read_ap_reg(AP_DRW);
    printf("return code: %d\n", swd_reg);

同様にセクター消去コマンドも実行。消去ではなく書き換えにしようかとも思ったが、元データのコピーなどめんどくさいので、セクターを消去してコードを無効にすることでリセット時にISPモードになるようにした。

    puts("erase sector");
    write_ap_reg(AP_TAR, 0x10000ee0);
    // erase sector 0 (0x00000000-0x00000fff)
    write_ap_reg(AP_DRW, 52);       // command code
    write_ap_reg(AP_DRW, 0);        // start sector number
    write_ap_reg(AP_DRW, 0);        // end sector number
    write_ap_reg(AP_DRW, 48000);    // system clock frequency (kHz)
    // invoke
    write_ap_reg(AP_TAR, 0x0000008c);
    write_ap_reg(AP_DRW, 0x10000e01);
    // wait
    for (int i = 0; i < 1000; i++)
        swd_data_out(0);
    // check return code
    write_ap_reg(AP_TAR, 0x10000ef0);
    read_ap_reg(AP_DRW);
    printf("return code: %d\n", swd_reg);

実行すると…

write IAP call code
prepare sector for write operation
return code: 0
erase sector
return code: 0

LPC11U35のプログラムが消えてLEDの点滅が止まり、リセットボタンを押すとISPモードになって再びプログラムを書き込めるようになった。バンザイ!

感想

元々覚悟していたが、マイコンのマニュアルやARMの資料を行ったり来たりしながらで結構な時間がかかってしまった。ちゃんとしたSWDデバッグアダプターがあればそもそもこのような事態にはならないはずで、労力を考えると安いSWDアダプターとか今回ならLPC Linkでも買っておくべきだなと思う(買うとは言ってない)。

ただ今回こうしてSWDやARM Cortex M0のデバッグ関係について知識を得られたのは、またどこかで役に立つと思いたい。逆にオリジナルSWDアダプターを作ってしまうのもありかもしれない。

冒頭のFT2232Dに関してはそのうち再挑戦したい。もう少し可変抵抗器とか3ステートバッファとかあれば楽なんだけど…。近くにパーツ屋のない田舎暮らしだと、たった数十円のパーツのために500円の送料を払うのはつらいのだ…。

LPC11U35にSWDでアクセス(3)

前回はSWD-DPのDPIDRレジスタにアクセスするところまで進めた。ここではSWD-DPの先にあるMEM-APを使って、マイコン内部のAHBバスにアクセスする。

MEM-APとは

MEM-APはAP(アクセスポート)の種類の1つで、メモリ(SRAMFlashなど)やメモリマップドI/Oなど、アドレスを使ってリソースにアクセスするような場合に使われる。普通はメインのバスにつながっているので、MEM-APに接続できればだいたいのことはSWDからもできるようになる。

MEM-APを使う

以下の手順で進めていく。

  1. APを使うための設定をDPレジスタで行う
  2. APレジスタにアクセス
  3. MEM-APを使ってAHBバス(につながっているメモリ)にアクセス

APを使うための準備

はじめに、APにアクセスするにはまずデバッグ回路の電源を入れる必要がある。またエラーフラグが残っていた場合、それらをクリアしておかなくてはならない。これらの操作はDPレジスタで行う。

    write_dp_reg(DP_CTRL, 0x50000000, "CTRL"); // debug component power on
    write_dp_reg(DP_ABORT, 0x1f, "ABORT");; // clear error flags
    do
        read_dp_reg(DP_STAT, "STAT");
    while ((swd_reg & 0xf0000000) != 0xf0000000);

APレジスタにアクセス

SWDからAPレジスタにアクセスする手順は次の通り。アクセスしたいAPレジスタのアドレスをAとすると

  1. DPのSELECT[7:4]に、A[7:4]をセット
  2. A[3:2]を使ってAPアクセス(APnDP = 1にして、他はDPレジスタアクセスと同様)
    • 書き込みの場合、WDATAとして書き込む値を送信する
    • 読み込みの場合、送られてくるRDATAは無視して3に進む
  3. 読み込みの場合、DPのRDBUFFに読み出された値がセットされるのでこれを読む

これも関数にしておく。

int read_ap_reg(uint32_t address, const char *name = NULL)
{
    int ack = swd_reg_access(0, 0, DP_SELECT, address & 0xf0);
    if (ack == SWD_OK)
    {
        ack = swd_reg_access(1, 1, address, 0);
        if (ack == SWD_OK)
            while ((ack = swd_reg_access(0, 1, DP_RDBUFF, 0)) == SWD_WAIT) { }
        if (name != NULL && ack == SWD_OK)
            printf("[AP] %6s  = 0x%08x\n", name, swd_reg);
    }
    return ack;
}

int write_ap_reg(uint32_t address, uint32_t value, const char *name = NULL)
{
    int ack = swd_reg_access(0, 0, DP_SELECT, address & 0xf0);
    if (ack == SWD_OK)
    {
        ack = swd_reg_access(1, 0, address, value);
        if (name != NULL && ack == SWD_OK)
            printf("[AP] %6s <- 0x%08x\n", name, value);
    }
    return ack;
}

この関数を使ってAPのIDRを読み出してみる。IDRにはAPの種類などの情報が含まれているので、どんなAPにアクセスしているのかがわかる。

    read_ap_reg(AP_IDR);
    printf("AP class = 0x%01x (if 0x8, MEM-AP)\n", swd_reg >> 13 & 0xf);
    printf("AP variant = 0x%01x\n", swd_reg >> 4 & 0xf);
    printf("AP type = 0x%01x (if 0x1, AHB bus)\n", swd_reg & 0xf);

結果は…

=== START ===
[DP]  DPIDR  = 0x0bb11477
[DP]   CTRL <- 0x50000000
[DP]  ABORT <- 0x0000001f
[DP]   STAT  = 0xf0000040
[AP]    IDR  = 0x04770021
AP class = 0x8 (if 0x8, MEM-AP)
AP variant = 0x2
AP type = 0x1 (if 0x1, AHB bus)

メモリにアクセス

IDRの情報からMEM-APにアクセスしていることがわかった。そしてMEM-APは内部のAHBバスにつながっているので、この時点でMEM-APを使ってAHBバスを操作できるはず。試しに0x000~0x100番地の値を読み出してみよう。MEM-APを使ってAHBバスのアドレス空間にアクセスする手順は次の通り。

  1. CSWレジスタでアクセス方法を設定
  2. TARレジスタにアクセスするアドレスをセット
  3. DRWレジスタを使ってアクセス
    • DRWへの読み書きがそのままTARが示すアドレスに対する読み書きに変換される

今回の場合はこのようになる。

    puts("configure memory access ...");
    read_ap_reg(AP_CSW);
    write_ap_reg(AP_CSW, swd_reg & 0xffffffc8 | 0x10 | 0x2, "CSW"); // increment single, 32bit

    puts("read 0x00000000 - 0x00000100");
    write_ap_reg(AP_TAR, 0, "TAR");
    for (int i = 0; i < 64; i++)
        read_ap_reg(AP_DRW, "DRW");

インクリメントアクセス(CSW[5:4] = 1)に設定するとDRWにアクセスするたびに自動でアドレスを進めてくれるので、このような連続アクセスにはとても便利。

=== START ===
[DP]  DPIDR  = 0x0bb11477
[DP]   CTRL <- 0x50000000
[DP]  ABORT <- 0x0000001f
[DP]   STAT  = 0xf0000040
[AP]    IDR  = 0x04770021
AP class = 0x8 (if 0x8, MEM-AP)
AP variant = 0x2
AP type = 0x1 (if 0x1, AHB bus)
configure memory access ...
[AP]    CSW <- 0x03000052
read 0x00000000 - 0x00000100
[AP]    TAR <- 0x00000000
[AP]    DRW  = 0x10002000
(以下略)

LPC11U35に書き込んだバイナリと比べてみると、割り込みテーブルが若干変わっている以外は同じ値が読み出せた。割り込みテーブルはOSが実行時に書き換えたと思われる。実際のコードは省くが、同じようにTARにアドレスをセットしてからDRWに書き込みすることでRAMに書き込むこともできる。これでAHBバスへのアクセスは成功した。

ついでに例のアドレスも読み出してみる。

    puts("read CRP option (0x000002fc)");
    write_ap_reg(AP_TAR, 0x000002fc);
    read_ap_reg(AP_DRW);
    printf("CRP option = 0x%08x\n", swd_reg);
read CRP option (0x000002fc)
CRP option = 0x4e697370

これを何とかしたいのだ…。

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)側に問題はなかったということか。

LPC11U35にSWDでアクセス(1)

いきさつ

コンパイル環境が整ってLPC11U35のソフトを書けるようになったので、次はデバッグ環境を用意したいと思った。そこで一緒に買ったFT2232DモジュールとOpenOCDを使ってSWD接続しようとしたんだけど、これがうまくいかない。どうやらマイコンのDPIDRを読むところ(つまり一番最初)で失敗しているようだ。デバッグ機能関係はほとんど知識がなくて原因もまるで見当がつかないまま、マニュアルに書いてあるそれっぽいことを手当たり次第に試していたら、いつの間にかISPモードに入れなくなってソフトの書き込みができなくなっていた。

原因は、マイコンに備わっているコードリードプロテクション(CRP)機能を「NO_ISP」に設定したため。SWDでアクセスできないのはCRPのせいではないかと思って、「NO_ISP」を「NO_CRP」と勘違いして実行してしまったのだ。Flashの0x2FC番地に0x4E697370を書き込むと、ブートローダーがそれを見てISPモードを回避する仕組みになっている。

復旧

このまま使えなくなってしまうのは悲しいので復旧させたい。なんとかして0x2FC番地を0x4E697370以外に戻せば、再びISPが有効になるはず。マニュアルを見るとNO_ISP設定でもSWDによるデバッグやプログラミングはできるらしい。これなら0x2FC番地を書き換えることもできそうだ。

しかし先に説明したようにFT2232Dを使ったSWDアクセスは失敗している…。ほかにデバッガもない中どうするかと考えた結果、一緒に買ったSTM32F042を使ってSWD信号を直接やり取りする実験をしてみたところ、これがうまくいった。最終的に0x2FC番地を含むセクターを消去してユーザープログラムを無効化し、無事ISPモードに入ることに成功した。めでたしめでたし。

以下は詳細のメモ(めちゃ長い)。

方針

  1. LPC11U35のSWD端子にSTM32F042を接続
  2. SWDインターフェイスからDPレジスタにアクセスする
  3. さらにAPレジスタにアクセスする
  4. MEM-APを利用してシステムのメモリアドレス空間にアクセスする
  5. FlashのIAP機能を利用してFlashを書き換え、CRPを解除

次回から具体的な作業を示す。

STM32F042でMbed OS 5 baremetal

LPC11U35に対してやったこととほぼ同じだが、若干違う部分もあるので書いておく。一番の違いはカスタムターゲットを定義しなくていいことに気づいたこと。

開発環境はMbed Studio。

手順

空のプロジェクトを用意する

Mbed Studioで「File」→「New Program...」→「Empty Mbed OS program」を選択して適当な名前をつけてプロジェクトを作る。

baremetalプロファイルに切り替える

以降ではプロジェクトのフォルダ(トップ階層)を「.」で表すことにする。例えば最初からあるmain.cppなら「./main.cpp」と書く。

./mbed_app.jsonを作り、以下のように編集。

{
    "requires": ["bare-metal"]
}

カスタムターゲットについて

LPC11U35のときはここでカスタムターゲットを作っていた。主な目的は

        "release_versions": ["5"],

という行を入れるためで、これがないとMbed Studioでビルドするときに「Mbed OS 5に対応していない」と怒られる。そこでわざわざ元とほぼ同じ内容のカスタムターゲットを作ってMbed OS 5に対応していることを宣言していた。それが上記の行だ。

しかし気づいたのだが、どうやらbaremetalプロファイルにするとrelease_versions"5"が含まれていなくてもちゃんとビルドしてくれるようだ。よって今回はカスタムターゲットを作らずに次に進む。

ソースコードを修正して差し替える

スタックポインタの初期値を表すINITIAL_SPというマクロがあるが、STM32F042用には定義されていないので、./mbed-os/targets/TARGET_STM/mbed_rtx.hを./TARGET_STMフォルダ(自分で作成)にコピーして42行目に次の2行を追加する。

#elif (defined(TARGET_STM32F042K6))
#define INITIAL_SP              (0x20001800UL)

また、./mbed-os/targets/TARGET_STM/TARGET_STM32F0/TARGET_NUCLEO_F042K6/device/us_ticker_data.hを./TARGET_STM/TARGET_NUCLEO_F042K6フォルダにコピーして24行目

#include "stm32f0xx_ll_tim.h":

となっているところを、最後のコロンを消して

#include "stm32f0xx_ll_tim.h"

にする。別にコロンがあってもエラーにはならないが警告が出てジャマなので対処しておく。

最後にコピー元のファイルを無効にするために./.mbedignoreを作り、以下のように編集。

mbed-os/targets/TARGET_STM/TARGET_STM32F0/TARGET_NUCLEO_F042K6/device/us_ticker_data.h
mbed-os/targets/TARGET_STM/mbed_rtx.h

コンフィグ情報を変更する

ここまででビルドは通るがthread_sleep_for()を実行するとエラーになるので、mbed_app.jsonを以下のように変更する。

{
    "requires": ["bare-metal"],
    "target_overrides": {
        "NUCLEO_F042K6": {
            "target.tickless-from-us-ticker": true
        }
    }
}

この例のようにAPIを使わないと出てこないエラーもあるので、他に判明したらここに情報を追加していこうと思う。

以上で作業は終了。STM32F042用のバイナリがビルドできるようになった。

ほかのプロジェクトでも使う

今回作成したファイルをそのまま別のプロジェクトのフォルダにコピーすれば、そのままビルドできる。

(余談)スタックサイズを調整する

Mbed Studioでビルドすると最後にRAMとFlashの使用量がレポートされるが、これまでの作業を行った状態で実際にプログラムをビルドしていると例えばこんな感じになる。

| Module              |       .text |  .data |      .bss |
|---------------------|-------------|--------|-----------|
| [lib]\libcppabi_p.l |      44(+0) |  0(+0) |     0(+0) |
| [lib]\mc_p.l        |    3240(+0) |  8(+0) |    40(+0) |
| [lib]\mf_p.l        |    1144(+0) |  0(+0) |     0(+0) |
| anon$$obj.o         |      32(+0) |  0(+0) | 4848(-64) |
| main.o              |    2008(+0) |  0(+0) |    60(+0) |
| mbed-os\drivers     |    447(+96) |  0(+0) |     0(+0) |
| mbed-os\hal         |   1948(+34) |  4(+0) |    66(+0) |
| mbed-os\platform    |  5708(+155) | 64(+0) |  301(+56) |
| mbed-os\targets     |    6135(+0) |  4(+0) |   332(+0) |
| Subtotals           | 20706(+285) | 80(+0) |  5647(-8) |
Total Static RAM memory (data + bss): 5727(-8) bytes
Total Flash memory (text + data): 20786(+285) bytes

よく見てみるとanon$$obj.oのRAM使用量がやたら多い。STM32F042は6KBしかRAMがないのに5KB近くも使っている。.mapファイルを調べてみると、そのうち4KBはスタック領域として使われている。Mbed OS 2のデフォルトのスタックサイズがそのまま使われているようだ(参考)。

これではすぐにRAMが尽きてしまうので、スタックサイズを調整する。./mbed_app.jsonを以下のように再編集する。

{
    "requires": ["bare-metal"],
    "target_overrides": {
        "NUCLEO_F042K6": {
            "target.boot-stack-size": "1024",
            "target.tickless-from-us-ticker": true
        }
    }
}

target.boot-stack-sizeを上書きすることでスタックサイズを変更できる。ここではとりあえず1KBにしてみた。再ビルドすると

| Module              |     .text |  .data |     .bss |
|---------------------|-----------|--------|----------|
| [lib]\libcppabi_p.l |    44(+0) |  0(+0) |    0(+0) |
| [lib]\mc_p.l        |  3240(+0) |  8(+0) |   40(+0) |
| [lib]\mf_p.l        |  1144(+0) |  0(+0) |    0(+0) |
| anon$$obj.o         |    32(+0) |  0(+0) | 4848(+0) |
| main.o              |  2008(+0) |  0(+0) |   60(+0) |
| mbed-os\drivers     |   447(+0) |  0(+0) |    0(+0) |
| mbed-os\hal         |  1948(+0) |  4(+0) |   66(+0) |
| mbed-os\platform    |  5708(+0) | 64(+0) |  301(+0) |
| mbed-os\targets     |  6135(+0) |  4(+0) |  332(+0) |
| Subtotals           | 20706(+0) | 80(+0) | 5647(+0) |
Total Static RAM memory (data + bss): 5727(+0) bytes
Total Flash memory (text + data): 20786(+0) bytes

…変わってない。.mapを見てみると

    0x20000450   0x00000ef0   Zero   RW            1    ARM_LIB_HEAP.bss    anon$$obj.o

減らした分は代わりにヒープにいったようだ。ちゃんと調整されてるのね。

秋月のLPC11U35ボードでMbedを使う

LPC11U35でMbed OSを動かす。開発環境はWindows + Mbed Studio。

問題点

秋月で売られているLPC11U35マイコンボードはQuick Start Board互換でMbed対応なのに850円と安く、使い勝手がよさそうなので買ってみた。

しかしいざソフトを作ろうとして気づいたのだが、このボード(というか元になっているQuick Start Board)が対応しているのはMbed OS 2のみで、Mbed OS 5はサポートされていなかった。Mbed StudioはMbed OS 5限定なのでこれではコンパイルできない。仕方ないのでMbed OS 5に対応させることにした。

ただし、Mbed OS 5をLPC11U35で動かすには別の問題がある。Mbed OS 5は2に比べてRTOSなど高度な機能が追加されているので、その分必要なRAM容量が増えている。どうやら16KB以下のRAMでは厳しいらしい(参考)。LPC11U35は12KBしか積んでいないのでダメそうだし、実際最初はRTOSも含めて移植しようとしてみたもののビルド時に「RAMが足りない」と怒られた。

一方、Mbed OS 5にはbaremetalプロファイルというものがある。これは機能的にはMbed OS 2とほとんど同じで、基本的なタイマーやIOなど最低限のAPIだけに絞られている。Mbed OS 2しか動かないようなMCUをMbed OS 5でもサポートする形になっていて、当然RTOSは使えないが、これならLPC11U35でも動きそうだということでbaremetalプロファイルを移植する。

まとめると

  1. 秋月LPC11U35はMbed OS 2しか対応していない
  2. Mbed OS 5のフル機能を移植するにはRAMが足りない
  3. 最低限の機能だけのbaremetalプロファイルを使おう

移植

移植といってもほとんどのコードはMbed OS 2用に書かれたものをそのまま使えるので大した作業量ではないが、手順を残しておく。

空のプロジェクトを用意する

Mbed StudioではプロジェクトごとにGithubにあるMbed OSのソースコードを丸々コピーしてきて利用するので、OSに対する修正もプロジェクトで行うことになる。Mbed Studioで「Empty Mbed OS program」か「mbed-os-example-blinky」あたりを選択してプロジェクトを作る。

baremetalプロファイルに切り替える

プロジェクトのフォルダができたらトップ階層(main.cppと同じ場所)に「mbed_app.json」というファイルを作り、以下のように編集。

{
    "requires": ["bare-metal"]
}

カスタムターゲットを作る

同じくトップ階層に「custum_targets.json」というファイルを作り、以下のように編集。ほとんどはmbed-os/targets/targets.jsonのLPC11U35_401の部分をコピーしたものだけど、一部変更している。ターゲット名の「AE_LPC11U35」は秋月のボードの名前を拝借した。

{
    "AE_LPC11U35": {
        "inherits": ["LPCTarget"],
        "core": "Cortex-M0",
        "default_toolchain": "ARM",
        "extra_labels": ["NXP", "LPC11UXX", "LPC11U35_401"],
        "macros": [
            "CMSIS_VECTAB_VIRTUAL",
            "CMSIS_VECTAB_VIRTUAL_HEADER_FILE=\"cmsis_nvic.h\""
        ],
        "supported_toolchains": ["ARM", "uARM", "GCC_ARM", "IAR"],
        "device_has": [
            "ANALOGIN",
            "I2C",
            "I2CSLAVE",
            "INTERRUPTIN",
            "PORTIN",
            "PORTINOUT",
            "PORTOUT",
            "PWMOUT",
            "SERIAL",
            "SLEEP",
            "SPI",
            "SPISLAVE",
            "USTICKER"
        ],
        "release_versions": ["5"],
        "OUTPUT_EXT": "bin"
    }
}

コンフィグ情報を変更する

先ほどのmbed_app.jsonを再度編集。二度手間で申し訳ない。

{
    "requires": ["bare-metal"],
    "target_overrides": {
        "AE_LPC11U35": {
            "target.tickless-from-us-ticker": true
        }
    }
}

区切りのコンマをお忘れなく。

コードを修正して差し替える

ここまででコンパイル自体は通るのだが、実際にmbed-os-example-blinkyを走らせてみるとthread_sleep_for()の部分でハードフォルトになり止まってしまう。調べた結果割り込みベクターテーブルに問題があることがわかり、ソースコードを修正するとちゃんと動くようになった。ここではmbed-osフォルダ以下にあるソースコードを直接変更するのではなく、別のソースファイルに差し替えることで修正を行う。

まずは修正対象のソースファイルを無効にする。トップ階層に「.mbedignore」というファイルを作成し、以下の1行を記述。これでmbed-os/targets/TARGET_NXP/TARGET_LPC11UXX/device/cmsis_nvic.cはコンパイル対象から除外される。

mbed-os/targets/TARGET_NXP/TARGET_LPC11UXX/device/cmsis_nvic.c

次にトップ階層に「TARGET_LPC11UXX」というフォルダを作り、その中にmbed-os/targets/TARGET_NXP/TARGET_LPC11UXX/device/cmsis_nvic.cをコピー。

そして、コピーしてきたcmsis_nvic.cの63行目

      uint32_t *old_vectors = (uint32_t *)0;         // FLASH vectors are at 0x0

を次のように修正(volatileを追加)。

      volatile uint32_t *old_vectors = (uint32_t *)0;         // FLASH vectors are at 0x0

これでmbed-os/targets/TARGET_NXP/TARGET_LPC11UXX/device/cmsis_nvic.cの代わりにTARGET_LPC11UXX/cmsis_nvic.cが使われるようになり、修正が反映される。

ここまでで、ファイル構成は下のようになる。

ファイル構成
1つのフォルダと4つのファイルを追加

コンパイルする

Mbed StudioのTarget欄で「AE_LPC11U35」を選択する。検索ボックスで「ae」と入力したら候補の下の方の「MCUs and custom targets」のところに出てくるはず。

Mbed StudioのTarget選択画面
Mbed StudioのTargetに「AE_LPC11U35」が追加される

あとは普通にビルド。

バイナリの書き込みは自動でやってくれたりしないので手動で。BUILD/AE_LPC11U35/ARMC6にある.binファイルを秋月の説明書の手順通りに書き込む。

ほかのプロジェクトで使う

新しくプロジェクトを作ったら、今回作成したmbed_app.json、custom_targets.json、.mbedignore、TARGET_LPC11UXXフォルダをプロジェクトのトップ階層にコピーしてくればOK。同じようにMbed StudioでTargetに選択してビルドできるようになる。

感想

当初は移植というので大変な作業になることも想像したが、思ったよりあっさりできて、Mbed OS 2とMbed OS 5の互換性の高さに助けられた。また、今回使ったようなMbedのソース管理やコンフィグ関係の仕組みはよくできていると思う。RTOSも使ってみたかったけど、安いボードだし諦めることにする。今回の作業でビルド環境が整って実際に動かせるようになったので、これからこのボードには頑張ってもらいたい。

Mbed開発環境の選択

Mbedの開発環境をどうするか考える。

3つの開発環境

Mbedの(Armが提供している)開発環境には現在3つの選択肢がある。

  • Arm Mbed Online Compiler
  • Arm Mbed CLI
  • Arm Mbed Studio

Mbed Online Compiler

いわゆるクラウド環境。プロジェクトやソースコードもすべてクラウドに保存され、ブラウザさえあればどこからでも作業できる。コンパイラーは対象ボード(ターゲット)によってArm Compiler 5または6が使われる。

ローカルへのインストールの必要がなく、サードパーティ製ライブラリのインポートなども面倒な設定なしに簡単にできるので、とにかく手っ取り早く動かしたいときに便利。また、Mbed OS 2を使うならこれ一択のようだ。

Mbed CLI

Pythonベースのコマンドラインツール。環境はpipやGitやMercurialを使ってインストールされる。テキストエディターは自分で用意。コンパイラー(ツールチェイン)はArm、GCC、IARに対応しているが、ライセンスは別途必要なので無料で使えるのはGCCのみ。

これの特徴はLinux PCでも使えること。LinuxユーザーならGUIでなくても大丈夫だろうし、お気に入りのエディターもあるだろう。

以前はMbed OS 2も対応してたようだが、いつからか使えなくなってそのまま放置されている模様(参考)。Mbed OS 2公式リポジトリのZIPもダウンロードできないし、さすがにもうサポートされてないのかもしれない。

Mbed Studio

ローカルGUI環境でWindowsMacで利用可能。コンパイラーはArm Compiler 6が同梱されていて、Mbed Studioならタダで使える。Mbed OS 5のみ対応。

今はまだベータ版で、正直IDEとしての完成度はとても低い。CLIと違って必要なもの(コンパイラーやGitなど)がセットになっているのでインストールは楽。

比較表

環境 プラットフォーム Mbed OS Arm GCC IAR
Online Compiler ブラウザ 2 / 5
Mbed CLI Win / Mac / Linux 5
Mbed Studio Win / Mac 5
  • △は別途ライセンス必要

結局

ローカルで純正コンパイラが無料で使えるMbed Studioを選択することにした。ただし、ソースコード編集は別のテキストエディター(GVIM)を使い、Mbed Studioには空のプロジェクトの作成とビルドだけをお願いすることにした。将来OpenOCDなどを使うことを考えるとできればLinuxで作業したいが、純正コンパイラーの誘惑に負けた(実際どんな性能差があるのかは知らない)。

参考:Overview - Tools | Mbed OS 5 Documentation