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

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

概要

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

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

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

ちなみにアセンブリやアーキテクチャ(使用者視点)の解説は ARM Cortex‐M3システム開発ガイド

という書籍や、 公式サイト ARM Information Center : Cortex-M3 にあるので、詳しくはこちらを見てください。

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

動作モード

Cortex-M3には2つのモードと2つの特権レベルがあります。

二つのモードというのはハンドラモードとスレッドモードで、 例外ハンドラを実行しているときがハンドラモード、それ以外のときはスレッドモードになります。

特権レベルには特権とユーザレベルがあり、特権レベルによって使用できる命令やアクセスできるレジスタに制限がかかったりします。 特権モードはOSのカーネルの実行に使い、ユーザモードが各プロセスの実行に使うという感じです。

初期状態では特権レベルになっていて、 特に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レジスタで確認、設定ができます。 2種類のスタックはプロセス側とカーネル側で使い分けることを想定して作られていて、 これによってプロセス側からカーネル側が破壊されないという安全性を得ることができます。

R14(LR)は分岐命令(ジャンプ命令)を使ったときの戻り番地を保存するために使われます。 飛んだ先でLRの内容をPCに入れることで呼び出し元に戻ることができます。 普通の関数呼び出しの際には戻り番地がLRに保存されますが、例外でジャンプしたときはスタック上に保存されます。 このとき、LRは例外や割り込みが発生していることを示す特殊な内容になっています。 復帰命令を使うと通常時はLRの内容がPCに入りますが、LRが特殊な値になっているときは自動的に 例外や割り込みハンドラからの復帰であると判定され、スタック上の情報を使って元の番地にPCが戻ります。 例外や割り込みハンドラからの復帰する際にLRの値をいじることで、戻り番地はメインスタックとプロセススタックどちらにあるものを使用するかを設定できます。

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

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

PRIMASK,FAULTMASK,BASEPRIは割り込みを許可するかどうかを設定できます。 今回は割り込みを使わないので詳しくは触れません。

命令

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

命令一覧

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

メモリの先頭

メモリの先頭にはスタックポインタの初期値と例外・割り込みベクタテーブルを設定します。

メモリの先頭(STM32だと0x0800 0000)にはスタックポインタの初期値(32bit)を入れます。 リセット時にこの先頭の番地に入っている内容がR13(MSP)にロードされます。 Cortex-M3はアドレスが減る方向にスタックが伸びていくので、 一般的にスタックポインタの初期値はRAMの一番後ろのアドレスにします。

スタックポインタの初期値の後(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.out led_blink.o

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

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

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

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

解説

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

    .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であらかじめメモリに配置されたデータをロードするという形になります。 これを自分でやろうとするとなかなか大変ですが、LDRと=を使うことで簡単化することができます。 “=RCC_APB2ENR”と書くことによって、RCC_APB2ENRの値がメモリ上に.wordを使って配置され、 配置されたメモリのアドレスが自動で計算されます。 アセンブル時にLDR命令はldr命令に置き換わり、

ldr	r0, [pc, #48]   //pc+48番地からr0にデータをロード

のようになります。48というのが自動計算されたアドレスの現在のpcからの相対位置になります。 これによって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)を扱うものも書く予定なのでそちらもよろしくお願いします。