SNSへはこちら

ARMマイコンのバイナリを手打ちする(on STM32) (1)

マイコンに書き込むバイナリファイルを objdump で眺めていたら「これアセンブラ使わなくてもマイコンプログラミングできんじゃね??」と思ったのでやってみました。使用マイコンは STM32F303K8T6 です。

「ARM でもアセンブラしたい!」のインラインアセンブラじゃない編に関する記事の投稿が先じゃないかと思われる方もいらっしゃいますよね。アハハ...まあそれはどうでもいいとして

用意するツール

  • シェル
    • bash でも zsh でもご自由に。Windows では出来ないですね
  • xxd
    • バイナリ生成に必須です。

留意事項

  • アセンブラ等の機械語翻訳ツールは一切使わないこととする
    • 今回の趣旨ですので。というかそもそもハンドアセンブルしたいと言う気持ちがあるのですね。
  • 所望の動作をしない場合はすぐにマイコンを止めること
    • 所謂暴走と言う状態です。このままでは未初期化のフラッシュ領域に書かれた命令を誤って実行して色々死ぬ可能性がありますので即電源を引っこ抜きましょう。おそらく勝手に HardFalut 出して動作停止してくれるとは思うんですがね。念のため。

参考サイト・ドキュメント

  • ARMアセンブリでLチカ
    • ARM の基礎についてまとめられています。実際僕がバイナリ解釈に困った時、id さん本人からこのページを読むことをお勧めして頂きました。
  • Thumb2 モードのリファレンス・マニュアル
    • STM32 は Thumb2 モードでしか動かないので、こちらのリファレンス・マニュアルを参考にしてください。32bit 命令も合わせて書いてあります。
    • ARM のサーバ内にあるこのドキュメントへのリンクが見つからなかったので...許して

とりあえず擬似コードを書く

アセンブラ風の擬似コードです。とりあえず以下のシーケンスで命令を実行することにしました。

  • RCC_AHBENRIOPF_EN ビットを立てる(22bit目)
  • GPIOF_MODER の 0bit 目を立てることで PF0 を GPIO の OUTPUT とする
  • GPIOF_ODR の 0bit 目を立てることで PF0 を High とする

以上を擬似アセンブリコード(?) としたのが以下です。

LDR r0, =RCC_AHBENR
ldr, r1, (1 << 22)
str r1, [r0]
LDR r0, =GPIOF_MODER
mov r1, #1
str r1, [r0]
LDR r0, =GPIOF_ODR
str r1, [r0:
loop: b loop

以下のバイナリやコードですが、筆者が 32bit 命令に出会う前の状態で書いたコードを掲載します。リファレンス・マニュアルが見つからず、藁をもつかむ思いで色々調べた結果ですのですみませんがご理解ください。特に RCC_AHBENR とか orr しなくて良いのかとか思われるかもしれませんが、32bit のオペコードがわからなかったのです。ご理解ください(2回目)。今この記事を書いているときには分かっていますよ。

さぁ、早速機械語書くぞ!と思いきや、ARM の Thumb2 モードについて以下のことをご留意下さい(ARM モードでも同じかもしれないが)。

  • ARM はデータ単位・命令単位でリトルエンディアンなので要注意。
  • 1ワード(32bit)の定数値はプログラム中に置き、プログラムカウンタ相対のロード命令として読み出されなければならない
  • 16bit 値は常にメモリ上のアドレスが2で割り切れるところに書かれていなければならない(アラインメント)
  • 32bit 値は常にメモリ上のアドレスが4で割り切れるところに書かれていなければならない(アラインメント)
  • ベクタに表される関数アドレスの下位1ビットは常にセットされている
  • プログラムカウンタ(PC)は 4byte ごとに先を見ている
    • 現時点で実行している命令の先頭アドレスが 0x02 とすると、PC は 0x04
    • 現時点で実行している命令の先頭アドレスが 0x04 とすると、PC は 0x08
    • 分岐命令で必要になってくる知識
    • 実際は常に4byte 先を見ています。ただ、実行する分岐命令の中には PC の 1ビット目が常に 0 に見えるものがあるそうです。

まだ分かってないこと
- プログラムカウンタ相対は負の数を指定できない?できそうだが
- 32bit 命令を実際に実行してみたい

機械語をハンドアセンブル!

やってみました。まずは0番地にスタックのトップアドレス、4番地にリセット時に実行される関数ポインタ(前述のように0bit目は1です)を書きます。

000 | 20 00 30 00 ; STK_TOP
004 | 08 00 00 09 ; init + mode
008 | init
008 | 48 04 ; ldr r0, [pc, 0x10]
00A | 49 07 ; ldr r1, [pc, 0x1C]
00C | 60 01 ; str r1, [r0]
00E | 40 04 ; ldr r0, [pc, 0x10]
010 | 21 01 ; mov r1, #1
012 | 60 01 ; str r1, [r0]
014 | 48 03 ; ldr r0, [rc, 0x0C]
016 | 60 01 ; str r1, [r0]
018 | loop
018 | E7 EC ; b loop
01A | FF FF ; Alignment
01C | 40 02 10 14 ; RCC_AHBENR
020 | 48 00 14 00 ; GPIOF_MODER
024 | 48 00 14 14 ; GPIOF_ODR
028 | 00 40 00 00 ; (1 << 22)

ということで、以下のシェル芸でバイナリを生成できます。

$ echo 00 30 00 20 09 00 00 08 04 48 07 49 01 60 04 48 01 21 01 60 03 48 01 60 FC E7 FF FF 14 10 02 40 00 14 00 48 14 14 00 48 00 00 40 00 | xxd -r -p > test.bin

後は書き込むのみ!光りました!!!やった!!!!!!

計算ミスやアラインメント・オペランドの仕様理解不足によって8回ほど機械語を書き直したのは秘密です。

次回はLチカをやりたいと思います。死ぬ気しかしない。

追記: 2018/04/19
出力ポートは PF0 です。LQFP32 ですと2ピン目になります。
あと、ひょっとしたら分岐命令の飛ぶ先が間違ってるっぽいんですけど、動くのでまあいいとします。