Cで書いたUSB3.0用DLLをC#から呼び出して配列を渡す方法
特殊電子回路からArtix-7評価ボードというのが発売されています。
このボードはUSB3.0とDDR3メモリとArtix-7 FPGAを搭載していて、USB3.0 SuperSpeedで毎秒300MB以上の速度でデータをやりとりできます。このボードのテストプログラムは今までBorland C++ Builder 6という古いツールで開発していたのですが、このたびようやく重い腰を上げてVisual C#に移植することにいたしました。
Borland C++ Builderで開発したプログラムがこちらです。
シンプルなアプリですが、IN方向で約370MB/s、OUT方向で約340MB/s出ているのが測れます。また、このアプリはFPGAをカメラ化するため受信したデータを画像として表示する機能もあります。今回はXorShiftで作った乱数をやりとりしているため、このような砂嵐が表示されます。
これをVisual C#に移植していくのですが、C++で書かれていてDLLを呼び出してハードウェアを操作するようなプログラムなので、困難が予想されます。
ポイントとなるのは、
- C/C++で書かれたハードウェア操作DLLを呼び出し、配列でデータを渡すこと
- 受け取った配列のデータを高速で画面に描画すること
- 乱数データを高速に生成すること
などです。
まずは、C++のDLLにデータを受け渡すプログラムを見ていきます。
DLLにはUSBWriteData()とUSBReadData()という関数があり、Cでは以下のようにプロトタイプが宣言されています。
TKUSBFX3_API int WINAPI USBWriteData(unsigned long addr,unsigned char *data,int len,unsigned short flag);
TKUSBFX3_API int WINAPI USBReadData(unsigned long addr,unsigned char *data,int len,unsigned short flag);
これをC#から使うには以下のようにしてクラスの中で宣言します。
[DllImport("tkusbfx3.dll")]
protected static extern int USBWriteData(uint addr, IntPtr data, int len, ushort flag);
[DllImport("tkusbfx3.dll")]
protected static extern int USBReadData(uint addr, IntPtr data, int len, ushort flag);
引数は以下のように変換すればよいようです。
- unsigned long → uint ※C#のintは32bit!!
- unsigned char * → IntPtr
- int → int ※C#のintは32bit!!
- unsigned short → ushort
これでC#とDLLの間でデータのやりとりができます。
実際に送信する(OUT方向の転送)C#の関数は
public int Write(uint addr, byte[] data, int flag)
{
int trlen = (int)((data.Length + 7) / 8) * 8; // 8の単位で切り上げる
int ret; // 戻り値
unsafe
{
fixed (byte* p = data)
{
IntPtr ptr = (IntPtr)p;
ret = TKUSBFX3.USBWriteData(addr, ptr, trlen, (ushort)(unchecked(flag)));
}
}
return ret;
}
でうまくいきました。
unsafeを使ってポインタを使えるようにし、fixedで囲むことで実行中にdataがガベージコレクトされないようにしています。
なお、プログラム中にuncheckedされているflagという引数がありますが、このArtix-7ボード用の転送オプションなので気にしなくていいです。
もし、清く正しいプログラムならば、Marshalを使って
public int WriteSafe(uint addr, byte[] data, int flag)
{
int trlen = (int)((data.Length + 7) / 8) * 8; // 8の単位で切り上げる
int ret; // 戻り値
IntPtr ptr = Marshal.AllocCoTaskMem(Marshal.SizeOf(typeof(byte)) * trlen);
Marshal.Copy(data, 0, ptr, data.Length);
ret = USBWriteData(addr * sizeof(byte), ptr, trlen, (ushort)(unchecked(flag)));
Marshal.FreeCoTaskMem(ptr);
return ret;
}
とやるのだと思いますが、実行速度を測ってみると、
- unsafe版 ・・・803ms・・・334MB/s
- Marshal版 ・・975ms・・・275MB/s
となり、大きく差が付きました。Marshal版だと安全なメモリを確保してコピーするのでどうしても遅くなります。
同様に受信(IN方向)のプログラムは、
public int Read(uint addr, byte[] data, int flag)
{
int trlen = (int)((data.Length + 7) / 8) * 8; // 8の単位で切り上げる
int ret = 0;
unsafe
{
fixed (byte* p = data)
{
IntPtr ptr = (IntPtr)p;
ret = TKUSBFX3.USBReadData(addr, ptr, trlen, (ushort)(unchecked(flag)));
}
}
return ret;
}
となります。
安全なMarshal版だと、
public int ReadSafe(uint addr, byte[] data, int flag)
{
int trlen = (int)((data.Length + 7) / 8) * 8; // 8の単位で切り上げる
int ret = 0;
IntPtr ptr = Marshal.AllocCoTaskMem(Marshal.SizeOf(typeof(byte)) * trlen);
ret = USBReadData(addr , ptr, trlen, (ushort)(unchecked(flag)));
Marshal.Copy(ptr, data, 0, data.Length);
Marshal.FreeCoTaskMem(ptr);
return ret;
}
となります。
結果は、
- unsafe版・・・745ms・・・360MB/s
- Marshal版・・・815ms・・・329MB/s
でした。
Marshal版はメモリを確保してコピーするので、どうしても遅くなりますし、メモリも食います。
C++で書かれたDLLを呼び出す時点でunsafeなわけですから、あまりMarshalとかこだわらずに、スパッとポインタ渡しでいきたいですね。
| 固定リンク
コメント