C++版デバイスドライバのclassと割り込み関数
C++で作成された組み込みソフトウェアのデバイスドライバの実装を見ると、 デバイスドライバのclassとは別に割り込み関数が独立しているケースが多いです。 ですが凝集度やカプセル化、マルチインスタンスへの対応を考えると、 割り込み関数をデバイスドライバclassのprivateメンバにしておきたいので、その実装方法を考えてみましょう。
マルチインスタンスへの対応はFPGA等で同じ周辺回路(TimerやUART, SPI等)を複数個インスタンスした際に、 ソフトウェア側も同じように複数個インスタンスして対応する方が自然です。 マルチインスタンスはソフトマクロCPU固有の考え方のように思われるかもしれませんが、 これはFPGA環境のソフトマクロCPUに限った話ではなく、 TimerやUART, SPI等の同一周辺回路を複数搭載しているハードマクロCPUも多いため、 いずれの環境でも有効な考えです。
では、どのように実装すればよいでしょうか。
マルチインスタンスへの対応
まず、複数個インスタンスする際はどのような情報をコンストラクタに与えればよいでしょうか。 同一の周辺回路でも通常はベースアドレス、割り込み番号も異なりますので、 このようなパラメータをコンストラクタに与えてマルチインスタンスに対応します。 次にどのようなメンバを持てばよいのかは制御対象の回路等に依存するので柔軟に考えるしかありませんが、 例えばUARTなら送信バッファ、受信バッファ、エラー情報等が考えられますね。
割り込み関数
例外はありますが、昨今の組み込みソフトウェアの開発環境は割り込みを管理する割り込みマネージャが提供されています。 このため、割り込み処理を通常の関数で記述し、その関数を割り込みマネージャに登録することで簡単に割り込みを受け付けることができます。 但し、登録できる関数は通常の関数でなければならないので、そのままデバイスドライバclassのメンバ関数を登録することはできません。
そういうことならとstaticメンバ関数にして通常の関数扱いで解決できるかと思っても一筋縄ではいきません。 staticメンバ関数ということはthisポインタが渡されないため、 インスタンスメンバにアクセスできないのでマルチインスタンスへの対応が困難になってしまいます。
解決策の1つとして割り込み関数の引数を利用する方法がある
割り込みを登録する際、通常は割り込み関数だけでなく割り込み関数の引数を予め渡すことができ、 割り込み関数が実行される際にその引数を受け取ることができます。 この割り込み関数の引数にthisポインタを渡すことで、マルチインスタンスへの対応も解決します。
実装例
文章だけの説明では苦しいので、NiosII版とMicroBlaze版のデバイスドライバclassのコードで簡単に説明します。
便宜上、class定義の中に関数定義を記述しています。
NiosIIの場合
割り込みサービスルーチンと登録関数のプロトタイプ(ベンダ提供)
NiosIIの割り込みマネージャが提供しています。
typedef void (*alt_isr_func)(void* isr_context); int alt_ic_isr_register(alt_u32 ic_id, alt_u32 irq, alt_isr_func isr, void* isr_context, void* flags);
NiosUart(Altera Avalon UART) class
class NiosUart : public Uart { public: ... private: ... const uint32_t kBASE_ADDR; const uint32_t kIC_ID; const uint32_t kIRQ; void setupInterrupt() { ... /* 割り込みサービスルーチンの引数にthisポインタをもらうため、alt_ic_isr_registerのisr_contextにthisを渡す */ alt_ic_isr_register(kIC_ID, kIRQ, interruptServiceRoutine, this, (void*)0); ... } static void interruptServiceRoutine(void* const isr_context) { /* isr_contextからthisポインタを得て、非staticなメンバにinstanceを介してアクセスしている。 */ NiosUart* const instance = reinterpret_cast<NiosUart*>(isr_context); const uint16_t status = IORD_ALTERA_AVALON_UART_STATUS(instance->kBASE_ADDR); ... if (status & ALTERA_AVALON_UART_STATUS_RRDY_MSK) { instance->receiveInterrupt(); } if (status & ALTERA_AVALON_UART_STATUS_TRDY_MSK) { instance->transmitInterrupt(); } } void transmitInterrupt(); void receiveInterrupt(); };
MicroBlazeの場合
割り込みハンドラと登録関数のプロトタイプ(ベンダ提供)
MicroBlazeの割り込みマネージャが提供しています。
typedef void (*XInterruptHandler) (void *InstancePtr); void XIntc_RegisterHandler(u32 BaseAddress, int InterruptId, XInterruptHandler Handler, void *CallBackRef); void XIntc_RegisterFastHandler(u32 BaseAddress, u8 Id, XFastInterruptHandler FastHandler);
MbUart(Xilinx Uart Lite) class
class MbUart : public Uart { public: ... private: ... const uint32_t kBASE_ADDR; const uint32_t kIC_BASE; const uint32_t kIRQ; void setupInterrupt() { ... /* 割り込みハンドラの引数にthisポインタをもらうため、XIntc_RegisterHandler関数のCallBackRefにthisを渡す */ XIntc_RegisterHandler(kIC_BASE, kIRQ, interruptHandler, this); ... } static void interruptHandler(void* const InstancePtr) { /* InstancePtrからthisポインタを得て、非staticなメンバにinstanceを介してアクセスしている。 */ MbUart* const instance = reinterpret_cast<MbUart*>(InstancePtr); const uint32_t status = XUartLite_GetStatusReg(instance->kBASE_ADDR); ... if (status & XUL_SR_RX_FIFO_VALID_DATA) { instance->receiveInterrupt(); } if ((status & XUL_SR_TX_FIFO_FULL) == 0) { instance->transmitInterrupt(); } XIntc_AckIntr(instance->kIC_BASE, (1UL << instance->kIRQ)); } void transmitInterrupt(); void receiveInterrupt(); ... };
まとめ
このような実装で割り込み関数(interruptServiceRoutine/interruptHandler)もデバイスドライバのclassにprivateで実装することができます。 似たような実例として、登録時に引数を渡し、実行時に引数を受け取れるならスレッド関数も同様の対応ができますので、 スレッドやタスク関数を内包したアクティブオブジェクトをマルチインスタンスしたい場合にも有効な実装方法となります。 むしろマルチインスタンスに対応可能とするため、割り込み関数やスレッド(タスク)関数で引数を受け取れるような仕様に定めていると思っています。
割り込みサービスルーチンと割り込みハンドラ
「割り込みサービスルーチン(Interrupt service routine)」と 「割り込みハンドラ(Interrupt handler)」は同じと説明されている場合もありますが、 個人的な認識では、割り込みマネージャーを介さず実行されるのが割り込みハンドラというイメージがあります。 その分高速に応答できますが、もちろん引数を受け取ることはできません。
なお、XILINXのMicroBlazeではこれに該当するのがXFastInterruptHandlerということになります。