USBインタフェースICのFT232Hを介してFPGAにデータを送信するC++コードについて説明する。
- FPGAはVHDLコードARRAY_FIFO8To64.vhdでコンパイルした
このハードウェアは内部に容量16kバイト,入力幅1バイト,出力幅8バイトのFIFOバッファを持つ。FT232Hの持2kバイトのFIFOから出力されるバイト単位のデータを受け取り,内部のFIFOに入力する。 - 書込み用C++コードは,FT232Hの動作確認と転送速度の評価を行う
当初はFPGA基板に搭載されたLEDにより正しく動作が行われたかどうかの確認を目的としていた。
コードの解説
#include <windows.h>
#include <stdio.h>
#include "ftd2xx.h" //ftd2xxドライバを使うためのヘッダファイル
#include <time.h>
// FT232Hを介するFPGAへのデータ転送の動作確認用
int main(int argc, char* argv[])
{
FT_HANDLE fthandle1; // FT232Hデバイスのハンドル変数
FT_STATUS status; // ドライバ関数実行時のステータス変数
const int N_CH = 8; // VHDLコードの「チャンネル数」
const int N_BYTE = 1; // VHDLコードの「チャンネル当たりのバイト数」
// デバイスをオープンしハンドルfthandle1を取得
// status = FT_Open(0, &fthandle1); FT232HチップのIDを使わない場合
// 複数のUSBデバイスを併用する場合は,チップIDを使う方がよい
// IDはFT_Progで確認してね
// FT232H Chip's serial number
// 対応するデバイスに応じてコメントを外す
PVOID id = (PVOID)"FT63ZE1L"; // 田村のシステム
//PVOID id = (PVOID)"FT6MVP8N"; // 工藤君のシステム
//PVOID id = (PVOID)"FT723GDQ"; // 田村のシステム 小林君用
// IDを指定してハンドルを取得
status = FT_OpenEx(id, FT_OPEN_BY_SERIAL_NUMBER,&fthandle1);
// ハンドル取得に失敗した場合は,プログラム終了
if (status != FT_OK)
{
printf("open device status not ok %d\n", status);
return 0;
}
// readとwriteのタイムアウト時間をmsec単位で設定
status = FT_SetTimeouts(fthandle1, 1000, 1000);
if (status != FT_OK)
printf("timeout device status not ok %d\n", status);
// デバイスのモードを設定
// Sync FIFO 出力モード
UCHAR MaskA = 0xFF; // Set data bus to outputs
UCHAR modeA = 0x40; // Configure FT232H into 0x40 Sync FIFO Mode
status = FT_SetBitMode(fthandle1, MaskA, modeA);
if (status != FT_OK)
printf("mode A status not ok %d\n", status);
Sleep(500); // 500msだけスリープ
// 動作確認用の送信データの準備
// FPGA基板のLED表示で動作を確認するためのパターンを幾つか作る
int i;
int num, drive_n, byte_n, sample_n;
drive_n = N_CH * N_BYTE * 8; // 出力ピンの個数
byte_n = drive_n / 8; // drive_n個のピンに対応するデータ数
int length = N_CH * N_BYTE * 2; // 周期的に送信する場合の周期を設定
UCHAR* data = new UCHAR[length]; // 1周期分のデータ
int pattern; // データのパターンを指定するためのスイッチ変数
pattern = 0; // この値を変えることでパターンを指定
switch (pattern) {
case 0: // 1ビットのみ1
for (int i = 0;i < N_CH * N_BYTE; i++) {
data[i] = 1 << i; // ビットシフトして右側からiビット番目を1に
data[N_CH * N_BYTE + i] = ~data[i]; // 半周期ずれた信号は反転して作る
}
break;
case 1: // 全チャンネルが同じ位相で反転を繰り返す信号
for (int i = 0;i < N_CH * N_BYTE; i++) {
data[i] = 0xFF; // 全てのチャンネルのデータの全ビットを1に
data[N_CH * N_BYTE + i] = ~data[i]; // 半周期経過したら反転
}
break;
}
num =8192; // 1回のコマンドで送信するデータ数
// この値をいろいろ変えて動かしてみる
// 送信用データdata_outを作る
UCHAR* data_out = new UCHAR[num]; // 送信するデータの配列
UCHAR* data_pt; // データ用ポインタ
// 配列dataを周期拡張してdata_outを作る
for (int i = 0;i < num;i++) {
data_out[i] = data[i % length];
}
printf("num = %d\n", num);
// 確認用
for (i = 0;i < 16;i++) {
printf("data = %x\n", data_out[i]);
}
// 以上で送信データdata_outの作成を終了
// データ送信と転送速度などの評価を開始
// そのための準備
DWORD data_written; // 実際に書込まれたデータ数を格納する変数
DWORD w_data_len; // 1回の送信のデータ数
// 受信および送信のFIFOバッファを空にする
status = FT_Purge(fthandle1, FT_PURGE_RX | FT_PURGE_TX);
if (status != FT_OK)
printf("status not ok %d\n", status);
// デバイスのリセット
status = FT_ResetDevice(fthandle1);
if (status != FT_OK)
printf("status not ok %d\n", status);
// USBパラメータのセット
status = FT_SetUSBParameters(fthandle1,0x10000,0x10000);
// ここからデータ送信の作業開始
// START用タクトスイッチを押せというプロンプトを表示しスイッチを押させる
printf("Please press the START button and then hit the ENTER key.\n");
// STARTボタンが押されると,FPGAはRUN状態になる
getchar(); //breakpoint ユーザはタクトスイッチを押したら,リターンキーを押す
// FIFOバッファにデータを書込む
// 送信1回に書込むデータは,data_outを長さw_data_lenだけ
w_data_len =num; // w_data_lenを設定
sample_n = 1; // 送信を何回繰り返すか・・・この値もいろいろ変えてみる
// 転送速度評価用の変数を設定
timespec ts1,ts2; // 転送速度計測用
double tstart, tend; // 転送開始・終了時刻
int error_n = 0; // 転送エラー回数
int timeout_n = 0; // タイムアウト回数
timespec_get(&ts1, TIME_UTC); // 時刻構造体の取得
// データ送信開始
for (int i = 0; i < sample_n;i++) {
data_pt = data_out + i*length;
status = FT_Write(fthandle1, data_pt, w_data_len, &data_written);
Sleep(0.001);
//if (status != FT_OK){
// error_n += 1;
//printf("status not ok %d\n", status);
//}
//if (data_written != w_data_len) {
// timeout_n += 1;
// printf("Time out! : %d\n", data_written);
//}
//}
}
timespec_get(&ts2, TIME_UTC);
tstart = (double)ts1.tv_sec + (double)ts1.tv_nsec * 1.0e-9;
tend = (double)ts2.tv_sec+(double)ts2.tv_nsec*1.0e-9;
printf("%f,%f)\n", tstart, tend);
printf("Error_n, Timeout_n = %d,%d\n", error_n,timeout_n);
double eTime = (tend - tstart)*1000.0;// 消費時間[ms]
double Rate = (double)(w_data_len * sample_n) / eTime/1000.0;
printf("Time = %f ms\nRate = %f MB/s\n",eTime,Rate);
getchar();
status = FT_Close(fthandle1); // デバイスを閉じる
delete[] data_out;
return 0;
}
それでは,コードリストの上から順番に説明していきます。
#ヘッダファイルの指定,デバイスの設定
1行目~4行目
#include <windows.h>
#include <stdio.h>
#include "ftd2xx.h" //ftd2xxドライバを使うためのヘッダファイル
#include <time.h>
3行目の#include “ftd2xx.h”で,コメントにもあるように,FT232Hのドライバを使うためにインクルードするヘッダファイルを指示します。(ftd2xxドライバのインストールについては,別のページにまとめる予定です。)
#include <time.h>で指定しているヘッダファイルtime.hは,データ転送速度の計測に使うために必要です。
デバイスハンドルなどの準備
FT_HANDLE fthandle1; // FT232Hデバイスのハンドル変数
FT_STATUS status; // ドライバ関数実行時のステータス変数
const int N_CH = 8; // VHDLコードの「チャンネル数」
const int N_BYTE = 1; // VHDLコードの「チャンネル当たりのバイト数」
FT_HANDLEとFT_STATUSなどは,ft2xx.hの中で定義されている変数型です。FT_HANDLE型のfthandle1は,FT232Hデバイスを指定するための“識別番号”のような変数です。FT_STATUS型のstatusは,ft2xxドライバが提供する関数を実行したときに,実行の失敗や成功などの“状態”を返すための変数です。
N_CHは,送信するデータの“チャンネル数”を表す整数型の定数です。一方,N_BYTEは,1つのチャンネルが何バイトで構成されるかを表す定数です。FPGAはN_BYTEつまりN_BYTE×8ビット幅の入力ポートを使ってFT232Hからのデータを受信します。
実際には,FT232Hの出力ポートは1バイト幅なので,N_BYTE = 1としています。なぜ,定数として定義しているかというと,将来,2バイト幅や4バイト幅の出力ポートを持つデバイスを採用することになったとき,この部分をN_BYTE = 2のように変更できるようにするためです。(現在使っているFT232HはN_BYTE = 1に固定です。しかし,FTDI社の製品の中には2バイト~8バイトの出力ポートを持つデバイスがラインアップされています。)
以上より,このシステムが扱えるスピーカの個数はN_CH×N_BYTE×8個となります。
FT232Hデバイスを使う準備
// デバイスをオープンしハンドルfthandle1を取得
// status = FT_Open(0, &fthandle1); FT232HチップのIDを使わない場合
// 複数のUSBデバイスを併用する場合は,チップIDを使う方がよい
// IDはFT_Progで確認してね
// FT232H Chip's serial number
// 対応するデバイスに応じてコメントを外す
PVOID id = (PVOID)"FT63ZE1L"; // 田村のシステム
//PVOID id = (PVOID)"FT6MVP8N"; // 工藤君のシステム
//PVOID id = (PVOID)"FT723GDQ"; // 田村のシステム 小林君用
// IDを指定してハンドルを取得
status = FT_OpenEx(id, FT_OPEN_BY_SERIAL_NUMBER,&fthandle1);
// ハンドル取得に失敗した場合は,プログラム終了
if (status != FT_OK)
{
printf("open device status not ok %d\n", status);
return 0;
}
コメントの通りです。デバイスをオープンし,関数を呼び出すときのハンドル変数を取得します。
コメントアウトされている,
status = FT_Open(0, &fthandle1);
は,FT232Hチップ固有のIDを指定しないでデバイスをオープンする関数です。しかし,PCに複数のUSBインタフェースを接続して動作させる際に,エラーが発生することがあったので,チップのIDを指定してオープンする関数,
status = FT_OpenEx(id, FT_OPEN_BY_SERIAL_NUMBER,&fthandle1);
を使うようにしました。変数idによって表されるIDは長さ8の文字列である。FTDI社が提供するアプリケーションソフトFT_progを使って調べました。(おそらく,ft2xxライブラリの中にある関数でも調べることができるはずですが,ここではFT_progで調べた値を使っています。)
デバイスのモード設定
// readとwriteのタイムアウト時間をmsec単位で設定
status = FT_SetTimeouts(fthandle1, 1000, 1000);
if (status != FT_OK)
printf("timeout device status not ok %d\n", status);
status = FT_SetTimeouts(fthandle1, 1000, 1000);
により,送信および受信のタイムアウトの時間をms単位で設定します。ここでは,どちらも1000msつまり1秒に設定しています。この値は,おそらく長すぎます。(なお,今回は,FT232は送信のみに使っているので,送信タイムアウトのみが意味を持ちます。)
Sync FIFO出力モードに設定
// デバイスのモードを設定
// Sync FIFO 出力モード
UCHAR MaskA = 0xFF; // Set data bus to outputs
UCHAR modeA = 0x40; // Configure FT232H into 0x40 Sync FIFO Mode
status = FT_SetBitMode(fthandle1, MaskA, modeA);
if (status != FT_OK)
printf("mode A status not ok %d\n", status);
Sleep(500); // 500msだけスリープ
FT232Hデバイスのモード設定する。モード設定は,
status = FT_SetBitMode(fthandle1, MaskA, modeA);
という関数で,MaskAとmodeAという2つの変数を引数にして実行します。この関数の前の式文,39行目と40行目でパラメータMaskAと modeAを設定しています。
- UCHAR MaskA = 0xFF;
8ビット幅のFT232Hの入出力ポートの全てのビットを出力に設定 - UCHAR modeA = 0x40;
Sync FIFOモードに設定
Sync FIFOモードというのは,Synchronous FIFOモード,“同期型FIFOモード”ということです。このモードでは,FT232Hが発生する公証値60MHzのクロック信号に同期してFIFO(First In First Out)を動作させます。最高で40MByte/sという高速でのデータ転送が可能です。
送信データの準備・・・1周期分のデータを作る
// 動作確認用の送信データの準備
// FPGA基板のLED表示で動作を確認するためのパターンを幾つか作る
int i;
int num, drive_n, byte_n, sample_n;
drive_n = N_CH * N_BYTE * 8; // 出力ピンの個数
byte_n = drive_n / 8; // drive_n個のピンに対応するデータ数
int length = N_CH * N_BYTE * 2; // 周期的に送信する場合の周期を設定
UCHAR* data = new UCHAR[length]; // 1周期分のデータ
int pattern; // データのパターンを指定するためのスイッチ変数
pattern = 0; // この値を変えることでパターンを指定
switch (pattern) {
case 0: // 1ビットのみ1
for (int i = 0;i < N_CH * N_BYTE; i++) {
data[i] = 1 << i; // ビットシフトして右側からiビット番目を1に
data[N_CH * N_BYTE + i] = ~data[i]; // 半周期ずれた信号は反転して作る
}
break;
case 1: // 全チャンネルが同じ位相で反転を繰り返す信号
for (int i = 0;i < N_CH * N_BYTE; i++) {
data[i] = 0xFF; // 全てのチャンネルのデータの全ビットを1に
data[N_CH * N_BYTE + i] = ~data[i]; // 半周期経過したら反転
}
break;
}
ここから,FT232Hを介してFPGAに送信するデータを作るための処理が続きます。このC++コードの目的は,FT232HとFPGAの動作確認です。つまり,これら2つのハードウェアがユーザが意図した通りの動作をしているかどうかを確認するためのものです。(動作確認ができたら,これを基にシステムに組み込むコードに発展させていくことになります。)
なお,このコードを作成し始めたときは,自宅の環境に物理的な波形を観測するためのオシロスコープなどの計測器がありませんでした。そこで,FPGA基板上のLEDにFPGA出力を接続し,データ転送終了時の出力を目視で観測することにしました。データ送信が終了すると,一番最後に転送されたデータが出力されることになります。送信の途中でデータの欠落などが発生すると,このデータが,送ったつもりのデータと異なってきます。間接的な方法ですが,この方法で動作確認をしようとしました。
そこで,目視での確認が容易なデータを2種類用意することにしました。
送信データの準備・・・データ全体を作る
num =8192; // 1回のコマンドで送信するデータ数
// この値をいろいろ変えて動かしてみる
// 送信用データdata_outを作る
UCHAR* data_out = new UCHAR[num]; // 送信するデータの配列
UCHAR* data_pt; // データ用ポインタ
// 配列dataを周期拡張してdata_outを作る
for (int i = 0;i < num;i++) {
data_out[i] = data[i % length];
}
printf("num = %d\n", num);
// 確認用
for (i = 0;i < 16;i++) {
printf("data = %x\n", data_out[i]);
}
// 以上で送信データdata_outの作成を終了
num =8192; で送信するデータ全体の長さを決めています。numの値を,いろいろ変えて実行します。なお,このC++コードはVisualStudioを使いコンパイルして実行しています。このため,コード内部の代入文でパラメータの値を変更する手法は,変更の度にコード変更・コンパイルを繰り返すので,望ましくはありません。しかし,コード自体が小さく,再コンパイルの時間もそれほど長くないため,この方法を使っています。
この部分は,コメントを参考にすれば何をしているかは理解できると思います。1周期分のデータである配列dataを基に,data_outを,
for (int i = 0;i < num;i++) {
data_out[i] = data[i % length];
}
というループで作ります。周期が変数lengthで与えられる周期的なデータが得られます。
データ送信前の準備
ここから,データ送信の前に転送速度計測のための準備をします。細かい説明は省きます。
一つ気になっているのは,
// ここからデータ送信の作業開始
// START用タクトスイッチを押せというプロンプトを表示しスイッチを押させる
printf("Please press the START button and then hit the ENTER key.\n");
// STARTボタンが押されると,FPGAはRUN状態になる
getchar(); //breakpoint ユーザはタクトスイッチを押したら,リターンキーを押す
FPGAのリセットとデータ転送のスタートのために2つのタクトスイッチを用意しておき,ユーザが押すようにしています。しかし,動作確認用ではなく,PCから送られる音楽データを処理する用途では,リセット信号やスタート信号だを,ソフトウェアでも発生できるようにする予定です。
いよいよデータ送信
// データ送信開始
for (int i = 0; i < sample_n;i++) {
data_pt = data_out + i*length;
status = FT_Write(fthandle1, data_pt, w_data_len, &data_written);
Sleep(0.001);
//if (status != FT_OK){
// error_n += 1;
//printf("status not ok %d\n", status);
//}
//if (data_written != w_data_len) {
// timeout_n += 1;
// printf("Time out! : %d\n", data_written);
//}
//}
}
timespec_get(&ts2, TIME_UTC);
tstart = (double)ts1.tv_sec + (double)ts1.tv_nsec * 1.0e-9;
tend = (double)ts2.tv_sec+(double)ts2.tv_nsec*1.0e-9;
printf("%f,%f)\n", tstart, tend);
printf("Error_n, Timeout_n = %d,%d\n", error_n,timeout_n);
double eTime = (tend - tstart)*1000.0;// 消費時間[ms]
double Rate = (double)(w_data_len * sample_n) / eTime/1000.0;
printf("Time = %f ms\nRate = %f MB/s\n",eTime,Rate);
getchar();
status = FT_Close(fthandle1); // デバイスを閉じる
delete[] data_out;
return 0;
}
- for (int i = 0; i < sample_n;i++) {
送信を繰り返したときの挙動を見るためのforループ。ただし,現在,繰り返し回数sample_nを
1に設定しているので,1回しか繰り返さない。 - data_pt = data_out + i*length;
送信する配列の先頭のポインタを計算しています。forループでiを増加させながら繰り返す場合は,ポインタの値をデータの周期lengthだけ増加していくことになる。 - status = FT_Write(fthandle1, data_pt, w_data_len, &data_written);
データ送信のコマンド。data_ptで指定される配列の先頭から,w_data_lenバイトのデータを送信する。このコマンドを実行後,実際に送信されたデータのバイト数がdata_writtenに格納されている。
data_written < w_data_lenのときには,なんらかの原因でデータが欠落したり,転送が完了していないことを示している。完全なデータ送信をしたい場合は,追加のデータ送信の操作などが必要となる。しかし,このコードでは,そのような再送信の操作はしていない。
また,データ送信速度の計測のための処理も実行する予定であるが,現在,その部分はコメントアウトされている。
コメント