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円の送料を払うのはつらいのだ…。