« Unityでガーバファイルを走り回るアプリを作っています | トップページ | C#で高速にビットマップ画像を描く方法 »

2020.05.09

Cで書いたUSB3.0用DLLをC#から呼び出して配列を渡す方法

特殊電子回路からArtix-7評価ボードというのが発売されています。

Art7

このボードはUSB3.0とDDR3メモリとArtix-7 FPGAを搭載していて、USB3.0 SuperSpeedで毎秒300MB以上の速度でデータをやりとりできます。このボードのテストプログラムは今までBorland C++ Builder 6という古いツールで開発していたのですが、このたびようやく重い腰を上げてVisual C#に移植することにいたしました。

Borland C++ Builderで開発したプログラムがこちらです。

Bcb

シンプルなアプリですが、IN方向で約370MB/s、OUT方向で約340MB/s出ているのが測れます。また、このアプリはFPGAをカメラ化するため受信したデータを画像として表示する機能もあります。今回はXorShiftで作った乱数をやりとりしているため、このような砂嵐が表示されます。

Bcb2

これを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とかこだわらずに、スパッとポインタ渡しでいきたいですね。

 

|

« Unityでガーバファイルを走り回るアプリを作っています | トップページ | C#で高速にビットマップ画像を描く方法 »

コメント

コメントを書く



(ウェブ上には掲載しません)




« Unityでガーバファイルを走り回るアプリを作っています | トップページ | C#で高速にビットマップ画像を描く方法 »