以前作成した,EDKでPCIe+DDR3のアクセラレータフレームワークを作る(1)に対応するデバイスドライバをシンプルな作ってみましょう,という話です.
ちなみに,あざとい宣伝ですが,e-treesではEDKを使わない比較的コンパクトかつRTL設計者フレンドリなコアも扱っています.
EDKでPCIe+DDR3のアクセラレータフレームワークを作る(1)〜(4)のコンセプトは,EDK(XPS)を使って,手軽にPCIeとDDR3を使ったアクセラレータ開発のためのフレームワークを用意すること,「目指せ!!GPUみたいに簡単に使えるFPGAアクセラレータフレームワーク」の実現でした.
できるだけ使える環境の制約を少なくし,手っ取り早く使えるように,ソフトウェアはユーザランドで開発するということをメインに考えています.とはいえ,ユーザランドでの開発で困ってしまうのが割り込み処理です.
というわけで,今回は割り込み処理をするためだけの最低限のPCIeデバイスドライバの設計を考えてみましょう.
必要な機能
ハードウェアのアクセスはユーザランドで処理,つまりmmapを使ったリード/ライトで処理して,割り込み処理だけを行うデバイスドライバ,というのを作るために必要な機能は次の通りです
- PCIeのBAR0や割り込みIDなどの情報を取得する
- 割り込み処理をOSに登録する
今回は,これらの機能をもった,キャラクタデバイスを作ることにします.それでは順番にみていきましょう.
0. 使用するリソース
サンプルとして実装したコード一式は↓の通りです.
デバイスドライバ関連リソース
改訂版ユーザプログラムリソース
(BAR0をプログラム内で取得するよう変更済)
Linuxのデバイスドライバはカーネルモジュールとして作成します.カーネルモジュールとしてプログラコンパイルするためには,カーネルのヘッダファイルやライブラリを利用する必要があります.カーネル2.6のモジュールコンパイル用Makefileいろいろ のMakefileのサンプルを利用させてもらいました.
1. 今回作るもの
1.1 割り込みの駆動回路について
作成したFPGA上の回路では,PCIeから二つの32bitのGPIOにアクセスできるようになっています.それぞれ,
- GPIO0のビット1に’1’を書くと割り込み回路を駆動
- GPIO1のビットは割り込みがアクティブのときは0xDEADBEAF,非アクティブのときは0xABADCAFEを返す
という風な回路になっています.実際には割り込み回路を駆動するイベントはGPIOへのデータ書き込みではなくて,FPGA内部での何かのイベント,たとえばDMAが終わった,とか,そういうのになると思いますが,今回は,PC(ソフトウェア)側から簡単に割り込みの動作を確認できるように,このようにしています.
1.2 割り込みが起きたらどうするか
割り込みが起きたらどうするか,は,作りたいアプリケーションに密接に絡んでくるでしょう.今回は,サンプルとして,デバイスをopenしてreadしたプロセスのブロックを割り込みで解除する,という簡単なプログラムを考えます.つまり,
#include <stdio.h> int main(int argc, char **argv) { FILE *fp; int buf[1]; fp = fopen("/dev/fpga", "rw"); printf("wait for intterupt\n"); fread(buf, 1, 1, fp); printf("intterupt arrival\n"); return 0; }
というプログラムが動くようにすることを考えます.
1.3 サンプルの使い方
次の手順でサンプル一式を使って動作の確認ができます.動作は,CentOS release 6.4 (Final) 2.6.32-358.11.1.el6.x86_64 で確認しました.$(WORK)が上記のサンプル一式を展開したディレクトリ名を示します.
- cd $(WORK)/driver-test # driver-tsetの下に移動
- make install # カーネルモジュールを登録,デバイスファイルを作成する
- gcc test.c # 割り込みを試すプログラムをコンパイル
- ./a.out & # ユーザアプリケーションをバックグラウンドで実行
- cd $(WORK)/axi-pcie # FPGAにアクセスするソフトウェアのディレクトリに移動
- make # 一式をコンパイル
- sudo ./gpio_write 1 # これでFPGAからPCに割り込みをかける
7で割り込みをかけると,3でバックグラウンド実行したソフトウェアが正常終了,つまりreadブロックが解除されたことが確認できます.
2. デバイスドライバの初期化
static int init_module_body(void) という関数でデバイスドライバを初期化しています.具体的には,
- デバイスをみつける
- デバイスを有効にする
- キャラクタデバイスとして登録する
- 割り込み処理を行う準備をして割り込み関数を登録する
という処理を行います.
2.1 デバイスの探索
PCIeデバイスは,デバイスドライバがなくてもOSがすでに情報を保持してくれています.保持されている情報は,
pci_find_device(ベンダID, デバイスID, デバイス情報構造体へのポインタ);
という関数を使って取得することができます.
デバイス情報構造体 struct pci_dev は,カーネルソースの inlucde/linux/pci.h で定義されています.同じベンダID,デバイスIDの複数のデバイスが物理的にPCに接続されていることも珍しくはないですが,pci_find_deviceで見つかった情報へのポインタを第三引数に渡すことで,次々とデバイスを見つけることができます.
サンプルコード中では,
for(i = 0; ; i++){ dev = pci_find_device(VENDOR_ID, DEVICE_ID, dev); if(dev == NULL){ break; } print_device_info(dev, i); fpga = dev; }
たとえば,こんな感じに書いています(109-116行).簡単のため,複数デバイスがあるときには最後にみつかったものを使うことにしています.
2.2 デバイスを有効にする
PCIeデバイスはみつけただけでは十分に使えません.たとえば,IRQ番号がOSで処理される仮想IRQと異なる値になっています.もろもろの設定を一手に行ってくれる便利関数 pci_enable_deviceを使って初期化します(123行目).
また,FPGA内の設定情報やGPIOに相当するレジスタがマップされているBAR0に,デバイスドライバからアクセスできるようにioremapを使って,アクセスポインタを取得しています(125行目).
2.3 キャラクタデバイスとして登録
open/read/writeといった方法でアクセスできるよう,ドライバをキャラクタデバイスとしてOSに登録します.登録にはregister_chrdevを使います(133-136行目).
2.4 割り込み関連の初期化と関数の登録
“割り込み”を使える形で実現するためには,一般には,
- 割り込みがあるまで待つルーチン
- 割り込みがきたら起こすルーチン
の2種類が必要です.今回は待つルーチンとして,read()を,起こすルーチンとしてfpga_interrupt()を使います.それぞれの「待ってる」「起こす」を管理するためにワークキューを使います.ワークキュー(具体的には,struct wait_queue_head_t型の変数)は,init_waitqueue_headという関数で初期化して使えるようになります(139行目).
最終的に,request_irq()関数でOSに割り込み関数を登録して,モジュールの初期化にかかる一連の処理が完了です(141-144目).
3. 割り込み処理・本体
割り込みが起きたときに呼び出されるのは,request_irqの引数として渡したfpga_interrupt関数(73-90行目)です.ここでの処理は,次ようなものです.
- 本当に自分の割り込みか確認する
- 割り込みを止めてもらう
- 割り込みに応じたソフトウェア処理を行う.
- OSに割り込みを処理したことを通知する
デバイスとの兼ね合いやソフトウェア処理によって,一般には,割り込み中に他の割り込みの禁止などの排他制御が必要になります.
3.1 自分の割り込みかどうか確認する
Linuxでは,複数のPCIeデバイスで割り込み番号(IRQ)を共有しています.なので,割り込み関数が呼ばれたからといって本当に自分のための処理なのかは分かりません.たいていの場合は,割り込みをかけているデバイスにアクセスする(特定のレジスタの値を読む)ことで,そのデバイスが割り込みをかけているのか判定できます.
今回のFPGAデバイスでは,割り込みをかけている間はGPIO1から0xDEADBEAFが読み出せるようになっているので,それを使って割り込みをかけたのがFPGAかどうかを判定します(76-78行目).
自分が対象とするデバイスではなかった場合,OSに自分のじゃなかった,ということを伝えます.そうすることで,OSは他のドライバの割り込み関数を呼びにいけます.これは,単に,IRQ_NONEという値を返すとOKです(89行目).
3.2 割り込みを止めてもらう
割り込み発生というのはイベントというより,「割り込まれている状態」なので止める必要があります.今回のFPGAデバイスの場合は,GPIO経由でGPIO0の0bit目を’0’にすれば割り込みを止めることができます.
具体的には,79行目で呼び出しているclear_interrupt()関数で処理しています.
3.3 割り込みに応じたソフトウェア処理を行う
ソフトウェア処理としては,割り込みが起きた回数をインクリメントし(81行目),キューに入っているタスクを起こす(82行目)ことで,「割り込みが起きた」という事象をソフトウェア的に保存し,また割り込みを待っているタスクの処理を再開しています.
割り込みを処理しなかった場合にはIRQ_NONEを呼び出し元に返しますが,自分が割り込みを処理した場合にはIRQ_HANDLEDを呼び出し元に返します.
4. ユーザランドプログラムとの架け橋
何のために割り込みを起こすのかというと,大抵の場合,それをきっかけに何かの処理をするためです.ですので,割り込みによって,カーネル空間内のプロセスとして何かを処理することもあるでしょう.
今回は簡単なフレームワークということで,割り込みが発生したことを単にユーザアプリケーションに伝えるだけ,を考えます.その実現にread()関数を使ってみます.
static ssize_t fpga_read( struct file* filp, char* buf, size_t count, loff_t* pos ){ int c = interrupt_counter; wait_event_interruptible(wq, (interrupt_counter > c)) ; return count; }
このようなread()関数で,read関数を呼び出した元の処理を割り込みが発生させるまで寝かしておいて,割り込みがきたら起こすことができます.
5. ユーザランドでは物足りないあなたへ
より高速なリード/ライト処理が必要な場合など,ユーザランドではなくてデバイスドライバ内で,もっといろいろな処理をしたいというケースもあるでしょう.
次回は,そんな要求のための,もう少しいろいろ面倒をみるデバイスドライバを書いてみることにしましょう.
コメント