組み込みソフトウェアを開発するときに欠かせないのが「スタートアップ処理」です。PCの世界ではBIOSやブートローダが担当している初期化を、組み込みシステムでは開発者自身が実装しなければならないケースが多々あります。本記事では、リセット直後からメイン関数が動き始めるまでのスタートアップ処理について解説します。
スタートアップ処理とは
スタートアップ処理とは、組み込みCPUがリセット(電源投入直後やハードリセット時)された直後から、メイン処理(main()
関数やOSのカーネル)を実行可能な状態に整えるための一連の初期化処理を指します。組み込みシステムでは、CPU自体はリセット直後に何をどのように初期化すべきかを認識しておらず、システム側で必要な設定や準備をすべて行う必要があります。これにより、メイン処理がハードウェアの詳細を意識せずに動作できる環境が整えられます。
以降、「電源投入直後やハードリセット」を単に「リセット」と呼びます
具体的には、次のような設定や処理を行います。
- CPUの割り込み設定
- スタック領域(RAM)の初期化、RAMアクセスに必要な設定
- BSS領域・DATA領域など、メモリ領域の初期化
- クロック源(PLL)の設定と安定待ち
- キャッシュの設定と有効化
これらが完了して初めて、メイン処理が呼び出されます。
なぜスタートアップ処理が必要なのか
「C言語であれば main()
を起動時にただ呼べばいいだけでは?」と思われると思いますが、現実にはそう単純ではありません。C言語のソースコード通りの動作をさせるためには、下準備ができていないといけないからです。
下準備ができていない段階では、例外や割り込みはもちろん、変数の使用やライブラリの使用もできません。
ソースコード通りの動作をさせる準備を、メイン処理に記述するのは問題があるわけです
ざっくりと全体像を見てみましょう。
CPUはリセット時に特定のアドレスから命令を実行する
CPUはリセット時、リセットベクタと呼ばれるアドレスから機械的に処理を開始するようになっています。メイン処理の開始アドレスをCPUが把握していないため、メイン処理はスタートアップ処理からジャンプしてもらう必要があります。
ハードウェア依存の準備が必要
RAMが外付け(特にDRAM系)であったり、キャッシュの有効/無効を指定しなければならない場合なども、CPUにはリセット直後に何も設定されていませんし、何を設定すれば良いかもわかりません。そのままだと、そもそもメモリアクセスができないなど、システム全体の安定性が損なわれます。
RAMのアクセス方法が不明、ということはスタックも最初から用意されていないということです。スタックについては後述します。
高パフォーマンスが求められるCPUでは特に、こういった設定を最適に行うことで「本気」を出させることができます。
静的変数の初期化が必要
BSS領域
や DATA領域
と呼ばれる静的変数領域は、C言語規格上「起動時にゼロ初期化・または指定値で初期化される」ことが前提となっています。リセット直後のRAMにはどんな値が入っているか分からないため、スタートアップ処理が初期化を行う必要があります。
「BSS領域」、「DATA領域」については後述します
クロックの設定と安定待ちが必要
CPUのデフォルトでは、各部に供給するクロックが遅すぎたり、速すぎたりすることがあります。それを正しく供給するための設定が、スタートアップ処理の段階で必要です。
スタートアップ処理の具体的な内容
スタートアップ処理で何をしているかを以下で解説します。順番は必ずこれでないといけない、というわけではありません。設定したものに関連する機能から順次使えるようになっていくため、「このタイミングで何をしたらいけないのか」を知る必要があります。
割り込み禁止
スタートアップ処理中に割り込みが発生すると、割り込みハンドラのアドレスやスタック領域が未初期化で、暴走する恐れがあります。それを防ぐために割り込みを禁止しておきます。
CPUの初期状態が割り込み禁止状態であることもありますが、そうでない場合は最初に割り込み禁止にする必要があります。
割り込み禁止状態でも、異常が要因の例外やリセットによる例外は発生します。リセットはともかく、異常が要因のものは発生しないように注意しないといけません。また、例外や割り込みが発生したことは保留にされるだけなので、割り込み禁止を解除した途端に処理が始まってしまうこともあります。割り込み禁止を解除する際にも十分な注意が必要です。
クロック(PLL)の設定
クロックは、CPUの全ての動作に影響します。特にSDRAMを使う場合は、RAMアクセス設定よりも前に正しいクロック設定を行う必要があります。
PLL(Phase Locked Loop)回路は入力クロックを整数倍したり、整数分の1にしたりする回路で、設定すると安定するまで数mS~数十mSほど待機する必要があります。リセット直後は低速クロックで動かしつつ、PLLが安定したら高速クロックに切り替えるといった手順が必要です。
「安定したかどうか」については確認方法がCPUによって異なり、場合によっては確認方法そのものが無く「十分な時間待つ」こともあります。
RAMアクセス設定
RAMがCPUに内蔵されていない場合は必須で、この場合はスタックポインタ初期化とセットで設定します。これらが両方完了していないと、スタックが使えません。「スタック」については後述します。
特にDRAM系を使う場合、DRAM系はコマンド形式で、そのタイミングも適切に設定しないと全くアクセスできません。
スタックポインタ初期化
関数コール時、例外発生時、自動変数の使用時に使われる一時領域をスタックといい、その位置をCPUが把握するために使われているのがスタックポインタというレジスタです。
RAMが外付けである場合はRAMアクセスの設定が先で、続いてスタックポインタを設定するのが一般的です。これらの設定が終わるまでは関数、例外や割り込み、自動変数を扱うことができません。
「割り込み」は「例外」の一種です
BSS領域、DATA領域の初期化
BSS領域は「Block Started by Symbol」の略で、もとはアセンブラの疑似命令でしたが、現在は「静的変数で初期値がゼロになっているか、未指定のもの」が割り当てられる領域のことです。ANSI以降のC言語では、静的変数の宣言時に初期値が指定されないときも、初期値をゼロにする」ことが決められています。なのでスタートアップ処理ではBSS領域に割り当てられた領域をゼロでベタ塗りします。
BSS領域に割り当てられる宣言の例
static int foo = 0 ;
static int bar ;
DATA領域は「静的変数でゼロでない値を初期値としているもの」が割り当てられる領域のことです。この領域はRAMとROMの両方に、同じサイズが用意されており、スタートアップ処理でROMの内容をRAMにコピーする処理が行われます。
DATA領域に割り当てられる宣言の例
#define NULL ((void *)0x80000000) /* これはただの定数の定義 */
static int boo = 5000 ;
static void *vp = NULL ;
ベクタテーブルの初期化
例外や割り込み処理を使うには、ここの項目を含めて全ての準備が終わってからになります。割り込み禁止の解除は、一般的にはスタートアップ処理の最後(メイン処理の直前)に行います。
ベクタテーブルとは?
例外発生時には、必要に応じて必要な処理(この処理を「ハンドラ」といいます)を行いますが、その処理のジャンプ先をCPUが把握するために用意される、開始アドレスを格納するレジスタの列のことです。なお「割り込み」は「例外」の一種です。
「必要に応じて」の条件を「例外(割り込み)要因」といい、発生する条件を「トリガー」といいます
CPUによっては割り込みベクタが1つしかなく、そのベクタで指定された処理で要因をソフト的に振り分けるケースもあります。
例外の要因
処理の継続ができなくなる可能性が高い条件(リセット、ゼロ除算、命令異常など)が発生すると「例外」となります。また、ソフトウェアで必要な条件(UART送受信完了やタイマーなど)が発生すると「割り込み」となります。
これらの要因ごとに必要な処理をソフトウェアで用意し、そのアドレスをベクタテーブルに設定しておくわけです。また「割り込み」については「優先度」も併せて設定します。
ベクタテーブルへの設定には、関数ポインタを使うのが一般的です。
メイン処理へ
ここまでやって、ようやくメイン処理が「ソースコード通り」動作できるようになります。大変ですよね。
メイン処理のアドレスはリンカが生成するため、そのアドレスをスタートアップ処理から呼び出す(ジャンプさせる)ことで、メイン処理が開始されます。コンパイル、リンクのたびにメイン処理のアドレスが変わるとスタートアップ処理も変更しないといけないため、これを避けるにはメイン処理の開始アドレスを固定にしたりします。
メイン処理の開始アドレスを固定にするには、メイン処理の開発環境においてリンカの設定が必要になるため、ハードウェアのことは知らずとも、リンカについては知っていないといけないこともあることに注意してください。
割り込み禁止の解除を忘れないこと
PCのBIOSとの違い
BIOSは「Basic Input/Output System」の略で、最近のPCでは非常に高機能なものになっています。PCのBIOSはスタートアップ処理の他に、ハードウェアの機能をOSが利用するためのサービスが提供されています。
組み込みシステムにおけるスタートアップ処理は、BIOSの最低構成という認識でいいでしょう。
実装のポイントと注意点
スタートアップ処理は、「ソースコードがCPU上でどう動いているか」を知っている方でないとイメージがつきにくく、上級者向けといえます。特に注意すべき点を以下にまとめます。
CPUのマニュアルを熟読する
スタートアップ処理は完全にシステム依存です。CPUの動作を正確に把握しないといけないため、リファレンスマニュアルやデータシートの熟読が必須です。
コンパイラ・リンカ動作の熟知
メモリ上に、何がどのように配置されるかはコンパイラやリンカの動作で決まります。先述のBSS領域、DATA領域などはリンカの動作の結果生成されるものですし、エントリーポイントもどう指定するのかはシステム依存です。データやコードがメモリのどこに配置されるのか、どうすればそれを意図的に操作できるのかを熟知していないといけません。
pragmaのように、書籍では学べないものも多用します。
「セクション」:メモリ上に配置されるコードやデータの種別ごとの「塊」のこと。BSS、DATA領域はその一種
スタートアップ処理の順序の決定
先述の通り、スタートアップ処理で設定した機能から使用できるようになるため、どこまで設定ができていれば何ができるのかを把握している必要があります。標準ライブラリも使用できず、場合によってはアセンブラで記述しないといけないケースもあります。
これらを知るには、ソースコードの動作がCPU動作とどう結びついているかを学ぶ必要があります。
ハードウェア依存部分の分離
特にOSを搭載するケースでは、メイン処理画はハードウェアの仕様を知らなくてもいいようにする必要があります。
そんな中で例外や割り込みのような、プログラマが意図しない要因で動作するものは「コールバック関数」を利用しますが、それには関数ポインタの知識が必須になります。
どういうインターフェースを提供すれば、メイン処理側がハードウェアを知らなくてもいいのかを考えられる知識が必要です。
まとめ
スタートアップ処理の理解と実装は、組み込みエンジニアとしての技術力を大きく向上させる重要なステップです。各工程のポイントを押さえ、仕様書やリンカスクリプト、アセンブラの知識を深めることで、より堅牢なシステム設計が実現できるでしょう。
本記事では、組み込みシステムにおけるスタートアップ処理の全工程とその重要性を網羅的に解説しました。これから組み込み開発に挑戦するエンジニアの方や、既に現場で活躍されている方にも、基礎から応用まで参考になる内容となっています。ぜひ、スタートアップ処理の理解を深め、実装の現場で役立ててください。