Visual C#に対応したUSBテストプログラムを公開しました
Artix-7ボードのページの中で、Visual C#に対応したUSBテストプログラムを公開しました。
ボードをお持ちの型はソースコードもダウンロード可能です。
Artix-7ボードのページの中で、Visual C#に対応したUSBテストプログラムを公開しました。
ボードをお持ちの型はソースコードもダウンロード可能です。
大きな配列に何かのデータを詰めたり、比較したりするときに、より広いデータ幅で比較したり代入したほうが高速に実行できると考えられます。
CやC++ならば、
void hoge(unsigned char *received_data) {
unsigned long *buffer = (unsigned long *)received_data;
・・・・
}
というふうにキャストを使うことで、配列を1バイト単位でアクセスしたり4バイト単位でアクセスしたり、自由自在なサイズでアクセスできました。(ここではアラインメントが合わない場合とかは考えない)
ところが、これをC#でやろうとすると難しくなります。
やりたいことはByte[]型の配列に乱数を詰めたいだけなのですが、乱数は32bit単位で生成されてくるので、4バイト単位で詰められれば4倍速くなるだろうということです。
試行錯誤の末、以下のようになりました。
public void FillArray (Byte[] buffer, int bufSize)
{
unsafe // この中ではポインタが使えるよ
{
fixed (byte* pb = buffer) // bufferがGCされないようにする
{
UInt32* p = (UInt32*)pb; // byte型ポインタをUInt32型ポインタに変換
for (int i = 0; i < bufSize / 4; i++)
{
UInt32 t = x ^ (x << 11); // XorShiftという乱数生成アルゴリズム
x = y; y = z; z = w;
w = (w ^ (w >> 19)) ^ (t ^ (t >> 8));
p[i] = w;
}
}
}
}
このコードはbufferというByte[]型の配列に乱数を詰めるというものですがUInt32型のポインタに変換して4バイト単位で詰めていきます。
unsafeやfixedを使うのはいいのですが、fixedではポインタの型までは変えられないようなので、fixedではbyte型のポインタにして次の分でキャストしています。
実際に本当に速くなるのか試してみたら、256MByteの配列を埋めるのに、
と、ちょうど4倍速くなっていました。
次に、USBでFPGAに送って戻ってきたデータをコンペアする関数を作りたいと思います。
C#で配列の中身を比較するのは、Enumerable.SequenceEqualというメソッドを使うようなのですが、これが遅い。256MByteのデータを比較するのに3200msほどかかっています。ReleaseビルドでもDebugビルドでもあまり差はありません。
unsafeとfixedの中でfor文を回して、8バイト単位で比較するプログラムを作ってみました。
// 8バイト単位で調べて、一致していればtrue、一致していなければfalseを返す
unsafe private bool UnsafeMemEqual8(byte[] s1, byte[] s2)
{
fixed (byte* pb1 = s1)
{
fixed (byte* pb2 = s2)
{
UInt64* p1 = (UInt64*)pb1;
UInt64* p2 = (UInt64*)pb2;
int len = Math.Min(s1.Length, s2.Length) / 8;
for(int i=0;i<len;i++)
{
if (*p1++ != *p2++) return false;
}
}
return true;
}
}
この関数の結果は、
と、Enumerable.SequenceEqualの3200msに比べて圧倒的に高速でした。
もしかすると、Enumerable.SequenceEqualは高度な関数なので、配列の比較のような単純なことに使うべきではないのかもしれません。
最後にC#で作ったUSB3.0 DDR3テストプログラムと、Borland C++ Builder 6版での実行時間をまとめます。
コンパイラ | Visual C# 2019 (x64) | Borland C++ Builder 6 | ||
ビルド | Debug | Release | Debug | Release |
乱数データ生成 | 320 | 143 | 224 | 147 |
USB3.0 送信 | 803 |
806 | 787 | 788 |
USB3.0 受信 | 745 | 749 | 728 | 728 |
データ比較 | 108 | 48 | 112 | 114 |
合計 | 1976 | 1746 | 1851 | 1777 |
※数字は[ms]
※データサイズは256MByte
いろいろ試してきましたが、unsafeとfixedを使うと、配列に(ガベージコレクトされないという意味で)安全に高速アクセスできることがわかりました。
実行時間もほとんど差がありません。
ハードウェアにアクセスするDLLを使ったり画像を生成したり、C言語と同じようにアクセスできることがわかったので、これからは組み込みのユーザインタフェースプログラムはC#で作ろうかなと思います。
FPGAを使ったカメラなどを作る場合、USBやイーサから受信してきたデータは配列に入っているので、これを画像として表示したいわけですが、C#で高速にやる方法がわかりましたのでここに書きます。
まず、Formに表示させたい領域にPictureBoxを貼り付けます。
一番safeで正攻法なやり方は、
Bitmap img = new Bitmap(pictureBox1.Width, pictureBox1.Height);
でBitmap型のオブジェクトを作り、
for(int y = 0; y < pictureBox1.Height; y++) {
for(int x = 0; y < pictureBox1.Width; x++) {
int val = buffer[y * pictureBox1.Width + x];
img.SetPixel(x, y, Color.FromArgb(val));
}
}
でポチポチと点を打っていく方法です。当然ながら遅すぎます。
高速化するには、
というやり方をします。ダブルバッファです。
Formのコンストラクタなどで
private Bitmap bmp;
を宣言したら、コンストラクタなどでBitmapを作っておきます。
bmp = new Bitmap(pictureBox1.Width, pictureBox1.Height);
実際の描画ではbmpData.Scan0というプロパティを使って、画像データ配列の先頭のポインタを得て、そのポインタに対して書き込みをします。ここでもunsafeが大活躍します。
public void Draw(byte[] buffer)
{
System.Drawing.Imaging.BitmapData bmpData = bmp.LockBits(rect, System.Drawing.Imaging.ImageLockMode.WriteOnly, bmpPixelFormat);
private Rectangle rect = new Rectangle(Point.Empty, bmp.Size);
private System.Drawing.Imaging.PixelFormat bmpPixelFormat = System.Drawing.Imaging.PixelFormat.Format32bppRgb;
IntPtr ptr = bmpData.Scan0;
unsafe
{
int* dst = (int*)ptr.ToPointer();
for (int i = 0; i < pictureBox1.Width * pictureBox1.Height; i++)
{
if (i >= buffer.Length) break;
byte val = buffer[i];
*dst++ = (val & 0xff) << 16 | (val & 0xff) << 8 | (val & 0xff);
}
pictureBox1.Image = bmp;
}
bmp.UnlockBits(bmpData);
}
unsafeを使うとだいぶんCっぽく書けるようになりますね。それに、ポインタの実体に整数を入れるという素直な代入によって、Color.FromArgbとかしなくていいので気分的に楽です。
配列にセットし終わったらpictureBox1.Image = bmp;で画像表示して終了です。
これでかなり高速に描画できるようになりました。
特殊電子回路から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++の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);
引数は以下のように変換すればよいようです。
これで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;
}
とやるのだと思いますが、実行速度を測ってみると、
となり、大きく差が付きました。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;
}
となります。
結果は、
でした。
Marshal版はメモリを確保してコピーするので、どうしても遅くなりますし、メモリも食います。
C++で書かれたDLLを呼び出す時点でunsafeなわけですから、あまりMarshalとかこだわらずに、スパッとポインタ渡しでいきたいですね。
昨日からUnityの勉強を始めました。
プリント基板のガーバデータを読み込んで、Unityの世界に再現して、パターンの上を走り回るゲームを作りたいと思っています。
こんな感じです。
空から眺めているところ。まだこの時点では斜めの線が作るのが面倒臭かったので縦横のパターンのみでした。
斜めの線を作るにはatan2を使えばよいことがわかり、斜めの線も実装できました。
BGAのランドの上を飛び石渡したりして遊べます。
ミアンダ配線から落ちないようにそーっと歩いたり。
作った基板を空から眺めるのは気持ちがいいものです。
線と線が斜めに交差するところでは欠けが出てしまうので、線の端っこにアパーチャを置くことでこの切れ目をなくしました。
だいぶん、プリント基板のそれっぽくなってきました。
FPGAの上からDDR3方面を眺めてみます。
Unityは面白いですね。
一つハマったところは、円柱(cyllinder)の上に乗ろうとすると弾かれること。円柱のコライダーがデフォルトで回転楕円体になっているのと、円柱の高さが指定した高さの2倍になってしまうのが原因のようでした。
円柱には、BGAのパッドと、穴の開いたViaがありますが、Viaは円柱ではなくどちらかというと土管なので、どうやって穴の開いた円柱を作ればよいのか悩むところです。
次は多層基板に対応します。実は多層基板も試してはいるのですが、上の層が影になってしまうのと、ベタパターンが反転パターンなのをどうやって作ればよいのか、悩むところです。サーマルはガーバのマクロ機能も使ってそうだし、いろいろと勉強しなければいけないことが多すぎます。
子供が休校なのでプログラムでも勉強しろと「Unity 3D 超入門講座」という本を買ったのですが、一向にやらないので、まずは親が何かを作ってみることにしました。
本を読みながらポチポチやっていたのですが、ColliderとRigid Bodyでドミノ倒しが作れるあたりから、自分の中で何かが覚醒しました。
Prefabというので雛形を作っておいて、それをC#のスクリプトで動的にたくさん作れることがわかってきました。
あまりにも高いところから落下すると、スピードが付きすぎているためか、床をすり抜けてしまって当たり判定が出ないようなのです。奈落に落ちてしまった場合の判別をどうすればよいかまだわかりません。
WebGLでビルドすれば、そのままWebブラウザで動くし、Unity凄すぎ!
不覚にもAge of Zとかいうゲームにはまってしまいました。
街の中を発展させて、外で戦って、同盟を組む。ビビットアーミーとだいたい同じだ・・
しかも、副官、欠片制度、長時間の放置と加速システム。ゲームの世界に慣れるまでに時間はかかりませんでした。
自分でもこんなゲームを作りたいなぁと思いながらプレイしています。
最近のコメント