みねっちょのマイコン関係ブログ

組込開発系フリーソフトやハードの情報発信ブログ

ARM Cortex-M のベアメタル用 C 言語ブートストラップを WSL で作る

サイト内 Google 検索:


最終更新: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 シミュレーションは【こちらの別記事】で紹介しています。

目次:



前提条件:

  • Windows 10 に WSL の Ubuntu-20.04 をインストール済なこと
  • crossbuild-essential-armel をインストール済なこと
    (パッケージの種類やインストールの仕方は、【こちらの別記事】で解説しています。)

使用するコンパイラ

Ubuntu-20.04 で標準パッケージとしてインストール可能な、arm-linux-gnueabi (浮動小数点ユニット無し用) を使用します。ハードウェア浮動小数点用を使用する場合は、arm-linux-gnueabihf に読み替えてください。none ではなく linux と名前が付いて居ますが、OS 無しベアメタル用のプログラムも問題無く作成可能です。


[広告]


ブート用アセンブラ記述

早速ですが、まずは 0 番地に配置するアセンブラ記述のソースコードを見て頂きます。従来型の ARM プロセッサとは異なり、先頭領域はベクタ テーブルとしてアドレスを列記する仕様になっています。

cat から ”EOF” までを ubuntu 上にコピー&ペーストすると、start.s というファイルが出来ます。

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"  

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 言語のプログラム:

例題として次の C プログラムを使用します。

cat から ”EOF” までを ubuntu 上にコピー&ペーストすると、main.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;
}

足し算の答が合って居たら、戻り値として 0 を返し、それ以外の場合には変数 c の値を返します。このプログラムは、FPGA や SoC として設計した CPU コア周辺回路のチェック ルーチンとして使う事を想定しています。



コンパイル~ディスアセンブル

これらのコードをコンパイルします。アセンブルコンパイルとリンクは、必ず別々に行います。理由は、一気にコンパイルからリンクまでを行うと、コンパイラLinux 標準のスタートアップ ルーチンをリンクしようとして「_start」ラベルの重複が発生してエラーとなる為です。

以下のコマンド群では、CPU に Cortex-M0 シリーズを指定しています。これにより、Thumb 命令が出力されます。M0 コア以外の Cortex-M シリーズの場合には、指定を変更してください。指定可能なコアの種類は、ubuntu 上で「man gcc」とコマンドを打って、「/cortex-m」で検索してください。

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

「-g」と「-gdwarf-2」を同時に指定すると、ICE (インサーキット エミュレータ) やデバッガの殆どで読み込み可能な、ソースコード デバッグ用の情報 DWARF2 が付与されます。
「-fPIC」の指定は、コンパイラにリロケータブルなコードを出力させる為の物です。
「-Og」を指定すると、ソースコード デバッグが可能な範囲内での最適化が行われます。

Objdump コマンドでは、「.text」セクション (実行可能なプログラムの格納される場所) のみをダンプしてディスアセンブルしています。尚、ディスアセンブル リスト中に表示される C 言語のソースコードは、コンパイル時に付与したデバッグ情報を元に、現在のディレクトリに有るソースコードを引用します。コンパイルに使用したソースコード以外では正しい表示が行われません。例えばコンパイル エラーで実行可能ファイルが生成出来なかった場合、ソースコード修正前のディスアセンブル リスト中に、修正後のソースコードが表示されますので注意が必要です。



各種ヘキサファイルの出力:

以下のコマンド群で、各種ヘキサファイルと、素のバイナリファイルを出力しています。

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

Objcopy コマンドの -O オプションの指定は、ihex でインテル ヘキサ、srec でモトローラ S レコード (mot ファイル) 、verilog でバイト単位の Verilog ROM コード ヘキサ、binary で ELF 形式の情報が取り除かれた素のバイナリとなります。
最後の2行の od (octal dump) コマンドで素のバイナリ形式のファイルをダンプし、awk で4バイトのワード単位にアドレスを振った Verilog 形式に変換しています。「ARM DesignStart Eval」パッケージを使った Verilog シミュレーションを行う場合には、このファイルを使用します。



【ARM 関係の目次へ戻る】   【WSL 関係の目次へ戻る】