PCI Expressボード用にDMAを行うためのWDMデバイスドライバを作っています。
DMAを行うには物理アドレスが必要になります。PCI(e)のバスに出てくるアドレスは物理アドレスだからです。しかし、Windowsのプログラミングで用いられるのは仮想アドレスなので、少々面倒な手続きを経て物理アドレスに変換しなければなりません。
NTドライバのころは、MmGetPhysicalAddress()という関数があって、任意の仮想アドレスに対応する物理アドレスを簡単に知ることができました。DMA用のバッファはMmAllocateContiguousMemory()を使って物理メモリ上の連続する領域を確保し、MmGetPhysicalAddress()でその先頭の物理アドレスを得て、DMAの転送先として使うという方法が用いられていました。TECH-Iの「PCIデバッグライブラリ」などでもそのようにしていました。
しかし、WDMになってからはMmGetPhysicalAddress()関数がなくなってしまいました。こうなったら正当な方法でDMA先のアドレスを調べなければなりません。
試行錯誤の末、次のようにしてやるのがもっとも簡単だということがわかってきました。
①DMAアダプタを作成
アドインカードがどのようなDMA機能を備えているかを申告して、DMAアダプタというオブジェクトを作る
②CommonBufferを作成
物理メモリアドレスと仮想アドレスの両方が返されるバッファを作る
③アドインカードに、転送対象の物理アドレスを知らせ、アドインカードにDMAを発行させる
④CommonBufferを開放
⑤DMAアダプタを破棄
この手順を順に見ていきます。
DMAアダプタを作成するには、まずDEVICE_DESCRIPTION構造体というのを作ります。この構造体を引数にしてIoGetDmaAdapterを呼び出します。
DEVICE_DESCRIPTION DeviceDescription;
RtlZeroMemory(&DeviceDescription,sizeof(DeviceDescription));
DeviceDescription.Version = DEVICE_DESCRIPTION_VERSION;
DeviceDescription.Master = TRUE;
DeviceDescription.ScatterGather = FALSE;
DeviceDescription.DemandMode = FALSE; // バスマスタなら必ずFALSE
DeviceDescription.AutoInitialize = FALSE; // バスマスタなら必ずFALSE
DeviceDescription.Dma32BitAddresses = TRUE;
DeviceDescription.IgnoreCount = TRUE; // ???
DeviceDescription.Reserved1 = FALSE; // Must be FALSE
DeviceDescription.Dma64BitAddresses = FALSE;
//DeviceDescription.BusNumber = 0; // 使わない
DeviceDescription.DmaChannel = 0; // ???
DeviceDescription.InterfaceType = PCIBus;
DeviceDescription.DmaWidth = Width32Bits; // バスマスタなので使わないけど
DeviceDescription.DmaSpeed = TypeF; // バスマスタなので使わないけど
DeviceDescription.MaximumLength = 4096;
DeviceDescription.DmaPort = 0; // obsolete
// DMAアダプタを作成
PDMA_ADAPTER dmaAdapter = IoGetDmaAdapter(dx->pdo,
&DeviceDescription, &nMapedRegs);
最後のIoGetDmaAdapter関数の引数に与えたdx->pdoは、親となるバスデバイスドライバのデバイスオブジェクトです。IoGetDmaAdapterを実行すると、マップレジスタの数が返されます。
次にCommonBufferを作ります。CommonBufferは、仮想アドレスと物理アドレスのセットが得られるバッファです。これが連続する物理メモリ領域上に確保されるのかとかページングされるのかとかは調べていません。
// CommonBufferを作成して物理アドレスを取得!!
PHYSICAL_ADDRESS combufPhysAddr;
PVOID comBuf = dmaAdapter->DmaOperations->AllocateCommonBuffer(
dmaAdapter, // さっき作ったやつ
transferLength, // ほしいサイズ
&combufPhysAddr, // 作ったバッファの物理アドレス
FALSE); // キャッシュ不可
DMAでアドインカードに書き込みするなら、この時点で、CommonBufferの仮想アドレスにデータを格納しておきます。RtlCopyMemなどが使えます。
バッファの物理アドレスが得られたら、アドインカードにそれを知らせてやります。今回はアドインカード上のBAR0空間に制御レジスタを作って、対象アドレスや転送長をその中に格納するようにしました。
PCIのコンフィギュレーションレジスタに書かれたアドレスは物理アドレスなので、MmMapIoSpaceを使って仮想アドレスに変換します。その仮想アドレスに対してWRITE_REGISTER_ULONGマクロなどを使うことで、アドインカードにデータが転送されます。
// 物理アドレスをメモリ空間にマップして仮想アドレスを得る
PULONG bar0va = (PULONG)MmMapIoSpace(dx->barStartAddress[0],
16, // 16バイト確保
MmNonCached); // キャッシュ不可
// 順序が大切なのでCombinedWriteは不可
WRITE_REGISTER_ULONG(&bar0va[7], 0);
TPDMA dmaInfo; // 自分で定義した構造体
dmaInfo.LowAddr = memaddr;//LogicalAddress.LowPart;
dmaInfo.HighAddr = 0;//combufPhysAddr.HighPart;
dmaInfo.LocalAddr = dx->barStartAddress[1].LowPart;
dmaInfo.DmaCommand = transferLength | dmaFlag; // DMA Read 開始!
WRITE_REGISTER_BUFFER_ULONG(&bar0va[4], (PULONG)&dmaInfo,4);
MmUnmapIoSpace(bar0va,16); // 最後に開放
これで、(FPGAにDMA機能を作っておけば)アドインカードはDMA転送を開始します。
ただし、このままではプログラムはDMAの完了を知ることができません。アドインカードがDMA完了割り込みなどを発生させるか、デバイスドライバのプログラムがFPGA内のレジスタをポーリングするなどしてDMAの完了を知る必要があります。
DMAで読み出しするなら、ここでCommonBufferの内容にポインタでアクセスするか、RtlCopyMemなどを使います。どうやってDMAが完了したかを知るのは難しい問題ですが・・・
最後にCommonBufferとDMAアダプタを開放します。
// CommonBufferを開放
dmaAdapter->DmaOperations->FreeCommonBuffer(dmaAdapter,
transferLength,combufPhysAddr,comBuf,TRUE);
// DMAアダプタを開放
dmaAdapter->DmaOperations->PutDmaAdapter(dmaAdapter);
こんな感じでDMA転送用のバッファを作れることがわかってきました。
ただし、この方法では、ユーザがDeviceIoControlで与えたデータ(ユーザの仮想メモリ空間上にある)とCommonBufferとの間でデータのコピーをしなければなりません。ユーザプログラムでmallocとかして作ったバッファに対して直接DMAで転送ができればパフォーマンス的には最高なのですが、それにはMDLというのを使わなければならないようです。それはまた今度試してみることにします。
さて、この方法でDMA転送を発行させられることがわかってきました。
次の図は、物理メモリの0番地から1024バイトをDMAで読み出したときの波形です。64バイトごとに小分けにされて転送されています。

PCI(e)からメモリリードリクエストを受け取ると、RootComplex(パソコンの中のチップセットがそれを兼ねている)は、要求されたとおりにメモリの内容を吐き出してしまいます。チップセットはOSの管理下ではないので、PCI Expressのアドインカードからはどのようなアドレスであっても物理メモリを自由に覗けてしまうようです。これに対してOSはプロテクトをかけることができず、メモリリード要求を拒否できません。
読み出したデータをJTAGロジックアナライザで見てみると、ソフトウェアで物理メモリの0番地をダンプしたものと一致しているのが確認できました。
これは、PC/AT互換機のアーキテクチャ自身が持つセキュリティーホールではないかと思います。昔やった実験の裏づけが取れました。
デスクトップパソコンならまだいい(箱を開けなければPCIeカードが挿せないから)のですが、ExpressCard付のノートパソコンはちょっと危険ではないかと思います。
最近のコメント