Tockの概要

Tock は、Cortex-M、および、RISC-Vマイクロコントローラ用のセキュアな組み込み オペレーティングシステムです。Tockは、ハードウェアがメモリ保護ユニット(MPU)を 持っていることを前提としています。MPUを持たないシステムでは複数の信頼できない プロセスを同時にサポートすることも、Tockの安全性とセキュリティな特性の維持をする こともできないからです。Tockカーネルとその拡張機能(カプセルと呼ばれる)は Rustで書かれています。

Tockは、任意の言語で書かれた複数の独立した信頼できないプロセスを実行することができます。 Tockが同時にサポートできるプロセスの数は、MCUのフラッシュとRAMにより制限されます。 Tockはさまざまなスケジュールアルゴリズムの使用を設定できますが、Tockのデフォルト スケジューラはプリエンプティブであり、ラウンドロビン方式を使用します。Tockはマイクロ カーネルアーキテクチャを採用しています。複雑なドライバとサービスの多くは信頼できない プロセスとして実装されており、アプリケーションなどの他のプロセスをプロセス間通信(IPC)を 介して呼び出すことができます。

このドキュメントでは、Tockのアーキテクチャの概要、Tockにおける異なるクラスのコード、 Tockが使用する保護機構、そしてこの構造がソフトウェアのディレクトリ構造にどのように 反映されているかについて説明します。

Tockのアーキテクチャ

Tock architecture

上の図は、Tockのアーキテクチャを示しています。コードは3つのカテゴリ、コアカーネルカプセルプロセスのいずれかに分類されます。

コアカーネルとカプセルはRustで書かれています。Rustは型安全なシステム言語です。この 言語とそのカーネル設計への影響については他のドキュメントで詳しく説明されていますが、 鍵となるのは、Rustのコードはメモリを意図したものと異なる使用ができない(たとえば、 バッファオーバーフロー、偽ポインタ、デッドスタックフレームポインタの保持など)ことです。 これらの制約は、OSカーネルがしなければならない多くのこと(たとえば、データシートで 指定されているメモリアドレスに存在するペリフェラルのアクセスなど)を妨げるため、非常に 小さなコアカーネルは「安全でない」Rustコードを使用することでこれらの制約を破る ことが許されています。しかし、カプセルは安全でない機能を使用することはできません。 これは、コアカーネルのコードは非常に小さく慎重に書かれているが、カーネルに追加される 新たなカプセルは安全なコードであり、信頼される必要はないということを意味します。

プロセスは任意の言語で書くことができます。カーネルは、ハードウェアメモリ保護ユニット (MPU)を使用して、自分自身と他のプロセスを不正なプロセスコードから保護します。 プロセスが許可されていないメモリにアクセスしようとすると、MPUは例外を発生させます。 カーネルはこの例外を処理してプロセスを終了させます。

カーネルは4つの主要なシステムコールを提供します。

  • command: プロセスからカーネルのコールを行います。
  • subscribe: カーネルからコールされるプロセスのコールバックを登録します。
  • allow: プロセス内のメモリにカーネルがアクセスできるようにします。
  • yield: コールバックが呼び出されるまでプロセスを一時停止します。

yield以外のすべてのシステムコールはノンブロッキングです。長時間かかる可能性のある コマンド(UART経由のメッセージ送信など)はすぐに戻り、処理が完了するとコールバックが 発行されます。yieldシステムコールは、コールバックが呼び出されるまでプロセスをブロック します。通常、ユーザーランドのコードは、コマンドを実行し、yieldを使用してコールバックが 完了するまで待機するというブロック関数を実装します。

command、subscribe, allowの各システムコールはすべて、最初の引数としてドライバIDを 受け取ります。これは、システムコールがカーネル内のどのドライバを対象としているかを 示します。ドライバはシステムコールを実装したカプセルです。

Tockのディレクトリ構造

Tockにはいくつかの主要なコードディレクトリがあります。

  • arch: アーキテクチャ固有のコードを格納します。つまり、Cortex-M0やCortex-M4 固有のコードです。これには、コンテキストスイッチの実行やシステムコール(ユーザーコード からカーネルコードへのトラップ)を行うコードが含まれます。

  • boards: imix、Hail、nrf52dkなどの特定のTockプラットフォーム用のコードを 格納します。通常、これはカーネルが持つすべてのカプセル、MCUのIOピンを適切な状態に 設定するコード、カーネルを初期化するコード、プロセスをロードするコードなどを定義する 構造体です。このディレクトリで最も重要なファイルはmain.rsであり、その最も重要な 初期化関数は(MCUがリセットされたときに実行される)reset_handlerです。ボード コードでは、システムコールデバイス識別子をカプセルにマッピングする方法も with_driver関数の中で定義しています。

  • capsules: 特定のペリフェラルのチップ固有実装の上に構築できるMCUに依存しない カーネル拡張を格納します。システムコールを提供するカプセルもあります。たとえば、 この ディレクトリにあるspiモジュールは、チップのSPI実装を使って、そのシステム コールを提供する実装を構築しています。

  • chips: SPI、I2C、GPIO、UARTの実装やその他のマイクロコントローラ固有のコード を格納します。chipsとboardsの区別はマイクロコントローラとフルプラットフォームの 違いです。たとえば、多くのマイクロコントローラは複数のUARTを持ちます。どのUARTが Tockと通信するのにはどのUARTが主に使われるのか、また、別のチップを制御するには どのUARTが使用されるのかは、ボード上にチップがどのように配置され、どのピンが公開 されているかにより定義されます。したがって、チップはUARTの実装を提供し、ボードは どのUARTが何に使われるかを定義します。

  • doc: 内部インターフェースの仕様やチュートリアルを含むTockのドキュメントを 格納します。

  • kernel: スケジューラ、プロセス、メモリ管理など、マイクロコントローラに依存しない カーネルコードを格納します。このディレクトリとarchがすべてのコアカーネルコードを 格納する場所です。

  • libraries: 内部で使用したり、外部と共有するライブラリを格納します。いくつかの プリミティブがTock用に作成されていますが、他のプロジェクトにも有用でないかと考えて います。ここは各クレートを置く場所です。

  • tools: コードフォーマットチェック、バイナリ変換、ビルドスクリプトなど、 コンパイルやコードメンテナンスに役立つ関連ツールを格納します。

  • vagrant: 仮想マシン環境でTockを動かすための情報を格納します。

Tockの設計

ほとんどのオペレーティングシステムは、プロセスなどの抽象化を使いコンポーネント間の隔離を 提供しています。各コンポーネントには他のコンポーネントがアクセスできない独自の(スタック、 ヒープ、データ用の)システムメモリが与えられます。プロセスが優れているのは、隔離と並行処理 の双方に便利な抽象化を提供するからです。しかし、1MB以下のメモリしか持たないマイクロ コントローラのようなリソースに制約のあるシステムでは、このアプローチは、隔離の粒度と リソース消費の間にトレードオフの関係を導きます。

Tockのアーキテクチャは、コンポーネントの隔離には言語サンドボックスを、カーネルの並行 処理には協調スケジューリングモデルを、各々使用することで、このトレードオフを解決します。 その結果、隔離は(多かれ少なかれ)リソース消費という面では自由になっていますが、 プリエンプティブなスケジューリングが犠牲になっています(つまり、悪意のあるコンポーネント は、例えば無限ループでスピンすることによりシステムをブロックすることができます)。

第一に、カーネル内のコンポーネントを含む、Tockのすべてのコンポーネントは互いを 信用していません。カーネル内ではメモリや計算のオーバーヘッドを発生しない_カプセル_と 呼ばれる言語ベースの隔離抽象化によりこれを実現しています。ユーザ空間では、Tockは伝統的な プロセスモデルを(多かれ少なかれ)使用しており、プロセスはハードウェア保護機構を使用して カーネルや他のプロセスから隔離されています。

さらに、Tockは他の組み込みシステム特有の目標を念頭に置いて設計されています。Tockは システムの全体的な信頼性を重視し、バグが発生した場合にはシステムの進行を妨げるような コンポーネントを抑制します (可能であれば停止します)。

アーキテクチャ

Tock architecture

Tockには3つのアーキテクチャコンポーネントがあります。一つは、Rustで書かれた小さな信頼 できるカーネルであり、ハードウェア抽象化層(HAL)、スケジューラ、プラットフォーム固有の 設定を実装しています。その他のシステムコンポーネントは、2つの保護機構の一つで実装されて おり、カプセルはカーネルと一緒にコンパイルされ、安全のためにRustの型とモジュール システムを使用し、プロセスは実行時の保護のためにMPUを使用します。

システムコンポーネント(アプリケーション、ドライバ、仮想化レイヤなど)は、カプセル またはプロセスのいずれかに実装することができますが、それぞれのメカニズムは、並行処理と 安全性において、メモリ消費量、パフォーマンス、粒度に関するトレードオフがあります。

カテゴリカプセルプロセス
保護言語ハードウェア
メモリオーバヘッドなし独立したスタック
保護の粒度細かい粗い
並行処理協調的プリエンプティブ
実行時更新いいえはい

結果として、それぞれが異なるコンポーネントの実装に適しています。一般に、ドライバや仮想化 レイヤはカプセルとして実装され、ネットワークスタックなどの既存のコード/ライブラリを 使用するアプリケーションや複雑なドライバはプロセスとして実装されます。

カプセル

カプセルは、Rustの構造体と関連する関数です。カプセルは直接相互に作用し、公開フィールドに アクセスしたり、他のカプセルの関数を呼び出したりします。信頼できるプラットフォーム設定 コードがカプセルを初期化し、必要とする他のカプセルやカーネルリソースへのアクセスを可能に します。カプセルは、特定の関数やフィールドをエクスポートしないことで内部状態を保護でき ます。

カプセルはカーネル内部で特権ハードウェアモードで実行されますが、Rustの型とモジュール システムは、バグや悪意のあるカプセルからコアカーネルを保護します。型安全性とメモリ安全性 はコンパイル時に適用されるため、安全性に関連するオーバーヘッドはなく、カプセルに必要な エラーチェックは最小限です。たとえば、カプセルは参照の有効性をチェックする必要がありま せん。参照が存在すれば、それは正しい型の有効なメモリを指しています。コンポーネントの分割 によるオーバーヘッドが実質的にないため、非常に細かい粒度の隔離が可能になります。

Rustの言語レベルの保護は強力な安全性を保証します。カプセルがRustの型システムを破壊 できない限り、カプセルは明示的に与えられたリソースにしかアクセスできず、そのリソースが 公開しているインターフェースにより許可された方法でしかアクセスできません。ただし、 カプセルはカーネルと同じシングルスレッドのイベントループで協調的にスケジュールされる ため、システムの生存のためには信頼できるものでなければなりません。カプセルがパニックに 陥ったり、イベントハンドラに復帰しなかったりすると、システムは再起動することでしか回復 できません。

プロセス

プロセスはカーネルから隔離された独立したアプリケーションであり、カーネルとは別の実行 スレッドで削減された権限で実行されます。カーネルはプロセスをプリエンプティブに スケジュールするので、プロセスはカプセルよりも強くシステムの生存を保証します。さらに、 実行時にプロセスの隔離を強制するためにハードウェア保護を使用します。これにより、 プロセスは任意の言語で記述でき、実行時に安全にロードすることができます。

メモリレイアウト

プロセスは、ハードウェアメモリ保護ユニット (MPU) により、他のプロセスやカーネル、 基盤となるハードウェアから明示的に隔離されています。MPUはプロセスがアクセスできる メモリアドレスを制限します。プロセスが許可された領域外にアクセスするとフォールトと なり、カーネルトラップを発生させます。

フラッシュに格納されているコードは、読み取り専用のメモリ保護領域でアクセス可能になります。 各プロセスはRAM上の連続した領域が割り当てられてます。プロセスのこれまでにない点として、 アドレス空間の先頭に「グラント」領域が存在することが挙げられます。これは、メモリ保護 領域によりカバーされたプロセスに割り当てられたメモリであり、プロセスは読むことも 書くこともできません。グラント領域は(後述しますが)システムコールに応じて生存と安全性を 確保するためにカーネルがプロセスからメモリを借りられるようにするために必要となります。

グラント

カプセルは動的にメモリを割り当てることができません。カーネル内での動的な割り当ては、 メモリが枯渇するか否かの予測を困難にするからです。単一カップセルによる不十分なメモリ 管理はカーネルの残りの部分のエラーの原因となります。さらに、単一のスタックを使用するので、 カーネルはカプセルのエラーから簡単に回復することができません。

しかし、カプセルはプロセスの要求に応じて動的にメモリを割り当てる必要があることがよく あります。たとえば、仮想タイマードライバは、プロセスが作成する新たなタイマーごとに メタデータを保持するために構造体を割り当てなければなりません。そのため、Tockは リクエストを行うプロセスのメモリからカプセルが動的に割り当てることを可能にしています。

しかし、カプセルがプロセスメモリへの参照を直接保持することは安全ではありません。 プロセスはクラッシュし、動的にロードされる可能性があるので、カーネルコード全体にわたる 明示的なチェックを行わなければ、プロセスメモリへの参照が未だに有効であることを保証する ことはできません。

カプセルがプロセスから安全にメモリを割り当てるためには、カーネルは次の3つの属性を強制 しなければなりません。

  1. 割り当てられたメモリはカプセルが型システムを破ることを許さない。

  2. カプセルはプロセスが生きている間だけプロセスメモリへのポインタにアクセスできる。

  3. カーネルは、終了したプロセスからメモリを取り戻すことができなければならない。

Tockは、メモリグラントを通じてこれら3つの要件を満たす安全なメモリ割り当て機構を提供 します。カプセルはカプセルと相互作用するプロセスのメモリから任意の型のデータを割り 当てることができます。このメモリはグラントセグメントから割り当てられます。

allowを介して渡されるバッファと同様に、グラントメモリへの参照は逆参照をする前に プロセスがまだ生きていることを保証する型安全な構造体でラップされます。カプセル内の バッファ型にしかなることができない共有バッファとは異なり、グラントメモリは任意の型 として定義することができます。そのため、プロセスはこのメモリにアクセスできません。 そうすることは型安全性を破ることになるからです。

カーネル設計原理

Tockの目標を達成し、ハードウェア間での移植性を促進し、持続可能なオペレーティング システムを保証するために、時がたつにつれ、Tockカーネルのための設計原理が現れてきました。 これらはカーネルへの新たな貢献が維持しなければならない一般原理です。しかし、これらの 原理はTockの開発によりわかってきたものであり、TockとRustのエコシステムの進化に 合わせて進化し続けるでしょう。

HILの役割

一般的に、Tockカーネルは次の3つのレイヤーで構成されています。

  1. チップ固有のドライバ。通常、これらのドライバはchipsサブディレクトリにある クレートか、別のリポジトリにある同等のクレートにあります(たとえば、Titanポートは ツリーの外にありますが、そのh1bクレートはこれと同等です)。これらのドライバは、 特定のマイクロコントローラのハードウェアに固有の実装を持っています。理想的には、 それらの実装はきわめてシンプルであり、単に共通のインタフェース(HIL)に従うだけ です。必ずしもそうとは限りませんが、それが理想です。

  2. チップ非依存でポータブルなペリフェラルドライバとサブシステム。通常、これらは capsulesクレートにあります。これらには、仮想アラームや仮想I2Cスタックのような ものや、チップ内には存在しないハードウェアペリフェラル(センサ、ラジオなど)用の ドライバが含まれます。通常、これらのドライバはHILを介してチップ固有のドライバに 依存します。

  3. システムコールドライバ。通常、これもcapsulesクレートにあります。これらは、 システムコールインタフェースのある一部を実装するドライバであり、(2)よりもさらに ハードウェアから抽象化されていることが多いです。たとえば、温度センサシステム コールドライバは、ポータブルペリフェラルドライバとして実装されているものを含め、 任意の温度センサを使用することができる。

    システムコールインタフェースは、様々な方法で実装できるもう一つの標準化のポイントです。 そのため、全く異なるハードウェアスタックを使用する、その結果、全く異なるHILやチップ 固有のドライバを使用する同一のシステムコールインタフェースの実装が複数存在することは 完全に合理的です(たとえば、USB経由で動作するコンソールドライバは、USBをUART HIL に合わせようとするのではなく、同じシステムコールを実装する別のシステムコールドライバ として実装することができるでしょう)。

その重要性から、これらのレイヤー間のインターフェースはTockの設計と実装の最重要な部分と なっています。これらのインターフェースはTockのハードウェアインターフェースレイヤ (HIL)と呼ばれています。HILはRustのトレイとのポータブルコレクションであり、ボータブル にも、非ポータルにも実装することができます。HILの非ポータブルな実装の例としては、特定の チップのカウンタレジスタと比較レジスタで実装されたアラームがあり、ポータブルな実装の例と しては、一つのアラームの上に複数のアラームを多重化する仮想化レイヤがあります。

HILは、一緒に使用することを意図した1つ以上のRustトレイトで構成されています。中には HILのトレイトのサブセットの実装だけで良いケースもあります。たとえば、アナログデジタル 変換(ADC)HILは、単一サンプルとストリームサンプルという2つのトレイを持つことがで ある実装では単一サンプルのみをサポートし、ストリーミングトレイトを実装しないことが ことができます。場合があります。

HILインターフェースの選択はきわめて重要であり、従うべきいくつかの一般原則があります。

  1. HILの実装はできる限り一般的なものであるべきです。さまざまなハードウェアを通じて あまりうまく動作しないインターフェースであるとしたら、おそらくそれは間違った インターフェースです。それは、高レベルすぎるか、低レベルすぎるか、あるいは、ただ 柔軟性にかけるものです。一般的に、HILは特定のアプリケーションやハードウェアに 最適になるように設計されるべきではありませんし、特定のアプリケーションとハード ウェアの組み合わせに最適化されるべきではありません。それが本当に必要な場合には、 ドライバをチップやボードに特化させ、HILをまったく使用しないことができます。

    ある便利なインターフェースに関して、ネイティブで提供できるチップもあれば、必要な ハードウェアサポートはないが、何からの方法でその機能をエミュレートできるチップが ある場合もあります。このような場合、TockはHILに「高度な」トレイトを使用します。 これは、HILのすべての実装者がその機能を実装する必要はなく、あるチップでより洗練 された機能を公開することを可能にするものです。たとえば、UART HILにはReceiveAdvanced トレイトがありますが、このトレイトにはバイト中に一時停止が検出されるまでUART上の バイトを受信する特別な関数receive_automatic()があります。この関数はSAM4L ハードウェアでは直接サポートされていますが、タイマとGPIO割り込みを使用して エミュレートすることも可能です。これを高度なトレイトに含めることにより、カプセルは このインターフェースを使用することができますが、必要な機能を持たない他のUART実装は それを実装する必要がありません。

  2. HILの実装は、それがデバイスが使用される唯一の方法であると仮定できます。その結果、 Tockは特定のサービスや抽象化のために複数のHILを持つことを避けようとします。一般に、 カーネルは同じデバイスに対して複数のHILを使用して同時にサポートすることはできない からです。たとえば、わずかに異なるAPIを持つUART用に2つの異なるHILがあるとします。 それぞれのHILに対するチップ固有の実装では、ハードウェアレジスタの読み書きや 割り込み処理を行う必要があるため、同時には存在することはできません。HILがデバイスを 使用する唯一の方法であると仮定することで、将来起きる可能性のある矛盾やユースケースを 心配することなく、TokcはHILのセマンティクスを正確に定義することが可能になります。

スプリットフェーズ操作

Tockではプロセスはタイムスライスされ、プリエンプションされますが、カーネルはそうでは ありません。すべてのプロセスはRun-to-completionです。これは重要な設計上の選択であり、 これによりカーネルが多くのタスクのために多くのスタックを割り当てることを避けることができ、 静的変数やその他の共有変数についてより単純に推論することが可能になるからです。

したがって、Tockカーネル内のすべてのI/O操作は非同期かつノンブロッキングです。メソッド コールは操作を開始して直ちに戻ります。操作が完了すると、操作を実装している構造体が コールバックをコールします。Tockはクロージャではなくコールバックを使用します。通常、 クロージャは動的メモリ割り当てを必要としますが、カーネルはこれを避け、一般にサポート しないからです。

この設計はドライバの作成を複雑にします。一般的に、ブロッキングAPIの方が使いやすいから です。しかし、これは個々のドライバの機能的な正しさ(エラーを誘発しやすいからであり、 正しく書けないからではありません)が発生しやすいからです)よりもカーネルの全体的な安全性 (たとえば、メモリ枯渇の回避や他のコードの実行独占の防止) を優先するための意識的な 選択です 。

カーネルが一時的にブロックできるケースは限られます。たとえば、SAM4LのGPIO コントローラは、操作の間に準備が整うまでに最大5サイクルかかることがあります。技術的 には、完全に非同期のドライバであれば、操作は直ちに返り、処理が完了したらコールバックを 発行するというようにフェーズを分割することができます。しかし、コールバックを設定する だけでも5サイクル以上かかるので、5サイクルスピンすることは単純なだけでなく、コストも 安くなります。そのため、この実装では返る前に数サイクルスピンするようにしてあります。 つまち、操作は同期的です。しかし、これらのケースはまれです。その操作は遅延中に他の コードを実行させる価値がないほど高速でなければなりません。

外部依存なし

Tockはカーネル内のすべてのクレートで外部ライブラリを使用しないことを選択しています。 これは安全性を促進するです。これにより、Tockコードの監査にはTockリポジトリのコード だけを検査すればよくなるからからです。Tockはunsafeの使用を非常に限定的なものに しようとしており、それが使用される際にはその理由を明確にするようにしています。外部に 依存関係を持つと、特に外部ライブラリが進化するにつれて、unsafeの使用が正しいことを 保証することが明らかに困難になります。

しかし、外部ライブラリが非常に有用であることも認識しています。Tockの妥協点は ライブラリの一部をlibrariesフォルダに取り込むことです。これにより、ライブラリの ソースを同じリポジトリに置く一方で、ライブラリを明確に別のクレートして維持することに なります。これを行う頻度は制限しようとしています。

将来的には、cargoやその他のRustツールによって、依存ライブラリの監査や管理が非常に 簡単になることを期待しています。たとえば、今のところ、cargoは依存ライブラリがunsafeを 使用している場合にエラーを発生させる機構を持っていません。依存コードが安全であることを 保証するための新しいツールが登場すれば、Tockは外部依存ライブラリを活用できるように なるでしょう。

unsafeとケイパビリティの使用

Tockはカーネルにおけるunsafeコードの量を最小限にしようとしています。もちろん、 カーネルが行わなければならない操作の中には、Rustのメモリ安全保証を根本的に破る ものが多数あります。これらの操作を区分化し、最終的に安全な方法で使用する方法を説明 しようとしています。

Rustの安全性に違反する操作に対して、Tockは関数、構造体、トレイトにunsafeとして マークします。これはこれらの要素を使用できるクレートを制限します。一般的に、Tockは 安全でない操作がどこで発生しているかを明確にするために、unsafeキーワードを付ける ことを要求しています。たとえば、メモリマップド入出力(MMIO)レジスタにおいて、任意の型を そのレジスタを表す構造体へキャスティングすることは、レジスタマップとアドレスが正しい ことが検証されない限りメモリの安全性を破ります。このことを示すために、キャストを行う ことはunsafeであると明示されています。しかし、キャストが完了すれば、これらの レジスタにアクセスすることはメモリの安全性を破ることはありません。したがって、 レジスタの使用にはunsafeキーワードは必要ありません。

しかし、潜在的に危険なすべてのコードがRustの安全モデルに違反するわけではありません。 たとえば、ボード上で実行中のプロセスを停止させることは言語レベルの安全性には違反 しません。しかし、セキュリティとシステムの信頼性の観点からは問題のある操作である 可能性があります。必ずしもすべてのカーネルコードが任意のプロセスを停止できるように するべきではないからです(特に、信頼できないカプセルはこのAPIに対してこれができる ようにするべきではありません)。これらのタイプの関数へのアクセスを制限する一つの方法は、 unsafe機構を再利用することでしょう。unsafeの使用を制限されているコードが unsafe関数を実行しようとした場合、cargoが警告を発するからです。しかし、これは unsafeの使用を混乱させ、コードが潜在的に安全性に違反しているのか、それとも制限された APIであるかを理解することが困難になります。

Tockではこの代わりに重要なAPIへのアクセスを制限するケーパビリティを 使用します。そのため、カーネル内の公開APIであっても、他のコードがそれらを使用できる ことが非常に制限されているものは、その関数シグネチャに特定のケイパビリティを必要と するべきです。これにより、明示的にケイパビリティを付与されていないコードが保護された APIを呼び出すことを防ぐことができます。

最小特権の原則を促進するために、ケイパビリティは比較的細かく設定されており、特定の APIへのアクセスを狭くしています。これは一般的に新たなAPIは新たなケイパビリティを 定義する必要があることを意味します。

使用と理解の容易さ

可能な限り、Tockの設計は新規ユーザや開発者がTockを理解して利用するための障壁を 低くするように最適化されています。これは、パフォーマンスよりも読みやすさや分かりやすさを 優先した設計を意図的に選択していることもあることを意味します。

例として、Tockは通常、Rustのfeatures#[cfg()]属性を使用した条件付きコンパイルの使用を 避けています。一連のfeaturesの使用はカーネルを構築する際にどのコードを含めるべきかを 正確に最適化することにつながりますが、featuresに慣れていないユーザにとって、いつどの featuresを有効にするかの決定は非常に難しいものなります。おそらく、このようなユーザは デフォルトの設定を使用するので、featuresを利用する利点を減らすことになるでしょう。 また、条件付きコンパイルは、featuresにより実行されているコードが大幅に変わるため、 特定のボードで実行中のカーネルのバージョンを正確に理解することが非常に困難になります。 最後に、デフォルト以外のオプションは、デフォルトの設定ほど十分にテストされている可能性が 低く、利用できないカーネルのバージョンになる可能性があります。

Tockはまた、Tockがユーザにとって「すぐに動く」ことを保証しようとしています。これは、 Tockを動作させるための手順を最小限にしようとしていることからも明らかです。ビルド システムには多くの開発者にお馴染みのmakeを使用しており、ボードフォルダでmakeを 実行するだけでカーネルがコンパイルされます。最もサポートされているボード(Hailと imix)ではmake programを実行するだけでプログラムすることができます。アプリを インストールするにはもう一つのコマンドtockloader install blinkを実行するだけ です。Tockloaderは今後もTockの使いやすさをサポートするために拡張を続けていきます。 現在のところ、Tockの「すぐに動く」という設計目標は完全には達成されていません。 しかし、今後の設計上の決定はTockが「すぐに動く」ことを促進するようにし続ける必要が あります。

実証済みの機能

Tockは明確なユースケースが確立されていない限り、カーネルに機能を追加することは ありません。たとえば、赤黒木の実装をkernel/src/commonに追加することは、将来的に Tockの新機能に役立つかもしれません。しかし、赤黒木を必要とする動機となるような カーネル内部のユースケースがなければそれがマージされることはないでしょう。この一般的な 原則は、プルリクエストの新たな機能を評価するための出発点を提供します。

また、ユースケースを必要とすることで、他の内部カーネルAPIが変更されたときに更新される だけでなく、コードがテストされたり、使用されたりする可能性が高くなります。

積極的にマージ、臆することなくアーカイブ

学術研究をルーツに持つ実験的な組み込みオペレーティングシステムとして、Tockは、新規の、 リスクの高い、実験的な、あるいは焦点の絞られたコードの貢献を受ける可能性が高く、 それらは Tockの長期的な成長に役立つかもしれませんし、そうでないかもしれません。 新しい実験的なコードのために"hold"や"contributions"リポジトリを使うのではなく、 Tockは新しい機能をTockの本流にマージしようとします。これはコードのメンテナンス負担を 軽減し(ツリー外でメンテナンスする必要がない)、機能をより目に見える形にすることが できます。

しかし、すべての機能が受け入れられたり、完成したり、有用性が証明されたりするわけではなく、 コードをTockの本流に置くことが全体的なメンテナンス負担になることもあります。このような 場合、Tockはコードをアーカイブリポジトリに移動します。

Tock Threat Model

Note: This threat model is not descriptive of Tock's current implementation. It describes how we intend Tock to work as of some future release, perhaps 2.0.

Overview

Tock provides hardware-based isolation between processes as well as language-based isolation between kernel capsules.

Tock supports a variety of hardware, including boards defined in the Tock repository and boards defined "out of tree" in a separate repository. Additionally, Tock's installation model may vary between different use cases even when those use cases are based on the same hardware. As a result of Tock's flexibility, the mechanisms it uses to provide isolation — and the strength of that isolation — vary from deployment to deployment.

This threat model describes the isolation provided by Tock as well as the trust model that Tock uses to implement that isolation. Users of Tock, which include board integrators and application developers, should use this threat model to understand what isolation Tock provides to them (and what isolation it may not provide). Tock developers should use this threat model as a guide for how to provide Tock's isolation guarantees.

Definitions

These definitions are shared between the documents in this directory.

A process is a runtime instantiation of an application binary. When an application binary "restarts", its process is terminated and a new process is started using the same binary. Note that the kernel is not considered a process, although it is a thread of execution.

Process data includes a process' binary in non-volatile storage, its memory footprint in RAM, and any data that conceptually belongs to the process that is held by the kernel or other processes. For example, if a process is reading from a UART then the data in the UART buffer is considered the process' data, even when it is stored in a location in RAM only readable by the kernel.

Kernel data includes the kernel's image in non-volatile storage as well as data in RAM that does not conceptually belong to processes. For example, the scheduler's data structures are kernel data.

Capsule data is data that is associated with a particular kernel capsule. This data can be either kernel data or process data, depending on its conceptual owner. For example, an ADC driver's configuration is kernel data, while samples an ADC driver takes on behalf of a process are process data.

Tock's users refers to entities that make use of Tock OS. In the context of threat modelling, this typically refers to board integrators (entities that combine Tock components into an OS to run on a specific piece of hardware) and application developers (who consume Tock's APIs and rely on the OS' guarantees).

Isolation Provided to Processes

Confidentiality: A process' data may not be accessed by other processes or by capsules, unless explicitly permitted by the process. Note that Tock does not generally provide defense against side channel attacks; see the Side Channel Defense heading below for more details. Additionally, Virtualization describes some limitations on isolation for shared resources.

Integrity: Process data may not be modified by other processes or by capsules, except when allowed by the process.

Availability: Processes may not deny service to each other at runtime. As an exception to this rule, some finite resources may be allocated on a first-come-first-served basis. This exception is described in detail in Virtualization.

Isolation Provided to Kernel Code

Confidentiality: Kernel data may not be accessed by processes, except where explicitly permitted by the owning component. Kernel data may not be accessed by capsules, except where explicitly permitted by the owning component. The limitations about side channel defense and Virtualization that apply to process data also apply to kernel data.

Integrity: Processes and capsules may not modify kernel data except through APIs intentionally exposed by the owning code.

Availability: Processes cannot starve the kernel of resources or otherwise perform denial-of-service attacks against the kernel. This does not extend to capsule code; capsule code may deny service to trusted kernel code. As described in Virtualization, kernel APIs should be designed to prevent starvation.

Isolation that Tock does NOT Provide

There are practical limits to the isolation that Tock can provide; this section describes some of those limits.

Side Channel Defense

In general, Tock's users should assume that Tock does NOT provide side channel mitigations except where Tock's documentation indicates side channel mitigations exist.

Tock's answer to "should code X mitigate side channel Y" is generally "no". Many side channels that Tock can mitigate in theory are too expensive for Tock to mitigate in practice. As a result, Tock does not mitigate side channels by default. However, specific Tock components may provide and document their own side channel mitigation. For instance, Tock may provide a cryptography API that implements constant-time operations, and may document the side channel defense in the cryptography API's documentation.

In deciding whether to mitigate a side channel, Tock developers should consider both the cost of mitigating the side channel as well as the value provided by mitigating that side channel. For example:

  1. Tock does not hide a process' CPU usage from other processes. Hiding CPU utilization generally requires making significant performance tradeoffs, and CPU utilization is not a particularly sensitive signal.

  2. Although Tock protects a process' data from unauthorized access, Tock does not hide the size of a process' data regions. Without virtual memory hardware, it is very difficult to hide a process' size, and that size is not particularly sensitive.

  3. It is often practical to build constant-time cryptographic API implementations, and protecting the secrecy of plaintext is valuable. As such, it may make sense for a Tock board to expose a cryptographic API with some side channel defenses.

Guaranteed Launching of Binaries

Tock does not guarantee that binaries it finds are launched as processes. For example, if there is not enough RAM available to launch every binary then the kernel will skip some binaries.

This parallels the "first-come, first-served" resource reservation process described in Virtualization.

Components Trusted to Provide Isolation

The Tock kernel depends on several components (including hardware and software) in order to implement the above isolation guarantees. Some of these components, such as the application loader, may vary depending on Tock's use case. The following documents describe the trust model that exists between the Tock kernel and its security-relevant dependencies:

  • Capsule Isolation describes the coding practices used to isolate capsules from the remainder of the kernel.

  • Application Loader describes the trust placed in the application deployment mechanism.

  • TBF Headers describes the trust model associated with the Tock Binary Format headers.

  • Code Review describes code review practices used to ensure the trustworthiness of Tock's codebase.

What is an "Application"?

Tock does not currently have a precise definition of "application", although there is consensus on the following:

  • Unlike a process, an application persists across reboots and updates. For example, an application binary can be updated without becoming a new application but the update will create a new process.

  • An application consists of at least one application binary (in the Tock Binary Format), although it is unclear whether multiple application binaries can collectively be considered a single application (e.g. if they implement a single piece of functionality).

This section will be updated when we have a more precise definition of "application".

ライフタイム

Tockカーネルの値は、以下の3つの方法で割り当てることができます。

  1. 静的割り当て。静的に割り当てられた値は解放されることはありません。 これらの値はRustでは'staticライフタイムの「借用」であると表現されます。

  2. スタック割り当て。スタック割り当てされた値はレキシカル境界のライフ タイムを持ちます。すなわち、ソースコードを見れば、それがいつ解放されるかがわかります。このような値への参照を作成すると、Rustの型システムは、参照に「ライフタイム」を割り当てることで、値が解放された後ではけっして参照が使用されないことを保証します。

  3. グラント値。プロセスのグラント領域から割り当てられた値は、ランタイム 依存のライフタイムを持ちます。たとえば、それがいつ解放されるかは、プロセスがクラッシュするか否かに依存します。Rustの型システムではランタイム依存のライフタイムを表現できないため、Tockでのグラント値への参照は、参照元が所有する Grant型を介して行われます。

次に、Rust のライフタイムの概念が Tock の値のライフタイムにどのように対応するか、また、これがカーネル内での異なるタイプの値の使用にどのように影響するかについて説明します。

Rustのライフタイム

Rustにおける参照(_借用_と呼ばれる)は、それがどのスコープで有効であるかを決定する その型に関連付けられた_ライフタイム_を持ちます。参照のライフタイムは、借用した値よりも 制限されたものでなければなりません。これにより、コンパイラは参照が有効なスコープから 抜け出せないことを保証します。

その結果、参照を格納するデータ構造体は、その参照の最小ライフタイムを宣言しなければ なりません。たとえば、次のようにします。


#![allow(unused)]
fn main() {
struct Foo<'a> {
  bar: &'a Bar
}
}

これは別の型であるBarへの参照を持つデータ構造体Fooを定義します。この参照は ライフタイム'aを持ち、これはFooの型パラメータです。'aはライフタイムを示す 名前として、ジェネリックList<E>におけるEのように任意に選んだものであることに 注意してください。また、参照が常に永遠に有効でなければならない場合は、その参照を 含む型(たとえばFoo)のライフタイムに関係なく、型パラメータではなく、明示的な ライフタイムである'staticを使用することも可能です。


#![allow(unused)]
fn main() {
struct Foo {
  bar: &'static Bar
}
}

バッファ管理

非同期ハードウェア操作で使用されるバッファは静的でなければなりません。ハードウェアが そのポインタを放棄する前にバッファが解放されないことを(ハードウェアに対して)保証する 必要がありますが、一方で、ハードウェアは、自分があるレキシカル境界の中でしかバッファに アクセスしないこと(なぜならハードウェアを非同期的に使用しているので)を我々(すなわち、 Rustコンパイラ)に伝える方法がありません。これを解決するために、ハードウェアに渡される バッファは静的に割り当てる必要があります。

循環依存

Tockはカプセルが互いにアクセスできるようにするために循環依存を使用します。具体的には、 2つの互いに依存しあうカプセルは、それぞれが他方への参照を含むフィールドを持ちます。 たとえば、タイマーAlarmトレイトのクライアントは、タイマーを開始/停止するために タイマーのインスタンスへの参照を必要とし、タイマーのインスタンスはイベントを伝播する ためにクライアントへの参照を必要とします。これはプラットフォームの定義でオブジェクトの 作成後にオブジェクトへの接続を可能にするset_client関数により処理されます。


#![allow(unused)]
fn main() {
impl Foo<'a> {
  fn set_client(&self, client: &'a Client) {
    self.client.set(client);
  }
}
}

Tockにおける可変参照 - メモリコンテナ(セル)

借用はRustの安全性を保証するためのRust言語の最重要な部分です。しかし、動的なメモリ 割り当てがない(ヒープがない)場合、イベント駆動型のコードはRustの借用セマンティクスに よる困難に直面することになります。多くの場合、複数の構造体は、どのイベントが発生した かに基づいてある構造体を呼び出せる(共有できる)必要があります。たとえば、無線 インターフェイスを表す構造体は使用するバスからのコールバックだけでなく、上位レイアで あるネットワークスタックからのコールも処理する必要があります。これらのコーラーは両者 とも無線構造体の状態を変更できる必要がありますが、Rustの借用チェッカは両者が構造体への 可変参照を持つことを許しません。

この問題を解決するために、Tockは、構造体内部のメモリへの参照が漏れない(内部での 変更がない)限り、構造体が変更可能な2つの参照を持つことは安全であるという観察に基づいて 構築されています。Tockはこの目標を達成するためにメモリコンテナと呼ばれる、可変である ことは許可するが、内部での変更は許さない一連の型を使用します。Rust標準ライブラリには CellRefCellという2つのメモリコンテナ型があります。TockはCellを広範囲に 使用していますが、5つの新しいメモリコンテナ型を追加しています。各々はカーネルコードに 広く見える特有な使用法に合わせたものになっています。

Rustにおける借用の簡単な概要

所有権と借用はRustの設計上の2つの特徴であり、競合状態を防ぎ、ダングリングポインタを 生み出すコードを書くことを不可能にします。

借用はメモリへの参照を可能にするためのRustの機構です。C++などの他言語の参照と同様に、 借用は、構造体全体をコピーするのではなく、ポインタを渡すことで大きな構造体を効率的に 渡すことを可能します。しかし、Rustのコンパイラは借用を制限し、メモリへの同時書き込みや 同時読み書きによって引き起こされる競合状態が発生できないようにしています。Rustでは、 コードを一つの可変(書き込み可能な)参照か、任意の数の読み取り専用参照に制限しています。

コードのある部分がメモリのある部分への可変参照を持っている場合、その他の部分のコードは そのメモリ内で他の参照を持たないことも重要です。そうでなければ言語は安全ではありません。 たとえば、ポインタと値のいずれかを持つことができるenumの場合を考えてみましょう。


#![allow(unused)]
fn main() {
enum NumOrPointer {
  Num(u32),
  Pointer(&'static mut u32)
}
}

Rustのenumは、型安全なCの共用体のようなものです。コードがNumOrPointerへの可変 参照とカプセル化されたPointerへの読み取り専用参照の両者を持っているとします。NumOrPointer参照を持つコードがそれをNumに変更すると、Numには任意の値を設定 することができます。しかし、Pointerへの参照は依然としてポインタとしてメモリに アクセスすることができます。これら2つの表現は同じメモリを使用しますので、これは Numへの参照はそれば望む任意のポインタを作成することができ、Rustの型の安全性を 破ることを意味します。


#![allow(unused)]
fn main() {
// 注意: 不正な例
let external : &mut NumOrPointer;
match external {
  &mut Pointer(ref mut internal) => {
    // これは安全性を破り
    // 0xdeadbeefにあるメモリに書き込む
    *external = Num(0xdeadbeef);
    *internal = 12345;
  },
  ...
}
}

Tockカーネルはシングルスレッドなので競合条件はなく、複数の参照があったとしても (数/ポインタの例のように)内部で互いにポイントしない限り安全な場合があります。しかし、 Rustはこのことを知りませんので、その規則は依然として残ります。実際のところ、Rustの 規則はイベント駆動型のコードで問題を引き起こします。

イベント駆動型コードにおける借用の問題点

イベント駆動型のコードでは同じオブジェクトへの書き込み可能な参照が複数必要になることが よくあります。たとえば、定期的にセンサをサンプリングし、シリアルポートを介してコマンドを 受信するイベント駆動型の組み込みアプリケーションを考えてみましょう。このアプリケーション では、タイマー、センサデータの取得、コマンドの受信といった2つまたは3つのイベント コールバックが登録される可能性があります。各コールバックはカーネル内の異なる コンポーネントに登録され、これらの各コンポーネントはコールバックを発行するために オブジェクトへの参照を必要とします。すなわち、各コールバックのジェネレータは、 アプリケーションへの書き込み可能な独自の参照を必要とします。しかし、Rustの規則は 複数の可変参照を許可しません。

TockにおけるCell

Tockはさまざまなデータ型のためにいくつかのCell型を 使用します。以下の表は様々な型をまとめたもので、以下に詳細を示します。

Cell型最適な用途  一般的な用途
Cellプリミティブ型Cell<bool>,
sched/mod.rs
状態変数(enumを格納), 真偽フラグ, 長さなどの整数パラメタ。
TakeCell小さな静的バッファTakeCell<'static, [u8]>,
spi.rs
データを送受信するための静的バッファを格納する。
MapCell大きな静的バッファMapCell<App>,
spi.rs
参照を大きなバッファ(たとえば、アプリケーションばっふぁ)に委譲する。
OptionalCellオプションパラメタclient: OptionalCell<&'static hil::nonvolatile_storage::
NonvolatileStorageClient>,
nonvolatile_to_pages.rs
セットされる前のクライアントのように、初期化可能な状態を保持する。
VolatileCellレジスタVolatileCell<u32>tock_registersクレートにより使用されるMMIOレジスタをアクセスする。

TakeCell抽象化

個々のメモリコンテナは各々に特化した用途を持っていますが、その操作のほとんどは これらの型の間で共通です。したがって、TakeCellのコンテキストにおけるメモリ コンテナの基本的な使い方を説明し、他の型が追加で持つ機能や特殊な機能については 各自のセクションで説明します。tock/libraries/tock-cell/src/take_cell.rsには次のように書かれています。

TakeCellは可変メモリへの潜在的な参照です。借用規則は、クライアントに メモリをセルの外に移動させるか、クロージャ内で借用操作を行うように強制する ことにより強制されています。

TakeCellは値があっても空でも構いません。nullにすることができる安全なポインタの ようなものです。コードがTakeCellに含まれているデータを操作したい場合は、 TakeCellの外にデータを移動させる(空にする)か、mapコールを使い クロージャ内で操作しなければなりません。mapを使用するにはTakeCellが実行する コードブロックを渡します。クロージャを使用すると制御パスが誤って値を置換しない という危険性なしに、コードがTakeCellの内容をインラインで変更することが可能に なります。しかし、クロージャであるため、TakeCellの内容への参照が漏れることは ありません。

TakeCellはコードが通常の(不変)参照を持っている場合は、その内容を変更する ことを可能にします。これは、構造体がその状態をTakeCellに格納している場合、 構造体への通常の(不変)参照を持つコードはTakeCellの内容を変更することができ、 その結果、構造体を修正することができることを意味します。したがって、複数の コールバックが構造体への参照を持ち、その状態を変更することが可能です。

takereplaceの使用例

TakeCell.take()が呼ばれると、メモリ内のロケーションの所有権はセルの外に 移動します。そして、所有権は誰が取得したとしても自由に使うことができ(所有権を 所有するので)TakeCell.put()TakeCell.replace()を使って元に戻す ことができます。

たとえば、以下のchips/nrf51/src/clock.rsから抽出したコードは、 ハードウェアクロックのコールバッククライアントを設定しています。


#![allow(unused)]
fn main() {
pub fn set_client(&self, client: &'static ClockClient) {
    self.client.replace(client);
}
}

すでにクライアントが存在する場合はclientで置き換えられます。 self.clientが空の場合はclientがセットされます。

以下のChips/sam4l/src/dma.rsから抽出したコードコードは、現在の ダイレクトメモリ操作(DMA)操作をキャンセルし、takeをコールすることで 現在のトランザクションバッファをTakeCellから削除します。


#![allow(unused)]
fn main() {
pub fn abort_transfer(&self) -> Option<&'static mut [u8]> {
    let registers: &DMARegisters = unsafe { &*self.registers };
    registers.interrupt_disable.set(!0);
    // カウンタをリセットする
    registers.transfer_counter.set(0);
    self.buffer.take()
}
}

mapの使用例

TakeCellの内容にはtakereplaceを組み合わせることで直接アクセスする ことができますが、通常、TockのコードはTakeCell.map()を使用します。これは TakeCell.take()TakeCell.replace()の間に提供されたクロージャを ラップします。このアプローチには、正しくreplaceを行わないという制御フロー のバグがあっても、誤ってTakeCellを空にしてしまうことがないという利点が あります。

以下は、chips/sam4l/src/dma.rsから取得したmapの簡単な使用法を 示しています。


#![allow(unused)]
fn main() {
pub fn disable(&self) {
    let registers: &SpiRegisters = unsafe { &*self.registers };

    self.dma_read.map(|read| read.disable());
    self.dma_write.map(|write| write.disable());
    registers.cr.set(0b10);
}
}

dma_readdma_writeはどちらもTakeCell<&'static mut DMAChannel> 型、すなわち、DMAチャンネルへの可変参照のためのTakeCellです。mapを呼び出す ことにより、この関数は参照にアクセスしてdisable関数を呼び出すことが できます。TakeCellが参照を持たない(空である)場合、mapは何もしません。

以下は、chips/sam4l/src/spi.rsから取得したmapのより複雑な使用例を 示しています。


#![allow(unused)]
fn main() {
self.client.map(|cb| {
    txbuf.map(|txbuf| {
        cb.read_write_done(txbuf, rxbuf, len);
    });
});
}

この例では、clientTakeCell<&'static SpiMasterClient>です。 mapに渡されるクロージャは引数を一つ持ち、その値はTakeCellが持つ値です。 したがって、この場合、cbSpiMasterClientへの参照です。client.map に渡されるクロージャはそれ自体がcbを使用してtxbufを渡してコールバックを 呼び出すクロージャを含んでいることに注意してください。

mapの変異

TakeCell.map()TakeCellに格納された内容を利用するための便利な メソッドを提供しますが、単にクロージャを実行しないことでTakeCellが 空であることを隠します。TakeCellが空の場合でも処理を可能にするために、 rust (とその延長であるTock)は追加の関数を提供しています。

最初の関数は.map_or()です。これはTakeCellが空であっても、値を持って いても値を返す場合に便利です。たとえば、次のような場合、


#![allow(unused)]
fn main() {
let return = if txbuf.is_some() {
    txbuf.map(|txbuf| {
        write_done(txbuf);
    });
    ReturnCode::SUCCESS
} else {
    ReturnCode::ERESERVE
};
}

.map_or()を使えば次のように書くことができます。


#![allow(unused)]
fn main() {
let return = txbuf.map_or(ReturnCode::ERESERVE, |txbuf| {
    write_done(txbuf);
    ReturnCode::SUCCESS
});
}

TakeCellが空の場合、第1引数(エラーコード)が返され、そうでない場合は クロージャが実行されSUCCESSが返されます。

TakeCellが空か否により異なるコードを実行したい場合もあります。繰り返しに なりますが、次のように書くことができます。


#![allow(unused)]
fn main() {
if txbuf.is_some() {
    txbuf.map(|txbuf| {
        write_done(txbuf);
    });
} else {
    write_done_failure();
};
}

しかし、代わりに.map_or_else()関数を使うことができます。これには2つの クロージャを渡すことができます。1つはTakeCellが空の場合、もう1つは データがある場合に使われます。


#![allow(unused)]
fn main() {
txbuf.map_or_else(|| {
    write_done_failure();
}, |txbuf| {
    write_done(txbuf);
});
}

.map_or()の場合も.map_or_else()の場合も、最初の引数はTakeCellが 空の場合に対応することに注意してください。

MapCell

MapCellはその目的とインターフェースがTakeCellに非常に似ています。 異なるのはその背後にある実装です。TakeCellでは、何かがセルの内容をtake() すると、実際にセル内のメモリが移動します。これは、TakeCell内のデータが 大きい場合はパフォーマンス上の問題になりますが、データが小さい場合(ポインタや スライスなど)はサイクルとメモリの双方が節約できます。内部のOptionは多くの 場合で最適化することができ、コードはメモリに対してではなくレジスタ上で動作する からです。一方、MapCellsは小さな型にいくらかのアカウンティングオーバー ヘッドをもたらし、アクセスするための最小サイクル数を必要とします。

MapCellを導入したコミットにはパフォーマンスベンチマークが 含まれていますが、正確なパフォーマンスは使用場面により異なります。一般的に 言えば、中型から大型のバッファはMapCellがふさわしいはずです。

OptionalCell

OptionalCellは 実質上、Cell<Option<T>>などのOptionを含むCellのラッパーです。 これはある程度TakeCellのインターフェイスを反映していますが、ここでは、 Optionが利用者からは隠されています。そのため、my_optional_cell.get().map(|| {})ではなく、my_optional_cell.map(|| {})のように書くことが できます。

OptionalCellCellが保持できるものと同じ値を保持することができますが、 値が事実上設定されていない場合は、単にNoneとすることもできます。 OptionalCellを使用すると(NumCellのように)コードがより明確になり、 余分で面倒な関数呼び出しを隠すことができます。

VolatileCell

VolatileCellは、値をvolatileに読み書きするためのヘルパー型です。 これは主にメモリマップドI/Oレジスタへのアクセスに使用されます。get()関数と set()関数は、各々core::ptr::read_volatile()core::ptr::write_volatile()のラッパーです。

Cellの拡張機能

Tockでは、カスタム型に加えて、ユーザビリティの拡大と簡易化のために標準のCellの 一部に拡張機能を追加しています。その仕組みは、 既存のデータ型にトレイトを追加して機能を向上させることです。拡張機能を使うには、 use kernel::common::cell::THE_EXTENSIONとして、新しいトレイトを スコープに入れるだけです。

NumericCellExt

NumericCellExtは (usizei32などの)「数値」型を含むcellを拡張し(add()subtract()などの)便利な関数を提供するものです。この拡張により、増減する 数値を格納する際のコードがより明確なものになります。たとえば、通常のCell では格納された値に1を追加するコードは、my_cell.set(my_cell.get() + 1) のようになりますが、NumericCellExtの場合は少し理解しやすいmy_cell.increment()(またはmy_cell.add(1))のようになります。

安全性とUnsafeの問題

オペレーティングシステムはどうしても安全でないコードを使用しなければなりません。 このドキュメントでは、安全でないコードを使用するが、OS全体の安全性を維持する 必要があるTockにおける主たる機構を支える根拠を説明します。

static_init!

static_init!の「型」は基本的には次のとおりです。


#![allow(unused)]
fn main() {
T => (fn() -> T) -> &'static mut T
}

つまり、T型の何かを返す関数が与えられた場合、static_init!はstatic ライフタイムを持つTへの可変参照を返します。

実質的には、これは可変静的変数の宣言と同じことを意味します。


#![allow(unused)]
fn main() {
static mut MY_VAR: SomeT = SomeT::const_constructor();
}

そして、それへの参照の作成は次のようになります。


#![allow(unused)]
fn main() {
let my_ref: &'static mut = &mut MY_VAR;
}

しかし、static宣言の左辺値はconstでなければなりません(Rustには pre-initializationセクションがないため)。そのため、static_init!は 基本的にconstではない初期化子を持つ静的変数を許すものです。

これらのケースではどちらも呼び出し元は関数をunsafeでラップしなければ ならないことに注意してください。可変変数の参照は(エイリアス規則により) 安全ではないからです。

使用

static_init!はTockではカプセルの初期化に使用され、最終的には互いに参照し 合うことになります。いずれの場合もこれらの参照は不変です。これらを静的に 割り当てることは2つの理由で重要です。第一に、リンク時のメモリ逼迫の問題を 表面化するのに役立つからです(これらがスタックに割り当てられた場合、スタック サイズが適切でなければ、メモリ不足リンクエラーとして簡単に示さることはない でしょう)。第二に、相互に依存するカプセルのライフタイムは同じである必要が あり、'staticはこれを実現する便利な方法だからです。

しかし、_誰が_特定のコールを行えるかを強制するために可変参照で始めることが 有用な場合があります。たとえば、SPIドライバにおけるバッファの設定は実用的な 理由から構築後まで延期されますが、プラットフォームの初期化関数だけが (main関数が起動する前に)呼び出せるようにしたい場合です。プラットフォーム 設定後のすべての参照は不変であり、config_buffersメソッドは引数に &mut selfを取るので、これは強制されています(注意: これは厳密には必要ではないように見えるので、これができなくても大したことはないかもしれません)。

安全性

static_init!の使用が安全でないものになるのは、可変参照へのエイリアスの 作成に使用された場合です。これが&'static mutを返すという事実が赤旗です ので、これが何故OKだと考えるかを説明する必要があります。

他の&mutと同じように、それは再借用されるとすぐに使えなくなります。具体的に Tockで行っていることは、あるケースにおいてstatic_init!を呼び出した直後に それを可変的に使用し、次にそれを不変的に再借用してカプセルに渡すことです。 あるカプセルが&mutを受け入れた場合、コンパイラは参照を移動しようとし、 その呼び出しが失敗する(すでに他の場所で不変的に再借用されている場合)か、 それ以上の再借用ができなくなります。これは実際に共有参照として使用されない 場合は問題ないことに注意してください(もっとも、そのような使用例はないと 思いますが)。

ただし、static_init!を呼び出すコードが二度実行されないことが重要です。 これは2つの大きな問題を引き起こします。第一に、技術的には複数の可変参照が 発生する可能性があります。第二に、コンストラクタを二度実行することになり、 同一メモリへの複数の参照に関するその他の安全性や機能的な問題が発生する 可能性があります。これは、静的変数への可変参照を取るコードと変わらないと 思います。繰り返しになりますが、重要なのはどちらの場合もunsafeでラップ しなければならないことです。

代替案

static_init!から代わりに不変静的参照を返すことは技術的には可能だと 思われます。それにはコードを少し変更する必要があり、初期化を特定のカプセル メソッドに制限することはできませんが、特に大きな問題ではないかもしれません。

また、Option型の何らかの静的変数をどこでも使えるようにする(これも 合理的かもしれません)。

ケイパビリティ: 特定の機能や操作へのアクセス制限

ある種の操作や関数、特にカーネルクレート内のものは、言語的に見れば「安全では ない」ことはありませんが、隔離やシステム操作の観点からは安全ではありません。 たとえば、プロセスの再起動は概念的には型やメモリの安全を破るものではありません が(Tockにおけるある実装では破りますが)、カーネル内の任意のコードが任意の プロセスを再起動できるとしたらシステム全体の安全性を損ねます。したがって、 Tockはrestart_process()のような関数の提供方法に注意しなければなりません。 特に、Rustによってサンドボックス化されるべき信頼できないコードであるカプセルが restart_process()関数にアクセスできるようにしてはいけません。

さいわい、Rustはこの制限を行うためのプリミティブを提供しています。unsafe キーワードの使用です。unsafeとマークされたすべての関数は、他のunsafe 関数かunsafeブロックからしか呼び出すことができません。したがって、クレートで #![forbid(unsafe_code)]属性を使用することにより、unsafeブロックを 定義する機能を削除することによりそのクレートのすべてのモジュールはunsafeと マークされたすべての関数を呼び出すことができなくなります。Tockではカプセル クレートにはこの属性を付けています。そのため、すべてのカプセルはunsafe関数 を使用できません。このアプローチは効果的ですが、すべてのunsafe関数への アクセスを提供するか、全く提供しないかという、非常に粒度の粗いものです。より 微妙な制御を提供するために、Tockはケイパビリティ(Capabilities)と 呼ばれる機構を持っています。

ケイパビリティとは、本質的には、特定の関数を呼び出すために必要なゼロメモリ オブジェクトのことです。抽象的には、restart_process()のような制約のある 関数は呼び出し元が特定のケイパビリティを持っている必要があります。

restart_process(process_id: usize, capability: ProcessRestartCapability) {}

ケイパビリティを持たずにその関数を呼び出そうとするとコンパイルできないコードに なります。ケイパビリティの不正な使用を防ぐために、ケイパビリティは信頼された コードでしか作成することができません。Tockではunsafeなトレイトとして ケイパビリティを定義することでこれを実装しています。これはunsafeな呼び出し ができるコードによるオブジェクトにしか実装することができません。そのため、 信頼できないカプセルクレートのコードはそれ自身ではケイパビリティを生成する ことができませんので、代わりに別のクレートのモジュールからケイパビリティを 渡さなければなりません。

ケイパビリティは、非常に広範な目的のためにも、非常に狭い目的のためにも定義する こともでき、コードは複数のケイパビリティを「要求」することができます。Tock では、1つのオブジェクトに複数のケイパビリティトレイトを実装することにより 複数のケイパビリティを渡すことができます。

ケイパビリティの例

  1. Tockにおいてケイパビリティがいかに有用であるかの一例として、プロセスの ロードがあります。プロセスのロードはボードの責任として残されています。 なぜなら、ボードは何らかの方法でプロセスを処理するか、ユーザランドプロセスを 全くサポートしないかを選択できるからです。しかし、カーネルクレートはプロセスを発見してロードするTockの標準的方法を提供するload_processes()という 便利な関数を提供しています。この関数はカーネルクレートで定義されているので すべてのTockボードが共有できます。これは関数をpublicにするよう強制します。 これは、カーネルクレートにアクセスできる_すべての_モジュールが load_processes() を呼び出すことができるという効果があります。ただし、 これを二回呼び出すと望まない結果を起こす可能性があります。一つの方法は、 関数をunsafeとマークし、信頼できるコードしかそれを呼び出せないように することです。これは効果的ですが、明示的ではなく、言語レベルの安全性と システムの操作レベルの安全性を混同してしまいます。代わりにload_processes()の呼び出し元が何らかのケイパビリティを持つことを要求することにより、 呼び出し元の期待がより明確になり、unsafeな関数を別の目的で利用する必要が なくなります。

  2. 同様の例として、restart_all_processes()のような関数があります。 この関数は、ボード上のすべてのプロセスをフォルト状態にし、すべてのグラントを 削除した上で元の_start地点から再起動します。繰り返しになりますが、これは システムレベルの目標に違反する可能性がある関数ですが、特定の状況やアプリが 失敗した際にグラントのクリーンアップをデバッグする用途には非常に役に立ちます。 しかし、load_processes()とは異なり、特定のイベントに反応したり、 ウォッチドッグとして動作させるためにカプセルがrestart_all_processes() を呼び出せるようにすることは理にかなっているかもしれません。その場合、 unsafeであるとマークしてアクセスを制限してもうまくいきません。カプセルは unsafeなコードを呼び出せないからです。ケイパビリティを使用することで、 正しいケイパビリティを持つ呼び出し元だけがrestart_all_processes()を 呼び出すことができ、個々のボードがどのカプセルにどのケイパビリティを付与する かについて非常に明確にすることができます。

どのようにTockをコンパイルするか

Tockには2種類のコンパイル生成物があります。カーネルとユーザレベルのプロセス (アプリ)です。両者は別々にコンパイルします。さらに、プラットフォーム毎に カーネルとプロセスのプログラミング方法が異なります。以下では、カーネルとプロセスの コンパイルを説明し、実際のボードに各プラットフォームをプログラムする方法の例を 示します。

カーネルのコンパイル

カーネルは5つのRustクレート(パッケージ)に分類できます。

  • コアカーネルクレート。これには、割り込みの処理やプロセスのスケジューリングなどの 主要なカーネル操作、TakeCellなどの共有カーネルライブラリ、HIL(Hardware Interface Layer)の定義を含みます。kernel/フォルダにあります。

  • アーキテクチャ(_ARM Cortex M4_など)クレート。コンテキストスイッチングを実装し、 メモリ保護とsystickドライバを提供します。arch/フォルダにあります。

  • チップ固有(_Atmel SAM4L_など)のクレート。割り込みを処理し、チップのペリフェラル 用のハードウェア抽象化レイヤを実装します。chips/フォルダにあります。

  • ハードウェアに依存しないドライバと仮想化レイヤのための1つ(または複数)のクレート。 capsules/フォルダにあります。Tockを使用する外部プロジェクトは各自のドライバ用に 追加のクレートを作成することができます。

  • プラットフォーム固有(_Imix_など)のクレート。チップとそのペリフェラルを設定し、 ドライバにペリフェラルを割り当て、仮想化レイヤを設定し、システムコールインタフェースを 定義します。boards/にあります。

これらのクレートはプラットフォームのクレートを依存関係グラフのベースとして、Rustの パッケージマネージャであるCargoを使ってコンパイルされます。 実際には、Cargoの使用はTockのMakefileシステムにより隠蔽されます。ユーザはboards/ の適切なディレクトリでmakeと入力するだけで、そのプラットフォーム用のカーネルをビルド できます。

内部的には、Makefileは単にビルドを処理するCargoを呼び出しているだけです。たとえば、 imixプラットフォームでのmakeは次のように変換されます。

$ cargo build --release --target=thumbv7em-none-eabi

--release引数は、最適化を有効にしてRustコンパイラを起動するようにCargoに指示します。 --targetは、コンパイラ用のLLVMデータレイアウト定義やアーキテクチャ定義を含むターゲット 仕様をCargoに指定します。

Tockコンパイルのライフ

Cargoがプラットフォームクレートのコンパイルを開始すると、まず、すべての依存関係を 再帰的に解決します。依存関係グラフにわたって要件を満たすパッケージバージョンを選択します。 依存関係は各クレートのCargo.tomlファイルで定義されており、ローカルファイルシステム、 リモートのgitリポジトリ、crates.ioで公開されているパッケージの パスを参照します。

依存関係が満たされると、Cargoは次に各クレイトを順番にコンパイルしていきます。 各クレートはrlib(オブジェクトファイルを含むarアーカイブ)としてコンパイルされ、 プラットフォームクレートのコンパイルによる実行可能なELFファイルに結合されます。

--verbose引数を渡すことによりCargoが実行する各コマンドを見ることができます。 Tockのビルドシステムではmake V=1と実行することで冗長コマンドを見ることができます。

LLVM Binutils

TockはRustツールチェーンに含まれているlldobjcopy sizeの各ツールを使用して、マイクロコントローラ上で実行されるカーネルバイナリを生成します。これには主に3つの意味が あります。

  1. このツールはGNUバージョンとは完全な機能互換性があるわけではありません。両者は 非常に似ていますが、全く同じ動作をしないエッジケースがあります。これは時間が経てば 改善されるでしょうが、予期せぬ問題が発生した場合に備えて注意しておく価値があります。

  2. このツールはRustのバージョンに合わせて自動的に更新されます。このツールはRust ツールチェーンのすべてのバージョンでコンパイルされて出荷されるrustup コンポーネントであるllvm-toolsで提供されます。したがって、RustがRust リポジトリで使用しているバージョンを更新した場合、Tockもその更新を使うことに なります。

  3. Tockはこれらのツールを提供する外部依存関係を使用しなくなりました。これにより、 すべてのTock開発者が同じバージョンのツールを使用することが保証されるはずです。

特別な.appsセクション

Tockカーネルは、アプリケーションがロードされるものと同じ物理アドレスにある.apps セクションをカーネルの.elfファイルの中に含んでいます。カーネルをコンパイルする際、 これは単なるプレースホルダであり、意味のあるデータは置かれません。これは、カーネルと アプリを一緒にフラッシュできるように、カーネル.elfファイルとアプリケーション バイナリをモノリシックな.elfファイルとして簡単に更新できるようにするために 存在しています。

Tockビルドシステムがカーネルバイナリを作成する際、このセクションを明示的に削除し、 プレースホルダがカーネルバイナリに含まれないようにします。

特別な.appsセクションを使用するためにobjcopyでプレースホルダを実際のアプリ バイナリに置き換えることができます。一般的なコマンドは次のようになります。

$ arm-none-eabi-objcopy --update-section .apps=libtock-c/examples/c_hello/build/cortex-m4/cortex-m4.tbf target/thumbv7em-none-eabi/release/stm32f412gdiscovery.elf target/thumbv7em-none-eabi/release/stm32f4discovery-app.elf

これは、カーネルELFであるstm32f412gdiscovery.elf内のプレースホルダセクション .appsを"c_hello"アプリケーションTBFに置き換え、stm32f4discovery-app.elfと いう名前の新しい.elfを作成します。

プロセスのコンパイル

他の多くの組み込みシステムとは異なり、Tockではアプリケーションコードのコンパイルは カーネルのコンパイルとは完全に分離されています。アプリケーションは少なくとも2つの ライブラリlibtocknewlibとともにコンパイルされ、独立したバイナリが構築されます。 このバイナリはTockプラットフォーム上にアップロードされ、ロードされてすでに存在する カーネルとともに実行することができます。

Tockは次の要件を満たす任意のプログラミング言語とコンパイラをサポートしています。

  1. アプリケーションは、位置独立なコード(PIC)としてビルドされていること。

  2. アプリケーションは、Flashコンテンツを0x80000000より上のアドレスに、RAM コンテンツをそれより下に配置するローダスクリプトでリンクされていること。

  3. アプリケーションのバイナリは、バイナリ内のセクション位置を詳細に記述したヘッダ から開始していること。

第一要件はこの直後に説明しますが、他の2つの要件はTockバイナリフォーマットで 詳しく説明します。

位置独立なコード

Tockはカーネルとは別にアプリケーションをロードし、複数のアプリケーションを同時に実行する ことができるので、アプリケーションは事前にどのアドレスにロードされるかを知ることが できません。この問題は多くのコンピュータシステムに共通しており、通常は、実行時に動的に コードをリンク・ロードすることで対処しています。

しかし、Tockはこれとは異なる選択をしており、位置独立なコードとしてコンパイルされることを アプリケーションに要求しています。PICでコンパイルすると、指定された絶対アドレスへの ジャンプは使用せず、すべての制御フローが現在のPC相対になります。すべてのデータアクセスは そのアプリのデータセグメントの開始アドレス相対になり、データセグメントのアドレスは base registerと呼ばれるレジスタに格納されます。これにより、FlashとRAM内の セグメントを任意の場所に配置することができ、OSはベースレジスタを正しく初期化するだけで 済みます。

PICコードはx86のようなアーキテクチャでは効率が悪い場合がありますが、ARM命令セットは PIC操作に最適化されており、ほとんどのコードをほぼオーバーヘッドなしで実行することが できます。PICの使用には実行時に多少の修正が必要ですが、再配置は容易であり、 アプリケーションがロードされる際に一度コストがかかるだけです。アプリケーションの動的 ローディングに関するより詳細な議論はTockのウェブサイト: Dynamic Code Loading on a MCUで見ることができます。

アプリケーションをarm-none-eabi-gccでコンパイルする際、Tockが必要とするPICコードを ビルドするには次の4つのフラグが必要です。

  • -fPIC: 相対アドレスを使用するコードのみを出力します。
  • -msingle-pic-base: データセクションに一貫して_ベースレジスタ_を使用するよう強制します。
  • -mpic-register=r9: ベースレジスタとしてr9レジスタを使用します。
  • -mno-pic-data-is-text-relative: データセグメントがテキストセグメントから一定のオフセットに配置されていると仮定しません。

Tockアプリケーションは、Flashをアドレス0x80000000に、SRAMをアドレス 0x00000000に配置するリンカスクリプトを使用します。これにより、Flashを指し示す 再配置とRAMを指し示す再配置を簡単に区別することができます。

Tockバイナリフォーマット

アプリケーションを正しく読み込むために、アプリケーションはTockバイナリフォーマットに 従う必要があります。これはTockがアプリケーションを正しくロードできるようにTockアプリの 先頭バイトがこのフォーマットに従わなければならないことを意味します。

実際にはこれはアプリケーションに対して自動的に処理されます。コンパイルプロセスの一貫として Elf to TABと呼ばれるツールが、ELFからTockが 期待するバイナリフォーマットへの変換を行い、セクションが期待する順序で配置されることを 保証し、ロード時に必要な再配置を列挙するセクションを追加し、TBFヘッダを作成します。

Tockアプリケーションバンドル

使い易く配布可能なアプリケーションをサポートするために、Tockアプリケーションは複数の アーキテクチャ用にコンパイルされ、"Tock Application Bundle"として.tab ファイルにまとめられます。これにより、Tockをサポートしている任意のボードにフラッシュ できるアプリケーション用のスタンドアロンのファイルが作成され、アプリケーションの コンパイル時にボードを指定する必要がなくなります。TABにはほとんどすべてのTock 互換ボードにフラッシュするために必要な情報が含まれており、コンパイル時ではなく、 アプリケーションがフラッシュされる際に正しいバイナリが選択されます。

TABフォーマット

.tabファイルは、TBF互換のバイナリとアプリケーションに関する追加情報を含む metadata.tomlファイルからなるtarアーカイブです。.tabファイルを作成する 簡単なコマンド例は次のとおりです。

tar cf app.tab cortex-m0.bin cortex-m4.bin metadata.toml

メタデータ

.tabファイル内のmetadata.tomlファイルは、1行に1つのキーと値の組を含む 一連のTOMLファイルであり、より詳細な情報を提供し、アプリケーションをフラッシュする際に 役立ちます。既存のフィールドは次のとおりです。

tab-version = 1                         // TABファイルフォーマットのバージョン
name = "<package name>"                 // アプリケーションのパッケージ名
only-for-boards = <list of boards>      // このアプリケーションがサポートするボードカーネルリスト(オプション)
build-date = 2017-03-20T19:37:11Z       // アプリケーションのコンパイル日時

カーネルとプロセスのボードへのロード

ボードにコードをロードする方法には特に制限はありません。JTAGと様々なブートローダが 全て等しく可能です。たとえば、Hailimixプラットフォームは主にシリアルの "tock-bootloader"を使用し、他のプラットフォームはjlinkopenocdを使って JTAG接続でコードをフラッシュします。一般に、これらの方法はそのプラットフォームの ユーザにとって何が最も簡単であるかに基づいて変更される可能性があります。

複数のアプリケーションを同時に使用するための最も簡単な選択肢はtockloadergit repo)を使用して プラットフォーム上で複数のアプリケーションを管理することです。重要なことは、 現在、アプリケーションはカーネルと同じアップロードプロセスを共有していますが、 将来的には別の方法がサポートされる予定があることです。特に、ワイヤレスによる アプリケーションのロードがTockの将来のエディションの目標になっています。

Tock Binary Format

Tockプロセスバイナリは、TBF (Tock Binary Format) でなければなりません。TBFは、プロセスに 関するメタデータをエンコードするヘッダ部分と直接実行されるバイナリブロブ、オプションのパディングから なります。

Tock App Binary:

Start of app -> +-------------------+
                | TBF Header        |
                +-------------------+
                | Compiled app      |
                | binary            |
                |                   |
                |                   |
                +-------------------+
                | Optional padding  |
                +-------------------+

ヘッダは、アプリの重要な側面を理解するためカーネル(および tockloaderなどのツール)により解釈されます。 特に、カーネルは、アプリを初めて実行する際に実行を開始すべきエントリーポイントがアプリケーションバイナリの どこにあるのかを知る必要があります。

ヘッダの後は、アプリが望むバイナリデータを自由に含めることができ、そのフォーマットは完全にアプリ次第です。 たとえば、再配置のためのすべてのサポートはアプリ自体によって処理されなければなりません。

最後に、アプリバイナリは特定の長さにパディングすることができます。これは、長さと始点が2の累乗でなければ ならないというMPUの制限のために必要です。

アプリの連結リスト

Tockのアプリは、フラッシュ内で実質的な連結リスト構造を作成します。つまり、次のアプリの開始点は、前の アプリの終了時点のすぐ後にあります。したがって、カーネルが次のアプリの開始を見つけることができるように、 TBFヘッダはアプリの長さを指定しなければなりません。

アプリ間にギャップがある場合は、連結リスト構造をそのまま維持するために「空のアプリ」を挿入することができます。

また、機能的には、Tockアプリはサイズの長いものから短いものへとソートされます。これは、MPUのアライメントに 関する規則に一致します。

空のTockアプリ

「アプリ」はコードを含む必要はありません。アプリは無効であるとマーク付けられ、実質的にはアプリ間の パディングとして機能します。

TBFヘッダー

TBFヘッダのフィールドは以下の通りである。ヘッダのすべてのフィールドはリトルエンディアンです。


#![allow(unused)]
fn main() {
struct TbfHeader {
    version: u16,            // Version of the Tock Binary Formatのバージョン(現在は2)
    header_size: u16,        // TBFヘッダの総バイト数
    total_size: u32,         // プログラムイメージのヘッダを含むバイトサイズ
    flags: u32,              // このアプリケーションに関連するさまざまなフラグ
    checksum: u32,           // 既存のオプションの構造体を含むヘッダの全4バイトワードのXOR

    // オプションの構造体。すべてのオプション構造は4バイト境界から開始する。
    main: Option<TbfHeaderMain>,
    pic_options: Option<TbfHeaderPicOption1Fields>,
    name: Option<TbfHeaderPackageName>,
    flash_regions: Option<TbfHeaderWriteableFlashRegions>,
}

// オプションのヘッダ構造体用の識別子。
enum TbfHeaderTypes {
    TbfHeaderMain = 1,
    TbfHeaderWriteableFlashRegions = 2,
    TbfHeaderPackageName = 3,
    TbfHeaderPicOption1 = 4,
    TbfHeaderFixedAddresses = 5,
}

// 各構造体を識別する型-長さ-値ヘッダ。
struct TbfHeaderTlv {
    tipe: TbfHeaderTypes,    // どの構造体に従うかを示す16ビットの識別子。
                             // 16ビット識別子の最上位ビットがセットされた場合は、
                             // ツリー外(プライベート)のTLVエントリを示す。
    length: u16,             // 続く構造体のバイト数
}

// すべてのアプリに必要な主な設定。これが存在しない場合、「アプリ」はパディングとみなされ、
// 空の連結リスト要素をアプリのフラッシュスペースに挿入するために使用されます。
struct TbfHeaderMain {
    base: TbfHeaderTlv,
    init_fn_offset: u32,     // アプリケーションを開始するためにコールする関数
    protected_size: u32,     // アプリケーションが書き込みできないバイト数
    minimum_ram_size: u32,   // アプリケーションが必要とするRAMの量
}

// アプリのパッケージ名(オプション)
struct TbfHeaderPackageName {
    base: TbfHeaderTlv,
    package_name: [u8],      // アプリケーション名のUTF-8文字列
}

// アプリのフラッシュ空間内の定義されたフラッシュ領域
struct TbfHeaderWriteableFlashRegion {
    writeable_flash_region_offset: u32,
    writeable_flash_region_size: u32,
}

//アプリが書き込みを意図している1以上の特に識別されたフラッシュ領域
struct TbfHeaderWriteableFlashRegions {
    base: TbfHeaderTlv,
    writeable_flash_regions: [TbfHeaderWriteableFlashRegion],
}

// RAMの処理およびフラッshの処理に必要な固定の必要なアドレス
struct TbfHeaderV2FixedAddresses {
    start_process_ram: u32,
    start_process_flash: u32,
}
}

すべてのヘッダは4バイトの倍数であり、すべてのTLV構造体は4バイトの倍数でなければならないので、 TBFヘッダ全体は常に4バイトの倍数になります。

TBFヘッダのベース

TBFヘッダは、ベースヘッダとそれに続く型-長さ-値がエンコードされた要素シーケンスを含む。ベースヘッダと TLV要素のすべてのフィールドはリトルエンディアンである。ベースヘッダは16バイトで、5つのフィールドを持つ。

0             2             4             6             8
+-------------+-------------+---------------------------+
| Version     | Header Size | Total Size                |
+-------------+-------------+---------------------------+
| Flags                     | Checksum                  |
+---------------------------+---------------------------+
  • Version TBFヘッダバージョンを示す16ビットの符号なし整数。常に2

  • Header Size TBFヘッダ全体(ベースヘッダとすべてのTLV要素を含む)の長さをバイト単位で示す 16ビットの符号なし整数。

  • Total Size TBF全体(ヘッダを含む)のサイズをバイト単位で示す32ビットの符号なし整数。

  • Flags プロセスの属性を指定する。

       3                   2                   1                   0
     1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    | Reserved                                                  |S|E|
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    
    • Bit 0 はプロセスが有効であるか否かを示す。1はプロセスが有効であることを示す。 有効でないプロセスは起動時に実行されない。
    • Bit 1 はプロセスがスティッキーであるか否かを示す。1はプロセスがスティッキーであることをindicates the process is 示す。スティッキープロセスは削除時に追加の確認が行われる。たとえば、tockloaderでは 削除するのに--forceフラグの指定が必要になる。これは常に利用可能であるべきプロセスとして 実行するサービスに有用である。
    • Bits 2-31 はリザーブで0をセットする必要がある。
  • Checksum は、チェックサムフィールドを含むワードを除いたヘッダーの各4倍とワードをXORした結果で ある。

TLV要素

ヘッダの後には直接、TLV要素のシーケンスが続きます。TLV要素は4バイトにアラインされます。TLV要素のサイズが 4バイトアラインでない場合は、最大3バイトのパディングが行われます。各要素は、16ビットの型と16 ビットの長さで 始まり、要素データが続きます。

0             2             4
+-------------+-------------+-----...---+
| Type        | Length      | Data      |
+-------------+-------------+-----...---+
  • Type 要素の型を示す16ビットの符号なし整数。
  • Length データフィードの長さをバイト単位で示す16ビットの符号なし整数。
  • Data 要素固有のデータ。dataフィールドの形式はtypeにより決定される。

TLVの型

TBFは任意の要素型を含むことができます。Tockプロジェクトで定義された要素と外部で定義された要素の間で 型IDが衝突しないように、ID空間は2つのセグメントに分割されています。Tockプロジェクトで定義された型IDは 最上位ビット(ビット15)がアンセットされ、外部で定義された型IDは最上位ビットがセットされている必要が あります。

1 Main

Main要素は3つの32ビットフィールを持ちます。

0             2             4             6             8
+-------------+-------------+---------------------------+
| Type (1)    | Length (12) | init_offset               |
+-------------+-------------+---------------------------+
| protected_size            | min_ram_size              |
+---------------------------+---------------------------+
  • init_offset 最初の実行命令(通常は_startシンボル)を含むバイナリペイロード(すなわち、 実際のアプリケーションバイナリ)の開始点からのバイト単位のオフセット値。
  • protected_size プロセスが書き込めないようにするヘッダ後のバイト単位のフラッシュの量。
  • minimum_ram_size プロセスが必要とするバイト単位のメモリの最小量。

Main TLVヘッダが存在しない場合、これらの値はすべてのデフォルト値の0となる。

2 Writeable flash regions(書き込み可能なフラッシュ領域)

Writeable flash regionsは、フラッシュ内のプロセスが変更を行うバイナリの領域を示します。

0             2             4             6             8
+-------------+-------------+---------------------------+
| Type (2)    | Length (8)  | offset                    |
+-------------+-------------+-------------+-------------+
| size                      |
+---------------------------+
  • offset 書き込み可能領域のバイナリ開始時点からのオフセット値。
  • size 書き込み可能領域のサイズ。

3 Package Name(パッケージ名)

Package nameは、バイナリのユニークな名前を指定します。唯一のフィールドはUTF−8エンコーディングの パッケージ名です。

0             2             4
+-------------+-------------+----------...-+
| Type (3)    |   Length    | package_name |
+-------------+-------------+----------...-+
  • package_nameはUTF−8エンコーディングのパッケージ名です。

5 Fixed Addresses(固定アドレス)

Fixed Addressesは、プロセスがフラッシュやRAMに必要な特定のアドレスを指定することを可能にします。 Tockは位置非依存のアプリをサポートしますが、すべてのアプリが位置非依存であるわけではありません。 これにより、カーネル(および他のツール)は、位置非依存でないバイナリを誤った場所にロードすることを 避けることができます。

0             2             4             6             8
+-------------+-------------+---------------------------+
| Type (5)    | Length (8)  | ram_address               |
+-------------+-------------+-------------+-------------+
| flash_address             |
+---------------------------+
  • ram_address プロセスのメモリアドレスがスタートすべきメモリ内のアドレス。固定アドレスが 不要な場合、0xFFFFFFFFを設定するべきです。
  • flash_address プロセスバイナリ(ヘッダではない)が位置すべきフラッシュ内のアドレス。 フラッシュ用にリンカに提供された値と一致する。固定アドレスが不要な場合、0xFFFFFFFFを 設定するべきです。

コード

プロセスのコード自体には特定のフォーマットはありません。それはフラッシュに存在しますが、特定のアドレスが プラットフォームによって決定されています。バイナリ内のコードは、位置非依存のコードを使用するなど、 どのようなアドレスでも正常に実行できなければなりません。

メモリレイアウト

このドキュメントでは、Tockにおいてメモリがどのように構造化され、カーネルや アプリケーション、状態のサポートのために使用されているかを説明します。

Tockは、一つのアドレス空間に不揮発性のFlashメモリ(コード用)とRAM(スタックと データ用)を持つCortex-Mのようなマイクロコントローラ上で動作することを意図しています。 Cortex-Mアーキテクチャはアドレス空間の高レベルなレイアウトを規定していますが、Tockの 正確なレイアウトはボードによって変えることができます。ほとんどのボードは単純にFlash とSRAMの開始と終了をlayout.ldファイルで定義し、汎用のTockメモリマップを インクルードしています。

Flash

不揮発性のflashメモリはカーネルコードおよびすべてのプロセスコードの リンクリストを格納します。

カーネルコード

カーネルコードは2つの主要な領域に分割されます。一つは.textであり、ベクタテーブルと プログラムコード、初期化ルーチン、その他の読み取り専用データを格納します。このセクションは flashの先頭に書き込まれます。

.text領域に続く2番目の主要領域は.relocate領域です。これはSRAMに存在する必要が あるが、非ゼロの初期値を持ち、Tockが初期化の一環としてflashからSRAMにコピーする値を 格納します(起動を参照してください)。

プロセスレコード

プロセスは_sappsというシンボルを使ってカーネルから取得できる既知の開始アドレスから flashに配置されます。各プロセスはTock Binary Format (TBF) ヘッダから始まり、 実際のアプリケーションバイナリが続きます。プロセスはflash内で連続的に配置され、 各プロセスのTBFヘッダにはflash内におけるプロセス全体のサイズが含まれています。 これによりカーネルがアプリを走査するために使用する連結リスト構造を作成します。 有効なプロセスの終了は不正なTBFヘッダによって示されます。通常、有効な最後のプロセスの 後のflashページにはすべて0x00か0xFFがセットされます。

RAM

RAMはカーネルとプロセスの両者により現在使用されているデータを格納します。

カーネルRAM

カーネルRAMには3つの主要な領域があります。

  1. カーネルスタック.
  2. カーネルデータ: 起動時にflashからコピーされた、初期化メモリ。
  3. カーネルBSS: ブート時にゼロ詰めされた未初期化メモリ。

プロセスRAM

プロセスRAMはすべての実行中のアプリケーションの間で分割されたメモリ空間です。

プロセスのRAMには4つの主要領域があります。

  1. プロセススタック
  2. プロセスデータ
  3. プロセスヒープ
  4. ぐたんと

次の図は一つのプロセスのメモリ空間を示したものです。

Process' RAM

ハードウェア実装

SAM4L

SAM4LはHailとImixプラットフォームで使用されているマイクロコントローラです。 そのflashとRAMの構成は次のとおりです。

Flash

アドレス範囲長さ(バイト)内容説明
0x0-3FF1024ブートローダブートローダ用にflashに予約済み。ベクタテーブルも。
0x400-0x5FF512フラッグフラッグ用に予約済みの空間。ブートローダが存在する場合、最初の14バイトは"TOCKBOOTLOADER"。
0x600-0x9FF1024属性ボードとその上で実行されるソフトウェアを記述する属性を示す最大16個のキー・バリューペア。
0xA00-0xFFFF61.5kブートローダカーネルとアプリケーションをプログラムする非JTAGの方法を提供するソフトウェアブートローダ。
0x10000-0x2FFFF128kカーネルカーネル用のflash空間。
0x30000-0x7FFFF320kアプリケーションアプリケーション用のflash空間。

RAM

アドレス範囲長さ(バイト)内容説明
0x20000000-0x2000FFFF64kカーネルとアプリケーションのRANカーネルはすべてのRAMとリンクし、アプリケーションが使用するバッファを内部的に割り当てる。

概要

以下の画像は、実際にどのように配置されるかの例を示しています。図では3つの アプリケーション(crc, ip_sense, analog_comparator)が実行中の flashとRAMの双方のアドレス空間を示しています。

Process memory layout

メモリ隔離

このドキュメントでは、カーネルとプロセスのアクセス許可の観点から、Tockでメモリが どのように隔離されているかを説明します。これを読む前に、Tockの設計Tockのメモリレイアウトを十分に理解していることを確認して ください。

メモリの隔離はTockの重要な属性です。これがなければ、プロセスはメモリの任意の部分に アクセスすることができ、システム全体のセキュリティが損なわれてしまいます。その理由は、 Rustはコンパイル時にメモリの安全性(二重解放やバッファオーバーフローがないなど)と 型の安全性を守りますが、どの言語でも記述可能なプロセスが、メモリ内のアクセスしては いけないアドレスにアクセスすることを防ぐことはできないからです。このようなことが 起きないようにするためには何らかの別のコンポーネントが必要であり、さもないと システムは信頼できないプロセスを安全にサポートすることができません。

信頼できないアプリケーションをサポートするために、Tockは多くの組み込みマイクロ コントローラに搭載されているメモリ保護ユニット(MPU)を使用しています。MPUは 特定のメモリ領域にアクセス許可を設定できるハードウェアコンポーネントです。これらの メモリ領域には、読み取り(R)、書き込み(W)、実行(X)の3つの基本的なアクセス型を 設定することができます。フルアクセスとは、特定のメモリ領域で3つのアクセス型が すべて許可されていることを意味します。

プロセスはデフォルトで互いのメモリへのアクセスを許可されていないので、MPUは、プロセスが フラッシュのどこに格納されているか、および、どのメモリが割り当てられているかに基づいて、 各プロセスを設定しなければなりません。つまり、MPUの設定はプロセスごとに異なります。 したがって、ユーザランドプロセスにコンテキストを切り替えるたびに、Tockはそのプロセス用 にMPUを再構成します。

システムがカーネルコードを実行しているときは、MPUは無効になっています。これは、 カーネルがアドレス空間全体にアクセスすることを妨げるハードウェア上の制限がないことを 意味します。カーネルができることはRustの型システムによって制限されています。たとえば、 カプセル(unsafeを使用できません)はプロセスのメモリにアクセスできません、任意の ポインタを作成することも逆参照することもできないからです。一般に、Tockは信頼できる コード(すなわち、unsafeを呼び出せるコード)の量を最小限に抑え、unsafeを必要と するコードをカプセル化して、そのコードが何をするのか、システム全体の安全性を侵害しない 方法でそれをどのように使うのかを明確にしようとしています。

プロセスの隔離

アーキテクチャの観点から見ると、プロセスはバグのある、悪意さえあるかもしれない任意の コードであると考えられます。そのため、Tockでは、アプリケーションの動作不良がシステム 全体の整合性を損なわないように注意を払っています。

Flash

フラッシュは、マイクロコントローラ上の不揮発性メモリ空間です。一般にプロセスは フラッシュ内の任意のアドレスにアクセスすることはできず、ブートローダやカーネル コードにアクセスすることは絶対に禁止されています。また、他のプロセスの不揮発性 領域を読み書きすることも禁止されています。

プロセスはフラッシュ内の各自のメモリにはアクセスできます。Tock Binary Format(TBF) ヘッダとヘッダの後の保護領域を含む特定の領域は読み取り専用になっています。カーネルが ヘッダの整合性を保証できなければならないからです。中でも、カーネルはフラッシュ内の次の アプリを見つけるためにアプリの合計サイズを知る必要があります。また、カーネルはアプリを 変更することができないようにするためにアプリに関する不揮発性の情報(たとえば、Failure 状態に何回入ったかなど)を保存したい場合もあるかもしれません。

アプリの残りの部分、特にアプリの実際のコードは、アプリが所有していると考えられます。 アプリは自身のコードを実行するためにフラッシュを読むことができます。MCUがその不揮発性 メモリにフラッシュを使用している場合、アプリが自身のフラッシュ領域を直接変更することが できない可能性が高くなります。通常、フラッシュは消去したり書き込んだりするために何らかの ハードウェアペリフェラルとの相互作用を必要とするからです。この場合、アプリは自身の フラッシュ領域を変更するためにカーネルのサポートが必要になります。

RAM

プロセスRAMは実行中のすべてのアプリ間で分割されるメモリ空間です。下図はプロセスの メモリ空間を示しています。

Process' RAM

プロセスは自身のRAM領域にはフルアクセスできます。グラント領域と呼ばれるRAM領域の セグメントはプロセスではなくカーネルの使用専用に予約されています。グラント領域には カーネルのデータ構造体が含まれているためプロセスはこの領域を読むことも書くことも できません。

プロセスのメモリ領域の残りの部分は、プロセスが適切と判断した通りにスタックやヒープ、 データセクションとして使用することができます。プロセスはこれらの使用方法を完全に 制御します。スタックとヒープをどこに置いたかをカーネルに知らせるためにプロセスが 使用できるできるmemシステムコールがありますが、これらは完全にデバッグ用です。 通常操作のためにプロセスがどのようにメモリを組織化しているかをカーネルが知る必要は ありません。

プロセスはallowシステムコールを使って自身のRAMの一部をカーネルと共有することを 明示的に選択することができます。これによりカプセルは特定の操作で使用するための プロセスのメモリへの読み書きアクセスが可能になります。

プロセスは、プロセス間通信(IPC)機構により 相互に通信することができます。IPCを使用するには、プロセスは共有バッファとして使用する ためにRAM内のバッファを指定し、このバッファを他のプロセスと共有したい旨をカーネルに 通知します。すると、このIPC機構を使う他のユーザがこのバッファを読み書きすることが 許されます。IPC以外ではプロセスは他のプロセスのRAMを読むことも書くこともできません。

Tockのレジスタインターフェース

このクレートはメモリマップドレジスタとビットフィールドの定義と操作のための インターフェースです。

レジスタの定義

このクレートは、メモリマップドレジスタを操作する3つの型、ReadWriteReadOnlyWriteOnlyを提供します。各型は各々、読み書き、読み取り専用、 書き込み専用の機能を提供します。

レジスタの定義はregister_structsマクロで行います。このマクロは各レジスタについて、 オフセット、フィールド名、型を求めます。レジスタはオフセットの昇順に連続して定義しなければ なりません。レジスタ定義の際にはギャップをオフセットとギャップ識別子(慣例により _reservedNという名前のフィールドを使用する)で明示的に注記する必要がありますが、 型は指定しません。マクロは自動的にギャップサイズを計算し、適切なパッディングを構造体に 挿入します。構造体の終わりにはそのサイズとレジスタリストの直前のオフセットを効率的に 指すように@ENDキーワードでマーク付けします。


#![allow(unused)]
fn main() {
use tock_registers::registers::{ReadOnly, ReadWrite, WriteOnly};

register_structs! {
    Registers {
        // Controlレジスタ: read-write
        // 'Control'パラメータは、このレジスタが特定のグループのフィールド
        // (ビットフィールドセクションで定義されている)のみを使用するように
        // 制約する。
        (0x000 => cr: ReadWrite<u8, Control::Register>),

        // Statusレジスタ: read-only
        (0x001 => s: ReadOnly<u8, Status::Register>),

        // レジスタはバイト、ハーフワード、ワードのいずれか。Registers can be bytes, halfwords, or words:
        // 2番めの型パラメタは省略可能なことに注意せよ。この場合、これらのレジスタには
        // 定義されたビットフィールドが存在しないことを意味する。
        (0x002 => byte0: ReadWrite<u8>),
        (0x003 => byte1: ReadWrite<u8>),
        (0x004 => short: ReadWrite<u16>),

        // レジスタ間の空空間は以下のように定義したパッディングフィールドでマーク
        // する必要がある。このパッディングの長さはマクロにより自動的に計算される。
        (0x006 => _reserved),
        (0x008 => word: ReadWrite<u32>),

        // レジスタの型は何でも良いが、都合の良いことに、一連の同じレジスタが
        // 存在する場合は配列を使うことができる。
        (0x00C => array: [ReadWrite<u32>; 4]),
        (0x01C => ... ),

        // その他その他

        // 構造体の最後は次のようにマーク付する。
        (0x100 => @END),
    }
}
}

これは次のような形のCスタイルの構造体を生成します。


#![allow(unused)]
fn main() {
#[repr(C)]
struct Registers {
    // Control register: read-write
    // The 'Control' parameter constrains this register to only use fields from
    // a certain group (defined below in the bitfields section).
    cr: ReadWrite<u8, Control::Register>,

    // Status register: read-only
    s: ReadOnly<u8, Status::Register>

    // Registers can be bytes, halfwords, or words:
    // Note that the second type parameter can be omitted, meaning that there
    // are no bitfields defined for these registers.
    byte0: ReadWrite<u8>,
    byte1: ReadWrite<u8>,
    short: ReadWrite<u16>,

    // The padding length was automatically computed as 0x008 - 0x006.
    _reserved: [u8; 2],
    word: ReadWrite<u32>,

    // Arrays are expanded as-is, like any other type.
    array: [ReadWrite<u32>; 4],

    // Etc.
}
}

デフォルトで構造体のstdユニットテストも生成されます(すなわち、#[test]属性のついた テストです)。このユニットテストはオフセットとパディングが構造体の実際のフィールドと 一致していることとアライメントが正しいことを確認します。

これらのテストはcustom-test-frameworks環境ではコンパイルを中断してしまうので、 テスト生成を省略することができます。そのためには、以下のカーゴ機能を追加します。

[dependencies.tock-registers]
version = "0.4.x"
features = ["no_std_unit_tests"]

警告: 今のところ、make ci-travisではオフセットとアライメントをチェックするユニットテストは実行されません。 これは、レジスタ間に意図しないギャップが残った場合も検出されないことを意味し、 register_structsマクロは警告なしで不正なオフセットを持つ構造体を生成することに なります。https://github.com/tock/tock/pull/1393 の議論に従ってください。

たとえば、次のようにマクロを呼び出した場合、


#![allow(unused)]
fn main() {
register_structs! {
    Registers {
        (0x000 => foo: ReadOnly<u8>),
        (0x008 => bar: ReadOnly<u8>),
        (0x009 => @END),
    }
}
}

これは次の構造体を生成します。アドレス0x004(レジスタfooの終わり)と0x008 (意図したレジスタbarの始まり)の間に意図しない4バイトのギャップがあります。


#![allow(unused)]
fn main() {
#[repr(C)]
struct Registers {
    foo: ReadOnly<u32>,
    bar: ReadOnly<u32>,
}
}

デフォルトで生成された構造体とフィールドの可視性はプライベートです。構造体名または フィールド識別子の直前にpubキーワードを付けることでそれらをpublicにすることができます。

たとえば、次のようにマクロを呼び出した場合、


#![allow(unused)]
fn main() {
register_structs! {
    pub Registers {
        (0x000 => foo: ReadOnly<u32>),
        (0x004 => pub bar: ReadOnly<u32>),
        (0x008 => @END),
    }
}
}

次の構造体を生成します。


#![allow(unused)]
fn main() {
#[repr(C)]
pub struct Registers {
    foo: ReadOnly<u32>,
    pub bar: ReadOnly<u32>,
}
}

ビットフィールドの定義

ビットフィールドはregister_bitfields!マクロで定義します。


#![allow(unused)]
fn main() {
register_bitfields! [
    // 第1パラメタはレジスタの幅です。u8, u16, u32, u64が指定できます。
    u32,

    // 後続の各パラメータは、レジスタの略語、その記述名、関連するビットフィールドで
    // ある。記述名はこのビットフィールドの「グループ」を定義する。
    // ReadWrite<_, Control::Register>として定義されたレジスタだけが
    // これらのビットフィールドを使用することができる。
    Control [
        // ビットフィールドは次のように定義する。
        // 名前 OFFSET(シフト数) NUMBITS(ビット数) [ /* オプションの値 */ ]

        // これはビット4と5から成る2ビットフィールである。
        RANGE OFFSET(4) NUMBITS(2) [
            // 以下の各々はビットフィールドに書き込む、またはマッチさせる
            // ことができる値の名前を定義する。このセットは排他的なものでは
            // ないことに注意されたい。フィールドには任意の定数を書き込むことが
            // 可能である。
            VeryHigh = 0,
            High = 1,
            Low = 2
        ],

        // よくあるのは1ビットのビットフィールドで、通常は単に何かを「有効」または
        //「無効」にすることを意味します。
        EN  OFFSET(3) NUMBITS(1) [],
        INT OFFSET(2) NUMBITS(1) []
    ],

    // もう一つの例:
    // Statusレジスタ
    Status [
        TXCOMPLETE  OFFSET(0) NUMBITS(1) [],
        TXINTERRUPT OFFSET(1) NUMBITS(1) [],
        RXCOMPLETE  OFFSET(2) NUMBITS(1) [],
        RXINTERRUPT OFFSET(3) NUMBITS(1) [],
        MODE        OFFSET(4) NUMBITS(3) [
            FullDuplex = 0,
            HalfDuplex = 1,
            Loopback = 2,
            Disabled = 3
        ],
        ERRORCOUNT OFFSET(6) NUMBITS(3) []
    ],

    // 単純なケースでは、オフセットは単なる数字でよく、ビット数は1にセットされる。
    InterruptFlags [
        UNDES   10,
        TXEMPTY  9,
        NSSR     8,
        OVRES    3,
        MODF     2,
        TDRE     1,
        RDRF     0
    ]
]
}

レジスタインターフェースのまとめ

レジスタインターフェースにより4つの型が提供されています。ReadOnlyWriteOnlyReadWriteAliasedです。これらは以下の関数を提供します。


#![allow(unused)]
fn main() {
ReadOnly<T: IntLike, R: RegisterLongName = ()>
.get() -> T                                    // 生のレジスタ値を取得する
.read(field: Field<T, R>) -> T                 // 指定したフィールドの値を読み取る
.read_as_enum<E>(field: Field<T, R>) -> Option<E> // 指定したフィールドの値をenumメンバとして読み取る
.is_set(field: Field<T, R>) -> bool            // フィールドの1つ以上のビットがセットされているかチェックする
.matches_any(value: FieldValue<T, R>) -> bool  // 指定した任意の部分がフィールドにマッチするかチェックする
.matches_all(value: FieldValue<T, R>) -> bool  // 指定したすべての部分がフィールドにマッチするかチェックする
.extract() -> LocalRegisterCopy<T, R>          // レジスタのローカルコピーを作成する

WriteOnly<T: IntLike, R: RegisterLongName = ()>
.set(value: T)                                 // 生のレジスタ値をセットする
.write(value: FieldValue<T, R>)                // 一つ以上のフィールドの値を書き込み、
                                               //  その他のフィールドはゼロで上書きする
ReadWrite<T: IntLike, R: RegisterLongName = ()>
.get() -> T                                    // 生のレジスタ値を取得する
.set(value: T)                                 // 生のレジスタ値をセットする
.read(field: Field<T, R>) -> T                 // 指定したフィールドの値を読み取る
.read_as_enum<E>(field: Field<T, R>) -> Option<E> // 指定したフィールドの値をenumメンバとして読み取る
.write(value: FieldValue<T, R>)                // 一つ以上のフィールドの値を書き込み、
                                               //  その他のフィールドはゼロで上書きする
.modify(value: FieldValue<T, R>)               // 一つ以上のフィールドの値を書き込み、
                                               //  その他のフィールドは変更せずそのまま残す
.modify_no_read(                               // 一つ以上のフィールドの値を書き込み、
      original: LocalRegisterCopy<T, R>,       //  その他のフィールドは変更せずそのまま残すが、
      value: FieldValue<T, R>)                 //  レジスタを読み取るのではなくオリジナル値を返す
.is_set(field: Field<T, R>) -> bool            // フィールドの1つ以上のビットがセットされているかチェックする
.matches_any(value: FieldValue<T, R>) -> bool  // 指定した任意の部分がフィールドにマッチするかチェックする
.matches_all(value: FieldValue<T, R>) -> bool  // 指定したすべての部分がフィールドにマッチするかチェックする
.extract() -> LocalRegisterCopy<T, R>          // レジスタのローカルコピーを作成する

Aliased<T: IntLike, R: RegisterLongName = (), W: RegisterLongName = ()>
.get() -> T                                    // 生のレジスタ値を取得する
.set(value: T)                                 // 生のレジスタ値をセットする
.read(field: Field<T, R>) -> T                 // 指定したフィールドの値を読み取る
.read_as_enum<E>(field: Field<T, R>) -> Option<E> // 指定したフィールドの値をenumメンバとして読み取る
.write(value: FieldValue<T, W>)                // 一つ以上のフィールドの値を書き込み、
                                               //  その他のフィールドはゼロで上書きする
.is_set(field: Field<T, R>) -> bool            // フィールドの1つ以上のビットがセットされているかチェックする
.matches_any(value: FieldValue<T, R>) -> bool  // 指定した任意の部分がフィールドにマッチするかチェックする
.matches_all(value: FieldValue<T, R>) -> bool  // 指定したすべての部分がフィールドにマッチするかチェックする
.extract() -> LocalRegisterCopy<T, R>          // レジスタのローカルコピーを作成する
}

Aliased型は、異なる意味を持つ読み取り専用レジスタと書き込み専用レジスタが同じメモリ位置にエイリアスされている場合を表します。

最初の型パラメータ(IntLike型)はu8、u16、u32、u64のいずれかです。

レジスタとビットフィールの使用例

先の2つのセクションで述べたようにRegisters構造体とそれに対応するビットフィールドを定義した仮定します。また、registersという名前のRegisters構造体への不変参照があるとします。


#![allow(unused)]
fn main() {
// -----------------------------------------------------------------------------
// RAW ACCESS
// -----------------------------------------------------------------------------

// レジスタの生値を直接、取得またはセットする。なんでもない。
registers.cr.set(registers.cr.get() + 1);


// -----------------------------------------------------------------------------
// READ
// -----------------------------------------------------------------------------

// `range`はRANGEフィールドの値(たとえば、0, 1, 2, 3のいずれか)を持つことになる。
// 型アノテーションは不要であるが、ここでは明確にするため指定した。
let range: u8 = registers.cr.read(Control::RANGE);

// あるいは、enumとして`range`に読み取り、`match`させることができる。
let range = registers.cr.read_as_enum(Control::RANGE);
match range {
    Some(Control::RANGE::Value::VeryHigh) => { /* ... */ }
    Some(Control::RANGE::Value::High) => { /* ... */ }
    Some(Control::RANGE::Value::Low) => { /* ... */ }

    None => unreachable!("invalid value")
}

// `en`は0か1である
let en: u8 = registers.cr.read(Control::EN);


// -----------------------------------------------------------------------------
// MODIFY
// -----------------------------------------------------------------------------

// ビットフィールドに値を書き込む。他のフィールドの値は変えずにそのまま。
registers.cr.modify(Control::RANGE.val(2)); // Leaves EN, INT unchanged

// 生の値の代わりに名前付き定数を使用できる。
registers.cr.modify(Control::RANGE::VeryHigh);

// 生の値をフィールドに書き込むもう一つの例。
registers.cr.modify(Control::EN.val(0)); // RANGEとINTは変更せずそのまま

// 1ビットフィールでは、名前付きの値、SETとCLEARが自動的に定義されている。
registers.cr.modify(Control::EN::SET);

// 複数の値を一度に書き込む。他のフィールドは変えずにそのまま。
registers.cr.modify(Control::EN::CLEAR + Control::RANGE::Low); // INT unchanged

// フィールドが重ならなければ任意の数の値を組み合わせることができる。
registers.cr.modify(Control::EN::CLEAR + Control::RANGE::High + CR::INT::SET);

// (保護レジスタのように).modify()の使用は適切でない場合がある
// 読み取りと書き込むが対になっていないレジスタの更新を可能にするためには
// modify_no_read()を使用する。
let original = registers.cr.extract();
registers.cr.modify_no_read(original, Control::EN::CLEAR);


// -----------------------------------------------------------------------------
// WRITE
// -----------------------------------------------------------------------------

// modifyと同じインターフェスだが、指定しないフィールドはすべてゼロで
// 上書きする。
registers.cr.write(Control::RANGE.val(1)); // 暗示的に他のすべてのビットに
                                           // ゼロをセットする

// -----------------------------------------------------------------------------
// BITFLAGS
// -----------------------------------------------------------------------------

// 1ビットフィールドでセットされているかクリアされているかを簡単にチェックする。
let txcomplete: bool = registers.s.is_set(Status::TXCOMPLETE);

// -----------------------------------------------------------------------------
// MATCHING
// -----------------------------------------------------------------------------

// `matches_[any|all]`を使って指定したレジスタの状態を調べることもできる。

// TXCOMPLETEとMODE以外のフィールドの状態については問わない。
let ready: bool = registers.s.matches_all(Status::TXCOMPLETE:SET +
                                     Status::MODE::FullDuplex);

// 指定した状態になるのを待つのに非常に便利。
while !registers.s.matches_all(Status::TXCOMPLETE::SET +
                          Status::RXCOMPLETE::SET +
                          Status::TXINTERRUPT::CLEAR) {}

// あるいは、任意の割り込みが有効になっているかをチェックする。
let any_ints = registers.s.matches_any(Status::TXINTERRUPT + Status::RXINTERRUPT);

// また、一連のenum値を持つレジスタをenumとして読み取り、`match`させる
// こともできる。
let mode = registers.cr.read_as_enum(Status::MODE);

match mode {
    Some(Status::MODE::FullDuplex) => { /* ... */ }
    Some(Status::MODE::HalfDuplex) => { /* ... */ }

    None => unreachable!("invalid value")
}

// -----------------------------------------------------------------------------
// LOCAL COPY
// -----------------------------------------------------------------------------

// より複雑なコードでは、レジスタ値を一度読み取ってローカル変数に保存し、
// そのローカルコピーを使って通常のレジスタインターフェイス関数を使用
// したい場合がある。

// レジスタ値のコピーをローカル変数として作成する。
let local = registers.cr.extract();

// これでReadOnlyレジスタのすべての関数が動作する。
let txcomplete: bool = local.is_set(Status::TXCOMPLETE);

// -----------------------------------------------------------------------------
// In-Memory Registers
// -----------------------------------------------------------------------------

// 上記のすべてのレジスタ機能においてメモリ配置場所を編集したい場合があるが、実際の
// メモリ配置場所は固定されたMMIOレジスタではなく、メモリ内の任意の位置です。
// この場所がハードウェアと共有された場合(すなわち、DMA経由で)、ソフトウェアが
// 知らないうちに値が変更される可能性があるため、コードはvolatileな読み取りと
// 書き込みを行う必要があります。 これをサポートするために、ライブラリには
// `InMemoryRegister`型があります。

let control: InMemoryRegister<u32, Control::Register> = InMemoryRegister::new(0)
control.write(Contol::BYTE_COUNT.val(0) +
              Contol::ENABLE::Yes +
              Contol::LENGTH.val(10));
}

modifyは正確に一回のvolatileロードと一回のvolatileストアを、 writeは正確に一回のvolatileストアを、readは正確に一回のvolatileロードを それぞれ実行することに注意してください。 したがって、1回の呼び出しで すべてのフィールドが同時に設定または照会されることが保証されます。

パフォーマンス

このインターフェイスのテスト中にバイナリを調べると、すべてが最適なインライン ビット調整命令にコンパイルされています。言い換えれば、非公式の予備調査で 判明した限り、実行時のコストはゼロです。

素敵な型チェック

このインターフェースは型チェックを介してコンパイラが一般的な種類のバグを捕捉する のに役立ちます。

たとえば、コントロールレジスタのビットフィールドを定義する場合、Controlの ようなわかりやすいグループ名を付けることができます。このビットフィールドのグループは ReadWrite <_、Control>型(またはReadOnly/WriteOnly型など)のレジスタで しか機能しません。たとえば、上記で定義したビットフィールドとレジスタがある場合、


#![allow(unused)]
fn main() {
// この行はコンパイルされる。registers.crはビットフィールドのContorlグループ
// に関連付けられているからである。
registers.cr.modify(Control::RANGE.val(1));

// この行はコンパイルされない。registers.sはControlグループではなく、
// Statusグループに関連付けられているからである。
let range = registers.s.read(Control::RANGE);
}

命名規約

レジスタ定義にはいくつかの関連する名前があります。以下は、それぞれの命名規約の 説明です。


#![allow(unused)]
fn main() {
use tock_registers::registers::ReadWrite;

#[repr(C)]
struct Registers {
    // 構造体におけるレジスタ名はデータシートに記載されているレジスタの
    // 省略形を小文字にしたものにするべきである。
    cr: ReadWrite<u8, Control::Register>,
}

register_bitfields! [
    u8,

    // 名前は'register'を除いたキャメルケースの長い説明的なレジスタ名に
    // するべきである。
    Control [
        // フィールド名はデータシートに記載されているフィールド名の省略形を
        // 大文字にしたものにするべきである。
        RANGE OFFSET(4) NUMBITS(3) [
            // 各フィールド値はキャメルケースでその値をできるだけ説明する
            // ものにするべきである。
            VeryHigh = 0,
            High = 1,
            Low = 2
        ]
    ]
]
}

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月)において、主要なチップ(sam4Lnrf52 など)はすべての割り込みに対し同じハンドラを使用しています。それは、次の ようになります。


#![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スケジューラとカーネルのメイン操作を開始します。

システムコール

このドキュメントでは、カーネルとアプリケーションの両者に関して、Tockにおいて システムコールが どのように動作するかを説明します。これは、ドライバやアプリケーションでの システムコールの使い方のチュートリアルではなく、現在のシステムコールの実装の 背後にある設計上の考慮事項を説明したものです。

Tockにおけるシステムコールの概要

システムコールは、アプリケーションからカーネルに情報を送信するために使用 される方法です。アプリケーションは、カーネル内の関数を直接呼び出すのでは なく、サービスコール(svc)割り込みをトリガしてカーネルへのコンテキスト スイッチを発生させます。カーネルは割り込みコール時のレジスタとスタックの 値を使用して、システムコールをどのようにルートするか、どのドライバ関数を どのデータ値で呼び出すかを決定します。

システムコールの使用には3つの利点があります。第一に、サービスコール割り込みの トリガ動作をプロセッサの状態変更に使用することができます。(アプリケーションが 実行されている)非特権モードにおいてメモリ保護ユニット(MPU)によって制限 されるのではなく、サービスコールの後、カーネルがシステムリソースを完全に 制御できる特権モードに切り替わります(詳細はARMのプロセッサモード)を参照してください)。第二に、カーネルへのコンテキスト スイッチにより、カーネルはアプリケーションに戻る前に他のリソース処理を行う ことが可能になります。これには他のアプリケーションの実行やキューに入って いるコールバックの処理など、多くの操作が含まれます。最後に、そして最も重要な ことは、システムコールを使うことで、アプリケーションをカーネルから独立して 構築することが可能になります。カーネル全体のコードベースは変更される可能性が ありますが、システムコールインタフェースが同一である限り、アプリケーションは そのプラットフォーム上で動作するために再コンパイルする必要さえありません。 カーネルから分離されたアプリケーションは、もはやカーネルと同時にロードする 必要はありません。後でアップロードしたり、修正したり、新しいバージョンを アップロードしたりすることが、プラットフォーム上で動作するカーネルを修正する 必要なく行なえます。

プロセスの状態

Tockでは、プロセスは次の3つの状態のいずれかにあります。

  • Running: 通常の動作。Runningプロセスは実行がスケジューリングされる 資格がありますが、割り込みハンドラや他のプロセスの実行を許可するために Tockによって一時停止される可能性があります。通常の動作中は、プロセスは 明示的にyieldされるまでRunning状態を維持します。他のカーネル操作 からのコールバックはRunningプロセスには配信されません(つまり、コールバック はプロセスを中断しません)。これらのコールバックはプロセスがyieldするまで キューに置かれます。
  • Yielded: 中断された動作。YieldedプロセスはTockによってスケジュール されません。プロセスは、I/Oやその他の操作が完了するのを待っており、すぐに 有用な作業を行う必要がない場合に、yieldします。カーネルがYieldedプロセスに コールバックを発行すると、プロセスはRunning状態に遷移します。
  • Fault: 誤った動作。Fault状態のプロセスは、Tockによって スケジューリングされません。プロセスは、アドレス空間外のメモリへのアクセス などの不正な操作を行うとFault状態になります。

起動

プロセスの初期化時に、関数呼び出しタスクが一つそのコールバックキューに追加されます。 この関数は、プロセスのTBFヘッダのENTRYポイントによって決定されており(通常は _startシンボル)、レジスタr0 - r3を通じて次の引数が渡されます。

  • r0:プロセスコードのベースアドレス
  • r1:メモリ領域に割り当てられたプロセスのベースアドレス
  • r2:その領域におけるメモリの合計量
  • r3:現在のプロセスのメモリブレーク

システムコール

Yield(失敗することはありません)を除くすべてのシステムコールは整数のリターン コード値をユーザ空間に返します。負のリターンコードはエラーを示します。ゼロ以上の 値は成功を示します。システムコールのリターン値が有用なデータをエンコードする場合が あります。たとえば、gpioドライバーではピンの値を読み取るコマンドは、ピンの 状態に基づいて0または1を返します。

現在、次のリターンコードが定義されており、tock.hヘッダによりCからも #definesして使用できます(TOCK_が前置する)。


#![allow(unused)]
fn main() {
pub enum ReturnCode {
    SuccessWithValue { value: usize }, // 成功値は >= 0 でなければならない
    SUCCESS,
    FAIL, //.......... 一般的な失敗状態
    EBUSY, //......... 対象のシステムがビジー; リトライせよ
    EALREADY, //...... 要求された状態はすでにセットされている
    EOFF, //.......... コンポーネントの電源が入っていない
    ERESERVE, //...... 使用前に予約が必要
    EINVAL, //........ 不正なパラメタが渡された
    ESIZE, //......... 渡されたパラメタが大きすぎる
    ECANCEL, //....... 呼び出しにより操作がキャンセルされた
    ENOMEM, //........ 要求されたメモリが利用できない
    ENOSUPPORT, //.... 操作またはコマンドはサポートされていない
    ENODEVICE, //..... 装置が存在しない
    EUNINSTALLED, //.. 装置が物理的に設置されていない
    ENOACK, //........ パケット送信が確認されなかった
}
}

0: Yield

Yieldは、現在のプロセスをRunning状態からYielded状態に移行し、別のコールバックが プロセスを再スケジュールするまでプロセスは再度実行されません。

Yieldが呼び出された際、プロセスが実行待機のコールバックをキューに入れていた 場合、スケジューラが他の操作を最初に優先することを選択しない限り、プロセスは 直ちにRunning状態に戻り、最初のコールバックが実行されます。


#![allow(unused)]
fn main() {
yield()
}

引数

なし.

リターン

なし.

1: Subscribe

Subscribeはさまざまなイベントにより実行されるコールバック関数を割り当てます。

コールバック関数は(driversubscribe_number)ペア、別名コールバックIDに より一意に識別されます。subscribeを呼び出す際、このコールバックIDに対して 保留中のコールバックが存在する場合、それらはキューから削除され、新しいコールバック 関数がそのコールバックIDにバインドされます。

プロセスはコールバック引数としてnullポインタを渡することで(このコールバックIDを 持つ保留中のコールバックをフラッシュすることに加えて)ドライバに以前に設定された コールバックを無効にするように要求できます。


#![allow(unused)]
fn main() {
subscribe(driver: u32, subscribe_number: u32, callback: u32, userdata: u32) -> ReturnCode as u32
}

引数

  • driver: どのドライバをコールするかを指定する整数。
  • subscribe_number:どの関数をサブスクライブさせるかを示す整数インデックス
  • callback: このイベントが生じた際に実行させるコールバック関数へのポインタ。 すべてのコールバックはCスタイルの関数シグニチャ void callback(int arg1, int arg2, int arg3, void* data)に従う。
  • userdata: カーネルによりcallbackの最後の引数として渡される任意の型の 値へのポインタ。

個々のドライバーは、そのコールバックを生成する可能性のあるイベントとsubscribe_numberとのマッピングと、各コールバック引数の意味を定義します。

リターン

  • ENODEVICEdriverが有効なカーネルドライバーを参照していない場合。
  • ENOSUPPORT。ドライバ存在するが、subscribe_numberをサポートしていない場合。
  • 特定のドライバーに基づくその他のリターンコード。

2: Command

Commandは、特定の動作を実行するようにドライバーに指示します。


#![allow(unused)]
fn main() {
command(driver: u32, command_number: u32, argument1: u32, argument2: u32) -> ReturnCode as u32
}

引数

  • driver: どのドライバをコールするかを指定する整数。
  • command_number: 要求するコマンドを指定する整数。
  • argument1: コマンド固有の引数。
  • argument2: コマンド固有の引数。

command_numberは、ユーザ空間からどのコマンドが呼び出されたのかをドライバーに 通知し、2つのargumentはドライバーとコマンド番号に固有です。実際に使用されて いる引数の一例は、ledドライバにあります。ここでは、LEDを点灯するコマンドが LEDの指定に引数を使用しています。

TockにおけるCommandシステムコールに関する規約の1つは、実行中のカーネルで ドライバがサポートされている場合、コマンド番号0は常に0以上の値を返すというものです。 これは、すべてのアプリケーションは任意のドライバ番号に対してコマンド番号0を呼び出して、 ドライバが存在するか、関連する機能がサポートされているかを判別できることを意味します。 ほとんどの場合、このコマンド番号は0を返し、ドライバーが存在することを示します。 ただし、ボード上に何個のLEDが存在するかを示すledドライバの場合のように、 存在するデバイスの数のような追加の意味をリターン値に持たせることも可能です。

リターン

  • ENODEVICEdriverが有効なカーネルドライバを参照していない場合。
  • ENOSUPPORT。ドライバは存在するが、command_numberをサポートしていない場合。
  • 特定のドライバーに基づくその他のリターンコード。

3: Allow

Allowはカーネルとアプリケーション間で共有されるものとしてあるメモリ領域を マーク付けします。NULLポインタを渡すと対応するドライバに共有メモリ領域への アクセスの停止を要求します。


#![allow(unused)]
fn main() {
allow(driver: u32, allow_number: u32, pointer: usize, size: u32) -> ReturnCode as u32
}

引数

  • driver: どのドライバにアクセスを与えるかを指定する整数。
  • allow_number: このバッファの目的を指定するドライバ固有の整数。
  • pointer: プロセスメモリ空間におけるバッファの開始地点へのポインタ。
  • size: バッファの長さを指定する整数のバイト数。

多くのドライバコマンドは実行前にバッファがAllowされていることを要求します。 一度Allowれたバッファは使用するために再度Allowされる必要はありません。

この記事を書いている時点では、ほとんどのTockドライバは各アプリケーションに複数の 仮想デバイスを提供していません。アプリケーションがあるドライバの複数のユーザを必要と する場合(たとえば、I2Cを使った2つのライブラリがある場合)、各ライブラリは操作を 開始する前にバッファを再度許可する必要があります。

リターン

  • ENODEVICEdriverが有効なカーネルドライバを参照していない場合。
  • ENOSUPPORT。ドライバは存在するが、allow_numberをサポートしていない場合。
  • EINVAL the buffer referred to by pointersizeで参照された バッファが完全に、またはその一部がプロセスがアクセス可能なRAMの外にある。
  • Other return codes based on the specific driver.

4: Memop

Memopはプロセスが利用可能なメモリセグメントを拡張し、割り当てられたメモリ空間への ポインタをプロセスが取得可能にし、スタックとヒープの開始位置をプロセスがカーネルに 伝えるメカニズムを提供し、その他、プロセスメモリに関わる操作を行います。


#![allow(unused)]
fn main() {
memop(op_type: u32, argument: u32) -> [[ VARIES ]] as u32
}

引数

  • op_type: これがbrk (0)であるか、sbrk (1)であるか、その他のmemop コールであるかを指定する整数。
  • argument: brk, sbrk, その他のコールに対する引数。

memopの操作は各々固有のものであり、各コールの詳細は memopシステムコールドキュメントで見ることができます。

リターン

  • 各memopコールによる。

コンテキストスイッチ

コンテキストスイッチの処理はチップ固有ではなく実際にアーキテクチャに依存している 数少ないTockのコードの一つです。このコードは該当するアーキテクチャのarch/ フォルダのlib.rsにあります。このコードはプロセッサの低レベルの機能を扱うため、 Rustの関数呼び出しとしてラップされたアセンブリで書かれています。

コンテキストスイッチインターフェース

/archフォルダにある)アーキテクチャークレートは、カーネルがユーザ空間に正しく 切り替えられるようにするために必要な関数を定義するUserspaceKernelBoundary トレイトの実装を担当しています。これらの関数は、どのレジスタをスタックに保存するか、 スタックポインタをどこに保存するか、どのデータをTockシステムコールインタフェースに 渡すかなど、アーキテクチャ固有のコンテキストスイッチの実現方法の詳細を処理します。

Cortex-Mアーキテクチャの詳細

すべてのアプリケーションが実行される前だがプロセスが作成された後に、カーネルで 開始され、カーネルはswitch_to_userを呼び出します。このコードはPIC ベースレジスタとプロセススタックポインタを含むアプリケーション用のレジスタを設定した 後、svcのコールによりサービスコール割り込みをトリガします。svcハンドラコードは システムがアプリケーションへの切り替えとカーネルへの切り替えのどちらを希望しているかを 自動的に判断し、プロセッサモードを設定します。最後に svcハンドラは復帰し、PCを アプリのエントリーポイントに向けます。

アプリケーションは実行中は非特権モードにあります。カーネルリソースを使用する必要が ある場合、svc命令を実行してシステムコールを発行します。svc_handlerは、 アプリからカーネルに切り替えるべきだと判断し、プロセッサモードを特権モードに設定 して復帰します。スタックは(プロセススタックポインタではなく)カーネルスタック ポインタに変更されたので、実行はsvcの直後にswitch_to_userへと返り、 アプリケーションが起動されます。switch_to_userはレジスタを保存してカーネルへと 返り、システムコールが処理されることになります。

次のswitch_to_user呼び出しでは、アプリケーションは、実行をカーネルに切り替えた システムコールの後の命令を指しているプロセススタックポインタに基づいて実行を再開 します。

システムコールはユーザ空間のメモリを上書きする可能性があります。カーネルはAllowに より以前に与えられたバッファに書き込む場合があるからです。カーネルはリターン値 レジスタ(r0)以外のユーザ空間レジスタの上書きはしません。しかし、Yieldは戻る前に ユーザ空間のコールバックを呼び出すことができるので、より多くのレジスタを上書きする ものとして扱わなければなりません。このコールバックは、r0-r3, r12, lrを上書きする 可能性があります。Yieldに関する詳細はlibtock-cのsyscallコードにあるこのコメントを 参照してください。

RISC-V Architecture Details

Tock assumes that a RISC-V platform that supports context switching has two privilege modes: machine mode and user mode.

The RISC-V architecture provides very lean support for context switching, providing significant flexibility in software on how to support context switches. The hardware guarantees the following will happen during a context switch: when switching from kernel mode to user mode by calling the mret instruction, the PC is set to the value in the mepc CSR, and the privilege mode is set to the value in the MPP bits of the mstatus CSR. When switching from user mode to kernel mode using the ecall instruction, the PC of the ecall instruction is saved to the mepc CSR, the correct bits are set in the mcause CSR, and the privilege mode is restored to machine mode. The kernel can store 32 bits of state in the mscratch CSR.

Tock handles context switching using the following process. When switching to userland, all register contents are saved to the kernel's stack. Additionally, a pointer to a per-process struct of stored process state and the PC of where in the kernel to resume executing after the app switches back to kernel mode are stored to the kernel's stack. Then, the PC of the app to start executing is put into the mepc CSR, the kernel stack pointer is saved in mscratch, and the previous contents of the app's registers from the per-process stored state struct are copied back into the registers. Then mret is called to switch to user mode and begin executing the app.

When the app calls a syscall, it uses the ecall instruction. This causes the trap handler to execute. The trap handler checks mscratch, and if the value is nonzero then it contains the stack pointer of the kernel and this trap must have happened while the system was executing an application. Then, the kernel stack pointer from mscratch is used to find the pointer to the stored state struct, and all app registers are saved. The trap handler also saves the app PC from the mepc CSR and the mcause CSR. It then loads the kernel address of where to resume the context switching code to mepc and calls mret to exit the trap handler. Back in the context switching code, the kernel restores its registers from its stack. Then, using the contents of mcause the kernel decides why the application stopped executing, and if it was a syscall which syscall the app called. Returning the context switch reason ends the context switching process.

All values for the syscall functions are passed in registers a0-a4. No values are stored to the application stack. The return value for syscalls is set in a0. In most syscalls the kernel will not clobber any userspace registers except for this return value register (a0). However, the yield() syscall results in a callback getting run in the app. This can clobber all caller saved registers, as well as the return address (ra) register.

システムコールがドライバーに接続する方法

システムコールが行われた後、そのコールはsched.rsにあるTockカーネルにより 一連の手順を経て処理、ルーティングされます。

  1. カーネルは、プラットフォームが提供するシステムコールフィルタ関数を呼び出して、 そのシステムコールを処理すべきか判断します。これはyieldには適用されません。 フィルタ関数はシステムコールと、システムコールを発行したプロセスを受け取り、 システムコールが処理されるべきか、またはプロセスにエラーが返されるべきかを 知らせるためにResult((), ReturnCode)を返します。

フィルタ関数がシステムコールを拒否する場合、Err(ReturnCode)を返し、 ReturnCodeがシステムコールのリターンコードとしてアプリに提供されます。 それ以外はシステムコールを処理します。

フィルタインタフェースは現段階ではunsatableであり、変更される可能性があります。

  1. システムコール番号が有効なシステムコール型と照合されます。yieldとmemopは カーネルによって処理される特別な機能を持っています。commadnsubscribeallowはドライバに転送され処理されます。

  2. commandsubscribeallowの各システムコールを転送するために、 各ボードはPlatformトレイトを実装した構造体を作成します。このトレイトを 実装するには、ドライバ番号という1つの引数を取り、それがサポートされていれば 正しいドライバへの参照を、サポートされていなければNoneを返すwith_driver() 関数を実装するだけです。カーネルは残りのシステムコール引数を使ってドライバ上で 適切なシステムコール関数を呼び出します。

Platformトレイトを実装したボードの例は次のようになります。


#![allow(unused)]
fn main() {
struct TestBoard {
    console: &'static Console<'static, usart::USART>,
}

impl Platform for TestBoard {
    fn with_driver<F, R>(&self, driver_num: usize, f: F) -> R
        where F: FnOnce(Option<&kernel::Driver>) -> R
    {

        match driver_num {
            0 => f(Some(self.console)), // 実際のコードでは0ではなく
                                        // capsules::console::DRIVER_NUMを使用する
            _ => f(None),
        }
    }
}
}

TestBoardはUARTコンソールという1つのドライバをサポートしており、それを ドライバ番号0にマップしています。ドライバ番号0に対するすべてのcommandsubscribeallowシステムコールはコンソールに転送され、その他のすべての ドライブ番号はReturnCode::ENODEVICEを返します。

割り当てられたドライバ番号

ドキュメント化されているドライバはすべてdoc/syscalls フォルダにあります。

with_driver()関数はドライバを識別するための引数driver_numを取ります。 最上位のビットがセットされたdriver_numはプライベートであり、ツリー外のドライバで 使用可能です。

ユーザランド

このドキュメントでは、アプリケーションコードがTockにおいてどのように動作するかを 説明します。これは、各自のアプリケーションを作成するためのガイドではなく、 アプリケーションがどのように機能するかの背後にある設計思想に関するドキュメントです。

Tockにおけるアプリケーションの概要

Tockにおけるアプリケーションはエンドユーザのために何らかのタスクを達成するための ユーザレベルのコードです。デバイスドライバとチップ固有の詳細、一般的な オペレーティングシステムのタスクを処理するカーネルコードとアプリケーションは 区別されます。既存の多くの組み込みオペレーティングシステムとは異なり、Tockでは アプリケーションがカーネルと一緒にはコンパイルされていません。アプリケーションは システムコールを介して カーネルや他のアプリケーションと相互作用する完全に分離されたコードです。

アプリケーションはカーネルの一部ではないので、マイクロコントローラ上で実行可能な コードにコンパイルできる任意の言語で書くことができます。Tockは複数のアプリケーションの 同時実行をサポートします。協調的マルチプログラミングがデフォルトですが、 アプリケーションをタイムスライスすることも可能です。アプリケーションは、 システムコールを介したプロセス間通信(IPC)で相互に通信できます。

アプリケーションはインストールアドレスとロードアドレスをコンパイル時に知ることが できません。現在のTockの設計では、アプリケーションは[位置独立なコード] (https://en.wikipedia.org/wiki/Position-independent_code)(PIC)と してコンパイルされる必要があります。これにより、アプリケーションはロードされた 任意のアドレスから実行できるようになります。TockアプリにおけるPICの使用は基本的な 選択ではありませんので、将来のシステムのバージョンでは、実行時再配置可能なコードを サポートする可能性もあります。

アプリケーションは非特権コードです。メモリのすべての部分にアクセスできるわけは ありません。各自の境界を超えたメモリにアクセスしようとするとフォールトが発生します (Linuxコードにおけるセグメンテーションフォールトと同じです)。ハードウェアを 使用するには、アプリケーションはカーネルを呼び出さなければなりません。

システムコール

システムコール(別名、syscalls)はカーネルへのコマンドの送信に使用されます。 システムコールには、ドライバへのコマンド、コールバックの購読、アプリケーション 関連データを保存できるようにカーネルにメモリを付与すること、他のアプリケーション コードとの通信など多くのものが含まれます。実際には、システムコールはライブラリ コードを通して行われ、アプリケーションが直接対応する必要はありません。

たとえば、GPIOピンをハイに設定する次のようなシステムコールを考えます。

int gpio_set(GPIO_Pin_t pin) {
  return command(GPIO_DRIVER_NUM, 2, pin);
}

commandシステムコール自体はARMアセンブリ命令svc(サービスコール)として 実装されています。

int __attribute__((naked))
command(uint32_t driver, uint32_t command, int data) {
  asm volatile("svc 2\nbx lr" ::: "memory", "r0");
}

より深い考察はシステムコールドキュメントで見ることことができます。

コールバック

Tockは、コールバック関数を 使用して非同期イベントを処理することが多い、組み込みアプリケーションをサポートする ように設計されています。たとえば、タイマーコールバックを受け取るためには、 まず、タイマーが発火した際に呼び出してもらいたい関数への関数ポインタを指定して timer_subscribeを呼び出します。コールバックを実行させたい特定の状態は ポインタuserdataとして渡すことができます。アプリケーションがタイマーを起動 した後は、yieldを呼び出するとタイマーが発火し、コールバック関数が呼び出されます。

現在のTockの実装では、イベントが処理されるにはyieldが呼び出される必要があることに 注意してください。アプリケーションへのコールバックはイベントが発生した際にキューに 入れられますが、アプリケーションはyieldを呼び出すまでイベントを受け取れません。 これはTockにとって根本的なものではありませんので、将来のバージョンでは、任意の システムコールに対して、あるいはアプリケーションのタイムスライシングを実行する際に コールバックを実行するようになるかもしれません。コールバックを受け取り、実行した後、 アプリケーションコードはyield後も継続します。「終了した」アプリケーション (すなわち、main()から返った)アプリケーションは、カーネルにスケジュール されないようにループ内でyieldを呼び出す必要があります。

プロセス間通信

IPCにより複数のアプリケーションが共有バッファを介して直接通信を行うことができます。 TockではIPCはサービス・クライアントモデルで実装されています。各アプリはサービスを 1つサポートすることができ、サービスはアプリのTockバイナリフォーマットヘッダに 含まれるパッケージ名により識別されます。アプリは複数のサービスと通信することができ、 検出されたサービスごとに固有のハンドルを取得します。クライアントとサービスは共有 バッファを介して通信します。各クライアントは自身のアプリケーションメモリの一部を サービスと共有し、サービスに通知して共有バッファを解析するように指示することが できます。

サービス

サービスはアプリのTBFヘッダに含まれるパッケージ名により命名されます。サービスを 登録するためにアプリはipc_register_svc()を呼び出してコールバックを設定します。 このコールバックは、クライアントがそのサービスのnotifyを呼び出すごとに呼び出されます。

クライアント

クライアントはまずipc_discover()関数を使って使いたいサービスを発見する必要が あります。次に、ipc_share()を呼び出してサービスとバッファを共有することが できます。サービスにバッファを使って何かをするよう指示するためにクライアントは ipc_notify_svc()を呼び出すことができます。アプリがサービスから通知を得たい 場合は、サービスがipc_notify_client()を呼び出した時にサービスからイベントを 受け取るためにipc_register_client_cb()を呼び出す必要があります。

これらの関数の詳細についてはlibtock-cipc.hを参照してください。

アプリケーションエントリポイント

アプリケーションはTBFヘッダに変数init_fn_offsetを設定し、カーネルが最初に 呼び出すべき関数を指定します。この関数は次のシグネチャを持つ必要があります。

void _start(void* text_start, void* mem_start, void* memory_len, void* app_heap_break);

Tockカーネルはアプリケーションプロセスのスタックやヒープレイアウトに関しては制限 しないようにしています。そのため、プロセスは非常にミニマムな環境で起動し、初期 スタックはシステムコールのサポートには十分ですが、それ以上のものはありません。 アプリケーションの起動ルーチンはまず、プログラムブレークを動かして 希望のレイアウトに合わせ、実行に追従するようスタックとヒープを設定するべきです。

スタックとヒープ

アプリケーションはTBFヘッダにminimum_ram_size変数を設定することで必要な メモリ量を指定することができます。Tockカーネルはこれを最小値として扱うことに 注意してください。使用するプラットフォームによってはメモリ量が要求した量より 大きくなることがありますが、小さくなることは絶対にありません。

アプリケーションをロードするためのメモリが不足している場合、カーネルはロード中に 失敗し、メッセージを表示します。

アプリケーションが実行時に割り当てられたメモリを超過した場合、アプリケーションは クラッシュします(その例はデバッグセクションを参照してください)。

デバッグ

アプリケーションがクラッシュした場合、Tockは多くの有用な情報を提供します。 デフォルトでは、アプリケーションがクラッシュすると、Tockはプラットフォームの デフォルトコンソールインターフェイス上にクラッシュダンプを表示します。

アプリケーションはロード時に再配置されるので、アプリケーションが最初にコンパイル された時に生成されたバイナリとデバッグ用の.lstファイルは、ボードで実際に 実行中のアプリケーションとは一致しないことに注意してください。一致したファイル (特に一致した.lstファイル)を生成するには、対象となるappディレクトリで make debugを実行することでアプリケーションが実際に実行されるものにマッチする 適切な.lstファイルを作成することができます。コマンドの呼び出し例については デバッグプリントの最後を参照してください。

---| Fault Status |---
Data Access Violation:              true
Forced Hard Fault:                  true
Faulting Memory Address:            0x00000000
Fault Status Register (CFSR):       0x00000082
Hard Fault Status Register (HFSR):  0x40000000

---| App Status |---
App: crash_dummy   -   [Fault]
 Events Queued: 0   Syscall Count: 0   Dropped Callback Count: 0
 Restart Count: 0
 Last Syscall: None

 ╔═══════════╤══════════════════════════════════════════╗
 ║  Address  │ Region Name    Used | Allocated (bytes)  ║
 ╚0x20006000═╪══════════════════════════════════════════╝
             │ ▼ Grant         948 |    948
  0x20005C4C ┼───────────────────────────────────────────
             │ Unused
  0x200049F0 ┼───────────────────────────────────────────
             │ ▲ Heap            0 |   4700               S
  0x200049F0 ┼─────────────────────────────────────────── R
             │ Data            496 |    496               A
  0x20004800 ┼─────────────────────────────────────────── M
             │ ▼ Stack          72 |   2048
  0x200047B8 ┼───────────────────────────────────────────
             │ Unused
  0x20004000 ┴───────────────────────────────────────────
             .....
  0x00030400 ┬─────────────────────────────────────────── F
             │ App Flash       976                        L
  0x00030030 ┼─────────────────────────────────────────── A
             │ Protected        48                        S
  0x00030000 ┴─────────────────────────────────────────── H

  R0 : 0x00000000    R6 : 0x20004894
  R1 : 0x00000001    R7 : 0x20004000
  R2 : 0x00000000    R8 : 0x00000000
  R3 : 0x00000000    R10: 0x00000000
  R4 : 0x00000000    R11: 0x00000000
  R5 : 0x20004800    R12: 0x12E36C82
  R9 : 0x20004800 (Static Base Register)
  SP : 0x200047B8 (Process Stack Pointer)
  LR : 0x000301B7
  PC : 0x000300AA
 YPC : 0x000301B6

 APSR: N 0 Z 1 C 1 V 0 Q 0
       GE 0 0 0 0
 EPSR: ICI.IT 0x00
       ThumbBit true

 Cortex-M MPU
  Region 0: base: 0x20004000, length: 8192 bytes; ReadWrite (0x3)
  Region 1: base:    0x30000, length: 1024 bytes; ReadOnly (0x6)
  Region 2: Unused
  Region 3: Unused
  Region 4: Unused
  Region 5: Unused
  Region 6: Unused
  Region 7: Unused

To debug, run `make debug RAM_START=0x20004000 FLASH_INIT=0x30059`
in the app's folder and open the .lst file.

アプリケーション

アプリケーションの例については、言語固有のユーザランドリポジトリを参照してください。

Tock Networking Stack Design Document

NOTE: This document is a work in progress.

This document describes the design of the Networking stack on Tock.

The design described in this document is based off of ideas contributed by Phil Levis, Amit Levy, Paul Crews, Hubert Teo, Mateo Garcia, Daniel Giffin, and Hudson Ayers.

Table of Contents

This document is split into several sections. These are as follows:

  1. Principles - Describes the main principles which the design of this stack intended to meet, along with some justification of why these principles matter. Ultimately, the design should follow from these principles.

  2. Stack Diagram - Graphically depicts the layout of the stack

  3. Explanation of queuing - Describes where packets are queued prior to transmission.

  4. List of Traits - Describes the traits which will exist at each layer of the stack. For traits that may seem surprisingly complex, provide examples of specific messages that require this more complex trait as opposed to the more obvious, simpler trait that might be expected.

  5. Explanation of Queuing - Describe queueing principles for this stack

  6. Description of rx path

  7. Description of the userland interface to the networking stack

  8. Implementation Details - Describes how certain implementations of these traits will work, providing some examples with pseudocode or commented explanations of functionality

  9. Example Message Traversals - Shows how different example messages (Thread or otherwise) will traverse the stack

Principles

  1. Keep the simple case simple

    • Sending an IP packet via an established network should not require a more complicated interface than send(destination, packet)
    • If functionality were added to allow for the transmission of IP packets over the BLE interface, this IP send function should not have to deal with any options or MessageInfo structs that include 802.15.4 layer information.
    • This principle reflects a desire to limit the complexity of Thread/the tock networking stack to the capsules that implement the stack. This prevents the burden of this complexity from being passed up to whatever applications use Thread
  2. Layering is separate from encapsulation

    • Libraries that handle encapsulation should not be contained within any specific layering construct. For example, If the Thread control unit wants to encapsulate a UDP payload inside of a UDP packet inside of an IP packet, it should be able to do so using encapsulation libraries and get the resulting IP packet without having to pass through all of the protocol layers
    • Accordingly, implementations of layers can take advantage of these encapsulation libraries, but are not required to.
  3. Dataplane traits are Thread-independent

    • For example, the IP trait should not make any assumption that send() will be called for a message that will be passed down to the 15.4 layer, in case this IP trait is used on top of an implementation that passes IP packets down to be sent over a BLE link layer. Accordingly the IP trait can not expose any arguments regarding 802.15.4 security parameters.
    • Even for instances where the only implementation of a trait in the near future will be a Thread-based implementation, the traits should not require anything that limit such a trait to Thread-based implementations
  4. Transmission and reception APIs are decoupled

    • This allows for instances where receive and send_done callbacks should be delivered to different clients (ex: Server listening on all addresses but also sending messages from specific addresses)
    • Prevents send path from having to navigate the added complexity required for Thread to determine whether to forward received messages up the stack

Stack Diagram

IPv6 over ethernet:      Non-Thread 15.4:   Thread Stack:                                       Encapsulation Libraries
+-------------------+-------------------+----------------------------+
|                         Application                                |-------------------\
----------------------------------------+-------------+---+----------+                    \
|TCP Send| UDP Send |TCP Send| UDP Send |  | TCP Send |   | UDP Send |--\                  v
+--------+----------+--------+----------+  +----------+   +----------+   \               +------------+  +------------+
|     IP Send       |     IP Send       |  |         IP Send         |    \      ----->  | UDP Packet |  | TCP Packet |
|                   |                   |  +-------------------------+     \    /        +------------+  +------------+
|                   |                   |                            |      \  /         +-----------+
|                   |                   |                            |       -+------->  | IP Packet |
|                   |                   |       THREAD               |       /           +-----------+
| IP Send calls eth | IP Send calls 15.4|                   <--------|------>            +-------------------------+
| 6lowpan libs with | 6lowpan libs with |                            |       \ ------->  | 6lowpan compress_Packet |
| default values    | default values    |                            |        \          +-------------------------+
|                   |                   |                            |         \         +-------------------------+
|                   |                   +                +-----------|          ------>  | 6lowpan fragment_Packet |
|                   |                   |                | 15.4 Send |                   +-------------------------+
|-------------------|-------------------+----------------------------+
|     ethernet      |          IEEE 802.15.4 Link Layer              |
+-------------------+------------------------------------------------+

Notes on the stack:

  • IP messages sent via Thread networks are sent through Thread using an IP Send method that exposes only the parameters specified in the IP_Send trait. Other parameters of the message (6lowpan decisions, link layer parameters, many IP header options) are decided by Thread.
  • The stack provides an interface for the application layer to send raw IPv6 packets over Thread.
  • When the Thread control plane generates messages (MLE messages etc.), they are formatted using calls to the encapsulation libraries and then delivered to the 802.15.4 layer using the 15.4 send function
  • This stack design allows Thread to control header elements from transport down to link layer, and to set link layer security parameters and more as required for certain packers
  • The application can either directly send IP messages using the IP Send implementation exposed by the Thread stack or it can use the UDP Send and TCP send implementation exposed by the Thread stack. If the application uses the TCP or UDP send implementations it must use the transport packet library to insert its payload inside a packet and set certain header fields. The transport send method uses the IP Packet library to set certain IP fields before handing the packet off to Thread. Thread then sets other parameters at other layers as needed before sending the packet off via the 15.4 send function implemented for Thread.
  • Note that currently this design leaves it up to the application layer to decide what interface any given packet will be transmitted from. This is because currently we are working towards a minimum functional stack. However, once this is working we intend to add a layer below the application layer that would handle interface multiplexing by destination address via a forwarding table. This should be straightforward to add in to our current design.
  • This stack does not demonstrate a full set of functionality we are planning to implement now. Rather it demonstrates how this setup allows for multiple implementations of each layer based off of traits and libraries such that a flexible network stack can be configured, rather than creating a network stack designed such that applications can only use Thread.

Explanation of Queuing

Queuing happens at the application layer in this stack. The userland interface to the networking stack (described in greater detail in Networking_Userland.md) already handles queueing multiple packets sent from userland apps. In the kernel, any application which wishes to send multiple UDP packets must handle queueing itself, waiting for a send_done to return from the radio before calling send on the next packet in a series of packets.

List of Traits

This section describes a number of traits which must be implemented by any network stack. It is expected that multiple implementations of some of these traits may exist to allow for Tock to support more than just Thread networking.

Before discussing these traits - a note on buffers:

Prior implementations of the tock networking stack passed around references to 'static mut [u8] to pass packets along the stack. This is not ideal from a standpoint of wanting to be able to prevent as many errors as possible at compile time. The next iteration of code will pass 'typed' buffers up and down the stack. There are a number of packet library traits defined below (e.g. IPPacket, UDPPacket, etc.). Transport Layer traits will be implemented by a struct that will contain at least one field - a [u8] buffer with lifetime 'a. Lower level traits will simply contain payload fields that are Transport Level packet traits (thanks to a TransportPacket enum). This design allows for all buffers passed to be passed as type 'UDPPacket', 'IPPacket', etc. An added advantage of this design is that each buffer can easily be operated on using the library functions associated with this buffer type.

The traits below are organized by the network layer they would typically be associated with.

Transport Layer

Thus far, the only transport layer protocol implemented in Tock is UDP.

Documentation describing the structs and traits that define the UDP layer can be found in capsules/src/net/udp/(udp.rs, udp_send.rs, udp_recv.rs)

Additionally, a driver exists that provides a userland interface via which udp packets can be sent and received. This is described in greater detail in Networking_Userland.md

Network Stack Receive Path

  • The radio in the kernel has a single RxClient, which is set as the mac layer (awake_mac, typically)
  • The mac layer (i.e. AwakeMac) has a single RxClient, which is the mac_device(ieee802154::Framer::framer)
  • The Mac device has a single receive client - MuxMac (virtual MAC device).
  • The MuxMac can have multiple "users" which are of type MacUser
  • Any received packet is passed to ALL MacUsers, which are expected to filter packets themselves accordingly.
  • Right now, we initialize two MacUsers in the kernel (in main.rs/components). These are the 'radio_mac', which is the MacUser for the RadioDriver that enables the userland interface to directly send 802154 frames, and udp_mac, the mac layer that is ultimately associated with the udp userland interface.
  • The udp_mac MacUser has a single receive client, which is the sixlowpan_state struct
  • sixlowpan_state has a single rx_client, which in our case is a single struct that implements the ip_receive trait.
  • the ip_receive implementing struct (IP6RecvStruct) has a single client, which is udp_recv, a UDPReceive struct.
  • The UDPReceive struct is a field of the UDPDriver, which ultimately passes the packets up to userland.

So what are the implications of all this?

  1. Currently, any userland app could receive udp packets intended for anyone else if the app implmenets 6lowpan itself on the received raw frames.

  2. Currently, packets are only muxed at the Mac layer.

  3. Right now the IPReceive struct receives all IP packets sent to the MAC address of this device, and soon will drop all packets sent to non-local addresses. Right now, the device effectively only has one address anyway, as we only support 6lowpan over 15.4, and as we haven't implemented a loopback interface on the IP_send path. If, in the future, we implement IP forwarding on Tock, we will need to add an IPSend object to the IPReceiver which would then retransmit any packets received that were not destined for local addresses.

Explanation of Configuration

This section describes how the IP stack can be configured, including setting addresses and other parameters of the MAC layer.

  • Source IP address: An array of local interfaces on the device is contained in main.rs. Currently, this array contains two hardcoded addresses, and one address generated from the unique serial number on the sam4l.

  • Destination IP address: The destination IP address is configured by passing the address to the send_to() call when sending IPv6 packets.

  • src MAC address: This address is configured in main.rs. Currently, the src mac address for each device is configured by default to be a 16-bit short address representing the last 16 bits of the unique 120 bit serial number on the sam4l. However, userland apps can change the src address by calling ieee802154_set_address()

  • dst MAC address: This is currently a constant set in main.rs. (DST_MAC_ADDR). In the future this will change, once Tock implements IPv6 Neighbor Discovery.

  • src pan: This is set via a constant configured in main.rs (PAN_ID). The same constant is used for the dst pan.

  • dst pan: Same as src_pan. If we need to support use of the broadcast PAN as a dst_pan, this may change.

  • radio channel: Configured as a constant in main.rs (RADIO_CHANNEL).

Tock Userland Networking Design

This section describes the current userland interface for the networking stack on Tock. This section should serve as a description of the abstraction provided by libTock - what the exact system call interface looks like or how libTock or the kernel implements this functionality is out-of-scope of this document.

Overview

The Tock networking stack and libTock should attempt to expose a networking interface that is similar to the POSIX networking interface. The primary motivation for this design choice is that application programmers are used to the POSIX networking interface design, and significant amounts of code have already been written for POSIX-style network interfaces. By designing the libTock networking interface to be as similar to POSIX as possible, we hope to improve developer experience while enabling the easy transition of networking code to Tock.

Design

udp.c and udp.h in libtock-c/libtock define the userland interface to the Tock networking stack. These files interact with capsules/src/net/udp/driver.rs in the main tock repository. driver.rs implements an interface for sending and receiving UDP messages. It also exposes a list of interace addresses to the application layer. The primary functionality embedded in the UDP driver is within the allow(), subscribe(), and command() calls which can be made to the driver.

Details of this driver can be found in doc/syscalls folder

udp.c and udp.h in libtock-c make it easy to interact with this driver interface. Important functions available to userland apps written in c include:

udp_socket() - sets the port on which the app will receive udp packets, and sets the src_port of outgoing packets sent via that socket. Once socket binding is implemented in the kernel, this function will handle reserving ports to listen on and send from.

udp_close() - currently just returns success, but once socket binding has been implemented in the kernel, this function will handle freeing bound ports.

udp_send_to() - Sends a udp packet to a specified addr/port pair, returns the result of the tranmission once the radio has transmitted it (or once a failure has occured).

udp_recv_from_sync() - Pass an interface to listen on and an incoming source address to listen for. Sets up a callback to wait for a received packet, and yeilds until that callback is triggered. This function never returns if a packet is not received.

udp_recv_from() - Pass an interface to listen on and an incoming source address to listen for. However, this takes in a buffer to which the received packet should be placed, and returns the callback that will be triggered when a packet is received.

udp_list_ifaces() - Populates the passed pointer of ipv6 addresses with the available ipv6 addresses of the interfaces on the device. Right now this merely returns a constant hardcoded into the UDP driver, but should change to return the source IP addresses held in the network configuration file once that is created. Returns up to len addresses.

Other design notes:

The current design of the driver has a few limitations, these include:

  • Currently, any app can listen on any address/port pair

  • The current tx implementation allows for starvation, e.g. an app with an earlier app ID can starve a later ID by sending constantly.

POSIX Socket API Functions

Below is a fairly comprehensive overview of the POSIX networking socket interface. Note that much of this functionality pertains to TCP or connection- based protocols, which we currently do not handle.

  • socket(domain, type, protocol) -> int fd

    • domain: AF_INET, AF_INET6, AF_UNIX
    • type: SOCK_STREAM (TCP), SOCK_DGRAM (UDP), SOCK_SEQPACKET (?), SOCK_RAW
    • protocol: IPPROTO_TCP, IPPROTO_SCTP, IPPROTO_UDP, IPPROTO_DCCP
  • bind(socketfd, my_addr, addrlen) -> int success

    • socketfd: Socket file descriptor to bind to
    • my_addr: Address to bind on
    • addrlen: Length of address
  • listen(socketfd, backlog) -> int success

    • socketfd: Socket file descriptor
    • backlog: Number of pending connections to be queued

    Only necessary for stream-oriented data modes

  • connect(socketfd, addr, addrlen) -> int success

    • socketfd: Socket file descriptor to connect with
    • addr: Address to connect to (server protocol address)
    • addrlen: Length of address

    When used with connectionless protocols, defines the remote address for sending and receiving data, allowing the use of functions such as send() and recv() and preventing the reception of datagrams from other sources.

  • accept(socketfd, cliaddr, addrlen) -> int success

    • socketfd: Socket file descriptor of the listening socket that has the connection queued
    • cliaddr: A pointer to an address to receive the client's address information
    • addrlen: Specifies the size of the client address structure
  • send(socketfd, buffer, length, flags) -> int success

    • socketfd: Socket file descriptor to send on
    • buffer: Buffer to send
    • length: Length of buffer to send
    • flags: Various flags for the transmission

    Note that the send() function will only send a message when the socketfd is connected (including for connectionless sockets)

  • sendto(socketfd, buffer, length, flags, dst_addr, addrlen) -> int success

    • socketfd: Socket file descriptor to send on
    • buffer: Buffer to send
    • length: Length of buffer to send
    • flags: Various flags for the transmission
    • dst_addr: Address to send to (ignored for connection type sockets)
    • addrlen: Length of dst_addr

    Note that if the socket is a connection type, dst_addr will be ignored.

  • recv(socketfd, buffer, length, flags)

    • socketfd: Socket file descriptor to receive on
    • buffer: Buffer where the message will be stored
    • length: Length of buffer
    • flags: Type of message reception

    Typically used with connected sockets as it does not permit the application to retrieve the source address of received data.

  • recvfrom(socketfd, buffer, length, flags, address, addrlen)

    • socketfd: Socket file descriptor to receive on
    • buffer: Buffer to store the message
    • length: Length of the buffer
    • flags: Various flags for reception
    • address: Pointer to a structure to store the sending address
    • addrlen: Length of address structure

    Normally used with connectionless sockets as it permits the application to retrieve the source address of received data

  • close(socketfd)

    • socketfd: Socket file descriptor to delete
  • gethostbyname()/gethostbyaddr() Legacy interfaces for resolving host names and addresses

  • select(nfds, readfds, writefds, errorfds, timeout)

    • nfds: The range of file descriptors to be tested (0..nfds)
    • readfds: On input, specifies file descriptors to be checked to see if they are ready to be read. On output, indicates which file descriptors are ready to be read
    • writefds: Same as readfds, but for writing
    • errorfds: Same as readfds, writefds, but for errors
    • timeout: A structure that indicates the max amount of time to block if no file descriptors are ready. If None, blocks indefinitely
  • poll(fds, nfds, timeout)

    • fds: Array of structures for file descriptors to be checked. The array members are structures which contain the file descriptor, and events to check for plus areas to write which events occurred
    • nfds: Number of elements in the fds array
    • timeout: If 0 return immediately, or if -1 block indefinitely. Otherwise, wait at least timeout milliseconds for an event to occur
  • getsockopt()/setsockopt()

Tock Userland API

Below is a list of desired functionality for the libTock userland API.

  • struct sock_addr_t ipv6_addr_t: IPv6 address (single or ANY) port_t: Transport level port (single or ANY)

  • struct sock_handle_t Opaque to the user; allocated in userland by malloc (or on the stack)

  • list_ifaces() -> iface[] ifaces: A list of ipv6_addr_t, name pairs corresponding to all interfaces available

  • udp_socket(sock_handle_t, sock_addr_t) -> int socketfd socketfd: Socket object to be initialized as a UDP socket with the given address information sock_addr_t: Contains an IPv6 address and a port

  • udp_close(sock_handle_t) sock_handle_t: Socket to close

  • send_to(sock_handle_t, buffer, length, sock_addr_t)

    • sock_handle_t: Socket to send using
    • buffer: Buffer to send
    • length: Length of buffer to send
    • sock_addr_t: Address struct (IPv6 address, port) to send the packet from
  • recv_from(sock_handle_t, buffer, length, sock_addr_t)

    • sock_handle_t: Receiving socket
    • buffer: Buffer to receive into
    • length: Length of buffer
    • sock_addr_t: Struct where the kernel writes the received packet's sender information

Differences Between the APIs

There are two major differences between the proposed Tock APIs and the standard POSIX APIs. First, the POSIX APIs must support connection-based protocols such as TCP, whereas the Tock API is only concerned with connectionless, datagram based protocols. Second, the POSIX interface has a concept of the sock_addr_t structure, which is used to encapsulate information such as port numbers to bind on and interface addresses. This makes bind_to_port redundant in POSIX, as we can simply set the port number in the sock_addr_t struct when binding. I think one of the major questions is whether to adopt this convention, or to use the above definitions for at least the first iteration.

Example: ip_sense

An example use of the userland networking stack can be found in libtock-c/examples/ip_sense

Implementation Details for potential future Thread implementation

This section was written when the networking stack was incomplete, and aspects may be outdated. This goes for all sections following this point in the document.

The Thread specification determines an entire control plane that spans many different layers in the OSI networking model. To adequately understand the interactions and dependencies between these layers' behaviors, it might help to trace several types of messages and see how each layer processes the different types of messages. Let's trace carefully the way OpenThread handles messages.

We begin with the most fundamental message: a data-plane message that does not interact with the Thread control plane save for passing through a Thread-defined network interface. Note that some of the procedures in the below traces will not make sense when taken independently: the responsibility-passing will only make sense when all the message types are taken as a whole. Additionally, no claim is made as to whether or not this sequence of callbacks is the optimal way to express these interactions: it is just OpenThread's way of doing it.

Data plane: IPv6 datagram

  1. Upper layer (application) wants to send a payload
  • Provides payload
  • Specifies the IP6 interface to send it on (via some identifier)
  • Specifies protocol (IP6 next header field)
  • Specifies destination IP6 address
  • Possibly doesn't specify source IP6 address
  1. IP6 interface dispatcher (with knowledge of all the interfaces) fills in the IP6 header and produces an IP6 message
  • Payload, protocol, and destination address used directly from the upper layer
  • Source address is more complicated
    • If the address is specified and is not multicast, it is used directly
    • If the address is unspecified or multicast, source address is determined from the specific IP6 selected AND the destination address via a matching scheme on the addresses associated with the interface.
  • Now that the addresses are determined, the IP6 layer computes the pseudoheader checksum.
    • If the application layer's payload has a checksum that includes the pseudoheader (UDP, ICMP6), this partial checksum is now used to update the checksum field in the payload.
  1. The actual IP6 interface (Thread-controlled) tries to send that message
  • First step is to determine whether the message can be sent immediately or not (sleepy child or not). This passes the message to the scheduler. This is important for sleepy children where there is a control scheme that determines when messages are sent.
  • Next, determine the MAC src/dest addresses.
    • If this is a direct transmission, there is a source matching scheme to determine if the destination address used should be short or long. The same length is used for the source MAC address, obtained from the MAC interface.
  • Notify the MAC layer to notify you that your message can be sent.
  1. The MAC layer schedules its transmissions and determines that it can send the above message
  • MAC sets the transmission power
  • MAC sets the channel differently depending on the message type
  1. The IP6 interface fills up the frame. This is the chance for the IP6 interface to do things like fragmentation, retransmission, and so on. The MAC layer just wants a frame.
  • XXX: The IP6 interface fills up the MAC header. This should really be the responsibility of the MAC layer. Anyway, here is what is done:
    • Channel, source PAN ID, destination PAN ID, and security modes are determined by message type. Note that the channel set by the MAC layer is sometimes overwritten.
    • A mesh extension header is added for some messages. (eg. indirect transmissions)
  • The IP6 message is then 6LoWPAN-compressed/fragmented into the payload section of the frame.
  1. The MAC layer receives the raw frame and tries to send it
  • MAC sets the sequence number of the frame (from the previous sequence number for the correct link neighbor), if it is not a retransmission
  • The frame is secured if needed. This is another can of worms:
    • Frame counter is dependent on the link neighbor and whether or not the frame is a retransmission
    • Key is dependent on which key id mode is selected, and also the link neighbor's key sequence
    • Key sequence != frame counter
    • One particular mode requires using a key, source and frame counter that is a Thread-defined constant.
  • The frame is transmitted, an ACK is waited for, and the process completes.

As you can see, the data dependencies are nowhere as clean as the OSI model dictates. The complexity mostly arises because

  • Layer 4 checksum can include IPv6 pseudoheader
  • IP6 source address (mesh local? link local? multicast?) is determined by interface and destination address
  • MAC src/dest addresses are dependent on the next device on the route to the IP6 destination address
  • Channel, src/dest PAN ID, security is dependent on message type
  • Mesh extension header presence is dependent on message type
  • Sequence number is dependent on message type and destination

Note that all of the MAC layer dependencies in step 5 can be pre-decided so that the MAC layer is the only one responsible for writing the MAC header.

This gives a pretty good overview of what minimally needs to be done to even be able to send normal IPv6 datagrams, but does not cover all of Thread's complexities. Next, we look at some control-plane messages.

Control plane: MLE messages

  1. The MLE layer encapsulates its messages in UDP on a constant port
  • Security is determined by MLE message type. If MLE-layer security is required, the frame is secured using the same CCM* encryption scheme used in the MAC layer, but with a different key discipline.
  • MLE key sequence is global across a single Thread device
  • MLE sets IP6 source address to the interface's link local address
  1. This UDP-encapsulated MLE message is sent to the IP6 dispatch again
  2. The actual IP6 interface (Thread-controlled) tries to send that message
  3. The MAC layer schedules the transmission
  4. The IP6 interface fills up the frame.
  • MLE messages disable link-layer security when MLE-layer security is present. However, if link-layer security is disabled and the MLE message doesn't fit in a single frame, link-layer security is enabled so that fragmentation can proceed.
  1. The MAC layer receives the raw frame and tries to send it

The only cross-layer dependency introduced by the MLE layer is the dependency between MLE-layer security and link-layer security. Whether or not the MLE layer sits atop an actual UDP socket is an implementation detail.

Control plane: Mesh forwarding

If Thread REED devices are to be eventually supported in Tock, then we must also consider this case. If a frame is sent to a router which is not its final destination, then the router must forward that message to the next hop.

  1. The MAC layer receives a frame, decrypts it and passes it to the IP6 interface
  2. The IP6 reception reads the frame and realizes that it is an indirect transmission that has to be forwarded again
  • The frame must contain a mesh header, and the HopsLeft field in it should be decremented
  • The rest of the payload remains the same
  • Hence, the IP6 interface needs to send a raw 6LoWPAN-compressed frame
  1. The IP6 transmission interface receives a raw 6LoWPAN-compressed frame to be transmitted again
  • This frame must still be scheduled: it might be destined for a sleepy device that is not yet awake
  1. The MAC layer schedules the transmission
  2. The IP6 transmission interface copies the frame to be retransmitted verbatim, but with the modified mesh header and a new MAC header
  3. The MAC layer receives the raw frame and tries to send it

This example shows that the IP6 transmission interface may need to handle more message types than just IP6 datagrams: there is a case where it is convenient to be able to handle a datagram that is already 6LoWPAN compressed.

Control plane: MAC data polling

From time to time, a sleepy edge device will wake up and begin polling its parent to check if any frames are available for it. This is done via a MAC command frame, which must still be sent through the transmission pipeline with link security enabled (Key ID mode 1). OpenThread does this by routing it through the IP6 transmission interface, which arguably isn't the right choice.

  1. Data poll manager send a data poll message directly to the IP6 transmission interface, skipping the IP6 dispatch
  2. The IP6 transmission interface notices the different type of message, which always warrants a direct transmission.
  3. The MAC layer schedules the transmission
  4. The IP6 transmission interface fills in the frame
  • The MAC dest is set to the parent of this node and the MAC src is set to be the same length as the address of the parent
  • The payload is filled up to contain the Data Request MAC command
  • The MAC security level and key ID mode is also fixed for MAC commands under the Thread specification
  1. The MAC layer secures the frame and sends it out

We could imagine giving the data poll manager direct access as a client of the MAC layer to avoid having to shuffle data through the IP6 transmission interface. This is only justified because MAC command frames are never 6LoWPAN-compressed or fragmented, nor do they depend on the IP6 interface in any way.

Control plane: Child supervision

This type of message behaves similarly to the MAC data polls. The message is essentially and empty MAC frame, but OpenThread chooses to also route it through the IP6 transmission interface. It would be far better to allow a child supervision implementation to be a direct client of the MAC interface.

Control plane: Joiner entrust and MLE announce

These two message types are also explicitly marked, because they require a specific Key ID Mode to be selected when producing the frame for the MAC interface.

Caveat about MAC layer security

So far, it seems like we can expect the MAC layer to have no cross-layer dependencies: it receives frames with a completely specified description of how they are to be secured and transmitted, and just does so. However, this is not entirely the case.

When the frame is being secured, the key ID mode has been set by the upper layers as described above, and this key ID mode is used to select between a few different key disciplines. For example, mode 0 is only used by Joiner entrust messages and uses the Thread KEK sequence. Mode 1 uses the MAC key sequence and Mode 2 is a constant key used only in MLE announce messages. Hence, this key ID mode selection is actually enabling an upper layer to determine the specific key being used in the link layer.

Note that we cannot just reduce this dependency by allowing the upper layer to specify the key used in MAC encryption. During frame reception, the MAC layer itself has to know which key to use in order to decrypt the frames correctly.

構成

Tockは種々のプラットフォーム(複数のアーキテクチャと利用可能なさまざまなペリフェラル) において、また、複数のユースケース(たとえば、「本番環境」とさまざまなレベルのデバッグ 詳細のデバッグビルド)を念頭に置いて動作することを意図しています。

Tockでは構成は"ifdef"条件付きコードの落とし穴(テストがやりにくくなります)を 避けるためにいくつかの原則に従っています。これは現在、2つの方法で行われています。

  • コードの複数パッケージへの分離。各抽象化レベル(コアカーネル、CPU アーキテクチャ、チップ、ボード)は各自パッケージを持ち、ボードの構成は関連する チップに基づいて、ボード上で利用可能なペリフェラルに関連するドライバを宣言する ことで行われます。詳細はコンパイルドキュメントを参照して ください。

  • カスタムカーネル構成。カーネルの詳細な構成(たとえば、syscallsトレースの デバッグ出力の有効化など)を容易にするためにconfig構造体が kernel/src/config.rsで定義されています。構成を変更するには、このファイルで 定義されている静的なconstオブジェクトの値を変更します。構成を使用するには、 単に値を読み込むだけです。たとえば、ブール値の構成を使用するにはif文を使用 します。構成がconstであるという事実により、コンパイラは構文と型をチェック する一方で、(この設定のコストがゼロになるように) 最適化によりデッドコードを 削除することができます。

Tockシステムコール

このフォルダには、ユーザ空間とカーネルの間のインターフェースに関する詳細な ドキュメントが含まれています。それは、ABIインターフェース、カーネルが提供する システムコール、(allowschedulecommandを使用する)ドライバ固有の インターフェースの詳細です。一般的なシステムコールの詳細については、 システムコールを参照してください。

システムコールのバイナリインターフェース

アプリケーションバイナリインターフェースの詳細。

コアカーネル提供のシステムコール

  • memop: メモリ関連の操作。

カプセル提供のドライバ

恒久的なドライバ番号が割り当てられているドライバ型は、以下の表の通りです。 "1.0"の列は、Tock 1.0リリースでドライバが安定しているか否かを示します ("✓"は安定を示します)。

基本

1.0ドライバ番号ドライバ記述 
0x00000Alarmユーザ空間のタイマーとして使用される
0x00001ConsoleUART console
0x00002LEDボード上のLEDを制御する
0x00003Buttonボード上のボタンから割り込みを取得する
0x00005ADCアナログ・デジタルコンバータ
0x00006DACデジタル・アナログコンバータ
0x00007AnalogComparatorアナログコンパレータ
0x00008Low-Level Debug低レベルデバッグツール

カーネル

1.0ドライバ番号ドライバ記述 
0x10000IPCプロセス間通信

Hardware Access

1.0ドライバ番号ドライバ記述 
0x00004GPIOGPIOピンの設定と読み取り
0x20000UARTUART
0x20001SPI生のSPIマスタインターフェース
0x20002SPI Slave生のSPIスレーブインターフェース
0x20003I2C Master生のI2Cマスタインターフェース
0x20004I2C Slave生のI2Cスレーブインターフェース
0x20005USBUSBインターフェース

注: GPIOはTock 2.0で番号が付け直される予定です。

無線通信

1.0ドライバ番号ドライバ記述 
0x30000BLEBluetooth Low Energy
0x30001802.15.4IEEE 802.15.4
0x30002UDPUDP/6LoWPANインターフェース

暗号

1.0ドライバ番号ドライバ記述 
0x40000AESAES共通鍵暗号
0x40001RNG乱数生成器
0x40002CRC巡回冗長検査計算

ストレージ

1.0ドライバ番号ドライバ記述 
0x50000App Flashアプリが各自のFlashに書き込みを可能にする
0x50001Nonvolatile Storage永続ストレージ用の汎用インターフェース
0x50002SDCardSDカードへの生ブロックアクセス

センサ

1.0ドライバ番号ドライバ記述 
0x60000Ambient Temp.環境温度(摂氏)
0x60001Humidity湿度センサ(%)
0x60002Luminance環境光センサ(ルーメン)
0x60003Pressure圧力センサ
0x60004Ninedof仮想加速度計/磁力計/ジャイロスコープ

センサIC

1.0ドライバ番号ドライバ記述 
0x70000TSL2561光センサ
0x70001TMP006温度センサ
0x70004LPS25HB圧力センサ
0x70005L3GD203軸ジャイロスコープ、温度センサ
0x70006LSM303DLHC3軸加速度計、磁力計、温度センサ

その他のIC

1.0ドライバ番号ドライバ記述 
0x80000LTC294XバッテリゲージIC
0x80001MAX17205バッテリゲージIC
0x80002PCA9544AI2Cアドレス多重化装置
0x80003GPIO Async非同期GPIOピン
0x80004nRF51822nRF51822 BLE SoCへのnRF シリアル化リンク
0x80005HD44780LCD HD44780カプセル

Memop

Overview

memop is a core Tock syscall. Most memop syscalls are read-only information such as where a process was loaded in flash and ram. Processes also use memop to grow the application heap (brk and sbrk) or to provide optional debugging information such as the top of the stack for processes that manage their own stack.

All memop calls pass an operation type as the first parameter. Some include an argument in the second parameter:


#![allow(unused)]
fn main() {
memop(op_type: u32, argument: u32) -> [[ VARIES ]] as u32
}

Memory Operations

  • Operation type 0: brk

    Description: Change the location of the program break to the absolute address provided.

    Argument 1 as *u8: Address of the new program break (aka maximum accessible value).

    Returns ReturnCode as u32: SUCCESS or ENOMEM.

  • Operation type 1: sbrk

    Description: Move the program break up or down by the specified number of bytes.

    Argument 1 as i32: Number of bytes to move the program break.

    Returns as *u8: The previous program break (the start of the newly allocated memory) or ENOMEM.

  • Operation type 2: Memory start

    Description: Get the address of the start of the application's RAM allocation.

    Argument 1: unused

    Returns as *u8: The address.

  • Operation type 3: Memory end

    Description: Get the address pointing to the first address after the end of the application's RAM allocation.

    Argument 1: unused

    Returns as *u8: The address.

  • Operation type 4: Flash start

    Description: Get the address of the start of the application's flash region. This is where the TBF header is located.

    Argument 1: unused

    Returns as *u8: The address.

  • Operation type 5: Flash end

    Description: Get the address pointing to the first address after the end of the application's flash region.

    Argument 1: unused

    Returns as *u8: The address.

  • Operation type 6: Grant start

    Description: Get the address of the lowest address of the grant region for the app. (Note: the grant end is by definition the memory end, so there is no corresponding grant end syscall.)

    Argument 1: unused

    Returns as *u8: The address.

  • Operation type 7: Flash regions

    Description: Get the number of writeable flash regions defined in the header of this app.

    Argument 1: unused

    Returns as u32: The number of regions.

  • Operation type 8: Flash region start address

    Description: Get the start address of the writeable region indexed from 0.

    Argument 1 as u32: Which region.

    Returns as *u8: The start address of the selected region, or (void*) -1 if the requested region does not exist.

  • Operation type 9: Flash region end address

    Description: Get the end address of the writeable region indexed from 0.

    Argument 1 as u32: Which region.

    Returns as *u8: The address immediately after the selected region, or (void*) -1 if the requested region does not exist.

  • Operation type 10: (debug) Specify stack location

    Description: Specify the top of the application stack.

    Argument 1 as *const u8: Address of the stack top.

    Returns ReturnCode as u32: Always SUCCESS.

  • Operation type 11: (debug) Specify heap location

    Description: Specify the start of the application heap.

    Argument 1 as *const u8: Address of the heap start.

    Returns ReturnCode as u32: Always SUCCESS.


driver number: 0x00000

Alarm

Overview

The alarm driver exposes a wrapping hardware counter to processes. An alarm can report the current tic value and notify via a callback when the counter reaches a certain value.

The alarm's frequency is platform-specific, but must be at least 1kHz.

Command

  • Command number: 0

    Description: Does the driver exist?

    Argument 1: unused

    Argument 2: unused

    Returns: The number of concurrent notifications supported per process, 0 if unbounded, otherwise ENODEVICE

  • Command number: 1

    Description: Returns the clock frequency of the alarm.

    Argument 1: unused

    Argument 2: unused

    Returns: The frequency in Hertz.

  • Command number: 2

    Description: Read the current counter tic value.

    Argument 1: unused

    Argument 2: unused

    Returns: The counter value in tics.

  • Command number: 3

    Description: Stop an outstanding alarm notification.

    Argument 1: Alarm notification identifer as returned from command 4.

    Argument 2: unused

    Returns: EINVAL if the notification identifier is invalid, EALREADY if the notification is already disabled, or SUCCESS.

  • Command number: 4

    Description: Set an alarm notification for a counter value. Notification invokes the callback set with subscribe.

    Argument 1: The counter tic value to notify.

    Argument 2: unused

    Returns: EINVAL if the notification identifier is invalid, EALREADY if the notification is already disabled, or SUCCESS.

  • Command number: 5 (experimental)

    Description: Set an alarm notification for a counter value relative to the current value. Notification invokes the callback set with subscribe.

    Argument 1: The relative counter tic value to notify.

    Argument 2: unused

    Returns: EINVAL if the notification identifier is invalid, EALREADY if the notification is already disabled, or SUCCESS.

Subscribe

  • Subscribe number: 0

    Description: Subscribe to alarm notifications.

    Callback signature: The callback recieves two arguments: the counter tic value when the alarm notifiation expired and the notification identifier returned from command 4. The value of the remaining argument is undefined.

    Returns: SUCCESS if the subscribe was successful or ENOMEM if the driver failed to allocate memory for the transaction.


driver number: 0x00001

Console

Overview

The console driver allows the process to write buffers to serial device. To write a buffer, a process must share the buffer using allow then initiate the write using a command call. It may also using subscribe to receive a callback when the write has completed.

Once the write has completed, the buffer shared with the driver is released, so can be deallocated by the process. This also means that it is necessary to share a buffer for every write transaction, even if it's the same buffer.

Command

  • Command number: 0

    Description: Does the driver exist?

    Argument 1: unused

    Argument 2: unused

    Returns: SUCCESS if it exists, otherwise ENODEVICE

  • Command number: 1

    Description: Initiate a write transaction of a buffer shared using allow. At the end of the transaction, a callback will be delivered if the process has subscribed.

    Argument 1: The maximum number of bytes to write.

    Argument 2: unused

    Returns: SUCCESS if the command was successful, EBUSY if no buffer was shared, or ENOMEM if the driver failed to allocate memory for the transaction.

  • Command number: 2

    Description: Initiate a read transaction into a buffer shared using allow. At the end of the transaction, a callback will be delivered if the process has subscribed to read events using subscribe number 2.

    Argument 1: The maximum number of bytes to write.

    Argument 2: unused

    Returns: SUCCESS if the command was successful, EBUSY if no buffer was shared, or ENOMEM if the driver failed to allocate memory for the transaction.

  • Command number: 3

    Description: Abort any ongoing read transactions. Any received bytes will be delivered via callback if the process has subscribed to read events using subscribe number 2.

    Argument 1: unused

    Argument 2: unused

    Returns: SUCCESS if the command was successful, EBUSY if no buffer was shared, or ENOMEM if the driver failed to allocate memory for the transaction.

Subscribe

  • Subscribe number: 1

    Description: Subscribe to write transaction completion event. The callback will be called whenever a write transaction completes.

    Callback signature: The callback receives a single argument, the number of bytes written in the transaction. The value of the remaining arguments is undefined.

    Returns: SUCCESS if the subscribe was successful or ENOMEM if the driver failed to allocate memory for the transaction.

  • Subscribe number: 2

    Description: Subscribe to read transaction completion event. The callback will be called whenever a read transaction completes.

    Callback signature: The callback receives a single argument, the number of bytes read in the transaction. The value of the remaining arguments is undefined.

    Returns: SUCCESS if the subscribe was successful or ENOMEM if the driver failed to allocate memory for the transaction.

Allow

  • Allow number: 1

    Description: Sets a shared buffer to be used as a source of data for the next write transaction. A shared buffer is released if it is replaced by a subsequent call and after a write transaction is completed. Replacing the buffer after beginning a write transaction but before receiving a completion callback is undefined (most likely either the original buffer or new buffer will be written in its entirety but not both).

    Returns: SUCCESS if the subscribe was successful or ENOMEM if the driver failed to allocate memory for the transaction.

  • Allow number: 2

    Description: Sets a shared buffer to be read into by the next read transaction. A shared buffer is released in two cases: if it is replaced by a subsequent call or after a read transaction is completed. Replacing the buffer after beginning a read transaction but before receiving a completion callback is undefined (most likely either the original buffer or new buffer will be sent in its entirety but not both).

    Returns: SUCCESS if the subscribe was successful or ENOMEM if the driver failed to allocate memory for the transaction.


driver number: 0x00002

LEDs

Overview

The LEDs driver provides userspace with synchronous control of an array of discrete LEDs. The LEDs can be turned on, off, and toggled.

LEDs are indexed in the array starting at 0. The order of the LEDs and the mapping between indexes and actual LEDs is set by the kernel in the board's main file.

Command

  • Command number: 0

    Description: How many LEDs are supported on this board.

    Argument 1: unused

    Argument 2: unused

    Returns: The number of LEDs on the board, or ENODEVICE if this driver is not present on the board.

  • Command number: 1

    Description: Turn on an LED.

    Argument 1: The index of the LED to turn on, starting at 0.

    Argument 2: unused

    Returns: SUCCESS if the LED index is valid, EINVAL otherwise.

  • Command number: 2

    Description: Turn off an LED.

    Argument 1: The index of the LED to turn off, starting at 0.

    Argument 2: unused

    Returns: SUCCESS if the LED index is valid, EINVAL otherwise.

  • Command number: 3

    Description: Toggle an LED. If the LED is currently on it will be turned off, and vice-versa.

    Argument 1: The index of the LED to toggle, starting at 0.

    Argument 2: unused

    Returns: SUCCESS if the LED index is valid, EINVAL otherwise.

Subscribe

Unused for the LED driver. Will always return ENOSUPPORT.

Allow

Unused for the LED driver. Will always return ENOSUPPORT.


driver number: 0x00003

Buttons

Overview

The buttons driver allows userspace to receive callbacks when buttons on the board are pressed (and depressed). This driver can support multiple buttons.

Buttons are indexed in the array starting at 0. The order of the buttons and the mapping between indexes and actual buttons is set by the kernel in the board's main file.

Command

  • Command number: 0

    Description: How many buttons are supported on this board.

    Argument 1: unused

    Argument 2: unused

    Returns: The number of buttons on the board, or ENODEVICE if this driver is not present on the board.

  • Command number: 1

    Description: Enable interrupts for a button. The interrupts will occur both when the button is pressed and depressed. The callback will indicate which event occurred. This command will succeed even if a callback is not registered yet.

    Argument 1: The index of the button to enable interrupts for, starting at

    Argument 2: unused

    Returns: SUCCESS if the command was successful, ENOMEM if the driver cannot support another app, and EINVAL if the app is somehow invalid.

  • Command number: 2

    Description: Disable the interrupt for a button. This will not remove the callback (if one is set).

    Argument 1: The index of the button to disable interrupts for, starting at

    Argument 2: unused

    Returns: SUCCESS if the command was successful, ENOMEM if the driver cannot support another app, and EINVAL if the app is somehow invalid.

  • Command number: 3

    Description: Read the current state of the button.

    Argument 1: The index of the button to read, starting at 0.

    Argument 2: unused

    Returns: 0 if the button is not currently pressed, and 1 button is currently being pressed.

Subscribe

  • Subscribe number: 0

    Description: Subscribe a callback that will fire when any button is pressed or depressed. Registering the callback does not have an effect on whether any button interrupts are enabled.

    Callback signature: The callback receives two arguments. The first is the index of the button that was pressed or depressed, and the second is whether the button was pressed or depressed. If the button was pressed, the second value will be a 1, if the button was released the value will be a 0.

    Returns: SUCCESS if the subscribe was successful, ENOMEM if the driver cannot support another app, and EINVAL if the app is somehow invalid.

Allow

Unused for the LED driver. Will always return ENOSUPPORT.


driver number: 0x00004

GPIO

Overview

The GPIO driver allows userspace to synchronously control the output and receive callbacks on changes in the input for a set of GPIO pins.

This is a low-level GPIO driver designed to export "hardware-like" control of GPIO pins to userspace. Not all platforms will expose all or even any pins using this interface to userspace.

Unstable: As this is a low-level interface, the mapping of GPIO pins is currently unstable and unspecified. Users of this driver must consult their board for a mapping from pin identifiers used in this driver to actual hardware pins. This mapping is currently subject to change.

Command

  • Command number: 0

    Description: Whether GPIO pins are exported by this board.

    Argument 1: unused

    Argument 2: unused

    Returns: Unstable: Most boards return the number of GPIO pins available, however users should consult their board for details of this return value. ENODEVICE if this driver is not present on the board.

  • Command number: 1

    Description: Enable output on a GPIO pin. After enabling output, output-related operations on the pin (set, clear, toggle) are available. Using input operations on a pin in this state is undefined.

    Argument 1: The identifier of the GPIO pin to enable output for.

    Argument 2: unused

    Returns: SUCCESS if the command was successful, and EINVAL if the argument refers to a non-existent pin.

  • Command number: 2

    Description: Set the output of a GPIO pin (high). Using this command without first enabling output is undefined.

    Argument 1: The identifier of the GPIO pin to set.

    Argument 2: unused

    Returns: SUCCESS if the pin identifier is valid, EINVAL otherwise.

  • Command number: 3

    Description: Clear the output of a GPIO pin (low). Using this command without first enabling output is undefined.

    Argument 1: The identifier of the GPIO pin to clear.

    Argument 2: unused

    Returns: SUCCESS if the pin identifier is valid, EINVAL otherwise.

  • Command number: 4

    Description: Toggle the output of a GPIO pin. If the pin was previously high, this operation clears it. If it was previously low, sets it. Using this command without first enabling output is undefined.

    Argument 1: The identifier of the GPIO pin to toggle.

    Argument 2: unused

    Returns: SUCCESS if the pin identifier is valid, EINVAL otherwise.

  • Command number: 5

    Description: Enable and configure input on a GPIO pin. After enabling input, input-related operations on the pin (e.g. read) are available. Using output operations on a pin in this state is undefined.

    Argument 1: The identifier of the GPIO pin to toggle.

    Argument 2: requested resistor to attach to the pin: 0 for pull-none, 1 for pull-up, or 2 for pull-down. Other values are undefined.

    Returns: SUCCESS if the pin identifier is valid, EINVAL if it is invalid, and ENOSUPPORT if the resistor configuration is not supported by the hardware. If any error is returned, no state will be changed.

  • Command number: 6

    Description: Read the current value of a GPIO pin.

    Argument 1: The identifier of the GPIO pin to read.

    Argument 2: unused

    Returns: EINVAL if the identifier of the pin is invalid, 1 if the value of the pin is high or 0 if it is low.

  • Command number: 7

    Description: Configure interrupts on a GPIO pin. After enabling interrupts, the callback set in subscribe will be called when the pin level changes. Using this command without first enabling input is undefined.

    Argument 1: The identifier of the GPIO pin to read.

    Argument 2: Indicates which events trigger callbacks: 0 for either edge, 1 for rising edge, or 2 for falling edge. Other values are undefined.

    Returns: SUCCESS if the pin identifier is valid, EINVAL if it is invalid, and ENOSUPPORT if an invalid interrupt mode is passed in the configuration field of the argument. If any error is returned, no state will be changed.

Subscribe

  • Subscribe number: 0

    Description: Subscribe a callback that will fire when any GPIO pin whose interrupts have been enabled changes level. Registering the callback does not have an effect on whether any GPIO pin interrupts are enabled.

    Callback signature: The callback receives two arguments. The first is the identifier of the GPIO pin whose level has changed, and the second is the value of the pin when the interrupt occurred. The second argument has the same semantics as the return value for the read command: 0 for low, 1 for high.

    Returns: SUCCESS if the subscribe was successful, ENOMEM if the driver cannot support another app, and EINVAL if the app is somehow invalid.

Allow

Unused for the GPIO driver. Will always return ENOSUPPORT.


driver number: 0x00005

ADC (Analog-to-Digital Converter)

Overview

The ADC driver allows userspace to measure analog signals. The signals, such as a sensor measurement or voltage level, are usually connected to external pins on the microcontroller, referred to as channels, and this driver supports selecting which channel to measure from. Channels available to userspace are selected by the board configuration, and are indexed starting from zero. The number of bits of data in each sample and the possible voltage range of each sample are chip specific.

The ADC driver is capable of requesting single samples, single samples repeated at a specified frequency, a buffer full of samples at a specified frequency, and continuously sampling at a specified frequency. The minimum and maximum sampling frequencies are chip specific.

Command

  • Command number: 0

    Description: How many ADC channels are supported on this board.

    Argument 1: Unused.

    Argument 2: unused

    Returns: The number of channels on the board, or ENODEVICE if this driver is not present on the board.

  • Command number: 1

    Description: Measure the analog value of a single channel once. The callback will return the sample. This command will succeed even if a callback is not registered yet.

    Argument 1: The index of the channel to sample, starting at 0.

    Argument 2: unused

    Returns: SUCCESS if the command was successful, EBUSY if the ADC is already sampling a channel, and EINVAL if the channel index is invalid. FAIL may also be returned if the hardware has a fault.

  • Command number: 2

    Description: Measure the analog value of a single channel repeatedly. The callback will return each sample value individually. This command will succeed even if a callback is not registered yet.

    Argument 1: The index of the channel to sample, starting at 0.

    Argument 2: The frequency at which to sample the value.

    Returns: SUCCESS if the command was successful, EBUSY if the ADC is already sampling a channel, and EINVAL if the channel index is invalid or the frequency is outside of the acceptable range. FAIL may also be returned if the hardware has a fault.

  • Command number: 3

    Description: Measure the analog value of a single channel repeatedly, filling a buffer with data before sending a callback. The callback will return the buffer of samples. This command will succeed even if a callback is not registered yet. A buffer must have previously been provided through an allow call before this command will succeed.

    Argument 1: The index of the channel to sample, starting at 0.

    Argument 2: The frequency at which to sample the value.

    Returns: SUCCESS if the command was successful, EBUSY if the ADC is already sampling a channel, ENOMEM if a buffer has not been provided, and EINVAL if the channel index is invalid or the frequency is outside of the acceptable range. FAIL may also be returned if the hardware has a fault.

  • Command number: 4

    Description: Measure the analog value of a single channel continuously. Two buffers must be provided by allow calls before this command will succeed. The buffers will be filled with samples in an alternating fashion, with the callback returning the buffer full of samples. Special care must be taken when using this command to ensure that the buffer sizes are large enough for the specified sampling frequency that all samples can be read before the next buffer is filled with samples. This command will succeed even if a callback is not registered yet.

    Argument 1: The index of the channel to sample, starting at 0.

    Argument 2: The frequency at which to sample the value.

    Returns: SUCCESS if the command was successful, EBUSY if the ADC is already sampling a channel, ENOMEM if both buffers have not been provided, and EINVAL if the channel index is invalid or the frequency is outside of the acceptable range. FAIL may also be returned if the hardware has a fault.

  • Command number: 5

    Description: Stop any active sampling operation. This command is successful even if no sampling operation was in progress.

    Argument 1: Unused.

    Argument 2: unused

    Returns: SUCCESS in all cases.

Subscribe

  • Subscribe number: 0

    Description: Register a callback that will fire when requested samples are ready, replacing any previously registered callback. The samples collected before a callback is fired depends on the command which began the sampling operation. Registering the callback does not start or stop any sampling operations.

    Callback signature: The signature of the callback depends on the command used to begin sampling operations. In all cases, the first argument is the type of ADC sampling operation that triggered this callback. If the operation provides individual samples (singly or repeatedly), the second argument will be the channel on which sampling occurred and the third argument will be the sample value. If the operation provides buffered samples (singly or repeatedly), the second argument will contain the channel index in the least significant 8 bits and the length of the buffer in the most significant 24 bits, while the third argument will be a pointer to the buffer filled with samples.

    Returns: SUCCESS in all cases.

Allow

  • Allow number: 0

    Description: Provide a buffer into which samples values can be placed, replacing any previously provided buffer. This buffer will be used for any singly collected buffered data and will be used in addition to the second buffer for repeated buffered sampling. Future ADC operations will continue to use the same buffer.

    Returns: SUCCESS in all cases.

  • Allow number: 1

    Description: Provide a buffer into which samples values can be placed when repeatedly buffered sampling, replacing any previously provided buffer. This buffer and the other provided buffer will be alternated between. Future ADC operations will continue to use the same buffer.

    Returns: SUCCESS in all cases.


driver number: 0x00007

Analog Comparator

Overview

The Analog Comparator driver allows userspace to compare voltages and gives an output depending on this comparison.

Analog Comparators (ACs) can be first of all be configured in the 'normal mode', in which each AC performs a single comparison of two voltages. They can also be configured to send an interrupt as soon as a voltage is higher than another voltage, i.e. when a voltage exceeds a certain threshold.

A specific AC is referred to as ACx, where x is any number from 0 to n, and n is the index of the last AC module.

Command

  • Command number: 0

    Description: Does the driver exist?

    Argument 1: unused

    Argument 2: unused

    Returns: SUCCESS if it exists, otherwise ENODEVICE

  • Command number: 1

    Description: Do a comparison of two inputs, referred to as the positive input voltage and the negative input voltage (Vp and Vn).

    Argument 1: The index of the Analog Comparator for which the comparison needs to be made, starting at 0.

    Argument 2: unused

    Returns: The output of this function is True when Vp > Vn, and False if Vp < Vn.

  • Command number: 2

    Description: Start interrupts on an analog comparator. This analog comparator will then listen, and the callback set in subscribe will be called when the positive input voltage is higher than the negative input voltage (Vp > Vn).

    Argument 1: The index of the Analog Comparator for which the comparison needs to be made, starting at 0.

    Argument 2: unused

    Returns: SUCCESS if starting interrupts was succesful.

  • Command number: 4

    Description: Stop interrupts on an analog comparator.

    Argument 1: The index of the Analog Comparator for which the comparison needs to be made, starting at 0.

    Argument 2: unused

    Returns: SUCCESS if stopping interrupts was succesful.


driver number: 0x00008

Low-Level Debug

Overview

The low-level debug driver provides tools to diagnose userspace issues that make normal debugging workflows (e.g. printing to the console) difficult. It allows libraries to print alert codes and apps to print numeric information using only the command system call, and is easy to call from handwritten assembly. The driver is in capsules/src/low_level_debug.rs.

Command

  • Description: command() is used to print alert codes and numbers. The driver does not provide a way for an app to wait for the print to complete. If the app prints too many messages in a row, the driver will print a message indicating it has dropped some debug messages.

  • Command Number: 0

    Description: Driver check.

    Argument 1: Unused

    Argument 2: Unused

    Returns: SUCCESS

  • Command Number: 1

    Description: Print a predefined alert code. The available alert codes are listed later in this doc. Predefined alert codes are intended for use in library code, and are defined here to avoid collisions between projects.

    Argument 1: Alert code to print

    Argument 2: Unused

    Returns: SUCCESS

  • Command Number: 2

    Description: Print a single number. The number will be printed in hexadecimal. In general, this should only be added temporarily for debugging and should not be called by released library code.

    Argument 1: Number to print

    Argument 2: Unused

    Returns: SUCCESS

  • Command Number: 3

    Description: Print two numbers. The numbers will be printed in hexadecimal. Like command 2, this is intended for temporary debugging and should not be called by released library code. If you want to print multiple values, it is often useful to use the first argument to indicate what value is being printed.

    Argument 1: First number to print

    Argument 2: Second number to print

    Returns: SUCCESS

Predefined Alert Codes

The following alert codes are defined for use with the predefined alert code command (#1). As an alternative to this table, the binary in tools/alert_codes may be used to decode the alert codes.

Alert CodeDescription
0x01Application panic (e.g. panic!() called in Rust code)
0x02A statically-linked app was not installed in the correct location in flash

driver number: 0x30002

UDP

Overview

The UDP driver allows a process to send and receive UDP packets using the Tock networking stack. Currently, this driver allows for tx and rx of UDP packets via 6LoWPAN, which sits on top of the 802.15.4 radio.

This driver can be found in capsules/src/net/udp/driver.rs driver.rs implements an interface for sending and receiving UDP messages. It also exposes a list of interace addresses to the application layer. The primary functionality embedded in the UDP driver is within the allow(), subscribe(), and command() calls which can be made to the driver.

Allow

  • Description allow() is used to setup buffers to read/write from. This function takes in an allow_num and a slice. These allow_nums determine which buffer is being setup as follows:

  • Allow Number: 0

    Description: Read Buffer.

    Argument 1: Slice into which the received payload should be stored

    Returns: SUCCESS

  • Allow Number: 1

    Description: Write Buffer.

    Argument 1: Slice containing the UDP payload to be transmitted

    Returns: SUCCESS

  • Allow Number: 2

    Description: Tx Config Buffer.

    Argument 1: Slice containing config information, namely source/destination addresses and ports. Specifically, the config buffer should be the size of two sock_addr_t structs. The first half of the buffer should contain the source address/port (represented as a sock_addr_t) from which the application expects to send. The second half of the buffer should contain the destination address/port which the application wishes to send the next packet to.

    Returns: SUCCESS

  • Allow Number: 3

    Description: RX Config Buffer.

    Argument 1: Slice containing the Rx config buffer. Used to contain source/destination addresses and ports for receives (separate from 2 because receives may be waiting for an incoming packet asynchronously). Specifically, the rx config buffer should be the size of two sock_addr_t structs. The first half of the buffer should contain the address/port (represented as a sock_addr_t) on which the application is listening. The second half of the buffer should contain the incoming source address/port which the application wishes to listen for.

    Returns: SUCCESS

Subscribe

  • Description: subscribe() is used to setup callbacks for when frames are transmitted or received. It takes in a callback and a subscribe number. The subscribe number indicates the callback type:

  • Subscribe Number: 0

    Description: Setup callback for when frame is received. This callback cannot be set unless the app is bound to a local UDP endpoint.

    Argument 1: The callback

    Argument 2: AppId

    Returns: ERESERVE if the app is not currently bound to a port, SUCCESS otherwise.

  • Subscribe Number: 1

    Description: Setup callback for when frame is transmitted.

    Argument 1: The callback

    Argument 2: AppId

    Returns: SUCCESS

Command

  • Description: command() is used to get the interface list or to transmit a payload. The action taken by the driver is determined by the passed command_num:

  • Command Number: 0

    Description: Driver check.

    Argument 1: Unused

    Argument 2: Unused

    Argument 3: Unused

    Returns: SUCCESS

  • Command Number: 1

    Description: Get the interface list

    Argument 1: Number of requested interface addresses

    Argument 2: Unused

    Argument 3: AppId

    Returns: SuccessWithValue, where value is the total number of interfaces

  • Command Number: 2

    Description: Transmit Payload

    Argument 1: Unused

    Argument 2: Unused

    Argument 3: AppId

    Returns: EBUSY is this process already has a pending tx. Returns EINVAL if no valid buffer has been loaded into the write buffer, or if the config buffer is the wrong length, or if the destination and source port/address pairs cannot be parsed. Otherwise, returns the result of do_next_tx_immediate(). Notably, a successful transmit can produce two different success values. If success is returned, this simply means that the packet was queued. In this case, the app still still needs to wait for a callback to check if any errors occurred before the packet was passed to the radio. However, if SuccessWithValue is returned with value 1, this means the the packet was successfully passed the radio without any errors, which tells the userland application that it does not need to wait for a callback to check if any errors occured while the packet was being passed down to the radio. Any successful return value indicates that the app should wait for a send_done() callback before attempting to queue another packet. Currently, only will transmit if the app has bound to the port passed in the tx_cfg buf as the source address. If no port is bound, returns ERESERVE, if it tries to send on a port other than the port which is bound, returns EINVALID.

             Notably, the currently transmit implementation allows for starvation - an
             an app with a lower app id can send constantly and starve an app with a
             later ID.
    
  • Command Number: 3

    Description: Bind to the address and port in rx_cfg. This command should be called after allow() is called on the rx_cfg buffer, and after subscribe() is used to set up the recv callback. If this command is called and the address in rx_cfg is 0::0 : 0, this command will reset the option containing the bound port to None, and set the rx callback to None.

    Argument 1: Unused

    Argument 2: Unused

    Argument 3: AppId

    Returns: Returns SUCCESS if that addr/port combo is free, returns EINVAL if the address requested is not a local interface, or if the port requested is 0. Returns EBUSY if that port is already bound to by another app.

  • Command Number: 4

    Description: Returns the maximum payload that can be transmitted by apps using this driver. This represents the size of the payload buffer in the kernel. Apps can use this syscall to ensure they do not attempt to send too-large messages.

    Argument 1: Unused

    Argument 2: Unused

    Argument 3: AppId

    Returns: Returns SUCCESSWithValue, where the value is the maximum tx payload length


driver number: 0x60000

Ambient Temperature

Overview

The ambient temperature driver allows a process to read the ambient temperature from a sensor. Temperature is reported in degrees centigrate at a precision of hundredths of degrees

Command

  • Command number: 0

    Description: Does the driver exist?

    Argument 1: unused

    Argument 2: unused

    Returns: SUCCESS if it exists, otherwise ENODEVICE

  • Command number: 1

    Description: Initiate a sensor reading. When a reading is ready, a callback will be delivered if the process has subscribed.

    Argument 1: unused

    Argument 2: unused

    Returns: EBUSY if a reading is already pending, ENOMEM if there isn't sufficient grant memory available, or SUCCESS if the sensor reading was initiated successfully.

Subscribe

  • Subscribe number: 0

    Description: Subscribe to temperature readings.

    Callback signature: The callback receives a single argument, the temperature in hundredths of degrees centigrate.

    Returns: SUCCESS if the subscribe was successful or ENOMEM if the driver failed to allocate memory to store the callback.


driver number: 0x60001

Humidity

Overview

The humidity driver allows a process to read the ambient humidity from a sensor. Humidity is reported in percent at a precision of hundredths of a percent.

Command

  • Command number: 0

    Description: Does the driver exist?

    Argument 1: unused

    Argument 2: unused

    Returns: SUCCESS if it exists, otherwise ENODEVICE

  • Command number: 1

    Description: Initiate a sensor reading. When a reading is ready, a callback will be delivered if the process has subscribed.

    Argument 1: unused

    Argument 2: unused

    Returns: EBUSY if a reading is already pending, ENOMEM if there isn't sufficient grant memory available, or SUCCESS if the sensor reading was initiated successfully.

Subscribe

  • Subscribe number: 0

    Description: Subscribe to humidity readings.

    Callback signature: The callback receives a single argument, the humidity in hundredths of percent.

    Returns: SUCCESS if the subscribe was successful or ENOMEM if the driver failed to allocate memory to store the callback.


driver number: 0x60002

Luminance

Overview

The ambient light driver allows a process to read the ambient light from a sensor. Luminance is reported in lux (lx).

Command

  • Command number: 0

    Description: Does the driver exist?

    Argument 1: unused

    Argument 2: unused

    Returns: SUCCESS if it exists, otherwise ENODEVICE

  • Command number: 1

    Description: Initiate a sensor reading. When a reading is ready, a callback will be delivered if the process has subscribed.

    Argument 1: unused

    Argument 2: unused

    Returns: EBUSY if a reading is already pending, ENOMEM if there isn't sufficient grant memory available, or SUCCESS if the sensor reading was initiated successfully.

Subscribe

  • Subscribe number: 0

    Description: Subscribe to luminance readings.

    Callback signature: The callback receives a single argument, the luminance in lux (lx).

    Returns: SUCCESS if the subscribe was successful or ENOMEM if the driver failed to allocate memory to store the callback.


driver number: 0x70005

L3GD20

Overview

Three axis gyroscope and temperature sensor.

Manual

Command

  • Command number: 0

    Description: Returns SUCCESS

    Argument 1: unused

    Argument 2: unused

    Returns: 0

  • Command number: 1

    Description: Verifies whether the hardware sensor is present.

    Argument 1: unused

    Argument 2: unused

    Returns: SUCCESS if there is no other command in progress, EBUSY otherwise.

  • Command number: 2

    Description: Powers on the sensor.

    Argument 1: unused

    Argument 2: unused

    Returns: SUCCESS if there is no other command in progress, EBUSY otherwise.

  • Command number: 3

    Description: Sets the sensors scale

    Argument 1: 0, 1 or 2 (see manual 34)

    Argument 2: unused

    Returns: SUCCESS if there is no other command in progress, EBUSY otherwise.

  • Command number: 4

    Description: Enables or disables the high pass filter

    Argument 1: 1 for enable, 0 for disable

    Argument 2: unused

    Returns: SUCCESS if there is no other command in progress, EBUSY otherwise.

  • Command number: 5

    Description: Sets the high pass filter mode and divider

    Argument 1: mode (0, 1 or 2, see manual page 33)

    Argument 2: divider (0 .. 9, see manual page 33)

    Returns: SUCCESS if there is no other command in progress, EBUSY otherwise.

  • Command number: 6

    Description: Reads X, Y and Z

    Argument 1: unused

    Argument 2: unused

    Returns: SUCCESS if there is no other command in progress, EBUSY otherwise.

  • Command number: 7

    Description: Reads the temperature

    Argument 1: unused

    Argument 2: unused

    Returns: SUCCESS if there is no other command in progress, EBUSY otherwise.

Subscribe

All the commands return a callback when done.

  • Subscribe number 0

    Description: Called when commands are done

    Argument 1:

    • Command 1: 1 present, 0 not present
    • Command 6: X rotation
    • Command 7: temperature in deg C

    Argument 2:

    • Command 6: Y rotation

    Argument 3:

    • Command 6: Z rotation

Allow

Unused for the L3GD20 driver. Will always return ENOSUPPORT.


driver number: 0x70006

LSM303DLHC

Overview

Three axis accelerometer, magnetometer and temperature sensor.

Manual

Command

  • Command number: 0

    Description: Returns SUCCESS

    Argument 1: unused

    Argument 2: unused

    Returns: 0

  • Command number: 1

    Description: Performs a test read/write to verify that the hardware sensor is present.

    Argument 1: unused

    Argument 2: unused

    Returns: SUCCESS if presence test has started successfully, EBUSY otherwise.

  • Command number: 2

    Description: Set Accelerometer Power Mode.

    Argument 1: Accelerometer Data rate defined in manual table 20, page 25

    Argument 2: Low power mode (1 on, 0 off)

    Returns: SUCCESS if there is no other command in progress, EBUSY otherwise.

  • Command number: 3

    Description: Set Accelerometer Scale and Resolution.

    Argument 1: Accelerometer scale defined in manual table 27, page 27

    Argument 2: High resolution (1 on, 0 off)

    Returns: SUCCESS if there is no other command in progress, EBUSY otherwise.

  • Command number: 4

    Description: Set Magnetometer Temperature Enable and Data Rate.

    Argument 1: Temperature enable (1 on, 0 off)

    Argument 2: Magnetometer Data rate defined in manual table 72, page 37

    Returns: SUCCESS if there is no other command in progress, EBUSY otherwise.

  • Command number: 5

    Description: Set magnetometer range.

    Argument 1: Magnetometer range defined in manual table 75, page 38

    Argument 2: unused

    Returns: SUCCESS if there is no other command in progress, EBUSY otherwise.

  • Command number: 6

    Description: Reads Acceleration X, Y and Z

    Argument 1: unused

    Argument 2: unused

    Returns: SUCCESS if there is no other command in progress, EBUSY otherwise.

  • Command number: 7

    Description: Reads the temperature

    Argument 1: unused

    Argument 2: unused

    Returns: SUCCESS if there is no other command in progress, EBUSY otherwise.

  • Command number: 8

    Description: Reads Magnetometer X, Y and Z

    Argument 1: unused

    Argument 2: unused

    Returns: SUCCESS if there is no other command in progress, EBUSY otherwise.

Subscribe

All the commands return a callback when done.

  • Subscribe number 0

    Description: Called when commands are done

    Argument 1:

    • Command 1: 1 present, 0 not present
    • Command 6: X acceleration in m/s2 (not scaled)
    • Command 7: temperature in deg C * 8
    • Command 8: X magnetometer in Gauss (not scaled)

    Argument 2:

    • Command 6: Y acceleration in m/s2 (not scaled)
    • Command 8: Y magnetometer in Gauss (not scaled)

    Argument 3:

    • Command 6: Z acceleration in m/s2 (not scaled)
    • Command 8: Z magnetometer in Gauss (not scaled)

Allow

Unused for the LSM303DLHC driver. Will always return ENOSUPPORT.


driver number: 0x80005

HD44780

Overview

The HD44780 LCD driver allows userspace access to a device connected this way:

  • RS pin: GPIO PIN
  • RW pin: GND
  • EN pin: GPIO PIN
  • DATA4 pin: GPIO PIN
  • DATA5 pin: GPIO PIN
  • DATA6 pin: GPIO PIN
  • DATA7 pin: GPIO PIN

The pins numbers are set up in the HD44780 component.

Command

  • Command number: 0

    Description: Initialize the HD44780 LCD using two arguments given.

    Argument 1: Number of columns on the LCD.

    Argument 2: Number of lines on the LCD.

    Returns: SUCCESS if the command was saved successfully in the command buffer, EBUSY otherwise (the buffer was full).

  • Command number: 1

    Description: Set cursor to a given position.

    Argument 1: The column on which to set the cursor.

    Argument 2: The line on which to set the cursor.

    Returns: SUCCESS if the command was saved successfully, EBUSY otherwise.

  • Command number: 2

    Description: Home command, clears the display and sets the cursor to (0,0).

    Argument 1: unused

    Argument 2: unused

    Returns: SUCCESS if the command was saved successfully, EBUSY otherwise.

  • Command number: 3

    Description: Clear command, clears the display and sets the cursor to (0,0).

    Argument 1: unused

    Argument 2: unused

    Returns: SUCCESS if the command was saved successfully, EBUSY otherwise.

  • Command number: 4

    Description: Left_to_right or Right_to_left command.

    Argument 1:

    0: - Left_to_right: flow the text from left to right.

    1 - Right_to_left: flow the text from right to left.

    Argument 2: unused

    Returns: SUCCESS if the command was saved successfully, EBUSY otherwise.

  • Command number: 5

    Description: Autoscroll or No_autoscroll command.

    Argument 1:

    0: - Autoscroll: 'right justify' the text from the cursor.

    1: - No_autoscroll: 'left justify' the text from the cursor.

    Argument 2: unused

    Returns: SUCCESS if the command was saved successfully, EBUSY otherwise.

  • Command number: 6

    Description: Cursor or No_cursor command.

    Argument 1:

    0 - Cursor: Turn on the underline cursor.

    1 - No_cursor: Turn off the underline cursor.

    Argument 2: unused

    Returns: SUCCESS if the command was saved successfully, EBUSY otherwise.

    Description: Display or No_display command.

    Argument 1:

    0 - Display: Turn on the display very quickly.

    1 - No_display: Turn off the display very quickly.

    Argument 2: unused

    Returns: SUCCESS if the command was saved successfully, EBUSY otherwise.

    Description: Blink or No_blink command.

    Argument 1:

    0 - Blink: Turn on the blinking cursor display.

    1 - No_blink: Turn off the blinking cursor display.

    Argument 2: unused

    Returns: SUCCESS if the command was saved successfully, EBUSY otherwise.

    Description: Scroll_display_left or Scroll_display_right command.

    Argument 1:

    0 - Scroll_display_left: Scroll the display to the left without changing the RAM.

    1 - Scroll_display_right: Scroll the display to the right without changing the RAM.

    Argument 2: unused

    Returns: SUCCESS if the command was saved successfully, EBUSY otherwise.

Allow

  • Allow number: 0

    Description: Send a buffer from the userspace to the kernelspace to be displayed on the LCD.

    Argument 1: Slice -> the buffer sent to be displayed.

    Returns: SuccessWithValue, the number of bytes saved to the command buffer to the displayed.

Subscribe

Unimplemented for the LCD_16x2 driver. Will always return ENOSUPPORT.

Yield

Unimplemented.

Internal API Reference Documents

These files document internal Tock APIs.

Tock Reference Documents

Tock Reference Document (TRD) Structure and Keywords

TRD: 1
Working Group: Kernel
Type: Best Common Practice
Status: Final
Authors: Philip Levis, Daniel Griffin

Abstract

This document describes the structure followed by all Tock Reference Documents (TRDs), and defines the meaning of several key words in those documents.

1 Introduction

To simplify management, reading, and tracking development, all Tock Reference Documents (TRDs) MUST have a particular structure. Additionally, to simplify development and improve implementation interoperability, all TRDs MUST observe the meaning of several key words that specify levels of compliance. This document describes and follows both.

2 Keywords

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in TRD1.

Note that the force of these words is modified by the requirement level of the document in which they are used. These words hold their special meanings only when capitalized, and documents SHOULD avoid using these words uncapitalized in order to minimize confusion.

2.1 MUST

MUST: This word, or the terms "REQUIRED" or "SHALL", mean that the definition is an absolute requirement of the document.

2.2 MUST NOT

MUST NOT: This phrase, or the phrase "SHALL NOT", mean that the definition is an absolute prohibition of the document.

2.3 SHOULD

SHOULD: This word, or the adjective "RECOMMENDED", mean that there may exist valid reasons in particular circumstances to ignore a particular item, but the full implications must be understood and carefully weighed before choosing a different course.

2.4 SHOULD NOT

SHOULD NOT: This phrase, or the phrase "NOT RECOMMENDED" mean that there may exist valid reasons in particular circumstances when the particular behavior is acceptable or even useful, but the full implications should be understood and the case carefully weighed before implementing any behavior described with this label.

2.5 MAY

MAY: This word, or the adjective "OPTIONAL", mean that an item is truly optional. One implementer may choose to include the item because a particular application requires it or because the implementer feels that it enhances the system while another implementer may omit the same item. An implementation which does not include a particular option MUST be prepared to interoperate with another implementation which does include the option, though perhaps with reduced functionality. Similarly, an implementation which does include a particular option MUST be prepared to interoperate with another implementation which does not include the option (except, of course, for the feature the option provides.)

2.6 Guidance in the use of these Imperatives

Imperatives of the type defined in this memo must be used with care and sparingly. In particular, they MUST only be used where it is actually required for interoperation or to limit behavior which has potential for causing harm (e.g., limiting retransmissions) For example, they must not be used to try to impose a particular method on implementors where the method is not required for interoperability.

3 TRD Structure

A TRD MUST begin with a title, and then follow with a header and a body. The header states document metadata, for management and status. The body contains the content of the proposal.

All TRDs MUST conform to Markdown syntax to enable translation to HTML and LaTeX, and for useful display in web tools.

3.1 TRD Header

The TRD header has several fields which MUST be included, as well as others which MAY be included. The TRD header MUST NOT include fields which are not specified in TRD 1 or supplementary Best Common Practice TRDs. The first five header fields MUST be included in all TRDs, in the order stated below. The Markdown syntax to use when composing a header is modeled by this document's header.

The first field is "TRD," and specifies the TRD number of the document. A TRD's number is unique. This document is TRD 1. The TRD type (discussed below) determines TRD number assignment. Generally, when a document is ready to be a TRD, it is assigned the smallest available number. BCP TRDs start at 1 and all other TRDs (Documentary, Experimental, and Informational) start at 101.

The second field, "Working Group," states the name of the working group that produced the document. This document was produced by the Kernel Working Group.

The third field is "Type," and specifies the type of TRD the document is. There are four types of TRD: Best Current Practice (BCP), Documentary, Informational, and Experimental. This document's type is Best Current Practice.

Best Current Practice is the closest thing TRDs have to a standard: it represents conclusions from significant experience and work by its authors. Developers desiring to add code (or TRDs) to Tock SHOULD follow all current BCPs.

Documentary TRDs describe a system or protocol that exists; a documentary TRD MUST reference an implementation that a reader can easily obtain. Documentary TRDs simplify interoperability when needed, and document Tock implementations.

Informational TRDs provide information that is of interest to the community. Informational TRDs include data gathered on radio behavior, hardware characteristics, other aspects of Tock software/hardware, organizational and logistic information, or experiences which could help the community achieve its goals.

Experimental TRDs describe a completely experimental approach to a problem, which are outside the Tock release stream and will not necessarily become part of it. Unlike Documentary TRDs, Experimental TRDs may describe systems that do not have a reference implementation.

The fourth field is "Status," which specifies the status of the TRD. A TRD's status can be either "Draft," which means it is a work in progress, or "Final," which means it is complete and will not change. Once a TRD has the status "Final," the only change allowed is the addition of an "Obsoleted By" field.

The "Obsoletes" field is a backward pointer to an earlier TRD which the current TRD renders obsolete. An Obsoletes field MAY have multiple TRDs listed. For example, if TRD 121 were to replace TRDs 111 and 116, it would have the field "Obsoletes: 111, 116".

The "Obsoleted By" field is added to a Final TRD when another TRD has rendered it obsolete. The field contains the number of the obsoleting TRD. For example, if TRD 111 were obsoleted by TRD 121, it would have the field "Obsoleted By: 121".

"Obsoletes" and "Obsoleted By" fields MUST agree. For a TRD to list another TRD in its Obsoletes field, then that TRD MUST list it in the Obsoleted By field.

The obsoletion fields are used to keep track of evolutions and modifications of a single abstraction. They are not intended to force a single approach or mechanism over alternative possibilities.

The final required field is "Authors," which states the names of the authors of the document. Full contact information should not be listed here (see Section 3.2).

There is an optional field, "Extends." The "Extends" field refers to another TRD. The purpose of this field is to denote when a TRD represents an addition to an existing TRD. Meeting the requirements of a TRD with an Extends field requires also meeting the requirements of all TRDs listed in the Extends field.

If a TRD is a Draft, then four additional fields MUST be included: Draft-Created, Draft-Modified, Draft-Version, and Draft-Discuss. Draft-Created states the date the document was created, Draft-Modified states when it was last modified. Draft-Version specifies the version of the draft, which MUST increase every time a modification is made. Draft-Discuss specifies the email address of a mailing list where the draft is being discussed. Final and Obsolete TRDs MUST NOT have these fields, which are for Drafts only.

3.2 TRD Body

A TRD body SHOULD begin with an Abstract, which gives a brief overview of the content of the TRD. A longer TRD MAY, after the Abstract, have a Table of Contents. After the Abstract and Table of Contents there SHOULD be an Introduction, stating the problem the TRD seeks to solve and providing needed background information.

If a TRD is Documentary, it MUST have a section entitled "Implementation," which instructs the reader how to obtain the implementation documented.

If a TRD is Best Current Practice, it MUST have a section entitled "Reference," which points the reader to one or more reference uses of the practices.

The last three sections of a TRD are author information, citations, and appendices. A TRD MUST have an author information section titled entitled "Author's Address" or "Authors' Addresses." A TRD MAY have a citation section entitled "Citations." A citations section MUST immediately follow the author information section. A TRD MAY have appendices. Appendices MUST immediately follow the citations section, or if there is no citations section, the author information section. Appendices are lettered. Please refer to Appendix A for details.

4 File names

TRDs MUST be stored in the Tock repository with a file name of

trd[number]-[desc].md

Where number is the TRD number and desc is a short, one word description. The name of this document is trd1-trds.md.

5 Reference

The reference use of this document is TRD 1 (itself).

6 Acknowledgments

The definitions of the compliance terms are a direct copy of definitions taken from IETF RFC 2119. This document is heavily copied from TinyOS Enhancement Proposal 1 (TEP 1).

7 Author's Address

Philip Levis
409 Gates Hall
Stanford University
Stanford, CA 94305

phone - +1 650 725 9046

email - pal@cs.stanford.edu

Appendix A Example Appendix

This is an example appendix. Appendices begin with the letter A.

Kernel Analog-to-Digital Conversion HIL

TRD: 102
Working Group: Kernel
Type: Documentary
Status: Draft
Author: Philip Levis and Branden Ghena
Draft-Created: Dec 18, 2016
Draft-Modified: June 12, 2017
Draft-Version: 2
Draft-Discuss: tock-dev@googlegroups.com

Abstract

This document describes the hardware independent layer interface (HIL) for analog-to-digital conversion in the Tock operating system kernel. It describes the Rust traits and other definitions for this service as well as the reasoning behind them. This document also describes an implementation of the ADC HIL for the SAM4L. This document is in full compliance with TRD1.

1 Introduction

Analog-to-digital converters (ADCs) are devices that convert analog input signals to discrete digital output signals, typically voltage to a binary number. While different microcontrollers can have very different control registers and operating modes, the basic high-level interface they provide is very uniform. Software that wishes to use more advanced features can directly use the per-chip implementations, which may export these features.

The ADC HIL is the kernel crate, in module hil::adc. It provides three traits:

  • kernel::hil::adc::Adc - provides basic interface for individual analog samples
  • kernel::hil::adc::Client - receives individual analog samples from the ADC
  • kernel::hil::adc::AdcHighSpeed - provides high speed buffered analog sampling interface
  • kernel::hil::adc::HighSpeedClient - receives buffers of analog samples from the ADC

The rest of this document discusses each in turn.

2 Adc trait

The Adc trait is for requesting individual analog to digital conversions, either one-shot or repeatedly. It is implemented by chip drivers to provide ADC functionality. Data is provided through the Client trait. It has four functions and one associated type:

/// Simple interface for reading an ADC sample on any channel.
pub trait Adc {
    /// The chip-dependent type of an ADC channel.
    type Channel;

    /// Initialize must be called before taking a sample.
    fn initialize(&self) -> ReturnCode;

    /// Request a single ADC sample on a particular channel.
    /// Used for individual samples that have no timing requirements.
    fn sample(&self, channel: &Self::Channel) -> ReturnCode;

    /// Request repeated ADC samples on a particular channel.
    /// Callbacks will occur at the given frequency with low jitter and can be
    /// set to any frequency supported by the chip implementation. However
    /// callbacks may be limited based on how quickly the system can service
    /// individual samples, leading to missed samples at high frequencies.
    fn sample_continuous(&self, channel: &Self::Channel, frequency: u32) -> ReturnCode;

    /// Stop a sampling operation.
    /// Can be used to stop any simple or high-speed sampling operation. No
    /// further callbacks will occur.
    fn stop_sampling(&self) -> ReturnCode;
}

The initialize function configures the hardware to perform analog sampling. It MUST be called at least once before any samples are taken. It only needs to be called once, not once per sample. This function MUST return SUCCESS upon correct initialization or FAIL if the hardware fails to initialize successfully. If the driver is already initialized, the function SHOULD return SUCCESS.

The sample function starts a single conversion on the specified ADC channel. The exact binding of this channel to external or internal analog inputs is board-dependent. The function MUST return SUCCESS if the analog conversion has been started, EOFF if the ADC is not initialized or enabled, EBUSY if a conversion is already in progress, or EINVAL if the specified channel is invalid. The sample_ready callback of the client MUST be called when the conversion is complete.

The sample_continuous function begins repeated individual conversions on a specified channel. Conversions MUST continue at the specified frequency until stop_sampling is called. The sample_ready callback of the client MUST be called when each conversion is complete. The channels and frequency ranges supported are board-dependent. The function MUST return SUCCESS if repeated analog conversions have been started, EOFF if the ADC is not initialized or enabled, EBUSY if a conversion is already in progress, or EINVAL if the specified channel or frequency are invalid.

The stop_sampling function can be used to stop any sampling operation, single, continuous, or high speed. Conversions which have already begun are canceled. stop_sampling MUST be safe to call from any callback in the Client or HighSpeedClient traits. The function MUST return SUCCESS, EOFF, or EINVAL. SUCCESS indicates that all conversions are stopped and no further callbacks will occur, EOFF means the ADC is not initialized or enabled, and EINVAL means the ADC was not active.

The channel type is used to signify which ADC channel to sample data on for various commands. What it maps to is implementation-specific, possibly an I/O pin number or abstract notion of a channel. One approach used for channels by the SAM4L implementation is for the capsule to keep an array of possible channels, which are connected to pins by the board main.rs file, and selected from by userland applications.

3 Client trait

The Client trait handles responses from Adc trait sampling commands. It is implemented by capsules to receive chip driver responses. It has one function:

/// Trait for handling callbacks from simple ADC calls.
pub trait Client {
    /// Called when a sample is ready.
    fn sample_ready(&self, sample: u16);
}

The sample_ready function is called whenever data is available from a sample or sample_continuous call. It is safe to call stop_sampling within the sample_ready callback. The sample data returned is a maximum of 16 bits in resolution, with the exact data resolution being chip-specific. If data is less than 16 bits (for example 12-bits on the SAM4L), it SHOULD be placed in the least significant bits of the sample value.

4 AdcHighSpeed trait

The AdcHighSpeed trait is used for sampling data at high frequencies such that receiving individual samples would be untenable. Instead, it provides an interface that returns buffers filled with samples. This trait relies on the Adc trait being implemented as well in order to provide primitives like intialize and stop_sampling which are used for ADCs in this mode as well. While we expect many chips to support the Adc trait, we expect the AdcHighSpeed trait to be implemented due to a high-speed sampling need on a platform. The trait has three functions:

/// Interface for continuously sampling at a given frequency on a channel.
/// Requires the AdcSimple interface to have been implemented as well.
pub trait AdcHighSpeed: Adc {
    /// Start sampling continuously into buffers.
    /// Samples are double-buffered, going first into `buffer1` and then into
    /// `buffer2`. A callback is performed to the client whenever either buffer
    /// is full, which expects either a second buffer to be sent via the
    /// `provide_buffer` call. Length fields correspond to the number of
    /// samples that should be collected in each buffer. If an error occurs,
    /// the buffers will be returned.
    fn sample_highspeed(&self,
                        channel: &Self::Channel,
                        frequency: u32,
                        buffer1: &'static mut [u16],
                        length1: usize,
                        buffer2: &'static mut [u16],
                        length2: usize)
                        -> (ReturnCode, Option<&'static mut [u16]>,
                            Option<&'static mut [u16]>);

    /// Provide a new buffer to fill with the ongoing `sample_continuous`
    /// configuration.
    /// Expected to be called in a `buffer_ready` callback. Note that if this
    /// is not called before the second buffer is filled, samples will be
    /// missed. Length field corresponds to the number of samples that should
    /// be collected in the buffer. If an error occurs, the buffer will be
    /// returned.
    fn provide_buffer(&self,
                      buf: &'static mut [u16],
                      length: usize)
                      -> (ReturnCode, Option<&'static mut [u16]>);

    /// Reclaim ownership of buffers.
    /// Can only be called when the ADC is inactive, which occurs after a
    /// successful `stop_sampling`. Used to reclaim buffers after a sampling
    /// operation is complete. Returns success if the ADC was inactive, but
    /// there may still be no buffers that are `some` if the driver had already
    /// returned all buffers.
    fn retrieve_buffers(&self)
                        -> (ReturnCode, Option<&'static mut [u16]>,
                            Option<&'static mut [u16]>);
}

The sample_highspeed function is used to perform high-speed double-buffered sampling. After the first buffer is filled with samples, the samples_ready function will be called and sampling will immediately continue into the second buffer in order to reduce jitter between samples. Additional buffers SHOULD be passed through the provide_buffer call. However, if none are provided, the driver MUST cease sampling once it runs out of buffers. In case of an error, the buffers will be immediately returned from the function. The channels and frequencies acceptable are chip-specific. The return code MUST be SUCCESS if sampling has begun successfully, EOFF if the ADC is not enabled or initialized, EBUSY if the ADC is in use, or EINVAL if the channel or frequency are invalid.

The provide_buffer function is used to provide additional buffers to an ongoing high-speed sampling operation. It is expected to be called within a samples_ready callback in order to keep sampling running without delay. In case of an error, the buffer will be immediately returned from the function. It is not an error to fail to call provide_buffer and the underlying driver MUST cease sampling if no buffers are remaining. It is an error to call provide_buffer twice without having received a buffer through samples_ready. The prior settings for channel and frequency will persist. The return code MUST be SUCCESS if the buffer has been saved for later use, EOFF if the ADC is not initialized or enabled, EINVAL if there is no currently running continuous sampling operation, or EBUSY if an additional buffer has already been provided.

The retrieve_buffers function returns ownership of all buffers owned by the chip implementation. All ADC operations MUST be stopped before buffers are returned. Any data within the buffers SHOULD be considered invalid. It is expected that retrieve_buffers will be called from within a samples_ready callback after calling stop_sampling. Up to two buffers will be returned by the function. The return code MUST be SUCCESS if the ADC is not in operation (although as few as zero buffers may be returned), EINVAL MUST be returned if an ADC operation is still in progress.

5 HighSpeedClient trait

The HighSpeedClient trait is used to receive samples from a call to sample_highspeed. It is implemented by a capsule to receive chip driver responses. It has one function:

/// Trait for handling callbacks from high-speed ADC calls.
pub trait HighSpeedClient {
    /// Called when a buffer is full.
    /// The length provided will always be less than or equal to the length of
    /// the buffer. Expects an additional call to either provide another buffer
    /// or stop sampling
    fn samples_ready(&self, buf: &'static mut [u16], length: usize);
}

The samples_ready function receives a buffer filled with up to length number of samples. Each sample MAY be up to 16 bits in size. Smaller samples SHOULD be aligned such that the data is in the least significant bits of each value. The length field MUST match the length passed in with the buffer (through either sample_highspeed or provide_buffer). Within the samples_ready callback, the capsule SHOULD call provide_buffer if it wishes to continue sampling. Alternatively, stop_sampling and retrieve_buffers SHOULD be called to stop the ongoing ADC operation.

6 Example Implementation: SAM4L

The SAM4L ADC has a flexible ADC, supporting differential and single-ended inputs, 8 or 12 bit samples, configurable clocks, reference voltages, and grounds. It supports periodic sampling supported by an internal timer. The SAM4L ADC uses generic clock 10 (GCLK10). The ADC is peripheral 38, so its control registers are found at address 0x40038000. A complete description of the ADC can be found in Chapter 38 (Page 995) of the SAM4L datasheet.

The current implementation, found in chips/sam4l/adc.rs, implements the Adc and AdcHighSpeed traits.

6.1 ADC Channels

In order to provide a list of ADC channels to the capsule and userland, the SAM4L implementation creates an AdcChannel struct which contains and enum defining its value. Each possible ADC channel is then statically created. Other chips may want to consider a similar system.

/// Representation of an ADC channel on the SAM4L.
pub struct AdcChannel {
    chan_num: u32,
    internal: u32,
}

/// SAM4L ADC channels.
#[derive(Copy,Clone,Debug)]
#[repr(u8)]
enum Channel {
    AD0 = 0x00,
    AD1 = 0x01,
    ...
    ReferenceGround = 0x17,
}

/// Initialization of an ADC channel.
impl AdcChannel {
    /// Create a new ADC channel.
    /// channel - Channel enum representing the channel number and whether it is
    ///           internal
    const fn new(channel: Channel) -> AdcChannel {
        AdcChannel {
            chan_num: ((channel as u8) & 0x0F) as u32,
            internal: (((channel as u8) >> 4) & 0x01) as u32,
        }
    }
}

/// Statically allocated ADC channels. Used in board configurations to specify
/// which channels are used on the platform.
pub static mut CHANNEL_AD0: AdcChannel = AdcChannel::new(Channel::AD0);
pub static mut CHANNEL_AD1: AdcChannel = AdcChannel::new(Channel::AD1);
...
pub static mut CHANNEL_REFERENCE_GROUND: AdcChannel = AdcChannel::new(Channel::ReferenceGround);

6.2 Client Type

It is difficult in Rust to require a argument that implements two types. However, it is convenient for the implementation to expect a single client that implements both the adc::Client and adc::HighSpeedClient interfaces. It is possible to do so by defining a new trait that requires each.

/// Create a trait of both client types to allow a single client reference to
/// act as both
pub trait EverythingClient: hil::adc::Client + hil::adc::HighSpeedClient {}
impl<C: hil::adc::Client + hil::adc::HighSpeedClient> EverythingClient for C {}

6.3 Clock Initialization

The ADC clock on the SAM4L is poorly documented. It is required to both generate a clock based on the PBA clock as well as GCLK10. However, the clock used for samples by the ADC run at 1.5 MHz at the highest (for single sampling mode). In order to handle this, the SAM4L ADC implementation first divides down the clock to reach a value less than or equal to 1.5 MHz (exactly 1.5 MHz in practice for a CPU clock running at 48 MHz).

6.4 ADC Initialization

The process of initializing the ADC is well documented in the SAM4L datasheet, unfortunately it seems to be entirely false. While following the documentation allows for single sampling, high speed sampling fails in practice after a small number of samples (order less than 100) have been collected. After much experimentation and comparison to other SAM4L code available online, it was determined that the initialization process should be:

  1. Enable clock
  2. Configure ADC
  3. Reset ADC
  4. Enable ADC
  5. Wait until ADC status is set to enabled
  6. Enable the Bandgap and Reference Buffers
  7. Wait until the buffers are enabled

It is quite possible that other orders of initialization are valid, however proceed with caution.

7 Authors' Address

Philip Levis
409 Gates Hall
Stanford University
Stanford, CA 94305

phone - +1 650 725 9046

email - pal@cs.stanford.edu
Branden Ghena

email - brghena@umich.edu

8 Citations

[TRD1] Tock Reference Document (TRD) Structure and Keywords

Tock Getting Started Guide

This covers how to install the toolchain on your platform to start using and developing Tock.

Requirements

  1. Rust

  2. rustup to install Rust (version >= 1.11.0)

  3. Command line utilities: make

  4. A supported board or QEMU configuration.

    If you are just starting to work with TockOS, you should look in the boards/ subdirectory and choose one of the options with tockloader support to load applications, as that is the configuration that most examples and tutorials assume.

    Note: QEMU support in Tock is in the early stages. Please be sure to check whether and how QEMU is supported for a board based on the table in the boards/ subdirectory. The make ci-job-qemu target is the authority on QEMU support.

Super Quick Setup

Nix:

$ nix-shell

MacOS:

$ curl https://sh.rustup.rs -sSf | sh
$ pip3 install --upgrade tockloader

Ubuntu:

$ curl https://sh.rustup.rs -sSf | sh
$ pip3 install --upgrade tockloader --user
$ grep -q dialout <(groups $(whoami)) || sudo usermod -a -G dialout $(whoami) # Note, will need to reboot if prompted for password

Then build the kernel by running make in the boards/<platform> directory.

Installing Requirements

These steps go into a little more depth. Note that the build system is capable of installing some of these tools, but you can also install them yourself.

Rust (nightly)

We are using nightly-2020-06-03. We require installing it with rustup so you can manage multiple versions of Rust and continue using stable versions for other Rust code:

$ curl https://sh.rustup.rs -sSf | sh

This will install rustup in your home directory, so you will need to source ~/.profile or open a new shell to add the .cargo/bin directory to your $PATH.

Then install the correct nightly version of Rust:

$ rustup install nightly-2020-06-03

Tockloader

tockloader programs the kernel and applications onto boards, and also has features that are generally useful for all Tock boards, such as easy-to-manage serial connections, along with the ability to list, add, replace, and remove applications over JTAG (or USB if a bootloader is installed).

  1. tockloader (version >= 1.0)

Tockloader is a Python application and can be installed with the Python package manager (pip).

(Linux): pip3 install --upgrade tockloader --user
(MacOS): pip3 install --upgrade tockloader

Compiling the Kernel

Tock builds a unique kernel for every board it supports. Boards include details like pulling together the correct chips and pin assignments. To build a kernel, first choose a board, then navigate to that board directory. e.g. cd boards/nordic/nrf52840dk ; make.

Some boards have special build options that can only be used within the board's directory. All boards share a few common targets:

  • all (default): Compile Tock for this board.
  • debug: Generate build(s) for debugging support, details vary per board.
  • doc: Build documentation for this board.
  • clean: Remove built artifacts for this board.
  • flash: Load code using JTAG, if available.
  • program: Load code using a bootloader, if available.

The board-specific READMEs in each board's subdirectory provide more details for each platform.

Loading the kernel onto a board

The process to load the kernel onto the board depends on the board. There are two main variants: some boards (notably the Imix and Hail boards) have a serial bootloader, most other boards use a programming adapter that supports the JTAG or SWD protocol instead.

To load a kernel onto a board using a serial bootloader, no other software is required and you can just run

$ make program

in the board's directory. To load the kernel using a programming adapter, you need the appropriate software that supports the adapter and can then install the kernel by running

$ make flash

Depending on the adapter, you will need either the free openocd or Segger's proprietary JLinkExe. Programming adapters are available as standalone devices (for example the JLink EDU JTAG debugger available on Digikey), but most development boards come with an onboard programming and debugging adapter. In that case, the board you use determines which software you will need and the Makefile in the board directory will know which one to call. Again, the board-specific READMEs provide the required details.

Installing JLinkExe

JLink is available from the Segger website. You want to install the "J-Link Software and Documentation Pack". There are various packages available depending on operating system. We require a version greater than or equal to 5.0.

Installing openocd

Openocd works with various programming and debugging adapters. For most purposes, available distribution packages are sufficient and it can be installed with:

(Linux/Debian): sudo apt-get install openocd
(MacOS): brew install open-ocd

We require at least version 0.8.0 to support the SAM4L on imix if you choose to flash it using an adapter instead of the bootloader. Some boards (at the time of writing the HiFive1 RISC-V board) may require newer or unreleased versions, in that case you should follow the installation instructions on the openocd website.

(Linux): Adding a udev rule

Depending on which programming adapter you use, you may want to add a udev rule in /etc/udev/rules.d that allows you to interact with the board as a user instead of as root. If you install the deb packet of the JLink software it will automatically install a /etc/udev/rules.d/99-jlink.rules that allows everyone to access the adapter. If you use something else, like for example the onboard programmer of a ST Nucleo board, you could install something like this as /etc/udev/rules.d/99-stlinkv2-1.rules:

# stm32 nucleo boards, with onboard st/linkv2-1
# ie, STM32F0, STM32F4.
# STM32VL has st/linkv1, which is quite different

SUBSYSTEMS=="usb", ATTRS{idVendor}=="0483", ATTRS{idProduct}=="374b", \
    MODE:="0660", GROUP="dialout", \
    SYMLINK+="stlinkv2-1_%n"

Installing your first application

A kernel alone isn't much use, as an embedded developer you want to see some LEDs blink. Fortunately, there is an example blink app available from the TockOS app repository which tockloader can download and install (if you are using a board that is supported by tockloader).

For certain boards (e.g. Hail and imix), tockloader can read attributes from the board to configure how it communicates with the board. For many boards, however, tockloader cannot know which board and communication method you want to use, so you have to tell it explicitly. For example:

$ tockloader install --board nrf52dk --jlink blink
Could not find TAB named "blink" locally.

[0]     No
[1]     Yes

Would you like to check the online TAB repository for that app?[0] 1
Installing apps on the board...
Using known arch and jtag-device for known board nrf52dk
Finished in 2.567 seconds

Boards that use openocd will of course require the parameter --openocd instead of --jlink. If your board has a serial bootloader, tockloader should work without any additional arguments:

$ tockloader install blink

However, you can specify the board type manually as well:

$ tockloader install --board imix blink

You can also tell it which serial port to use (which is useful if you have multiple boards plugged in) by passing it the --port parameter like --port /dev/ttyACM0 to use /dev/ttyACM0:

$ tockloader --port /dev/ttyACM0 install blink

To see the list of boards tockloader knows about you can run:

$ tockloader list-known-boards

If everything has worked until here, the LEDs on your board should now display a binary counter. Congratulations, you have a working TockOS installation on your board!

Compiling applications

The last remaining step is to compile applications locally. All user-level code lives in two separate repositories:

The C version of the Tock library and the example applications is older and more stable, so it is a good idea to look at these first. So look at the libtock-c README and follow the steps therein. Then you can do the same for the libtock-rs README. This should give you a first impression of how to build and deploy applications for TockOS.

For an introduction on how applications work in TockOS, have a look at the "Userland" document in this directory.

Developing TockOS

Formatting Rust source code

Rust includes a tool for automatically formatting Rust source code. Simply run:

$ make format

from the root of the repository to format all rust code in the repository.

Keeping build tools up to date

Occasionally, Tock updates to a new nightly version of Rust. The build system automatically checks whether the versions of rustc and rustup are correct for the build requirements, and updates them when necessary. After the installation of the initial four requirements, you shouldn't have to worry about keeping them up to date.

Tockのポーティング

このガイドは新しいプラットフォームにTockをポーティングする方法を扱います。

It is a work in progress. Comments and pull requests are appreciated!

概要

高レベルでは、Tockを新しいプラットフォームに移植するには、新しい "board"をクレートとして作成し、 場合によってはさらに、"chip"クレートと"arch"クレートを追加する必要があります。boardクレートは、 chipクレートと共にカプセルをつなぎ合わせることで、ハードウェアプラットフォーム上で利用可能な リソースを正しく指定します (ピンの割り当て、ボーレートの設定、ハードウェアペリフェラルの割り当てなど)。 chipクレートは、kernel/src/hilにあるトレイトを実装することにより特定のマイクロコントローラ用の ペリフェラルドライバ(UART、GPIO、アラームなど)を実装します。対象のプラットフォームが既にTockで サポートされているマイクロコントローラを使用している場合は、既存のchipクレートを使用することができます。 archクレートは特定のハードウェアアーキテクチャ用の低レベルのコードを実装します (たとえば、チップの 起動時に行うことやシステムコールの実装方法など)。

クレート詳細

このセクションでは新たなハードウェアプラットフォームのポーティングに必要な各種クレートの実装に 必要なことをより詳細に説明します。

archクレート

Tockは現在、ARM Cortex-M0、Cortex-M3、Cortex M4とriscv32imac アーキテクチャをサポートしています。Tockにはアーキテクチャ固有のコードは あまりありません。固有のコードが必要なものは以下のとおりです。

  • シスコールの入口/出口
  • 割り込みの設定
  • 上半分の割り込みハンドラ
  • MPUの設定(必要があれば)
  • パワー管理の設定(必要があれば)

Tockを他のARM Cortex M(具体的には M0+, M23, M4F, M7)やriscv32の バリアントに移植するのはかなり簡単だと思います。他のアーキテクチャへのTockの 移植はおそらくもっと大変な作業になるでしょう。我々はアーキテクチャに依存 しないことを目指していますが少数のアーキテクチャでしかテストされていません。

Tockを新しいアーキテクチャに移植することに興味がある場合は、あまり深く 掘り下げる前にメールやSlackで開発チームに連絡を取った方が良いでしょう。

chipクレート

chipクレートは特定のマイクロコントローラ固有のものですが、マイクロ コントローラファミリに対して一般的になるようにする必要があります。たとえば、 nRF58240nRF58230の両マイクロコントローラのサポートは chips/nrf52クレートとchips/nrf5xクレートで共有されています。 これにより、重複するコードが減り、新たなマイクロコントローラの追加を容易に することができます。

chipクレートにはkernel/src/hilで定義されているインターフェースの マイクロコントローラ固有の実装が含まれています。

チップには数多くの機能がありますが、Tockはそれらを表現する多くのインター フェースをサポートしています。新たなチップの実装を少しずつ行ってください。 リセットと初期化を行うコードが動くようにしてください。それをチップの デフォルトクロックで動作するように設定し、GPIOインターフェースを追加して ください。そのチップを使う最小のボードをまとめて、GPIOを使うエンド ツーエンドのユーザーランドアプリケーションで検証するのが良いでしょう。

GPIOのような小さなものが動作するようになったら、それはTockにプル リクエストを行う絶好の機会です。それはこのチップに対するあなたの努力を 他の人に知らせることになり、うまくいけば他の人からのサポートを引き寄せる ことができます。また、コードをさらに書き継ぐ前にTockのコアチームから フィードバックを得るチャンスでもあります。

作業を進めていくとチップは合理的な作業単位に分解されていく傾向があります。 対象のチップ用にkernel::hil::UARTのようなものを実装して、プル リクエストを提出してください。新しいペリフェラルを選んでそれを繰り返して ください。

boardボード

boards/srcにあるboardクレートは物理的なハードウェアプラット フォームに固有のものです。基本的に、ボードファイルは特定のハードウェアを 設定できるようにカーネルを構成するものです。これには、センサー用のドライバの インスタンス化、これらセンサと通信バスとのマッピング、GPIOピンの設定などが 含まれます。

Tockはboardクレートの設定に「コンポーネント」を活用します。コンポーネント とは、特定のドライバ用のすべての設定コードを含む構造体のことであり、特定の プラットフォームに固有のオプションをボードに渡すよう要求するだけです。 たとえば、


#![allow(unused)]
fn main() {
let ambient_light = AmbientLightComponent::new(board_kernel, mux_i2c, mux_alarm)
    .finalize(components::isl29035_component_helper!(sam4l::ast::Ast));
}

上のコードは、アンビエントライトセンサー用のコンポーネントのインスタンスを 作成します。ボードの初期化はほとんどの場合コンポーネントを使って行うべき ですが、まだすべてのコンポーネントが作成されているわけではないので、一般的に、 ボードファイルはコンポーネントと冗長なドライバ初期化コードが混在したものに なります。最善の策は、既存のボードのmain.rsファイルから始めて、それを 適応させることです。まずはほとんどのカプセルを削除し、動作するようになるまで 少しずつカプセルを追加していくとよいでしょう。

警告: コンポーネントはシングルトンです。複数回インスタンス化することは できません。複数回インスタンス化されないように、コンポーネントはリセット ハンドラでインスタンス化するべきです。

ボードのサポート

カーネルコードに加えて、ボードはいくつかのサポートファイルも必要とします。 これは、ボード名やコードをボードにロードする方法、ユーザランド アプリケーションがこのボードに必要とするなにか特別なことなどのメタデータを 指定します。

panic!(別名、io.rs

各ボードはpanic!を処理するカスタムルーチンを作成しなければなりません。 ほとんどのpanic!機構はTockカーネルによって処理されますが、ボード作者は ハードウェアインターフェース、具体的にはLEDやUARTへの最低限のアクセスを 提供しなければなりません。

まず最初に、LEDベースのpanic!が動作するようにさせるのが最も簡単です。 panic!ハンドラに目立つ色のLEDを設定してから、 kernel::debug::panic_blink_foreverを呼び出してください。

UARTが利用できれば、カーネルは非常に有用なデバッグ情報をたくさん表示する ことができます。しかし、panic!の状況にあるので実装は最小限にすることが 重要です。特に、提供されるUARTは同期的なものでなければなりません(これは カーネルの他のUARTインタフェースがすべて非同期であるのとは対照的である ことに注意してください)。通常は、単に一度に1バイト直接UARTに書き込むだけの 非常に単純なWriterを実装することが最も簡単かつ最善の方法です。panic! UARTライターは効率的であることは重要ではありません。これができたら、 kernel::debug::panic_blink_foreverの呼び出しを kernel::debug::panicの 呼び出しに置き換えることができます。

ほとんどは歴史的な理由から、すべてのボードのパニック実装は、ボードの main.rsファイルに隣接するio.rsというファイルに置きます。

ボート用のCargo.tomlとbuild.rs

すべてのboardクレートには、トップレベルマニフェストCargo.tomlを作成 しなければなりません。通常は、他のボードからコピーしてボード名と作成者を 適宜変更するだけです。Tockにはbuild.rsというビルドスクリプトも 含まれていますが、これをコピーする必要もあることに注意してください。 ビルドスクリプトはカーネルレイアウトに依存関係を追加するものです。

ボード用のMakefile

すべてのboardクレートのルートにはMakefileがあります。少なくとも、 ボード用のMakefileを含める必要があります。

# Hailプラットフォーム用のtockカーネルを構築するためのMakefile

TARGET=thumbv7em-none-eabi      # ターゲットトリプル
PLATFORM=hail                   # ボード名をここに

include ../Makefile.common      # ../ ボートは$(TOCK)/boards/<board>にあると仮定

Tockはビルドシステムのほとんどを担当するboards/Makefile.commonを 提供しています。一般的には、このMakefileを掘り下げる必要はありません。 もし、何かが動作していないようであれば、slackで聞いてみてください。

ビルドしたカーネルをボードにのせる

カーネルのビルドに加えて、ボードのMakefileには、ボードにコードをのせる ための規則も含める必要があります。これは当然ボード固有のものになりますが、 通常、Tockには2つのターゲットが用意されています。

  • program: 「プラグインプラグ」方式のロード用です。通常、ボードには ブートローダやその他のサポートICが搭載されています。通常操作として、 ユーザはボードをプラグインしてmake programと入力するだけで コードをロードできることが期待されます。
  • flash: 「より直接的な」ロード用です。通常、JTAGまたは同等の インターフェイスが使用されることを意味します。多くの場合、外部 ハードウェアが必要であることを意味します。ただし、開発キットボードの 中にはJTAGが内蔵されているものもあるので、外部ハードウェアは厳しい 条件ではありません。

_program_や_flash_をサポートしない場合は、ボードにプログラムする方法を 説明した次のような空の規則を定義する必要があります。

.PHONY: program
        echo "To program, run SPEICAL_COMMAND"
        exit 1
ボード用のREADME

すべてのボードは、クレートのトップレベルにREADME.mdファイルを 置かなくてはいけません。このファイルには以下を記載します。

  • プラットフォームに関する情報とプラットフォームの購入/入手方法へのリンクを 提供する。プラットフォームに異なるバージョンがある場合は、テストに 使用したバージョンを明記する。
  • 必要な追加の依存関係を含むハードウェアをプログラムする方法に課する 概要を含める。

アプリケーションのロード

Tockloaderが (おそらくは いくつかのフラグに特定の値を設定することにより)ボードへのアプリのロードを サポートするはずです。もしそうでない場合は、Tockloaderのリポジトリで 課題を作成してください。そうすれば対象のボードに対するローディングコードを サポートするようツールを更新することができます。

よくある落とし穴

  • ボードのmain.rsファイルを設定する際には注意が必要です。特に、 コールバックが失われないように、カプセルに必要なset_client関数が すべて呼び出されていることを確認することが重要です。これらを忘れると、 プラットフォームが何もしない結果になることが多いです。

Adding a Platform to Tock Repository

After creating a new platform, we would be thrilled to have it included in mainline Tock. However, Tock has a few guidelines for the minimum requirements of a board that is merged into the main Tock repository:

  1. The hardware must be widely available. Generally that means the hardware platform can be purchased online.
  2. The port of Tock to the platform must include at least:
    • Console support so that debug!() and printf() work.
    • Timer support.
    • GPIO support with interrupt functionality.
  3. The contributor must be willing to maintain the platform, at least initially, and help test the platform for future releases.

With these requirements met we should be able to merge the platform into Tock relatively quickly. In the pull request to add the platform, you should add this checklist:

### New Platform Checklist

- [ ] Hardware is widely available.
- [ ] I can support the platform, which includes release testing for the platform, at least initially.
- Basic features are implemented:
  - [ ] `Console`, including `debug!()` and userspace `printf()`.
  - [ ] Timers.
  - [ ] GPIO with interrupts.

Tockのツリー外

このガイドでは、Tockマスターリポジトリ以外でサブシステムを維持するための ベストプラクティスを説明します。

このガイドは作業中です。コメントやプルリクエストをお待ちしています。

概要

Tock は安定したシステムコールABIの維持を目的としていますが、カーネルインター フェースの安定性を保証するものではありません。Tockの開発状況を把握するには、 主に次の2 つのチャンネルがあります。

  • tock-devメーリングリスト: Tockの大きな変更はすべてこのメーリングリストで報告されます。 このメーリングリストは一般的なTockの開発もサポートしていますが、比較的 トラフィックは少ないです(1日平均で1通未満のメール)。
  • Tock GitHub: Tockの変更はすべて Pull Requestsを通じて行われます。些細でない変更は一般にフィードバックを得る ためにマージまで少なくとも一週間はかかります。

最後に、遠慮なく助けを求めてください

構造

通常、プロジェクトの中にTockをsubmoduleとして入れておくのが一番簡単です。

一般に次のようにTock のディレクトリ構造に倣うことを勧めます。

$ tree .
.
├── boards
│   └── my_board
│       ├── Cargo.toml
│       ├── Makefile
│       └── src
│           └── main.rs
├── my_drivers
│   ├── Cargo.toml
│   └── src
│       ├── my_radio.rs
│       └── my_sensor.rs
└── tock                   # Where this is a git submodule
│   ├── ...

ボード

ボードのMakefileにPLATFORM変数を設定し、このプラットフォームの名前を指定し、 最上位のTock Makefileを含める必要があります。また、カーネルをボードにロードする 方法を指定するprogramターゲットとflashターゲットを定義することを強く 勧めます。

PLATFORM = my_board

# Tockビルド規則を取り込む
include ../../tock/boards/Makefile.common

# bootloaderまたは簡単で直接的な接続経由でロードする規則
program:
  ...

# JTAGまたはその他の外部プログラマ経由でロードする規則
flash:
  ...

ボードのCargo.tomlにはボードが使用するすべてのコンポーネントを見つける方法を 記述する必要があります。これらのほとんどはTockの要素への参照になるでしょう。

[package]
name = "my_board"
version = "0.1.0"
authors = ["Example Developer <developer@example.com>"]

[profile.dev]
panic = "abort"
lto = true
opt-level = 0
debug = true

[profile.release]
panic = "abort"
lto = true

[dependencies]
cortexm4 = { path = "../../tock/arch/cortex-m4" }
capsules = { path = "../../tock/capsules" }
sam4l = { path = "../../tock/chips/sam4l" }
kernel = { path = "../../tock/kernel" }
my_drivers = { path = "../../my_drivers" }

その他すべてのこと

カスタムチップ、ドライバ、その他のコンポーネントはCargo.tomlを必要とするだけの はずです。

[package]
name = "my_drivers"
version = "0.1.0"
authors = ["Example Developer <developer@example.com>"]

[dependencies]
kernel = { path = "../tock/kernel" }

Debugging Help

Tock Style

This document overviews some stylistic conventions that Tock generally adheres to.

Code Style

Tock uses rustfmt for source code style and formatting. In general, all of Tock's code is formatted according to the rustfmt defaults. There are a few exceptions, but these should generally be avoided.

Commenting

Rust includes three types of comments: //, ///, and //!. Tock uses all three in line with their usage in Rust code more generally.

  • //: Two slashes are used for "internal" comments that document specific details about certain code, leave notes for other developers, or specify internal metadata like the primary author of a file. These comments are only visible in the current file and are not used for documentation generation.

  • ///: Three slashes are used to specify public documentation about data structures and functions. These comments generally describe what a certain element does and how to use it. All /// comments are used to automatically generate API documentation and will be shared outside of the file they are written in. In general, every public function or object should have a /// comment.

  • //!: Two slashes and a bang are used for document-level comments. These comments are only used at the top of a file to provide an overview of all of the code contained in the file. Typically these comments also include a general usage example. These comments will also be used for automatic documentation generation.

    The first line of a //! comment will be used as a descriptive tagline, and as such should be short and provide essentially a subtitle for the code file (where the file name acts as the title). Generally the first line should be no more than 80 characters. To identify the tagline, the second line of the comment should just be //! with no other text.

Both /// and //! comments support Markdown.

Example: mycapsule.rs


#![allow(unused)]
fn main() {
//! Prints "hello" on boot.
//!
//! This simple capsule implements hello world by printing a message when it
//! is initialized.
//!
//! Usage
//! -----
//!
//! ```
//! let helloworld = mycapsule::HelloWorld::new();
//! helloworld.initialize();
//! ```

/// This struct contains the resources necessary for the Hello World example
/// module. Boards should create this to run the hello world example.
struct HelloWorld {
    ...
}

impl HelloWorld {
    /// Start the hello world example and print out "Hello World".
    ///
    /// This should only be called after the debugging module is setup.
    // Someday we should use a UART directly, but that can be implemented later.
    fn initialize () {
        debug!("Hello World");
    }
}
}

Using Descriptive Names

Tock generally tries to avoid abbreviations in variable and object names, and instead use descriptive and clear names. This helps new readers of the code understand what different elements are doing. Plus, rustfmt helps with formatting the code when using the longer names, and Github does not charge us by the character.

  • ArrayIdxArrayIndex
  • BtnInterruptButtonInterrupt
  • RegVoltOutRegulatedVoltageOutput
  • GPIO.low_power()GPIO.deactivate_and_make_low_power()

Tock Working Groups

Working groups (wg) are focused groups to organize development around a particular aspect of Tock.

Existing Working Groups

Tock Pull Request Process

Abstract

This document describes how the Tock core working group merges pull requests for and makes releases of the main Tock repository.

1. Introduction

As Tock supports more chips and services, changes to core interfaces or capsules will increasingly trigger bugs or integration problems. This document describes the process by which pull requests for the main Tock repository are handled. This process is not set in stone, and may change as problems or issues arise.

Active development occurs on the master branch. Periodic releases (discussed more below) are made on branches.

2. Pull Requests

Any pull request against the master branch is reviewed by the core Tock team. Pull requests fall into two categories:

  1. Upkeep pull requests involve minor changes to existing implementations. Examples of upkeep requests involve bug fixes, documentation (that isn't specification), or minor reimplementations of existing modules.
  2. Significant pull requests involve new modules, significant re-implementations, new traits, new kernel components, or changes to the build system.

Whether a pull request is upkeep or significant is based not only on the magnitude of the change but also what sort of code is changed. For example, bug fixes that are considered upkeep for a non-critical capsule might be considered significant for kernel code, because the kernel code affects everything and has more potential edge cases.

The core team decides whether a pull request is upkeep or significant. The first person to look at the pull request can decide, or defer based on other core member feedback. Pull requests by a member of the core team need to be reviewed by a different member of the core team. If a team member decides that a pull request is significant but another team member decided it was upkeep and merged it, then the merging team member is responsible for backing out the merge and resolving the discussion. Any team member can decide that a pull request is significant. The assumption is that the core team will have good consensus on the boundary between upkeep vs. significant, but that specialized knowledge means that some team members will see implications that others may not.

Upkeep pull requests can be merged by any member of the core team. That person is responsible for the merge and backing out the merge if needed.

Significant pull requests require review by the entire core team. Each core team member is expected to respond within one week. There are three possible responses:

  • Accept, which means the pull request should be accepted (perhaps with some minor tweaks, as per comments).
  • No Comment, which means the pull request is fine but the member does not promote it.
  • Discuss, which means the pull request needs to be discussed by the core team before considering merging it.

Core team members can change their votes at any time, based on discussion, changes, or further thought.

3. Continuous Integration

Tock leans heavily on automated integration testing and as a project is generally willing to explore new and novel means of testing for hardware reliability.

With exceptions for drafts or works-in-progress, generally it is expected that a pull request pass the full continuous integration (CI) suite before core team members will perform an in-depth review.

One frequent challenge with CI setups is replicating failures in local development environments. Tock goes to great lengths to mitigate this as much as possible. Within reason, the inability to replicate a CI test in a local development environment shall be considered a bug (however, it is reasonable that local CI requires the install of non-trivial tooling, so long as there is a well-documented, reliable path to set up the tooling locally).

CI Organization

All CI is driven by make rules.

Generally, there are a series of fine-grained targets that do the actual tests, and then a meta layer of rules that are invoked depending on context.

The short answer: make prepush

This is a meta-target that runs what Tock considers the "standard developer CI". This is the rule that should be run locally before submitting PRs. It runs the quicker jobs that catch the majority of small errors. Developers are encouraged to consider wiring this to the git pre-push hook to run automatically when pushing upstream.

The complete CI setup

All CI is required to support the possibility of parallel make invocation (i.e. make -jN), but is not required to handle multiple independent make processes.

ci-job-*

To the extent reasonable, individual tests are broken into small atomic units. All actionable make recipes that run tests must be in ci-job-* rules. These perform individual, logical actions related only to testing. These rules must not perform any one-off or setup operations. No automated tooling should invoke job rules directly. If a CI check fails, developers should be able to run the failed ci-job-* locally, although in certain cases this may require installing supporting tooling.

ci-setup-*

These are rules that run any required setup for jobs to succeed. They may install arbitrary packages or do any other significant labor. Many jobs may rely on the same setup target. To the extent possible, setup targets should cache their results to avoid re-execution. Setup targets should handle upgrades automatically; this may include automatically clearing caches or other artifacts if needed. Setup targets may handle downgrades, but developers working on experimental branches may be required to handle these cases manually. Setup targets are permitted to expect "total ownership" of the directories they create and manage.

Setup rules may vary between runner and local environments, as they may perform automatic and possibly invasive (e.g. apt install) operations on runners.

When run locally, setup targets must prompt users prior to system-wide persistent changes. These prompts should be rare, as example, asking the user to install system-wide development packages needed for a build. These prompts must not generate on every invocation of the setup rule; that is, setup rules must first check if the install has already been completed and not prompt the user in that case. If an update or upgrade is required, setup targets must prompt before installing.

ci-runner-*[-*]

These are targets like ci-runner-netlify and ci-runner-github. They represent exactly what is run by various CI runners. For platform with multiple CI rules, like GitHub, the ci-runner-github is a meta target that runs all GitHub checks, while ci-runner-github-* are the rules that match the individual runners. These targets must execute correctly on a local development environment. Small deviations in behavior between the runner and local execution are permitted if needed, but should be kept to a minimum.

ci-all

A meta target that runs every piece of CI possible. If this passes locally, all upstream CI checks should pass.

4. Reviews

To be merged, a pull request requires two Accept and no Discuss votes. The review period begins when a review is requested from the Github team core-team. If a member does not respond within a week, their vote is considered No Comment. If a core team member stops responding to many significant pull requests they may be removed from the core team.

Core team members enter their votes through GitHub's comment system. An "Approve" is considered an Accept vote, a "Comment" is considered a "No Comment" vote and a "Request Changes" is considered a "Discuss". If, after discussion, non-trivial changes are necessary for the pull request, the review window is re-started after the changes are made.

5. Release Process

Tock releases are milestone-based, with a rough expectation that a new release of Tock would occur every 3-12 months. Before a release, a set of issues are tagged with the release-blocker tag, and the release will be tested when all of the release-blocker issues are closed. One week before the intended release date, all new pull requests are put on hold, and everyone uses/tests the software using the established testing process. Bug fixes for the release are marked as such (in the title) and applied quickly. Once the release is ready, the core team makes a branch with the release number and pull request reviews restart.

Release branches are named release-[version]. For example, 'release-1.4.1'.

Patches may be made against release branches to fix bugs.

Note: Previously, Tock operated with a time-based release policy with the goal of creating a release every two months. The intent was these periodic stable releases would make it easier for users to install and track changes to Tock. However, the overhead of keeping to that schedule was too daunting to make the releases reliably timed, and it often did not fit well with the inclusion of major features which might be in-flight at a release point.

Other Tock Repositories

This document covers the procedure of the core Tock repository (tock/tock). However, there are several other repositories that are part of the greater Tock project.

Userland Repositories

Tock has two userland environments that are heavily developed and supported:

  • tock/libtock-c The C/C++ runtime was the first runtime developed. It is fairly stable at this point and sees primarily maintenance support as needed. Its development process follows the main tock repository, with the same core team.

  • tock/libtock-rs The Rust runtime is an active work-in-progress. While basic application scenarios work, there are still major architectural changes coming as it converges. Thus, the Rust runtime follows a slightly less formal model to allow it to move faster. Primary owners of the Rust runtime are:

    • @alevy
    • @Woyten
    • @torfmaster
    • @jrvanwhy

    However the Tock core working group reserves the right to make final authoritative decisions if need merits.

Tertiary Repositories

Tock has several additional smaller support repositories. These generally do not have any formal contribution guidelines beyond pull requests and approval from the primary maintainer(s). Any member of the core working group can merge PRs in these repositories, however, generally things are deferred to the owner of the component.

  • tock/book Getting start guide and tutorials for Tock. Primarily maintained by @alevy and @bradjc (Dec 2019).
  • tock/elf2tab Tool to convert apps from .elf to Tock Application Bundles aka .tabs. Primarily maintained by @bradjc (Dec 2019).
  • tock/tockloader Tool for loading Tock kernel and applications onto hardware boards. Primarily maintained by @bradjc (Dec 2019).
  • tock/tock-archive Components of Tock (often hardware platforms) no longer under active development. Maintained by the core working group (Dec 2019).
  • tock/tock-bootloader Utility for flashing apps via USB; works with tockloader. Primarily maintained by @bradjc (Dec 2019).
  • tock/tock-www The tockos.org website. Primarily maintained by @alevy and @ppannuto (Dec 2019).

Other repositories under tock/ are either experimental or archived.