最終更新:2021-06-01
本記事では、ARM Cortex-M シリーズで、C 言語プログラムを簡単にブートする方法を紹介します。OS 無しベアメタルな ROM コードを 、Windows 10 の WSL 上 Ubuntu-20.04 の標準パッケージで作成します。
リセットや割込と例外用のベクタ テーブルの記述と、アセンブラから簡易的に C 言語の main() 関数を呼び出すブートストラップと、C プログラムの「戻り値」を拾うまでを解説します。また、Cortex-M シリーズのブートの仕方は、ARM7TDMI 以降の通常の ARM コア (Cortex-A を含む) とは少し違いますので、勘所を解説します。
尚、プログラムの動作は 「ARM DesignStart Eval」パッケージ の Verilog シミュレーションで確認済です。C 言語プログラムでの ROM コード作成から、Icarus Verilog や Verilator での Verilog HDL シミュレーションまでが、全て WSL の標準パッケージを使って無償で行えますが、Verilog シミュレーションは【こちらの別記事】で紹介しています。
目次:
Ubuntu-20.04 で標準パッケージとしてインストール可能な、arm-linux-gnueabi (浮動小数点ユニット無し用) を使用します。ハードウェア浮動小数点用を使用する場合は、arm-linux-gnueabihf に読み替えてください。none ではなく linux と名前が付いて居ますが、OS 無しベアメタル用のプログラムも問題無く作成可能です。 [広告]
早速ですが、まずは 0 番地に配置するアセンブラ記述のソースコードを見て頂きます。従来型の ARM プロセッサとは異なり、先頭領域はベクタ テーブルとしてアドレスを列記する仕様になっています。 cat から ”EOF” までを ubuntu 上にコピー&ペーストすると、start.s というファイルが出来ます。 2021-04-18: コード最終行に「.align 2」を追加 (=SIM_TOP が Not alignに配置されるのを防止) 「### Vector Table ###」とコメントの入って居る部分の「.word」文4行が、0x0, 0x4, 0x8, 0xc 番地に配置されます。先頭から、スタック ポインタ、リセット ベクタ、ノン マスカブル割り込み (NMI) ベクタ、ハード フォルト例外ハンドラ ベクタの各アドレスになります。この4つが必要最低限のベクタ テーブルの要素です。 ここでの注意点は、スタック ポインタ以外の、各ルーチンのスタート番地であるベクタ アドレスに1を足している事です。1を足すことにより、Thumb ステートで起動します。足しておかないと ARM ステートで起動してしまいます。一方、後程説明するコンパイル時には、Thumb ステート用のオブジェクトを生成しています。ここが食い違うと、Hard Fault の無限ループが発生して起動しません。また、何かの間違いでリセット前に NMI が入った場合にも備え、リセット、NMI、Hard Fault の全てのベクタ アドレスに1を足しておきます。 スタック ポインタのアドレスは、アセンブラの先頭で「.equ」疑似命令により定義しています。従来型の ARM プロセッサと異なり、ベクタ テーブルの先頭にアドレスを記載すると、リセット時に自動的にスタックポインタが設定されます。 次のブロックの「### Handlers ###」とコメントの入った部分が、NMI と例外の発生時に実行されるハンドラ ルーチンになります。「.align」文により、夫々 0x100 番地と 0x120 番地に配置しています。 最後の「### Main routine Call & Returned Value ###」のコメントの部分が、スタートアップ ルーチンの本体で、C 言語の main() 関数をコールしています。、r0 レジスタには int 型の戻り値が返って来ますので、その値を「SIM_STOP」で定義された番地にストアしています。尚、この時点で停止をさせない場合、Hard Fault が発生して「excp_hndl」ラベルの付いたルーチンに飛び込みます。 注意点として、この C 言語の呼び方は簡易的な物で、本来の C 言語の仕様には準拠していません。「.data」セクションに配置されるグローバル変数や構造体のプログラム中に明示された初期値の ROM から RAM へのコピー、及び「.bss」セクションに配置される初期値記載が無いグローバル変数と同 static 宣言された auto 変数のゼロ初期化を行って居ません。 例題として次の C プログラムを使用します。 cat から ”EOF” までを ubuntu 上にコピー&ペーストすると、main.c というファイルが出来ます。 足し算の答が合って居たら、戻り値として 0 を返し、それ以外の場合には変数 c の値を返します。このプログラムは、FPGA や SoC として設計した CPU コア周辺回路のチェック ルーチンとして使う事を想定しています。 これらのコードをコンパイルします。アセンブルとコンパイルとリンクは、必ず別々に行います。理由は、一気にコンパイルからリンクまでを行うと、コンパイラが Linux 標準のスタートアップ ルーチンをリンクしようとして「_start」ラベルの重複が発生してエラーとなる為です。 以下のコマンド群では、CPU に Cortex-M0 シリーズを指定しています。これにより、Thumb 命令が出力されます。M0 コア以外の Cortex-M シリーズの場合には、指定を変更してください。指定可能なコアの種類は、ubuntu 上で「man gcc」とコマンドを打って、「/cortex-m」で検索してください。 「-g」と「-gdwarf-2」を同時に指定すると、ICE (インサーキット エミュレータ) やデバッガの殆どで読み込み可能な、ソースコード デバッグ用の情報 DWARF2 が付与されます。 Objdump コマンドでは、「.text」セクション (実行可能なプログラムの格納される場所) のみをダンプしてディスアセンブルしています。尚、ディスアセンブル リスト中に表示される C 言語のソースコードは、コンパイル時に付与したデバッグ情報を元に、現在のディレクトリに有るソースコードを引用します。コンパイルに使用したソースコード以外では正しい表示が行われません。例えばコンパイル エラーで実行可能ファイルが生成出来なかった場合、ソースコード修正前のディスアセンブル リスト中に、修正後のソースコードが表示されますので注意が必要です。 以下のコマンド群で、各種ヘキサファイルと、素のバイナリファイルを出力しています。 Objcopy コマンドの -O オプションの指定は、ihex でインテル ヘキサ、srec でモトローラ S レコード (mot ファイル) 、verilog でバイト単位の Verilog ROM コード ヘキサ、binary で ELF 形式の情報が取り除かれた素のバイナリとなります。
前提条件:
(パッケージの種類やインストールの仕方は、【こちらの別記事】で解説しています。)使用するコンパイラ:
ブート用アセンブラ記述
cat <<"EOF" > start.s
.equ STACK_TOP, 0x20008000
.equ SIM_STOP, 0x80000000
.global _start
.global nmi_hndl
.global excp_hndl
.text
### Vector Table ###
.align 2
.word STACK_TOP // 0x00; 0: SP
.word (_start +1) // 0x04; 1: Reset
.word (nmi_hndl +1) // 0x08; 2: NMI
.word (excp_hndl +1) // 0x0c; 3: Hard Fault
### Handlers ###
.align 8
nmi_hndl:
nop; nop; nop; nop
bx lr
.align 5
excp_hndl:
nop; nop; nop; nop
bx lr
### Main Routine Call & Returned Value ###
.align 8
_start:
bl main
ldr r2, =SIM_STOP
str r0, [r2]
bkpt #0
nop; nop; nop; nop
### Padding ###
.align 2
"EOF"
簡単な C 言語のプログラム:
cat <<"EOF" >main.c
int main() {
volatile int a, b;
int c;
a=3;
b=4;
c=a+b;
if (c == 7) return 0;
else return c;
}
コンパイル~ディスアセンブル:
arm-linux-gnueabi-as start.s -g -gdwarf-2 -mcpu=cortex-m0 -o start.o
arm-linux-gnueabi-gcc main.c -g -gdwarf-2 -mcpu=cortex-m0 -fPIC -Og -c -o main.o
arm-linux-gnueabi-ld start.o main.o -Bstatic -Ttext 0x0 -o main.out
arm-linux-gnueabi-objdump main.out --disassemble-all --source --section=.text
「-fPIC」の指定は、コンパイラにリロケータブルなコードを出力させる為の物です。
「-Og」を指定すると、ソースコード デバッグが可能な範囲内での最適化が行われます。
各種ヘキサファイルの出力:
arm-linux-gnueabi-objcopy main.out -O ihex main.hex
arm-linux-gnueabi-objcopy main.out -O srec main.mot
arm-linux-gnueabi-objcopy main.out -O verilog main.verilog
arm-linux-gnueabi-objcopy main.out -O binary main.binary
od -A d -t u4 -v main.binary | \
awk '{printf("@%06x %08x %08x %08x %08x\n", $1/4, $2, $3, $4, $5)}' > main.v
最後の2行の od (octal dump) コマンドで素のバイナリ形式のファイルをダンプし、awk で4バイトのワード単位にアドレスを振った Verilog 形式に変換しています。「ARM DesignStart Eval」パッケージを使った Verilog シミュレーションを行う場合には、このファイルを使用します。