SNSへはこちら

ゼロからマイコンでLチカをするまで(2) - リンカスクリプトの準備(Forアセンブリ)

本記事は ゼロからマイコンでLチカをするまで というシリーズの第2弾です。

まだまだ準備は続きます。
前回ビルドしたツールチェインたちのパスが通っているという前提でお話したいと思います。パスを通すコマンドは以下。

$ PATH=/usr/local/cross/arm-none-eabi/bin:${PATH}

本記事ではリンカスクリプトの準備をしていきたいと思います。これがしっかりできるといよいよLチカし放題(ただし機能が限定的)なので、もうひと踏ん張り、頑張りましょう。

用意するもの

本記事で必要になるものです。予めダウンロードし、用意しておいてください。
ターゲットは STM32F303K8T6 です。

リンカスクリプトとは

いきなりですが、皆さん聞いたことがあるでしょうか。大半の方は無いでしょう。そのためにはコンパイルの一工程であるリンクについてざっくりと知っておく必要があります。

リンクとは

簡単に言うと、オブジェクトファイル同士の名前を解決し、定められたメモリ空間に適当なデータを配置することです。例えば func という関数はプログラム領域に置き、flag というグローバル変数はそれ用の領域に置き、という感じです。

C言語では static 宣言されていない関数は全て外部からアクセスすることのできる関数ですから、ヘッダファイル等で関数プロトタイプ宣言の記述を行なったものに関して、リンク時にジャンプ先アドレスを探して記録するという動作を行うのです。

...ん、わかりにくいですねぇ。少なくともここはあまり説明する気がありませんので、ググる等して理解を深めておいてください。

そのリンクの際に「これこれのオブジェクトはここの領域に配置してくれ」と書いた指令書がリンカスクリプトなのです。主な仕様や書き方はこちらを見れば分かるようになっていますが、所見だと分かりづらいので大体の説明をします。

リンカスクリプトの中身と役割

リンカスクリプトには主に以下が記録されています。

  • エントリポイント
  • メモリ領域
  • セクションのメモリ配置

エントリポイントとは、プログラムをどの関数から始めるかと記載されているものです。細かい話になりますが(初心者は読まないことをおすすめする)、実際ここに記載された関数からスタートするとは限りません。マイコンはリセット時に予め製造時に指定したメモリアドレスからプログラムを実行し始めるので、ここに正しい記述を書こうが実際の動作には無関係です。では何故書くかというと、それはリンク時の最適化に必要だからです。最適化を一切行わないのなら別にエントリポイントなんていう概念は要りませんが、ROM/RAM 容量の限られたマイコンでは最適化は必須なわけです。その際に使わない関数を排除するわけですが、ここでリンカスクリプトのエントリポイントが役立ちます。プログラム全体を見ていって、エントリポイントから様々な実行される関数を辿ります。そこで一度も到達しなかった関数を削除することで、プログラムサイズを抑えることができるというわけです。

メモリ領域とは、どこに(何番地に) ROM があって、どこに RAM がある等の情報です。今回の ARM Cortex-M では ROM と RAM しか記載しませんが(実際にはオプションバイトもあるはずだが無視)、他のマイコンではデバイスIDであったり、セキュリティコードであったり、様々です。

セクションのメモリ配置は「プログラムは ROM に」とか、「グローバル変数は RAM に」とか、それぞれシンボルの分類をするものです。

通常、PC上で gcc をつかうときはこんなもの要りませんよね?これは内部でデフォルトのリンカスクリプトを使用しているからなんです。組み込みではデバイスによってメモリ構成が異なりますから、必要になるのです。

リンカスクリプトをいじる

それではリンカスクリプトを持ってきましょう。
その前にプロジェクトディレクトリの構成をします。まずはお好きな名前のディレクトリをどこかに作成してください(プロジェクト名になります)。更にその中に src というソースファイルを格納するディレクトリを作ります。これで一旦構成は終わりとしましょう。

リンカスクリプトはイチから自分で書き上げてもいいのですが、テンプレートがコンパイラをインストールしたディレクトリに付属していますので、そこからもらってしまいましょう。どうやら調べたところ /usr/local/cross/arm-none-eabi/arm-none-eabi/lib/ldscripts/armelf.x が該当するらしいです。じゃあ適当にもらってしまいますか。

$ cp /usr/local/cross/arm-none-eabi/arm-none-eabi/lib/ldscripts/armelf.x linker.ld

リンカスクリプトの上部

中はどうなっているのですかね。見てみますか。下は SECTIONS の上部です。

OUTPUT_FORMAT("elf32-littlearm", "elf32-bigarm",
          "elf32-littlearm")
OUTPUT_ARCH(arm)
ENTRY(_start)
SEARCH_DIR("/usr/local/cross/arm-none-eabi/lib");

何やら色々書いてありますが、エントリポイント以外はコンパイラのオプションで好きに指定できる項目ですから、削除してしまいます。
また、エントリポイント名もどうせなら ARM っぽく、Reset_Handler にしましょう。

ENTRY(Reset_Handler)

MEMORYコマンド

このスクリプトには、メモリ領域を記載したコマンドが存在しません。あったほうが便利ですので書きましょうか。このコマンドを MEMORY コマンドと呼びます。
とりあえず ENTRY の直下にえいやで書きます(まだ完全ではありません)。

MEMORY {
    ROM(rx) /* とりあえず */
    RAM(wx) /* とりあえず */
}

ROM と RAM があるのだから、これでいいでしょう?という記述です。この名前は任意なので、お好みでどうぞ。
領域名(ROM とか RAM とか)を書き、その後にカッコを挟んで r(readable), w(writable and readable), x(executable) を付けています。最低限 wr がついていればいいのです。x は雰囲気で付けました。
続いて必要になるのは以下の情報です。

  • 領域の先頭アドレス
  • 領域の長さ

これらの値はデバイスによりますので、ここでデータシートの登場です。43ページ付近にメモリ空間を表したこんな図があります。

0x0000 0000 から 0x0000 FFFF まではブートモードでいろいろ変わる、ミラー領域のようです。
0x0800 0000 から 0x0800 FFFF までは Flash であり、ここにプログラムを配置したら良さそうです。
0x2000 0000 からは RAM で、このデバイスは 12KBytes あります。

さてそれをスクリプトに記述しましょう。先頭アドレスは ORIGIN、長さは LENGTH で指定可能です。

MEMORY {
    ROM(rx) : ORIGIN = 0x08000000, LENGTH = 64k
    RAM(wx) : ORIGIN = 0x20000000, LENGTH = 12k
}

これでメモリ領域が定義されました。それぞれの値は以下変数として取り出すことができ、ORIGIN(ROM) と書くとこれは 0x08000000 を指し、また LENGTH(RAM) と書くと 12000 を指します。便利ですね。

SECTIONSコマンド

このコマンドでは、メモリ領域とセクションを結びつけます。まずはじめに、一番最初の PROVIDE の一文が、なぜか数値を決め打ちで入力していてキモいので削除します。

言い忘れましたが、取り敢えず次回のために、アセンブラでLチカすることを目標にしたいと思います。C言語を使うとなると、説明が増えてしまうため取り敢えず一番最初のLチカは低レベルな記述で行きます。

続いて .text というセクション記述を見つけてください。このセクションはプログラムや不変の定数を格納する領域として使われます。ですので ROM に配置するのが正解ですよね?ということなので、閉じ波括弧の部分をこうします。

    } > ROM

> ROM はまさに「このセクションを ROM に配置してね」という指示です。これでプログラムが無事 ROM に配置されるわけです。
本当はC言語での利用を考えて他のセクションも指定が必要なのですが、取り敢えずアセンブラのみとします。(実はアセンブラオンリーではリンカスクリプトは不要なのですが、後々必要になるので、中途半端ながら説明しています...)

あ、そういえばリセット時にはどこからプログラムが開始されるのでしょう。リファレンスを見ると答えは書いてあります。

ふむ、0x0000 0004 に書くのですね。何を書くのだ!と思われるかも知れませんが、プログラミングマニュアルにはジャンプ先のアドレス + 1を書けと言われています。この +1 は、ARM の Thumb モードで動作をするということを指しています。取り敢えず関数アドレスに1を足せば良いと思っていてください。それを1ワード(32bit)でかけばいいのですね。こんな感じで、割り込み等に関係のあるアドレステーブル(配列)をベクタテーブルと呼びます。
他のマイコンだとこの通りとは限りません。例えば PIC32MX マイコンだと関数へのジャンプ命令を書く事になっています。ひとまず関数アドレスを書けば良いのです。

それでは割り込みベクタ用にセクションを追加しておきます。SECTIONS の波括弧直後に追加してください。

    .isr_vector ORIGIN(ROM) : {
        KEEP(*(.isr_vector))
        KEEP(*(.isr_vector.*))
    } > ROM

.isr_vector の後に ORIGIN(ROM) とあるのは、こうすることで配置アドレスを指定しています。指定していないとリンカが自動で(通常はスクリプトの上から)配置先を考えます。ということでこのセクションに属するシンボルは ROM の先頭に配置されるようになりました。めでたしめでたし。
ここで記法の説明をします。KEEP()プログラム中で使われていないシンボルも取り去らずにバイナリに含めることを意味します。ベクタテーブルは配置のみに意味があり、プログラム中で参照することはないのでこの書き方は必須です。最初の * はオブジェクトのファイル名を示します。例えば

main.o(hoge)

と書くと、main.o (main.c や main.cpp 由来) 内にある hoge というセクション指定をしたシンボルを示します。通常は特に指定する理由がないので、ワイルドカードでアスタリスクを用いています。
カッコの内部の * もワイルドカードです。これはコンパイラでオブジェクトファイルの最適化を掛ける準備をする際、-ffunction-sections-fdata-sections というオプションによってセクション名をバラバラにしますが、そのときに例えば .text なのに .text.hoge 等名前が変わります。それに対応できるようにマッチさせているのです。

マイコンガチ勢は「あれっ?これで終わり?」と思うでしょう。取り敢えず今のところは C言語ではなくアセンブリによってLチカするという目標でいるので、簡易的なリンカスクリプトの準備となりました。