STM32でフルスクラッチプログラミング
この記事は、 OIT Advent Calendar 2017 - Adventar の9日目の記事です。
はじめに
正直、全てを正しく書くにはあまりにも時間が足りませんでした。。コードと合わせた解説記事ともしたかったのですが、9日現在至っていません。これは自分のせいなので、今後も文章化していこうと考えています。
(1)背景
私はHxS OBで2010年の代表だった気がします。
気付けば社会人も6年目。今は京都で分析計測機器・産業機器・航空機器といった分野の組込みソフトウェアの開発を主にしています。
組込みソフトウェアとは、一般的なコンピュータ以外で何らかの製品に「組込」まれたソフトウェアのことです。
#ご興味がある方は連絡ください。
(2)今回の内容
今回は、STM32F401REマイコンでmain関数を実行するまでを解説します。本来、各マイコン向けの開発環境でプロジェクトを作成・実行しさえすればmain関数が実行されます。また、提供されるライブラリを使、IOの制御やシリアル通信のようなマイコンの機能を使うことはそこそこ簡単にできます。
推奨環境やライブラリを一切使用せず、自らの手で全てを用意する、フルスクラッチです。“車輪の再開発”でしかないのですが、やってみるととても勉強になります。
この記事の実行環境は下記のとおりです。
- ホストOS:Mac OS Siera 10.12.6(16G29)
- コンパイラ:gcc-arm-none-eabi-5_4-2016q3
- デバッガ:OpenOCD 0.10.0
- ターゲットボード:STM32F401RE Nucleo
<開発環境構築>
フルスクラッチとはいえ、コンパイラとデバッガはありものを使います。
(1) コンパイラ
STM32マイコンはARMアーキテクチャのマイコンです。コンパイラはマイコンのアーキテクチャに対応したものを用意します。
今回は、Arm提供のコンパイラをダウンロードします。
(2) デバッガ
オープンソースのデバッガ OpenOCDを導入します。
これで、プログラムの書込みや消去、ブレークポイントを貼った停止や再開、変数やアドレスの中身を覗いたりというようなことを行います。
OSによって導入方法は異なるので、適宜いい感じにOpenOCDを使えるようにします。
(3) エディタ
お好みでどうぞ。Emacsであれば、gdbフロントエンドが提供されているので、少し捗るかもしれません。
main関数にたどり着くまで
マイコンは電源投入直後、ROMの指定番地を実行します。つまり、プログラムの開始アドレスをこの指定番地に書きさえすれば、実行されるということです。ROMの指定番地は、各マイコンのリファレンスマニュアルを参照します。
“指定番地を実行する”ためにベクタテーブルを記述します。ベクタテーブルは、マイコンの割り込み時に実行する関数アドレスを定義するテーブルです。
ここでは最小限の記述として、
・ベクタテーブルの0x00にスタックポインタの開始アドレスを書く
・ベクタテーブルの0x04にリセット時に実行する処理のアドレスを書く
が必要となります。あくまで、ARMの場合です。
このベクタテーブルを指定番地に配置するためには、リンカスクリプトを記述します。リンカスクリプトは、プログラムのメモリ配置を記述することで、コンパイラがリンク処理を行う際に用いられます。このリンカスクリプトにて、ROMやRAMの領域に関する設定も行います。
さて、もう一つスタックポインタと書きました。PCのプログラムでも、スタックは出てくる概念ですね。あれです(雑)。
RAMのアドレスを割り当てるのですが、ARMのスタックアドレスは、下方向に進むので、RAMの終端アドレスにします。別に真ん中あたりでも動きますが、プログラムで使用する変数領域がRAMの開始アドレスから上方向に進む関係上、当たらないようにしましょう。
また、スタックのアドレスですが、バイト境界を意識しておく必要があります。もう難しいわ。
リセット時に実行する処理というのは、main関数でしょうか。そう書くこともできますが、一般的にはスタートアップルーチンを実行します。
スタートアップルーチンで行う処理として、
・スタックポインタ設定(ARMはベクタテーブルで設定するので済)
・マイコン固有の設定
・変数の初期化
・メイン処理(main関数)への遷移
があります。また、スタートアップルーチンは基本的にはアセンブラで書かれます。
マイコン固有の初期化というのは、割り込み許可設定や、マイコンのモード設定あたりが考えられますが、main関数でレジスタアクセスによって同じことができるということもあります。あと、エラッタ対策で予約領域のレジスタに対するアクセスをしたりということもあります。
変数の初期化について、例えば、初期値のある大域変数を考えてみます。初期値があるということは、ROM上に持たなくてはなりません。しかし、ROMにあるままだと、プログラム上から変更が効かないことになります。これは困るので、ROMの初期値をRAMに展開する、といった処理が必要となります。これが変数の初期化です。
メイン処理への遷移というのは、ただただmain関数のアドレスにブランチするだけです。
さて、これでmain関数が実行できました!
今までの説明について、実はPCソフトであっても同じようなことをOSがやってくれています。
このあとは、GPIOなどの周辺ペリフェラルを使うコードやライブラリを自分で書いていくことになります。そうそう、クロックの設定も重要です。今のままだとリセット時の初期設定クロックで動作しています。
さいごに
以上、HxS OB 3連打の2日目でした(示し合わせた訳ではない)。言葉足らずだったりする箇所も多いのですが、組込みシステムに少しでも興味を持っていただけたなら幸いです。