デバイスドライバ開発入門

HALを使うと開発は速い一方、オーバーヘッドでリアルタイム要件を満たせない場合があります。逆にレジスタの直接操作は高速ですが、移植性が課題になります。組み込みドライバ実装でこうした壁にぶつかった経験はないでしょうか。

この記事では、抽象化ライブラリと低レイヤライブラリと標準インターフェース規格の使い分けと、上位層の変更を防ぐ共通API設計の勘所を整理します。扱うのは層設計、レジスタ作法、初期化とデータの転送方式、ライブラリ比較の4テーマです。

本文は一般名称で記述し、具体例をあげる場合は特定製品の推奨を意図しない例示として扱います。ここで示す考え方は、各社のマイコン(MCU:マイクロコントローラユニット。組み込みシステム内で特定のタスクを管理できるように設計された小型のコンピュータ)に共通して応用できます。

組み込みデバイスドライバが担う役割と層設計

組み込みデバイスドライバの4層構造図。上からアプリケーション層、ペリフェラル抽象層(ドライバAPI)、レジスタ操作層(ハードウェア依存)、ハードウェアの順に並び、隣接する層どうしが双方向にやり取りする。右側にUARTでの対応例として、log_print()、uart_send()/uart_init()、USART_DRなどのレジスタ、UART(ハードウェア)を示している。

組み込みデバイスドライバは、ハードウェアとアプリケーション層を仲介するソフトウェアです。ドライバの抽象化レベルを低くしすぎると、特定ハードウェアへの依存が強まって移植性が落ちます。逆に高くしすぎると、オーバーヘッドによる速度低下を招くおそれがあります。製品要件に合った抽象度の決定が入門の第一歩です。

ハードウェアとアプリケーション層の境界を吸収する3層モデル

一般的には「レジスタ操作層」「ペリフェラル抽象層」「アプリケーション層」の3層構造を取ります。UARTなら、レジスタ層が周辺レジスタへの直接アクセス、抽象層がuart_send()、アプリケーション層がlog_print()という階層です。レジスタ名はマイコンの世代やベンダーにより異なります。例として、ある世代では送信と受信で共通のデータレジスタが用いられ、別の世代では送信用と受信用にレジスタが分離されている場合があります。

抽象化しすぎず薄すぎない層厚の決め方

ペリフェラルを1種類しか使わないなら層は薄くてもよいです。複数のマイコン間で移植する場合や、ペリフェラル種別(UART、I2C、SPI)を統一APIで扱う場合は抽象層を1段挟みます。割り込み内ホットパスは抽象を介さず、初期化や設定変更に抽象APIを使う折衷も多いです。

レジスタ直接制御の基本作法

メモリマップドI/Oの操作をおこなう場合は、コンパイラの最適化によってレジスタアクセスが省略または並べ替えられる可能性があることに留意が必要です。再現性のないバグの発生を防ぐためには、volatile修飾を用いて最適化の悪影響を防ぎ、メモリバリアを用いて実行順序を保証することが求められます。volatile、順序保証、ビット演算などの項目を押さえることが重要です。

volatile、メモリバリア、順序保証

レジスタアドレスを*(volatile uint32_t *)でキャストして参照することで、コンパイラが値の必要の都度レジスタから読み込むようにします。コンパイラ最適化やメモリ属性、割り込みや例外の境界によっては、DSB(Data Synchronization Barrier)、DMB(Data Memory Barrier)、ISB(Instruction Synchronization Barrier)で意図した順序を明示するのが有効です。たとえばベクタテーブルオフセットレジスタ(VTOR:Vector Table Offset Register)の変更後に例外を想定する場合は、DSBに加えてISBの使用が案内されています。パイプラインに残る古い命令フェッチが問題になるためです。割り込みコントローラの優先度変更後にも同様の配慮が推奨され、L1キャッシュを持つ上位のコアではキャッシュメンテナンス操作と組みあわせます。

ビット演算とビットフィールド構造体の扱い

レジスタの個別ビット操作は reg |= (1U<<3) や reg &= ~(1U<<3) のリード・モディファイ・ライト(Read-Modify-Write)が基本です。C言語のビットフィールド構造体は、メモリレイアウトが処理系に依存するため、レジスタ定義には推奨されないことに留意が必要です。マスク定数とシフト量で書けば移植性が高まります。

標準インターフェース規格の定義の活用と命名規則

マイコン向けに標準化されたソフトウェアインターフェース規格のヘッダは、PERIPH->REG のような構造体経由のアクセスを提供します。コア周りのレジスタ定義は規格側で共通化されている一方、UARTなどの周辺レジスタの定義は、各ベンダーが提供するデバイスヘッダに含まれるのが一般的です。デバイスヘッダの定義はベンダーのリファレンスマニュアルと対応づけやすい一方で、周辺名やレジスタ名がベンダー間で完全に同一とは限らない点に注意します。ビット定義(PERIPH_REG_BIT など)も併用すると、マジックナンバー(ソースコードに直接記載された数値)を排除できます。なお本規格は現在6系が提供されており、各コンポーネントに個別の版があります。信号処理やニューラルネットワーク向けの機能は独立パックへ分離されています。6系では旧世代のRTOS API(v1)のサポートが廃止され、後継のRTOS API(v2)へ一本化されました。

ペリフェラル初期化とデータ転送方式の選択

ドライバ実装の難しさはレジスタ操作より、初期化シーケンスと転送方式の選択にあります。クロック、ピン、割り込みの順序を誤れば初期化がエラー出力なく失敗し、転送方式の選択ミスはCPU負荷とリアルタイム性に直結します。

クロックゲーティングとピン設定の順序

ペリフェラルクロックが有効になる前にレジスタへ書いた場合は、値が反映されず期待する動作をしない可能性があります。一般的には、クロック制御レジスタでペリフェラルクロックを有効化し、GPIOの代替機能レジスタ(Alternate Function Register)を設定し、ペリフェラル制御レジスタを設定する順が定番です。

ペリフェラルごとに接続バスが異なる点にも注意します。同じUARTでも製品によって接続される周辺バスが異なる場合があります。使用する製品のリファレンスマニュアルで確認しましょう。

ポーリング、割り込み、DMAの使い分け基準

転送量、応答要件、CPU負荷の3軸で選びます。少量かつ低頻度ならポーリング、中頻度でリアルタイム性が必要なら割り込み、連続する大量の転送ならCPUを介さないDMA(ダイレクトメモリアクセス)が定番です。UARTを例にとると、デバッグ出力はポーリング、コマンド受信は割り込み、一定量のデータ送信はDMAで実装する構成が一般的です。

割り込みハンドラ(ISR)に書いてよいことと書いてはいけないこと

割り込みハンドラ(ISR:Interrupt Service Routine)内は「短く、最小限、ブロッキング処理なし」が原則です。フラグセットやリングバッファpush、セマフォ解放程度に留め、解析や送信はメインタスクへ委譲します。printf、malloc、長時間ポーリングはISR内での使用を避けます。

抽象化ライブラリと低レイヤライブラリの比較と移植性設計

ベンダー提供のHAL(ハードウェア抽象化レイヤ)は便利ですが、コードサイズの肥大化やオーバーヘッドが課題です。低レイヤライブラリ(レジスタ近傍の低抽象度ドライバ)はレジスタに近く軽量で、標準インターフェース規格はソフトウェアの標準化という強みがあります。製品要件で使い分けるのが実務的な選択です。

ベンダーHALの強みと弱み

ベンダー提供のHALの送信関数は、1行で送信を呼び出せる手軽さがあります。一方で、実装によってはペリフェラル管理構造体(ハンドル構造体)の管理やコールバック中継でオーバーヘッドが生じます。低レイヤ相当の送信関数はインライン展開が期待できる低オーバーヘッドのラッパー関数ですが、HAL経由では管理構造体の検証、状態遷移、タイムアウト管理が追加されます。

低レイヤライブラリへの置き換え判断

リアルタイム要件が厳しい箇所、ROM/RAM制約が厳しい量産設計、HALの仕様による制約を避けたい場合は低レイヤライブラリへの置き換えを検討します。よりレジスタに近い位置で動作する低レイヤ相当の送信関数を用いて、HALの送信関数を置き換えることも選択肢の一つです。試作の段階ではHALで立ち上げ、量産化で層を切り換える方法を選ぶ場合もあります。

標準インターフェース規格のベンダー横断的位置づけと使い所

この規格は、マイコン向けの共通標準として策定された機能別の独立規格群です。コアアクセス(割り込みコントローラ、システムタイマー、システム制御ブロック)はコア規格、信号処理は信号処理ライブラリ、RTOSのAPI仕様はRTOS API(v2)で提供されます。コア機能はコア規格で書き、周辺はベンダーライブラリに任せる構成が実務で多く採用されています。

自社共通APIで上位層の変更を防ぐ抽象化パターン

HALとLLのどちらでもアプリケーション側は自社APIで隔離するのが定番です。適切なAPIを活用すれば、内部実装を置き換える際もアプリケーション側の変更をおこなわずに済みます。

ドライバ層の役割、レジスタ直接制御の作法、初期化と転送方式の選択、抽象化ライブラリと低レイヤライブラリと標準インターフェース規格の組みあわせという4つの進め方で、ベンダーやペリフェラルの差異に影響されにくい実装ができます。

組み込みソフトの世界 トップへ戻る