C でのハードウェア抽象化レイヤー (HAL) の作成
ベナンのヤコブ | 2023 年 5 月 19 日
ハードウェア抽象化レイヤー (HAL) は、あらゆる組み込みソフトウェア アプリケーションにとって重要なレイヤーです。 HAL を使用すると、開発者はハードウェアの詳細をアプリケーション コードから抽象化または分離できます。 ハードウェアを切り離すことにより、アプリケーションのハードウェアへの依存関係が解消されます。これは、アプリケーションがオフターゲット、つまりホスト上で作成およびテストされるのに最適な位置にあることを意味します。 これにより、開発者はアプリケーションのシミュレーション、エミュレーション、テストをより迅速に行うことができ、バグが除去され、より早く市場に投入され、全体的な開発コストが削減されます。 組み込み開発者が C で記述された HAL をどのように設計して使用できるかを見てみましょう。
ハードウェアに直接アクセスする組み込みアプリケーション モジュールは比較的一般的です。 これによりアプリケーションの作成が簡単になりますが、アプリケーションがハードウェアと密接に結合するため、プログラミングの実践としては不適切です。 これは大したことではないと思うかもしれません。結局、複数のハードウェア セットでアプリケーションを実行したり、コードを移植したりする必要があるのは誰でしょうか? その場合、最近チップ不足に悩まされ、ハードウェアを再設計するだけでなく、すべてのソフトウェアを書き直す必要があったすべての人に案内します。 この問題の解決に役立つ、オブジェクト指向プログラミング (OOP) の多くの人が依存関係逆転の原則として知っている原則があります。
依存関係逆転の原則は、「高レベルのモジュールは低レベルのモジュールに依存すべきではないが、両方とも抽象化に依存すべきである」と述べています。 依存関係逆転の原則は、多くの場合、インターフェイスまたは抽象クラスを使用してプログラミング言語に実装されます。 たとえば、読み取りおよび書き込み関数をサポートするデジタル入出力 (dio) インターフェイスを C++ で作成すると、次のようになります。
クラス dio_base {
公共:
virtual ~dio_base() = デフォルト;
// クラスメソッド
virtual void write(dioPort_t ポート、dioPin_t ピン、dioState_t 状態) = 0;
仮想 dioState_t 読み取り (dioPort_t ポート、dioPin_t ピン) = 0;
}
C++ に詳しい方は、仮想関数を使用してインターフェイスを定義していることがわかります。これには、詳細を実装する派生クラスを提供する必要があります。 このタイプの抽象クラスを使用すると、アプリケーションで動的ポリモーフィズムを使用できます。
コードからは、依存関係がどのように逆転したかを確認するのは困難です。 代わりに、簡単な UML 図を見てみましょう。 以下の図では、led_io モジュールは依存性注入を通じて dio インターフェイスに依存しています。 led_io オブジェクトが作成されると、デジタル入出力の実装へのポインターが提供されます。 マイクロコントローラー dio の実装は、dio_base で定義された dio インターフェイスにも適合する必要があります。
上の UML クラス図を見ると、これは C++ などの OOP 言語でアプリケーションを設計するのには最適だが、C には当てはまらないと思われるかもしれません。しかし、実際には、C ではこのタイプの動作を得ることができます。依存関係を逆転させます。 C では構造体を使用して使用できる簡単なトリックがあります。
まず、インターフェースを設計します。 これは、インターフェイスがサポートする必要があると思われる関数シグネチャを記述するだけで実行できます。 たとえば、インターフェイスがデジタル入出力の初期化、書き込み、読み取りをサポートする必要があると判断した場合、次のような関数をリストするだけで済みます。
void write(dioPort_t const ポート、dioPin_t const ピン、dioState_t const 状態);
dioState_t read(dioPort_t const ポート、dioPin_t const ピン);
これは、virtual キーワードと純粋な抽象クラス定義 (= 0) を除いて、以前に C++ 抽象クラスで定義した関数によく似ていることに注意してください。
次に、これらの関数を typedef 構造体にパッケージ化します。 構造体は、dio インターフェイス全体を含むカスタム型のように機能します。 初期コードは次のようになります。
typedef 構造体 {
void init (DioConfig_t const * const Config);
void write (dioPort_t const ポート、dioPin_t const ピン、dioState_t const 状態);
dioState_t 読み取り (dioPort_t 定数ポート、dioPin_t 定数ピン);
ゴッドベース;
上記のコードの問題は、コンパイルできないことです。 C では、構造体に関数を含めることはできません。ただし、関数ポインターを含めることはできます。 最後のステップは、構造体の dio HAL 関数を関数ポインターに変換することです。 関数名の前に * を置き、その後 () で囲むことで関数を変換できます。 たとえば、構造体は次のようになります。
typedef 構造体 {
void (*init) (DioConfig_t const * const Config);
void (*write) (dioPort_t const ポート、dioPin_t const ピン、dioState_t const 状態);
dioState_t (*読み取り) (dioPort_t 定数ポート、dioPin_t 定数ピン);
ゴッドベース;
ここで、led_io モジュールで Dio HAL を使用したいとします。 dio_base 型へのポインターを受け取る LED init 関数を作成できます。 そうすることで、依存関係を注入し、低レベルのハードウェアへの依存関係を削除することになります。 LED init モジュールの C コードは次のようになります。
void led_init(dio_base * const dioPtr, dioPort_t const portInit, dioPin_t const pinInit){
dio = dioPtr;
ポート = portInit;
ピン = ピンヒート;
}
LED モジュールの内部では、開発者はハードウェアについて何も知らなくても HAL インターフェイスを使用できます。 たとえば、次のように led_toggle 関数で dio ペリフェラルに書き込むことができます。
void LED_toggle(void){
bool state = (dio->read(port, pin) == dio->HIGH) ? dio->LOW : dio->HIGH);
dio->write(ポート、ピン、状態};
}
LED コードは完全に移植可能で再利用可能であり、ハードウェアから抽象化されます。 ハードウェアへの実際の依存性はなく、インターフェイスのみに依存します。 この時点では、LED コードを使用できるようにするためのインターフェイスも実装するハードウェアの実装がまだ必要です。 これを実現するには、インターフェイスの署名に一致する関数を備えた dio モジュールを実装します。 次に、次のような C コードを使用して、これらの関数をインターフェイスに割り当てます。
god_base god_hal = {
Dio_Init、
ディオ_ライト、
ディオリード
}
次に、LED モジュールは次のようなものを使用して初期化されます。
LED_init(dio_hal、PORTA、PIN15);
それでおしまい! このプロセスに従えば、一連のハードウェア抽象化レイヤーを通じてアプリケーション コードをハードウェアから切り離すことができます。
ハードウェア抽象化レイヤーは、すべての組み込みソフトウェア開発者がハードウェアへの結合を最小限に抑えるために活用する必要がある重要なコンポーネントです。 インターフェイスを定義し、それを C で実装する簡単な手法を検討してきました。結局のところ、インターフェイスと抽象化レイヤーの利点を得るために C++ のような OOP 言語は必要ありません。 C にはそれを実現するのに十分な機能が備わっています。 留意すべき点の 1 つは、この手法ではパフォーマンスとメモリの観点から若干のコストがかかることです。 関数呼び出しに相当するパフォーマンスと、インターフェイスからの関数ポインターを格納するのに十分なメモリが失われる可能性が高くなります。 結局のところ、このわずかなコストにはそれだけの価値があります。
テキスト形式の詳細