Tockの起動
このドキュメントでは、Tockのすべてのコンポーネントがどのようにして起動するのかを説明します。
マイクロコントローラが起動(または、リセット、割り込みが発生)すると、割り込みの種類により索引化されている_ベクタテーブル_と呼ばれている表から関数のアドレスをロードします。メモリ内のベクタテーブルの位置はチップ固有のものであり、リンク用の特別なセクションに配置されています。
Cortex-Mマイクロコントローラでは、ベクタテーブルがアドレス0x00000000にあることが想定されています。これはソフトウェアブートローダかTockカーネル自体のいずれかになります。
RISC-Vでは起動の仕方についてハードウェア設計者が非常に自由に設計することができます。通常、RISC-Vプロセッサはリセット後、ROMから実行を開始しますが、これは設定可能です。たとえば、HiFive1ボードは、ROM、ワンタイムプログラマブル(OTP)メモリ、QSPIフラッシュコントローラからのブートアウトをサポートしています。
オプションのブートローダ
多くのTockボード(Hailとimixを含む)は、MCUが最初に起動したときに実行 されるソフトウェアブートローダを使用します。ブートローダは、シリアルを介した チップとの対話、新しいコードのロード、その他の管理タスクの実行をするための 手段を提供します。ブートローダが終了すると、MCUにベクトルテーブルが(既知の アドレスに)移動したことを伝え、新しいアドレスにジャンプします。
Tockの最初の命令
ARMのベクタテーブルとIRQテーブル
ARMチップにおいて、Tockはベクトルテーブルを2つのセクションに分割します。
.vectorsはすべてのARMコアに共通の最初の16エントリを保持し、.irqsは
その後ろに追加され、チップ固有の割り込みを保持します。
ベクタテーブルはソースコードでは.vectorsセクションに配置されるように
マークされた配列として現れます。
Rustではベクタテーブルは次のようになります。
#![allow(unused)] fn main() { #[link_section=".vectors"] #[used] // 最終的なバイナリになるまでシンボルが保持されることを保証する pub static BASE_VECTORS: [unsafe extern fn(); 16] = [ _estack, // スタックポインタの初期値 tock_kernel_reset_handler, // Tockのリセットハンドラ関数 /* NMI */ unhandled_interrupt, // 一般ハンドラ関数 ... }
Cではベクタテーブルは次のようになります。
__attribute__ ((section(".vectors")))
interrupt_function_t interrupt_table[] = {
(interrupt_function_t) (&_estack),
tock_kernel_reset_handler,
NMI_Handler,
これの執筆時点(2018年11月)において、主要なチップ(sam4Lやnrf52
など)はすべての割り込みに対し同じハンドラを使用しています。それは、次の
ようになります。
#![allow(unused)] fn main() { #[link_section = ".vectors"] #[used] // Ensures that the symbol is kept until the final binary pub static IRQS: [unsafe extern "C" fn(); 80] = [generic_isr; 80]; }
RISC-V
すべてのRISC-Vボードは、reset_handlerにジャンプする前に実行する最初の
関数として_start関数がリンクされています。これは、この記事を書いている
時点ではインラインアセンブリです。
#![allow(unused)] fn main() { #[cfg(all(target_arch = "riscv32", target_os = "none"))] #[link_section = ".riscv.start"] #[export_name = "_start"] #[naked] pub extern "C" fn _start() { unsafe { asm! (" }
リセットハンドラ
起動時、MCUはベクトルテーブルで定義されているリセットハンドラ関数を呼び出し
ます。Tockでは、リセットハンドラ関数の実装はプラットフォーム固有のものであり、
各ボードごとにboards/<board>/src/main.rsで定義されています。
メモリの初期化
リセットハンドラが最初に行う操作は、カーネルメモリをフラッシュからコピーしてセットアップすることです。SAM4Lの場合、これはchips/sam4l/src/lib.rsに
あるinit()関数にあります。
RISC-Vトラップのセットアップ
RISC-Vではトラップを処理するためにmtvecレジスタを設定する必要があります。
ベクタの設定はチップ固有の関数で処理されます。一般的なRISC-Vのトラップ
ハンドラは、arch/rv32i/src/lib.rsで定義されている_start_trapです。
MCUセットアップ
通常、一般的なMCUの初期化が次に処理されます。これには、正しいクロックの 有効化やDMAチャンネルの設定などが含まれます。
ペリフェラルとカプセルの初期化
MCUのセットアップが終わるとreset_handlerはペリフェラルとカプセルを
初期化します。ペリフェラルはUART、ADC、SPIバスなどのオンチップサブシステム
です。これらはメモリマップドI/Oレジスタを読み書きするチップ固有のコードで
あり、対応するchipsディレクトリにあります。ペリフェラルはチップ固有の
実装ですが、通常、kernel/src/hilにあるHIL(Hardware Independent
Layer)トレイトと呼ばれるハードウェアに依存しないトレイトを提供しています。
カプセルはソフトウェアの抽象化とサービスです。これらはチップに依存しない
ものであり、capsulesディレクトリにあります。たとえば、imixとhail
プラットフォームでは、SAM4L SPIペリフェラルはchips/sam4l/src/spi.rs
で実装されており、SPIを仮想化して複数のカプセルでSPIの共有を可能にする
カプセルはcapsules/src/virtual_spi.rsにあります。この仮想化はチップ
非依存にします。チップ固有のコードがSPI HILを実装しているからです
(kernel/src/hil/spi.rs)。プロセス用のSPIへのシステムコールAPIを
実装しているカプセルはcapsules/src/spi.rsにあります。
多くのペリフェラルやカプセルを初期化するボードは、reset_handlerからこの
複雑さをカプセル化するためにComponentトレイトを使用しています。
Componentトレイト(kernel/src/component.rs) は、特定のペリフェラル、
カプセル、カプセルセットが必要とする初期化を関数finalize()の呼び出しと
してカプセル化します。これにより、カーネルビルドに含まれるものの変更する
ために、reset_handlerを何行も変更せずに、初期化するコンポーネントを
変更だけですみます。コンポーネントは通常、/boardsフォルダのcomponents
クレートにありますが、ボード固有のためボードディレクトリのcomponents
サブディレクトリ、たとえば、boards/imix/src/imix_componentsに
ある場合もあります。
アプリケーションの起動
カーネルコンポーネントのセットアップと初期化が完了したら、アプリケーションを ロードしなければなりません。この手順は基本的にフラッシュに格納されている プロセスごとに繰り返され、Tockバイナリフォーマットヘッダの抽出と検証、 プロセス構造体の内部配列への追加が行われます。
このループの例としては、load_processes()関数としてkernel/src/process.rsに
あります。ポインタを設定した後、フラッシュ内の開始アドレスと指定の
メモリ残量からプロセスの作成を試みます。ヘッダが検証されると、プロセスを
メモリにロードし、プロセスに関連付けられたカーネル内のすべてのブック
キーピングの初期化を試みます。プロセスがチップ上で利用可能なメモリよりも
多くのメモリを必要とする場合、これは失敗する可能性があります。プロセスが
正常にロードされた場合、カーネルはプロセスの起動時に呼び出されるアプリ
ケーションのエントリ関数のアドレスを記録します。
このプロセスロードの繰り返しは、カーネルがプロセスを格納するために静的に 割り当てたメモリかプロセスに利用可能なRAMを使い果たした場合、または フラッシュに無効なTBFヘッダがあった場合は終了します。
スケジューラの実行
Tockは、さまざまなスケジューリングアルゴリズムのプラグインを可能にする
抽象化として機能するSchedulerトレイトを提供しています。スケジューラは
リセットハンドラの最後に初期化される必要があります。リセットハンドラが最後に
しなければならないことは、kernel.kernel_loop()の呼び出しです。これは
Tockスケジューラとカーネルのメイン操作を開始します。