MSP430のコードを小さくするテクニック


Cerevoの稲垣です。

私は組み込みソフトウェア開発の担当で、主にLinuxを扱っていますが、ボードに載っているマイクロコントローラのプログラミングもします。最近はMSP430というTIのマイコンを相手にしているので、MSP430のアセンブリ言語プログラミングについて、x86アセンブリの経験者を対象にして書きたいと思います。

MSP430のレジスタとアドレッシング

MSP430には16本のレジスタがありますが、そのうちのr0はPC (プログラムカウンタ)、r1はSP (スタックポインタ)、r2はSR (ステータスレジスタ) および定数ジェネレータ、r3は定数ジェネレータとなっていて、汎用レジスタとして使えるのはr4からr15の12本のレジスタです。スタックの扱いはx86と同じです (ARMにあるようなリンクレジスタはない)。

MSP430の2オペランド命令はソースとデスティネーションが直交していて、それぞれ以下のモードが使えます:

ソース:

  1. r4          ; レジスタモード
  2. foo(r4) ; インデックスモード
  3. @r4        ; 間接レジスタモード
  4. @r4+      ; 間接自動インクリメント

デスティネーション:

  1. r4       ; レジスタモード
  2. foo(r4) ; インデックスモード

ソース・デスティネーションの両方にメモリオペランドを使うこともできる点がx86とは大きく異なる点と言えます (x86だとそういう命令はpush、pop、movsくらいしかありません)。インデックスモードを使うとディスプレースメントが付くので命令は可変長です。

オペランドのエンコーディングは上記の4種類だけですが、PCおよび定数ジェネレータとの組み合わせによって以下のアドレッシングモード
が実現されています:

  1. foo       ; シンボリックモード (PCを使ったインデックスモード、いわゆるPC相対)
  2. &foo     ; 絶対モード (定数生成レジスタをインデックスにしたインデックスモード)
  3. #foo     ; 即時モード (PC間接自動インクリメントモード……これは面白い実装だと思います)

MSP430命令の注意点

演算命令はx86とほぼ共通なものが揃っていますが細かいところが違います:

  • オペランドはソース, デスティネーション の順に書きます。
  • バイト演算でレジスタをデスティネーションにすると上位バイトがクリアされます。それで、and #0xff, r4 は mov.b r4, r4 で代用すると1ワード節約できます。
  • シフト・ローテートは1ビットづつしかできません。上位バイトと下位バイトを入れ替えるswpb命令が用意されているので組み合わせてシフトすることになります。

フラグ関連は微妙でありながらけっこう重要な違いがあります:

  • bic (ビットクリア)、bis (ビットセット) ではフラグは変化しません。
  • andとxorでは、演算結果が0でなければキャリーフラグがセットされます。
  • subとcmpで生じるキャリーフラグはx86とは逆です。けっこう混乱します。
  • 補助キャリーとパリティはありません。x86でも滅多に使わないフラグですが。

callは、jmpのようなオフセット値によるエンコーディングではなく、通常のソースオペランドで実現されています。したがって、callを使ったコードをROMからRAMにコピーするときはリロケート処理が必要です。あるいはオペランドをレジスタやメモリ(PC相対)にしておく方が楽かも知れません。

MSP430の定数ジェネレータを利用する

定数ジェネレータは -1, 0, 1, 2, 4, 8 を生成することができ、これを使うと即値の分の1ワードを節約することができます。例えば7回ループしたいとき、普通は以下のようにします:

  mov #7, r4    ; カウンタ
foo:
  dec r4        ; 減算
  jnz foo

しかしこれを以下のように置き換えると1ワード節約できます:

  mov #2, r4    ; カウンタ
foo:
  add.b r4, r4  ; シフト
  jnz foo

定数ジェネレータのお蔭で、インクリメントに1以外の値を使ってもコードは短いままです。それで、例えば32回のループは以下のようにすれば短くなります:

  mov #0, r4
foo:
  add.b #8, r4   ; 32回ループ
  jnz foo

他に以下の回数のループは1ワード短く書くことができますので、暇があれば考えてみると面白いかも知れません:
3 5 6 7 9 13 14 15 16 31 63 127 255 8191 16383 32767 65535

MSP430の自動インクリメントも利用する

x86ではinc・decが1バイトでできるため、ループの終了条件はほぼカウンタ一択です。しかし、MSP430ではアドレスの自動インクリメントがあるため、メモリアクセスをループさせる場合は、終了アドレスとポインタを比較した方が短いコードになることがあります。例えば、通常のメモリ間コピーは以下のようにします:

  mov #src, r4
  mov #count, r5
foo:
  mov @r4+, dest-src-2(r4)  ; 自動インクリメント、メモリ間コピー
  dec r5
  jnz foo

しかしcountが定数ジェネレータで生成できない場合、mov #count, r5とdec r5で3ワードも消費してしまいます。これをアドレス比較に書き換えると1ワード節約できます:

  mov #src, r4
foo:
  mov @r4+, dest-src-2(r4)  ; 自動インクリメント、メモリ間コピー
  cmp #src+count*2, r4
  jnz foo

自動インクリメントと定数ジェネレータがあるMSP430ならではの最適化です。

最適化できない場合

x86だと即値とレジスタの間のmovには専用形が用意されていますが、MSP430にはありません。したがって、例えば r4 = r5 ^ 0x1234; を実行する場合、以下のどちらのコードでも結果は全く同じです:

 ; x86ではこっちの方が1バイト短い
 mov #0x1234, r4
 xor r5, r4

 ; x86では1バイト長くなる
 mov r5, r4
 xor #0x1234, r4

MSP430ではデスティネーションのエンコーディングが2種類しかないので、メモリデスティネーションをアクセスするとディスプレースメントが必ずついてしまいます。これを最適化する方法はありません。メモリマップされたペリフェラルを操作することが多いので多分これでいいのですが、なんだかすっきりしない気分になります。

終わりに

MSP430のROMは何KBとある上に、新しいチップは安くてRAMもたくさん載っているので、ここで書いたような1ワードを争う最適化は滅多に必要ありません。でも100クロックかかる処理が90クロックで済むようになったら、消費電力は10%減るわけです。そう考えると、21世紀もまだまだアセンブリ言語の出番はあるのかも知れません。あるといいな……