移植性
組み込み環境では、移植性は非常に重要なトピックです。すべてのベンダー、さらには同じメーカーの各ファミリであっても、提供されるペリフェラルや機能は異なり、同様に、それらのペリフェラルとやり取りする方法も異なります。
このような差異を均一化する一般的な方法として、Hardware Abstraction Layer、すなわち HAL と呼ばれるレイヤーを用いるものがあります。
ハードウェア抽象化とは、ソフトウェア内の一連のルーチンであり、プラットフォーム固有の詳細の一部をエミュレートすることで、プログラムにハードウェアリソースへの直接アクセスを提供するものです。
これにより、プログラマは多くの場合、ハードウェアに対する標準的なオペレーティングシステム(OS)呼び出しを提供することで、デバイス非依存で高性能なアプリケーションを記述できます。
Wikipedia: Hardware Abstraction Layer
組み込みシステムはこの点で少し特殊です。というのも、通常はオペレーティングシステムやユーザーがインストール可能なソフトウェアがあるのではなく、全体としてコンパイルされるファームウェアイメージがあり、さらに他にもさまざまな制約があるためです。そのため、Wikipedia で定義されている従来のアプローチは潜在的には機能するかもしれませんが、移植性を確保するための最も生産的なアプローチである可能性は高くありません。
では、Rust ではこれをどのように実現するのでしょうか? embedded-hal の出番です……
embedded-hal とは何でしょうか?
ひとことで言えば、これは HAL implementation、drivers、applications (or firmwares) の間における実装契約を定義する trait の集合です。これらの契約には、機能とメソッドの両方が含まれます(つまり、ある型に対して特定の trait が実装されていれば、その HAL implementation は特定の機能を提供します。また、ある trait を実装する型を構築できるのであれば、その trait で定義されたメソッドが利用可能であることが保証されます)。
典型的なレイヤー構成は次のようになります。
embedded-hal で定義されている trait の一部は次のとおりです。
- GPIO(入力ピンと出力ピン)
- シリアル通信
- I2C
- SPI
- タイマー/カウントダウン
- アナログ-デジタル変換
embedded-hal trait と、それらを実装・利用する crate が存在する主な理由は、複雑さを抑えることにあります。アプリケーションでは、ハードウェア内のペリフェラルの利用に加えて、アプリケーション自体、さらに追加のハードウェアコンポーネント向けのドライバまで実装しなければならない場合があることを考えると、再利用性が非常に限定的になることは容易に理解できるでしょう。これを数式で表すと、M をペリフェラル HAL 実装の数、N をドライバの数とした場合、各アプリケーションごとに車輪の再発明をすると、最終的に M*N の実装が必要になります。一方、embedded-hal trait が提供する API を利用すれば、実装の複雑さは M+N に近づきます。もちろん、明確に定義され、すぐに使える API によって試行錯誤が減るなど、追加の利点もあります。
embedded-hal の利用者
前述のとおり、HAL の主な利用者は 3 種類あります。
HAL implementation
HAL implementation は、ハードウェアと HAL trait の利用者との間のインターフェイスを提供します。典型的な実装は、次の 3 つの部分で構成されます。
- 1 つ以上のハードウェア固有の型
- そのような型を生成および初期化する関数。多くの場合、さまざまな設定オプション(速度、動作モード、使用するピンなど)を提供する
- その型に対する embedded-hal trait の 1 つ以上の
traitimpl
このような HAL implementation には、さまざまな形態があります。
- 低レベルのハードウェアアクセス経由。たとえばレジスタを介するもの
- オペレーティングシステム経由。たとえば Linux 上で
sysfsを使うもの - アダプタ経由。たとえばユニットテスト用の型のモック
- ハードウェアアダプタ用ドライバ経由。たとえば I2C マルチプレクサや GPIO エキスパンダ
Driver
Driver は、embedded-hal trait を実装するペリフェラルに接続された、内部または外部コンポーネント向けのカスタム機能群を実装します。このような driver の典型例としては、各種センサー(温度、磁力計、加速度計、光)、表示デバイス(LED アレイ、LCD ディスプレイ)、アクチュエータ(モーター、送信機)などがあります。
driver は、embedded-hal の特定の trait を実装する型のインスタンスで初期化される必要があり、これは trait bound によって保証されます。そして、対象デバイスとやり取りするための独自のメソッド群を持つ独自の型インスタンスを提供します。
Application
Application は、さまざまな部分を結び付け、目的とする機能が実現されるようにします。異なるシステム間で移植する際に最も適応の手間がかかるのはこの部分です。というのも、application は HAL implementation を通じて実際のハードウェアを正しく初期化する必要があり、しかもハードウェアごとに初期化方法が異なり、ときには大きく異なるからです。また、ユーザーの選択も大きな役割を果たします。コンポーネントは物理的に異なる端子に接続できる場合があり、ハードウェアバスは構成に合わせるために外部ハードウェアを必要とすることがあり、あるいは内部ペリフェラルの利用において異なるトレードオフが存在するためです(たとえば、能力の異なる複数のタイマーが利用可能であったり、あるペリフェラルが別のものと競合したりします)。