はじめに
このページは、Xillybus の Linux 向けプログラミングガイドに基づいています。以下のトピックについてより包括的に知りたい場合は、このガイドを参照することをお勧めします。同様のMicrosoft Windows 向けガイドもあります。
まだ「Hello, World」テストを行っていない場合は、先にそれを行うことをお勧めします。
Xillybus IP コアとの通信は、ホスト上のデバイスファイルを介して行われます。これらのデバイスファイルは通常のファイルと同じようにアクセスされます。ただし、通常のファイルとは異なり、デバイスファイルはディスクに保存されたデータを表しているわけではありません。デバイスファイルへの読み書きは、代わりに I/O 操作になります。
したがって、実質的にあらゆるプログラミング言語で Xillybus のデバイスファイルにアクセスできます。また、Linux のコマンドラインユーティリティをこの目的に使うことも可能です。それなら、なぜこのトピックを議論する必要があるのかと思うかもしれません。ファイルを正しく読み書きする方法を知っていれば、Xillybus を扱う方法も分かるはずだからです。
確かに、API の詳細な知識がなくても通常のファイルにアクセスするプログラムを書くことは可能です。しかし、I/O を扱うにはそれでは不十分です。ハードウェアとのやり取りでは、通常のファイルではほとんど起こらない状況が生じます。プログラマとしては、API を使ってこれらの状況を処理する方法を知っておく必要があります。
したがって、このページの大部分は、通常のファイルへのアクセスにも関連するトピックに割かれています。デバイスファイルとの違いは、API の使い方を誤った場合に寛容ではないことです。
API の理解不足は、主に 2 種類の混乱を招く可能性があります。
- I/O 操作のデータ量が予想と異なる(通常は予想より少ない)ことがあります。
- FPGA との通信が予想より遅れることがあります。
プログラミング言語とオペレーティングシステム
Linux では、ファイルにアクセスできるツールやプログラミング言語はどれでも Xillybus で使えます。
Windows の場合、C、C++、C#、Python、Perl、Cygwin に付属するものなど、一般的なプログラミング言語では問題ありません。ただし、一部のツール(MATLAB など)は Xillybus のデバイスファイルを扱おうとしないことがあります。これらのツールは、デバイスファイルが通常のファイルではないことを検出し、それをエラーと見なします。この状況には回避策があり、通常は低水準 I/O 向けの拡張機能を使います。
以下の議論は C 言語に基づいていますが、そのトピックはすべてのプログラミング言語に関係します。
サンプルコード
Xillybus のウェブサイトでは、ダウンロード可能なコーディング例が提供されています。これらの例は C で書かれており、低水準 API を正しく使う方法を示しています。
例をダウンロードするには、デモバンドルをダウンロードした Web ページ、つまりXillybus のページまたはXillyUSB のページにアクセスしてください。
Linux をお使いの場合は、Linux ドライバをダウンロードしてください。サンプルコードは同じ .tar.gz ファイルに含まれています。
Windows をお使いの場合は、Windows 用の Xillybus パッケージをダウンロードしてください。
どちらの場合も、サンプルコードは demoapps/ サブディレクトリ内にあります。このコードでは、各 I/O 操作で読み書きされるデータ量が小さいことに注意してください。これは、小さなバッファ(128 バイト)が割り当てられているためです。その目的はコードを単純にすることだけです。実際のアプリケーションでは、より大きなバッファが推奨されます(通常は 32 KB が適切な選択です)。
Linux と Windows の違いは小さいですが重要です。特に次のとおりです。
- 関数名が少し異なります:_open() 対 open() など。
- Windows では、ファイルを開くときに _O_BINARY を使わなければなりません。
とはいえ、サンプルコードの背後にある原理はまったく同じです。
バッファ付きファイル I/O
プログラミング言語は通常、ファイルへアクセスするための 2 つの別々の API を提供します。高水準 API と低水準 API です。高水準 API のほうが扱いやすいため、より一般的に使われます。
たとえば C 言語では、高水準 API は fopen()、fread()、fwrite()、fprintf()、fclose() などで構成されます。低水準 API は open()、read()、write()、close() などで構成されます。これら 2 つの API の違いは、関数名が少し異なるだけではありません。高水準 API はユーザー空間の RAM バッファを提供します。これらのバッファは C ランタイムライブラリによって実装されています。これらを、カーネル内のドライバによって制御される DMA バッファと混同しないでください。
最も重要な違いは fwrite() の動作です。この関数の呼び出しの結果として、データがユーザー空間バッファに格納されることがあります。FPGA への送信は後まで遅延される可能性があります。実際、データはファイルが閉じられるまで、fwrite() のバッファに無期限に残ることがあります。これは Xillybus のバグのように見えることがあります。データはファイルに書き込まれたのに、何も起こらないからです。
したがって、低水準(バッファなし)API を使うことをお勧めします。ほとんどのプログラミング言語が高水準 API の使用を推奨しているにもかかわらず、これは通常可能です。ツールやプログラミング言語が低水準 API をサポートしていない場合は、代わりに利用可能な API を使ってください。この場合、I/O がいつ行われるかを制御できないことを覚えておくことが重要です。それでも、データ収集などの多くのアプリケーションではこれで十分です。
繰り返しますが、バッファ付き I/O と Xillybus のバッファを混同してはいけません。最も重要なのは、Xillybus のバッファによってデータが無期限に詰まることは決してないということです。これについては、後ほどゼロ長 write() の文脈で説明します。
デバイスファイルからの読み取り:基本
デバイスファイルから読み取るためのサンプルコードは streamread.c です。このプログラムは demoapps/ ディレクトリにあります。ただし、以下で示すコードは別のプログラム memread.c(同じディレクトリ)からのものです。このプログラムは別の目的を意図していますが、allread() という名前の関数が含まれています。この関数を使うと、いくつかのトピックを説明するのに便利です。
ファイルが次のコマンドで開かれたとしましょう。
int fd, len;
char *buf;
fd = open("/dev/xillybus_read_32", O_RDONLY);
ここで、@len バイトをファイルからバッファに読み取りたいとします。ただし、部分的な結果は許されません。常に要求された量のデータを読み取る関数が必要です。
これが、allread() を次のように使ったときの動作です。
allread(fd, buf, len);
この関数は次のように定義されています。
void allread(int fd, unsigned char *buf, int len) {
int received = 0;
int rc;
while (received < len) {
rc = read(fd, buf + received, len - received);
if ((rc < 0) && (errno == EINTR))
continue;
if (rc < 0) {
perror("allread() failed to read");
exit(1);
}
if (rc == 0) {
fprintf(stderr, "Reached read EOF\n");
exit(1);
}
received += rc;
}
}
この関数は常に要求されたバイト数を読み取ることに注意してください。それが不可能な場合、この関数はプログラムを終了させます。これは通常の利用シナリオでは大げさかもしれません。allread() は、低水準 API でファイルにアクセスする方法の簡単なデモンストレーションと考えるべきです。
この関数を説明しましょう。最初の興味深い部分はこれです。
rc = read(fd, buf + received, len - received);
最初のループでは @received はゼロです。したがって、この行は次と同等です。
rc = read(fd, buf, len);
read() は、ファイルディスクリプタ(@fd)から @len バイトを読み取り、データをバッファ(@buf)に格納しようとします。
Xillybus のデバイスファイルについては、read() は、要求された量のデータ(@len バイト)が利用できない場合、最大 10 ms 待機します。この短い時間の後、関数は必要な量より少ないデータ(ただし少なくとも 1 バイト)を返します。データがまったくない場合、read() は FPGA からデータが到着するまで無期限に待機します(ただし例外があります。これについては後述します)。この動作は Xillybus のドライバに固有のものです(それでも標準 API には準拠しています)。
read() が何かを読み取れた場合、@rc は読み取られたバイト数になります。つまり、@rc は正の数であり、ループ内のすべての if 文はスキップされます。そこで次が実行されます。
received += rc;
その結果、@received には常にこれまでに読み取られたバイト数の合計が保持されます。@rc が @len(要求したバイト数)よりも小さいことは、完全に正当で正常なことです。
while ループは、@received(読み取られたバイト数の合計)が @len に達するまで続きます。これは while 文に反映されています。
while (received < len) { ... }
必要に応じて、より多くのデータが読み取られます。
rc = read(fd, buf + received, len - received);
今回は、バッファ内の開始位置が @received だけ移動しています。要求されるバイト数も同じ数だけ減っています。これらの調整は、これがデータ読み取りの繰り返しの試みであることを反映しているだけです。
read() が何も読み取らない場合
これまで、read() がデータを読み取れる場合に何が起こるかに焦点を当ててきました。しかし、read() が何も読み取らない状況が 3 つあります。それぞれの状況は、それぞれの if 文で処理されます。
POSIX シグナル
CTRL-C を押してプログラムを停止すると、オペレーティングシステムは POSIX シグナルをプロセスに送信します。これがプログラムを終了させるメカニズムです。同じ目的で「kill」コマンドを使った場合も同じことが起こります。しかし、ほとんどの状況では無視すべき他の多くのタイプのシグナルもあります。
では、read() の関数呼び出しの途中でプロセスがシグナルを受け取ったらどうなるでしょうか。Linux の規約によれば、read() はすぐにメインプログラムに制御を返さなければなりません。これが起こる前に read() がデータを読み取れていた場合、特に異常はありません。@rc にはバイト数が含まれ、シグナルを受け取ったことを示すものは何もありません。
しかし、新しいデータが到着していなかった場合、@rc は負の数になり、@errno は EINTR になります。この状況を処理する標準的な方法は、コードに示されているとおりです。何もなかったかのように振る舞い、もう一度試します。
if ((rc < 0) && (errno == EINTR))
continue;
これはシグナルが無視されることを意味しません。たとえば、シグナルの理由がユーザーによる CTRL-C の押下だった場合、プログラムは通常どおり終了します。それを処理する別のメカニズムがあります。この if 文の目的は、何か劇的なことを意図していないシグナルを処理することです。continue 文により、プロセスがこの種のシグナルを受信しても、おかしなことは起こりません。
たとえば、プロセスが CTRL-Z で停止された場合、実行が再開されたときにプログラムが続行されることを保証するために、この if 文が必要です。さらに、人間の介入なしに到着し得るシグナルが他にもいくつかあります。
実際のエラー
当然のことながら、データの読み取り中に何か問題が発生することがあります。この場合、@rc は負の数になり、@errno の値は EINTR 以外になります。サンプルコードは単にこのエラーを報告し、プログラムを終了させます。
if (rc < 0) {
perror("allread() failed to read");
exit(1);
}
EOF
ファイルの終わりに達したために read() がデータを提供できない場合、この関数は値ゼロを返します。これは通常のファイルではもちろん真です。しかし、Xillybus にもデータストリームが終了したことを宣言する機能があります。動作は同じです。
該当するコードはこれです。
if (rc == 0) {
fprintf(stderr, "Reached read EOF\n");
exit(1);
}
これもエラーとして扱われ、プログラムの終了を引き起こします。要求された量のデータ(@len バイト)が読み取られる前に EOF に達したことを意味します。この if 文には、@received が @len より小さい場合にのみ到達できます。
allread() の背後にある考え方は、常に要求された量のデータを読み取るというものであったことを思い出してください。それが不可能な場合、この関数はコンピュータプログラムを停止させます。
他のプログラミング言語との関連
上記のサンプルコードは C で書かれていますが、プログラミング言語に関係なく関連するいくつかの重要なポイントを示しています。
- read() は要求した量より少ないデータを返すことがあります。これはバッファ付き I/O(fread() など)でも起こり得ます。ただし、バッファ付き I/O では、これはエラーがあったか EOF に達した場合にのみ起こります。対照的に、read() ではこれは正常であり、何か特別なことが起こったことを示すものではありません。
- プログラムは POSIX シグナルを適切に処理しなければなりません。
- read() は、すべてのデータが読み取られ、EOF (end-of-file) に達すると、値ゼロを返します。
デバイスファイルへの書き込み
ファイルへの書き込みのための低水準 API は、ファイルからの読み取りとほぼ同じです。これを示すために、streamwrite.c にある allwrite() という名前の関数を示します。
void allwrite(int fd, unsigned char *buf, int len) {
int sent = 0;
int rc;
while (sent < len) {
rc = write(fd, buf + sent, len - sent);
if ((rc < 0) && (errno == EINTR))
continue;
if (rc < 0) {
perror("allwrite() failed to write");
exit(1);
}
if (rc == 0) {
fprintf(stderr, "Reached write EOF (?!)\n");
exit(1);
}
sent += rc;
}
}
上記の allread() と比較してください。違いは 3 つだけです。
- read() の代わりに write() を使っています。ただし、これらの関数はまったく同じ方法で使われます。
- 変数 @received の名前が @sent に変更されています。ただし、違いは変数の名前だけです。この変数の意味と使い方はまったく同じです。
- テキスト出力が調整されています。以前「read」と書かれていた場所に「write」と書かれています。
つまり、原則として書き込みと読み取りの間に違いはありません。
とはいえ、@rc は決してゼロになるべきではないことに注意してください。ファイルへの書き込みに EOF という意味はないからです。POSIX 標準によれば、@rc がゼロになるのは、write() がゼロバイトの書き込みを要求された場合だけです。しかし、これはこの while ループでは決して起こりません。
要約すると、allwrite() は常に要求されたバイト数を書き込みます。唯一の代替手段はプロセスを終了させることです。言い換えれば、デバイスファイルが次のように開かれていると仮定します。
int fd, len;
char *buf;
fd = open("/dev/xillybus_write_32", O_WRONLY);
@buf から @len バイトを書き込むには、次のようにします。
allwrite(fd, buf, len);
allread() について上で述べたことはすべて allwrite() にも当てはまります。これには他のプログラミング言語との関連も含まれます。
ゼロ長 write()
ゼロバイトで write() 関数を呼び出すことは許されています。標準 API はその特定のケースで何が起こるかを述べていません。しかし明らかに、これはデータが書き込まれないことを意味します。
この種の関数呼び出しは、Xillybus のデバイスファイルに対して特別な意味を持ちます。ゼロバイトの書き込みはフラッシュの要求を意味します。この意味を理解するために、まずデータがデバイスファイルに書き込まれるときに何が起こるかを見てみましょう。
デバイスファイルが非同期ストリーム (asynchronous stream) であると仮定しましょう。この用語は別のページで簡単に説明され、ドキュメントでより詳しく説明されています。
データがデバイスファイルに書き込まれると(write() で)、Xillybus ドライバはこのデータを RAM バッファに格納します。このデータの一部またはすべてがすぐに FPGA に送信される可能性があります。しかし一般的には、データの一部がバッファに残る可能性があり、関数呼び出しを行ったプログラムはそれでも続行します。このメカニズムの目的は、特に write() への関数呼び出しが多数ある場合に、パフォーマンスを向上させることです。
では、ドライバの RAM バッファ内のデータはいつ FPGA に送信されるのでしょうか。4 つの可能性があります。
- RAM バッファが満杯になる。
- デバイスファイルが閉じられる。
- 10 ms の時間が経過する(自動フラッシュ)。
- ゼロ長 write() が実行される。
したがって、データがドライバのバッファに長時間留まることは決してありません。これは、データが常に 10 ms 以内に FPGA に送信されるためです。しかし、一部のアプリケーションでは、この遅延さえ許容できません。その場合、ゼロ長 write() を使って、残りのデータをすぐに送信するよう要求できます。
C 言語では次のようにします。
write(fd, NULL, 0);
バッファのアドレスが NULL であることに注意してください。書き込むバイト数がゼロなので、これは問題ありません。ただし、この関数呼び出しは要求が成功することを保証しません。成功する可能性は非常に高いですが、これは正しい方法です。
while (1) {
rc = write(fd, NULL, 0);
if ((rc < 0) && (errno == EINTR))
continue; // Interrupted. Try again.
if (rc < 0) {
perror("flushing failed");
break;
}
break; // Flush successful
}
これまで述べたことはすべて非同期ストリームについてでした。デバイスファイルが同期ストリームの場合、データは write() の関数呼び出しの結果として常に即座に FPGA に送信されます。さらに、write() はデータが FPGA に到達するまで待機してから戻ります(ゼロ長 write() はそうではありません)。
したがって、ゼロ長 write() は非同期ストリームにのみ関係します。この機能は、FPGA との通信を遅くするため、必要な場合を除き使うべきではありません。
まとめ
前述のとおり、上記のほとんどすべてはあらゆるファイルへのアクセスにも当てはまります。Xillybus に固有のトピックはほんのわずかだけでした。
FPGA と通信する際に一貫した動作を確保するには、これらのガイドラインに従うことが重要です。これらのトピックを考慮せずに書かれたプログラムは、時折障害が発生する可能性があります。これらの障害は、FPGA やドライバの問題のように見えることがよくあります。したがって、適切なプログラミング技法は、多くの混乱と不要な労力を省くことになります。