ARMアセンブリを使ってnucleo(STM32F103)を動かします。
rogy Advent Calendar 2016の最終日、25日目の記事です。

Cortex-M3をアセンブリで動かす

概要

nucleo STM32F103(ARM Cortex-M3)をアセンブリで動かす話です。

まずはじめに、nucleoはC,C++で開発できるのでアセンブリを使う必要は全くありません。 コンパイラが無駄なくアセンブリ(機械語)を生成してくれます。 スタートアップルーチンやOS(RTOS)を作る際にはCPUのハードウェア的機能を使う必要があるため アセンブリが必要になりますが、それらを使うことはあっても作ることはほぼないのでしょう。

ただ、マイコンをアセンブリで動かすというのは、そのCPUの仕組みも合わせて理解することであり、マイコンの動作を理解する上では必要不可欠です。 プログラミング言語というよりはCortex-M3の仕組みを学ぶために今回はSTM32をアセンブリで動かしてみました。

ちなみにアセンブリやアーキテクチャ(使用者視点)の解説は ARM Cortex‐M3システム開発ガイド という書籍や、 ARM Information Center : Cortex-M3 にあるので、詳しくはこれらを見てください。

この記事ではnucleoでLチカをすることをゴールとして、それに必要な部分の解説をしようと思います。

動作モード

いきなり謎の概念が出てきますが、ARMのマニュアルを読む際に多く出てくるので、 動作モードの概念(動作モードと動作レベル)について紹介しておきます。

動作モード

Cortex-M3にはハンドラモードスレッドモードという二つのモードがあります。 簡単に言うと、例外ハンドラ(割り込みハンドラ)を実行しているときはハンドラモード、 それ以外のときはスレッドモードで動作をします。 例外(割り込み)発生時には通常とは違う動作をするため、二つのモードに分かれています。

動作レベル

Cortex-M3には特権レベルユーザーレベルという二つの動作レベルがあります。 動作レベルによって使用できる命令やアクセスできるレジスタに制限がかかったりします。 特権レベルはOSのカーネルの実行に使われ、ユーザレベルは各プロセスの実行に使われるという感じです。 実際にFreeRTOSではこの使い分けがされています。

例外が発生してハンドラモードになると特権レベルになります。 ハンドラモードでは特権レベルのみの動作になります。 スレッドモードでは特権レベル、ユーザーレベルどちらの動作レベルでも動作可能です。

動作レベルはあるレジスタをいじることで変えられるのですが、そのレジスタは特権レベルの時にしかいじれません。 なので特権レベル→ユーザーレベルはいつでもなれますが、ユーザーレベル->特権レベルにはなれません。 ユーザーモードで動作中に例外(割り込み)が発生して、ハンドラモードに入ったときに特権レベルになるのですが、 特権レベルのままハンドラを抜けると特権レベルでスレッドモードに入ることができます。

図にするとこんな感じです。

初期状態(リセットハンドラ)ではハンドラモード特権レベルになっていて、 特にOSなどを使わない単体のプログラムとして動かす場合は常に特権レベルで動きます。 今回は特権レベルになっているものとして話をします。

レジスタ

Cortex-M3には次のような全部で21本のレジスタがあります。 レジスタは基本的には32bitレジスタになっています。

レジスタ名 機能
R0~R7 下位汎用レジスタ
R8~R12 上位汎用レジスタ
R13(MSP,PSP) スタックポインタ
R14(LR) リンクレジスタ
R15(PC) プログラムカウンタ
xPSR プログラムステータスレジスタ
PRIMASK 割り込みマスクレジスタ1
FAULTMASK 割り込みマスクレジスタ2
BASEPRI 割り込みマスクレジスタ3
CONTROL 制御レジスタ

下位汎用レジスタは常に使用できる32bitレジスタで、 上位汎用レジスタは32bit命令のみから使用できる32bitレジスタです。

R13(MSP,PSP)はスタックポインタで、メモリ上のスタックの一番上をさしています。 Cortex-M3にはMSP(main stack pointer)とPSP(process stack pointer)の2種類があります。 R13はMSPかPSPのどちらか一つのを指していて、どちらを指しているかはCONTROLレジスタで確認、設定ができます。 スタックについてはもう少し後で詳しく紹介します。

R14(LR)は分岐命令(ジャンプ命令)を使ったときの戻り番地を保存するために使われます。 飛んだ先でLRの内容をPCに入れることで呼び出し元に戻ることができます。 普通の関数呼び出しの際には戻り番地がLRに保存されますが、例外でジャンプしたときはスタック上に保存されます。 このとき、LRは例外が発生していることを示す特殊な内容になっています。 今回は割り込みを使わないので詳しくは触れません。

xPSRにはZEROフラグなどの実行結果フラグや実行している例外ハンドラの情報などが入っています。

CONTROLレジスタは上記のスタックのどちらを使うかや特権レベルを選択・状態をするために使います。 これは特権レベルの時しかいじれません。

PRIMASK,FAULTMASK,BASEPRIは割り込みを許可するかどうかを設定できます。 これも特権レベルの時しかいじれません。また、今回は割り込みを使わないので詳しくは触れません。

命令

Thumb2命令セットの命令を実行することができます。 Thumb2はThumb(16bit)を拡張したもので、16bit命令と32bit命令が混在しています。

命令一覧

命令は多いので後のLチカのコードで最低限のものについて触れます。

スタック

Cortex-M3にはPSPとMSPという二つのスタックポインタがあります。 2種類のスタックはプロセス側とカーネル側で使い分けることを想定して作られていて、 これによってプロセス側からカーネル側が破壊されないという安全性を得ることができます。 FreeRTOSでは各タスクの動作にはPSPが使われ、カーネルの実行にはMSPが使われています。

初期状態では、スタックの操作にMSPが使われるようになっています。 特に設定をしなければそのままMSPが使われるので、RTOSなどを載せてない場合はずっとMSPだけが使われます。

Cortex-M3はアドレスが減る方向にスタックが伸びていきます(これはアーキテクチャレベルで決まっている)。 なので一般的にMSPの初期値はRAMの一番下を指すようにしておきます。

ヒープはアーキテクチャレベルではなく、C言語の実装によって決まるものですが、 上の図のようになっていることが多いと思います。

PSPの使い方はRTOSによるので、どこを初期値に持つかは分かりませんが、アドレスが減る方向に延びていくことは確かです。

メモリの先頭

メモリの先頭にはスタックポインタの初期値と例外・割り込みベクタテーブルを設定します。 詳細については次回紹介するので、特に例外を使わない場合でも動作に最低限必要な部分だけ紹介します。

メモリの先頭(STM32だと0x0800 0000)にはスタックポインタの初期値(32bit)を入れます。 リセット時にこの先頭の番地に入っている内容がR13(MSP)にロードされます。

スタックポインタの初期値の後(STM32だと0x0800 0004)には例外ハンドラ、割り込みハンドラベクタテーブルを順に定義していきます。
ベクタテーブルの一覧
ベクタテーブルには例外や割り込みに対応するハンドラの実行内容が記述されている番地をひたすら並べていきます。 例えばPICでは割り込みで飛んでくる番地に割り込み処理の書かれた番地にジャンプする命令を書いたりするのですが、 ベクタテーブルには命令ではなく飛びたい先の番地のみを書いておきます。 例外や割り込みが発生したときにはベクタテーブルの値が参照され、自動でその番地にジャンプします。 割り込みのたびに条件を見て分岐命令を発行するのではなく、設定された番地が自動でロードされてジャンプするので、非常に高速です。 よく聞くARMの高速な割り込みというのは、この仕組みのことだと思います。

Lチカをする

それでは早速実機を動かしてみます。 開発ツールとして、GCCのARM用ツールを使います。

GNU ARM Embedded Toolchain

今回は以下のソースを使います。

上のソースをビルドして実行ファイルを作るには、次のコマンドを実行します。

arm-none-eabi-as -mcpu=cortex-m3 -mthumb -o led_blink.o led_blink.s
arm-none-eabi-ld -Ttext 0x08000000 -o led_blink.elf led_blink.o

# hexファイルを作りたいとき
arm-none-eabi-objcopy -O binary led_blink.elf led_blink.hex
# binファイルを作りたいとき
arm-none-eabi-objcopy -O binary led_blink.elf led_blink.bin

led_blink.elfという実行バイナリができるので、これをマイコンに書き込むとLEDが点滅します。

実行バイナリは次のコマンド実行することで逆アセンブルすることもができます。

arm-none-eabi-objdump -D led_blink.elf

擬似命令がどう展開されたか、実際にメモリのどこに配置されたのかを確認することができます。

解説

ソースの先頭部分から解説をしていきます。

    .text
    .global _start
    .code   16
    .syntax unified

.textは以下のコードをtextセクションに出力するいう意味です。 textという名前は自由に決められるのですが、一般的に実行コードはtextセクションに出力します。 今回はtextセクションしか使いませんが、bssやdataセクションがあります。 各セクションをどこ(RAMかFLASHか)に配置するのかを書き連ねたものがリンカスクリプトになります。 今回はリンカスクリプトを使わない代わりにリンクするときに-Ttext 0x08000000としてtextセクションを 0x08000000に配置しています。ちなみに0x08000000というのは今回使ったマイコンのFLASHの先頭番地です。

.code 16はthumb命令セットを使うことを意味し、 .syntax unifiedはこのファイルが統合アセンブリ構文によって書かれていることを意味します。

.global _startは_startラベルを他のオブジェクトファイルでも利用可能なように公開します。 gccのリンカの仕様として実行ファイルは_startラベルからスタートするようになっているので、 必ず_startラベルを公開します。

.equ STACK_TOP, 0x20005000

上のように.equディレクティブを使うとこのファイル中ではSTACK_TOPという文字列が0x20005000に置き換えられます。 C言語でいう#defineによるマクロみたいなものです。

ここではスタックポインタの初期値であるRAMの終わりのアドレスと、 後のペリフェラルの設定に使うレジスタのアドレスを定義しています。

_start:
    .org 0x00000000
    .word   STACK_TOP
    .word   init
    .type init, function

ここでは_startラベルであるtextセクションの先頭を定義しています。 .orgの行では.orgディレクティブを使ってここのアドレスを0x00000000にセットしています。 .wordの行ではデータをword単位で配置しています。 先頭番地にSTACK_TOP(eqnによってRAMの一番後ろのアドレスを設定)、 次の番地にinitラベル(の指すアドレス)を設定しています。

この後のinit以降は具体的に実行していく内容を書いていきます。

    // GPIOAのクロックを有効にする
    LDR r0, =RCC_APB2ENR //r0にRCC_APB2ENRの値をロード
    movw r1, #0x0004     //r1に0x0004という即値をロード
    str r1, [r0]         //r0の指すアドレスにr1をストア

ここではLDRという擬似命令を使っています。 16bitまでの即値はmov命令でレジスタのロードできるのですが、 16bitよりも長い即値は.wordであらかじめメモリに配置されたデータをロードするという形になります。 RCC_APB2ENRの値は32bitなのでこの操作をする必要があります。 これを自分でやろうとするとなかなか大変ですが、LDRと=を使うことで簡単化することができます。

“=RCC_APB2ENR”と書くと、まずRCC_APB2ENRの値(0x40021018)がメモリ上に配置されます。 実行ファイルを逆アセンブルすると、0x0800002cから4byteの0x40021018という値が格納されています(境界がずれてて見にくくなっています)。

 800002a: 10180000 andsne  r0, r8, r0
 800002e: 08024002 stmdaeq r2, {r1, lr}

逆アセンブルした結果をみると、LDR命令の部分はldr命令に置き換わり、 先程の0x0800002c番地はpcからのオフセット値(#32)で表現されています。

 8000008: 4808 ldr r0, [pc, #32]   ;pc+32番地からr0にデータをロード

これによってr0にRCC_APB2ENRの値が入ります。

マイコンの設定は基本的は指定されたレジスタに設定データを書き込むことになると思うのですが、

    LDR r0, =書き込みたいアドレス
    movw r1, 書き込みたい値(16bit)
    str r1, [r0]

という構文を使うことで実現できます。

STM32のデータシートに従い、GPIOAへのクロックを供給し、出力に設定しています。 システムクロック周りの設定がされていませんが、STM32F103はデフォルトで内部発振の8MHzが有効になっていて、 内部バス(AHB,APB1,2)もすべて8MHzになっています。 今回はクロック周りの話まで扱えないので、デフォルトの8MHzで動かしています。

toggle_ledの部分ではXORを使って出力を判定してLEDを点滅させています。 点滅速度を調整するためにwait_loopで2000000回ループを回して時間を稼いでいます。

まとめ

詳細なところまで触れることはできませんでしたが、雰囲気くらいは伝わったでしょうか。 ただのLチカであってもアセンブリで書いてみると多くの勉強になります。

アセンブリが登場する場面として最も身近なものはスタートアップルーチンであると思います。 訳のわからないものではなく、何をしようとしているか理解しようとして追ってみる面白いです。

この続きとして例外(Systick)を扱うものも書く予定なのでそちらもよろしくお願いします。

続き ARMアセンブリでLチカ2