Vitis HLS のプログラム
Vitis HLS コーディング スタイル
この章では、さまざまな C、C++11/C++14 のコンストラクトがどのように FPGA ハードウェア インプリメンテーションに合成されるかと、標準 C コードに関する制限について説明します。
サポートされない C コンストラクト
Vitis HLS では、C 言語が広範囲にわたってサポートされいますが、合成ができず、後のデザイン フローでエラーを発生させる C コンストラクトもあります。このセクションでは、このような関数が合成されてデバイスにインプリメントされる場合の、コード変更が必要な箇所について説明します。
合成するには、次の条件を満たしている必要があります。
- デザインのすべての機能を含める必要あり。
- OS へのシステム コールで実行できる機能はなし。
- C コンストラクトが固定サイズまたは境界のあるサイズである必要あり。
- これらのコンストラクトのインプリメンテーションがあいまいではない必要あり。
詳細は、『Vitis HLS 移行ガイド』 (UG1391) を参照してください。
システム コール
システム コールは、C プログラムを実行している OS で一部のタスクが実行されるので、合成できません。
Vitis HLS はよく使用される、printf() および fprintf(stdout,) のようなデータを表示するだけでアルゴリズムの実行には影響のないシステム コールは無視します。通常はシステム コールは合成できないので、合成前に関数から削除しておく必要があります。getc()、time()、sleep() などのシステム コールも、OS に呼び出しを実行するので合成できません。
Vitis HLS では、合成が実行されたときに __SYNTHESIS__ マクロが定義されます。これにより、__SYNTHESIS__ マクロを使用して、デザインから合成不可能なコードを削除できるようになります。
__SYNTHESIS__ マクロは、合成されるコードにのみ使用します。このマクロは C シミュレーションまたは C RTL 協調シミュレーションには従っていないので、テストベンチには使用しないでください。__SYNTHESIS__はコードやコンパイラ オプションで定義または定義解除すると、コンパイル エラーになることがあります。次に、サブ関数からの中間結果をハード ドライブのファイルに保存するコード例を示します。__SYNTHESIS__ マクロを使用すると、合成不可能なファイルの書き込みが合成中に無視されるようになります。
#include "hier_func4.h"
int sumsub_func(din_t *in1, din_t *in2, dint_t *outSum, dint_t *outSub)
{
*outSum = *in1 + *in2;
*outSub = *in1 - *in2;
}
int shift_func(dint_t *in1, dint_t *in2, dout_t *outA, dout_t *outB)
{
*outA = *in1 >> 1;
*outB = *in2 >> 2;
}
void hier_func4(din_t A, din_t B, dout_t *C, dout_t *D)
{
dint_t apb, amb;
sumsub_func(&A,&B,&apb,&amb);
#ifndef __SYNTHESIS__
FILE *fp1; // The following code is ignored for synthesis
char filename[255];
sprintf(filename,Out_apb_%03d.dat,apb);
fp1=fopen(filename,w);
fprintf(fp1, %d \n, apb);
fclose(fp1);
#endif
shift_func(&apb,&amb,C,D);
}
__SYNTHESIS__ マクロを使用すると、C 関数からコード自体を削除せずに、合成不可能なコードを削除できます。ただし、このマクロを使用すると、シミュレーション用の C コードと合成用の C コードが異なることになります。
__SYNTHESIS__ マクロを C コードの機能を変更するために使用すると、C シミュレーションと C 合成の結果が異なります。このようなコードのエラーはデバッグしにくいので、機能を変更するために __SYNTHESIS__ マクロを使用するのはお勧めできません。ダイナミック メモリの使用
システム内のメモリ割り当てを管理するシステム コール、たとえば malloc()、alloc()、および free() は、OS のメモリにあるリソースを使用し、ランタイム中に作成およびリリースされます。ハードウェア インプリメンテーションを合成できるようにするには、デザインに必要なリソースがすべて指定され、含まれている必要があります。
メモリ割り当てのシステム コールは、合成前にデザインから削除する必要がありますが、ダイナミック メモリ操作がデザインの機能を定義するために使用されているので、同等の範囲が制限された表現に変換する必要があります。次に、malloc() を使用するデザインを合成可能なバージョンに変換するコード例を示します。次の 2 つの便利なコーディング スタイル手法が含まれます。
- このデザインでは
__SYNTHESIS__マクロは使用しません。合成可能なバージョンまたは合成不可能なバージョンの選択には、ユーザー定義の
NO_SYNTHマクロを使用します。これにより、同じコードが C でシミュレーションされ、Vitis HLS で合成されます。 malloc()を使用する元のデザインのポインターは、固定サイズのエレメント用に書き直す必要はありません。固定サイズのリソースは作成でき、既存ポインターは単に固定サイズのリソースを指定するようにできます。この方法により、既存デザインを手動で書き直す必要はなくなります。
#include "malloc_removed.h"
#include <stdlib.h>
//#define NO_SYNTH
dout_t malloc_removed(din_t din[N], dsel_t width) {
#ifdef NO_SYNTH
long long *out_accum = malloc (sizeof(long long));
int* array_local = malloc (64 * sizeof(int));
#else
long long _out_accum;
long long *out_accum = &_out_accum;
int _array_local[64];
int* array_local = &_array_local[0];
#endif
int i,j;
LOOP_SHIFT:for (i=0;i<N-1; i++) {
if (i<width)
*(array_local+i)=din[i];
else
*(array_local+i)=din[i]>>2;
}
*out_accum=0;
LOOP_ACCUM:for (j=0;j<N-1; j++) {
*out_accum += *(array_local+j);
}
return *out_accum;
}
コード変更はデザインの機能に影響するので、ザイリンクスでは __SYNTHESIS__ マクロの使用はお勧めしません。ザイリンクスでは、次の手順をお勧めします。
- ユーザー定義のマクロ
NO_SYNTHをコードに追加して、コードを修正します。 NO_SYNTHマクロをイネーブルにし、C シミュレーションを実行して結果を保存します。NO_SYNTHマクロをディスエーブルにし、C シミュレーションを実行して結果が同じになるかどうかを検証します。- このユーザー定義のマクロをディスエーブルにして合成を実行します。
この方法を使用すると、アップデートされたコードが C シミュレーションで検証され、同じコードが合成されるようにできます。C でのダイナミック メモリ使用に関する制限と同様、Vitis HLS では、ダイナミックに作成/削除される C++ オブジェクトも合成でサポートされません。
次のコードは、ランタイムで新しい関数を作成するので、合成できません。
Class A {
public:
virtual void bar() {â¦};
};
void fun(A* a) {
a->bar();
}
A* a = 0;
if (base)
a = new A();
else
a = new B();
foo(a);
ポインターの制限
一般的なポインターの型変換
Vitis HLS では、一般的なポインターの型変換はサポートされませんが、ネイティブ C 型間ではサポートされます。
ポインター配列
Vitis HLS では、各ポインターがスカラーまたはスカラーの配列を指定する場合に、ポインター配列が合成でサポートされます。ポインター配列では、別のポインターを指定することはできません。
関数ポインター
関数ポインターはサポートされていません。
再帰関数
再帰関数は合成できません。これは、次のような再帰が恒久的に繰り返される可能性のある関数に適用されます。
unsigned foo (unsigned n)
{
if (n == 0 || n == 1) return 1;
return (foo(n-2) + foo(n-1));
}
Vitis HLS では、有限数の関数呼び出しがある末尾再帰もサポートされません。
unsigned foo (unsigned m, unsigned n)
{
if (m == 0) return n;
if (n == 0) return m;
return foo(n, m%n);
}
C++ では、テンプレートで末尾再帰をインプリメントできます。C++ テンプレートについては、次で説明します。
標準テンプレート ライブラリ
std::complex のような標準データ型がサポートされます。関数
最上位関数は合成後に最上位 RTL デザインになり、サブ関数は RTL デザインのブロックに合成されます。
合成後、デザインの各関数に対して独自の合成レポートと HDL ファイル (Verilog および VHDL) が作成されます。
関数のインライン展開
サブ関数をオプションでインライン展開し、そのロジックと周囲の関数のロジックとを統合できます。関数をインライン展開することにより最適化が改善されることはありますが、メモリに維持して解析する必要のあるロジックが増えるので、ランタイムは増加する可能性があります。
inline 指示子を off に設定します。関数がインライン展開されると、その関数に対するレポートおよび別の RTL ファイルは作成されません。サブ関数のロジックおよびループは上の階層の関数と統合されます。
コーディング スタイルの影響
関数のコーディング スタイルは、主に関数の引数およびインターフェイスに影響します。
関数への引数サイズが正確に記述されていれば、Vitis HLS でこの情報がデザインに伝搬されます。すべての変数に対して任意精度型を作成する必要はありません。次の例では、2 つの整数が乗算されますが、下位 24 ビットのみが結果に使用されます。
#include "ap_cint.h"
int24 foo(int x, int y) {
int tmp;
tmp = (x * y);
return tmp
}
このコードが合成されると、出力が 24 ビットに切り捨てられた 32 ビット乗算器になります。
次のコード例 のように、入力のサイズが正しく 12 ビット型 (int12) になっていれば、最終 RTL では 24 ビット乗算器が使用されます。
#include "ap_cint.h"
typedef int12 din_t;
typedef int24 dout_t;
dout_t func_sized(din_t x, din_t y) {
int tmp;
tmp = (x * y);
return tmp
}
2 つの関数入力に任意精度型を使用するだけで、Vitis HLS で 24 ビット乗算器を使用するデザインが作成され、12 ビット型がデザインに伝搬されます。ザイリンクスでは、階層内のすべての関数の引数のサイズを正しく記述することを勧めしています。
通常は、変数が関数インターフェイスから、特に最上位関数インターフェイスから直接駆動されると、最適化の中に実行されないものがでてくることがあります。典型的な例は、入力がループ インデックスの上位制限として使用される場合です。
C ビルトイン関数
Vitis HLS では、次の C ビルトイン関数がサポートされます。
__builtin_clz(unsigned int x)の上位ビットの 0 の数を返します。x が 0 の場合は、結果は未定義です。__builtin_ctz(unsigned int x): x の下位ビットの 0 の数を返します。x が 0 の場合は、結果は未定義です。
次に、これらの関数の使用例を示します。この例では、in0 に上位ビットの 0 の数、in1 に下位ビットの 0 の数が返されます。
int foo (int in0, int in1) {
int ldz0 = __builtin_clz(in0);
int ldz1 = __builtin_ctz(in1);
return (ldz0 + ldz1);
}
ループ
ループはアルゴリズムの動作を示す簡単な方法で、C コードでよく使用されます。ループは合成でサポートされ、パイプライン化、展開、部分展開、統合、平坦化できます。
最適化の展開、部分展開、平坦化、統合により、コードを変更したかのようにループ構造を効率的に変更できるので、ループの最適化に必要なコード変更を最小限に抑えることができます。ただし、特定の状況にしか適用できない最適化もあり、そのためにコードを変更する必要のあることもあります。
可変ループ境界
Vitis HLS で適用できる最適化の中には、ループに可変境界があると実行されないものがあります。次のコード例では、ループ境界が最上位入力から駆動される変数 (width) により決定されます。この例の場合、Vitis HLS ではループがいつ終了するのか判断できないので、ループには可変境界があると考えられます。
#include "ap_cint.h"
#define N 32
typedef int8 din_t;
typedef int13 dout_t;
typedef uint5 dsel_t;
dout_t code028(din_t A[N], dsel_t width) {
dout_t out_accum=0;
dsel_t x;
LOOP_X:for (x=0;x<width; x++) {
out_accum += A[x];
}
return out_accum;
}
上記のデザインを最適化しようとすると、可変ループ境界による問題がわかります。可変ループ境界に関する最初の問題は、Vitis HLS でループのレイテンシが決定できなくなる点です。Vitis HLS はループの一巡目が終了するまでのレイテンシは決定できますが、可変幅の正確な値はスタティックに決定できないので、何度繰り返されるのかは判断できず、ループ レイテンシ (ループの繰り返しすべてを完全に実行するまでのサイクル数) はレポートできません。
ループ境界が可変の場合、Vitis HLS ではそのレイテンシが正確な値ではなく、クエスチョン マーク (?) でレポートされます。次は、上記の例の合成後の結果を示しています。
+ Summary of overall latency (clock cycles):
* Best-case latency: ?
* Worst-case latency: ?
+ Summary of loop latency (clock cycles):
+ LOOP_X:
* Trip count: ?
* Latency: ?
可変ループ境界には、デザインのパフォーマンスが未知になるという別の問題もあります。この問題は次の 2 つの方法で回避できます。
- pragma HLS loop_tripcount または set_directive_loop_tripcount を使用します。
- C コードで
assertマクロをコード内で使用します。
tripcount 指示子を使用すると、ループに指定する最小および最大の両方またはいずれかの tripcount を指定できます。tripcount はループの繰り返し回数を意味します。最初の例のように最大の tripcount の 32 が LOOP_X に適用されると、レポートは次のようにアップデートされます。
+ Summary of overall latency (clock cycles):
* Best-case latency: 2
* Worst-case latency: 34
+ Summary of loop latency (clock cycles):
+ LOOP_X:
* Trip count: 0 ~ 32
* Latency: 0 ~ 32
tripcount 指示子に対してユーザーが指定した値は、レポートにのみ使用されます。tripcount の値は Vitis HLS で数がレポートされるので、さまざまなソリューションからのレポートを比較できます。この同じループ境界情報を合成に使用するには、C コードをアップデートする必要があります。
次は、より小さい開始間隔で最初の例を最適化する手順を示しています。
- ループを展開し、並列累算が実行されるようにします。
- 配列入力を分割しないと、並列累算が 1 つのメモリ ポートに制限されます。
これらの最適化が適用されると、Vitis HLS で可変境界ループに関する最大の問題を示すメッセージが表示されます。
@W [XFORM-503] Cannot unroll loop 'LOOP_X' in function 'code028': cannot completely
unroll a loop with a variable trip count.
可変境界ループは展開できないので、unroll 指示子が適用できないだけでなく、そのループの上のレベルのパイプライン処理もできません。
この問題は、ループ内で条件付き実行を使用し、ループの繰り返し回数を固定値にすると回避できます。可変ループ境界のコードは、次のコード例に示すように書き直すことができます。この例では、ループ境界は可変幅の最大値に設定され、ループ本体は条件付きで実行されます。
#include "ap_cint.h"
#define N 32
typedef int8 din_t;
typedef int13 dout_t;
typedef uint5 dsel_t;
dout_t loop_max_bounds(din_t A[N], dsel_t width) {
dout_t out_accum=0;
dsel_t x;
LOOP_X:for (x=0;x<N; x++) {
if (x<width) {
out_accum += A[x];
}
}
return out_accum;
}
上記の例の for ループ (LOOP_X) は、展開できます。これは、ループに上位境界があり、Vitis HLS でハードウェアがどれくらい作成されるか認識されるからです。RTL デザインのループ本体には、N(32) 個のコピーがあります。このループ本体の各コピーには、それに関する条件付きロジックが含まれ、可変幅の値によって実行されます。
ループのパイプライン処理
ループをパイプライン処理する際は、通常一番内部のループをパイプライン処理すると、エリアとパフォーマンスの最適なバランスがわかります。これにより、ランタイムも最速になります。次のコード例は、ループおよび関数をパイプライン処理した場合のトレードオフを示しています。
#include "loop_pipeline.h"
dout_t loop_pipeline(din_t A[N]) {
int i,j;
static dout_t acc;
LOOP_I:for(i=0; i < 20; i++){
LOOP_J: for(j=0; j < 20; j++){
acc += A[i] * j;
}
}
return acc;
}
一番内側のループ (LOOP_J) がパイプライン処理されると、ハードウェア (単一の乗算器) には LOOP_J のコピーが 1 つできます。Vitis™ HLS では、できるだけループをフラットにするので、この場合は 20x20 の繰り返しの 1 つの新しいループが作成されます。スケジューリングする必要があるのは、乗算器演算 1 つと配列アクセス 1 回のみです。これらをスケジューリングしておくと、ループの繰り返しを 1 つのループ本体のエンティティ (20X20 のループ繰り返し) としてスケジューリングできます。
外側のループ (LOOP_I) がパイプライン処理されると、内部のループ (LOOP_J) が展開され、そのループの本体のコピーが 20 個作成されるので、乗算器 20 個と配列アクセス 20 回をスケジューリングする必要があります。こうしておくと、LOOP_I の各繰り返しを 1 つのエンティティとしてスケジューリングできるようになります。
最上位関数がパイプライン処理される場合は、どちらのループも展開する必要があります。この場合、乗算器 400 個と配列アクセス 400 回をスケジューリングする必要があります。ただし、Vitis HLS で 400 個の乗算器が作成されることはほぼありません。これは、ほとんどのデザインで、データ依存性のために最大の並列処理ができないことがよくあるからです。たとえば、この例の場合、デュアル ポート RAM が A[N] に使用されても、デザインはクロック サイクルの A[N] の 2 つの値にしかアクセスできません。
パイプライン処理する階層レベルを選択すると、たとえば一番内側のループをパイプライン処理したときに、ほとんどのアプリケーションで一般的に許容されるスループットで最小のハードウェアが提供されます。階層の上位をパイプライン処理すると、すべてのサブループが展開されるので、スケジューリングするためにさらに多くの演算が作成されますが (ランタイムとメモリ容量に影響する可能性あり)、スループットとレイテンシの観点から、通常はパフォーマンスの最も高いデザインになります。
上記のオプションをまとめると、次のようになります。
- パイプライン
LOOP_Jレイテンシは約 400 サイクル (20X20) になり、100 個未満の LUT およびレジスタが必要になります (I/O 制御および FSM は常にあります)。
- パイプライン
LOOP_Iレイテンシは約 20 サイクルになりますが、数百個の LUT およびレジスタが必要になります。ロジック数は、最初のオプションの約 20 倍の数からロジック最適化で処理されるロジックを引いた数になります。
- パイプライン関数
loop_pipelineレイテンシは約 10 個 (デュアル ポート アクセス 20 回) になりますが、何千個もの LUT およびレジスタが必要となります。ロジック数は最初のオプションの約 400 倍からロジック最適化で処理されるロジックを引いた数になります。
不完全な入れ子のループ
一番内側のループ階層がパイプライン処理されると、Vitis HLS は、内側のループをフラット化し、ループの遷移 (ループの入出時にループ インデックスで実行されるチェック) によるサイクルを削除することにより、レイテンシを削減してスループット全体を改善します。このようなチェックは、1 つのループから次のループへの遷移の際にクロック遅延を発生させます。
不完全な入れ子のループの場合、またはループをフラット化できない場合、ループの入出のためにクロック サイクルが追加されます。デザインに入れ子のループが含まれる場合は、結果を解析して、なるべく多くのループがフラット化されるようにします。ログ ファイルまたは合成レポートで、ループ ラベルが結合されていること (LOOP_I と LOOP_J が LOOP_I_LOOP_J としてレポートされるなど) を確認してください。
ループの並列処理
Vitis™ HLS では、レイテンシを削減するために、ロジックおよび関数ができるだけ早い段階でスケジューリングされます。これを実行するため、なるべく多くのロジック演算および関数が並列にスケジューリングされますが、ループを並列に実行することはできません。
次のコード例が合成されると、SUM_X ループがスケジューリングされ、その後 SUM_Y ループがスケジューリングされます。SUM_Y ループの開始は SUM_X ループの完了を待つ必要がなくても、SUM_X の後にスケジューリングされます。
#include "loop_sequential.h"
void loop_sequential(din_t A[N], din_t B[N], dout_t X[N], dout_t Y[N],
dsel_t xlimit, dsel_t ylimit) {
dout_t X_accum=0;
dout_t Y_accum=0;
int i,j;
SUM_X:for (i=0;i<xlimit; i++) {
X_accum += A[i];
X[i] = X_accum;
}
SUM_Y:for (i=0;i<ylimit; i++) {
Y_accum += B[i];
Y[i] = Y_accum;
}
}
これらのループには異なる境界 (xlimit および ylimit) があるため、統合はできません。次のコード例のようにループを別の関数に含めると、まったく同じ機能を達成でき、どちらのループも処理できます。
#include "loop_functions.h"
void sub_func(din_t I[N], dout_t O[N], dsel_t limit) {
int i;
dout_t accum=0;
SUM:for (i=0;i<limit; i++) {
accum += I[i];
O[i] = accum;
}
}
void loop_functions(din_t A[N], din_t B[N], dout_t X[N], dout_t Y[N],
dsel_t xlimit, dsel_t ylimit) {
sub_func(A,X,xlimit);
sub_func(B,Y,ylimit);
}
前の例が合成されると、レイテンシはシーケンシャル ループの例の半分になります。これは、ループが関数として並列に実行できるようになったからです。
シーケンシャル ループの例には、dataflow 最適化も使用できます。ここに示す並列処理のため、関数にループを取り込む方法は、dataflow 最適化が使用できない場合に使用します。たとえば、より大型のデザイン例では、dataflow 最適化を最上位のすべてのループ/関数、および各最上位ループおよび関数間に配置されたメモリに適用できます。
ループ依存性
ループ依存性は、ループを最適化されないように (通常はパイプライン処理) するデータ依存性のことです。これらは、ループの 1 回の反復内またはループ内の異なる反復間にできます。
ループ依存性を理解するには、極端な例を見てみるのがわかりやすいです。次の例では、ループの結果がそのループの継続/終了条件として使用されています。次のループを開始するには、前のループの各反復が終了する必要があります。
Minim_Loop: while (a != b) {
if (a > b)
a -= b;
else
b -= a;
}
このループはパイプライン処理できません。ループの前の反復が終了するまで次の反復を開始できないからです。すべてのループ依存性がこのように極端なわけではありませんが、ほかの演算が終了するまで開始できない演算があることに注意してください。ソリューションとしては、最初の演算ができるだけ早い段階で実行されるようにします。
ループ依存性はすべてのデータ型で発生する可能性はありますが、特に配列を使用する場合によく発生します。
C++ クラスのループの展開
ループを C++ クラスで使用する場合、ループ帰納変数がクラスのデータ メンバーにならないように注意する必要があります。ループ帰納変数がクラスのデータ メンバーになると、ループを展開できなくなります。
この例では、ループ帰納変数 k がクラス foo_class のメンバーです。
template <typename T0, typename T1, typename T2, typename T3, int N>
class foo_class {
private:
pe_mac<T0, T1, T2> mac;
public:
T0 areg;
T0 breg;
T2 mreg;
T1 preg;
T0 shift[N];
int k; // Class Member
T0 shift_output;
void exec(T1 *pcout, T0 *dataOut, T1 pcin, T3 coeff, T0 data, int col)
{
Function_label0:;
#pragma HLS inline off
SRL:for (k = N-1; k >= 0; --k) {
#pragma HLS unroll // Loop will fail UNROLL
if (k > 0)
shift[k] = shift[k-1];
else
shift[k] = data;
}
*dataOut = shift_output;
shift_output = shift[N-1];
}
*pcout = mac.exec1(shift[4*col], coeff, pcin);
};
UNROLL プラグマ指示子で指定したように Vitis™ HLS でループを展開できるようにするには、コードを記述し直して k をクラス メンバーからはずす必要があります。
template <typename T0, typename T1, typename T2, typename T3, int N>
class foo_class {
private:
pe_mac<T0, T1, T2> mac;
public:
T0 areg;
T0 breg;
T2 mreg;
T1 preg;
T0 shift[N];
T0 shift_output;
void exec(T1 *pcout, T0 *dataOut, T1 pcin, T3 coeff, T0 data, int col)
{
Function_label0:;
int k; // Local variable
#pragma HLS inline off
SRL:for (k = N-1; k >= 0; --k) {
#pragma HLS unroll // Loop will unroll
if (k > 0)
shift[k] = shift[k-1];
else
shift[k] = data;
}
*dataOut = shift_output;
shift_output = shift[N-1];
}
*pcout = mac.exec1(shift[4*col], coeff, pcin);
};
配列
コーディング スタイルによって合成後の配列のインプリメンテーションがどのように変わるかについて説明する前に、C シミュレーションなど合成が実行される前に発生する可能性のある問題について説明します。
次のようにかなり大きな配列が指定される場合は、C シミュレーションでメモリ不足によりエラーになる可能性があります。
#include "ap_cint.h"
int i, acc;
// Use an arbitrary precision type
int32 la0[10000000], la1[10000000];
for (i=0 ; i < 10000000; i++) {
acc = acc + la0[i] + la1[i];
}
シミュレーションはメモリ不足のためエラーになることがあります。これは、配列がメモリに存在するスタックに配置され、OS で管理されローカル ディスクを使用可能なヒープには配置されないためです。
つまり、デザインを実行したときにメモリ不足になり、次のような状況によって問題が悪化することもあります。
- PC では使用可能なメモリが大型の Linux ボックスよりも少ないことがよくあり、使用可能なメモリが少ないことがあります。
- 任意精度型を使用すると、標準 C 型よりも多くのメモリが必要になるので、この問題が悪化する可能性があります。
- C++ のより複雑な固定小数点任意精度型を使用すると、さらに多くのメモリが必要となるので、メモリ不足になる可能性があります。
C/C++ コード開発のメモリ リソースを向上するには、リンカー オプションを使用してスタックのサイズを増加するのが標準的な方法です。たとえば、-Wl,--stack,10485760 のようにスタック サイズを明示的に設定します。Vitis™ HLS でこれを適用するには、 をクリックするか、次のように Tcl コマンドのオプションとして指定します。
csim_design -ldflags {-Wl,--stack,10485760}
cosim_design -ldflags {-Wl,--stack,10485760}
マシンに十分なメモリがない場合は、スタック サイズを増加しても効果はありません。
次の例のように、シミュレーションにはダイナミック メモリ割り当てを、合成には固定サイズの配列を使用して、問題を回避してください。この場合、こ必要なメモリはヒープに割り当てられ、OS で管理されるので、ローカル ディスク空間が使用できるようになります。
このようなコードへの変更は、シミュレーションされるコードと合成されるコードが異なってしまうため、理想的ではありませんが、デザインを実行するにはこれしか方法がない場合があります。これを実行した場合は、C テストベンチでこの配列にアクセスするすべての点が記述されるようにしてください。これにより、cosim_design で実行される RTL シミュレーションでメモリ アクセスが正しいかどうかが検証されるようになります。
#include "ap_cint.h"
int i, acc;
#ifdef __SYNTHESIS__
// Use an arbitrary precision type & array for synthesis
int32 la0[10000000], la1[10000000];
#else
// Use an arbitrary precision type & dynamic memory for simulation
int32 *la0 = malloc(10000000 * sizeof(int32));
int32 *la1 = malloc(10000000 * sizeof(int32));
#endif
for (i=0 ; i < 10000000; i++) {
acc = acc + la0[i] + la1[i];
}
__SYNTHESIS__ マクロは、合成されるコードにのみ使用します。このマクロは C シミュレーションまたは C RTL 協調シミュレーションには従っていないので、テストベンチには使用しないでください。配列は、通常合成後にメモリ (RAM、ROM、または FIFO) としてインプリメントされます。最上位関数インターフェイスの配列はメモリ外部にアクセスする RTL ポートとして合成されます。デザインに対して内部にある 1024 未満の配列は、SRL に最適化されます。1024 を超える配列は、最適化設定によって内部ブロック RAM、LUTRAM、UltraRAM に合成されます。
ループと同様、配列も簡単なコード構文なので、C プログラムでよく使用されます。また、ループのように、Vitis HLS には多くの最適化が含まれ、コードを修正しなくても RTL のインプリメンテーションを最適化するための指示子が含まれます。
配列により RTL で問題となるのは、次のような場合です。
- 配列アクセスがパフォーマンスの障害となってしまうことがよくあります。メモリとしてインプリメントされると、メモリ ポートの数によりデータへのアクセスが制限されます。配列初期化は、注意して実行しないと、RTL でのリセットおよび初期化が不必要に長くなってしまうことがあります。
- 読み出しアクセスのみを必要とする配列は、RTL では ROM としてインプリメントされるようにする必要があります。
Array[10]; のようなサイズ指定された配列はサポートされますが、Array[]; のようにサイズ指定のない配列はサポートされません。配列アクセスとパフォーマンス
次のコード例では、配列へのアクセスにより最終 RTL デザインでパフォーマンスが制限されます。この例では、配列 mem[N] に 3 回アクセスして合計を作成しています。
#include "array_mem_bottleneck.h"
dout_t array_mem_bottleneck(din_t mem[N]) {
dout_t sum=0;
int i;
SUM_LOOP:for(i=2;i<N;++i)
sum += mem[i] + mem[i-1] + mem[i-2];
return sum;
}
合成中、配列は RAM としてインプリメントされます。RAM をシングル ポート RAM として指定すると、クロック サイクルごとに新しいループ反復を処理するように SUM_LOOP ループをパイプライン処理することは不可能です。
SUM_LOOP を開始間隔 1 でパイプライン処理しようとすると、次のようなメッセージが表示されます。スループット 1 を達成できなかったため、Vitis HLS により制約が緩和されます。
INFO: [SCHED 61] Pipelining loop 'SUM_LOOP'.
WARNING: [SCHED 69] Unable to schedule 'load' operation ('mem_load_2',
bottleneck.c:62) on array 'mem' due to limited memory ports.
INFO: [SCHED 61] Pipelining result: Target II: 1, Final II: 2, Depth: 3.
ここでの問題は、シングル ポート RAM にはシングル データ ポートしかないので、各クロック サイクルで 1 つの読み込み (および 1 つの書き出し) が実行できる点にあります。
- SUM_LOOP サイクル 1:
mem[i]を読み出し - SUM_LOOP サイクル 2:
mem[i-1]を読み出して値を合計 - SUM_LOOP サイクル 3:
mem[i-2]を読み出して値を合計
デュアル ポート RAM も使用できますが、クロック サイクルごとに 2 つのアクセスしか許容されません。合計値を計算するのに 3 つの読み出しが必要なので、クロック サイクルごとに新しい反復でループをパイプライン処理するためには、クロック サイクルごとに 3 つのアクセスが必要になります。
上記の例のコードをスループット 1 でパイプラインができるように変更すると、次のコードになります。次のコード例では、先行読み出しを実行し、データ アクセスを手動でパイプライン処理することで、ループの各反復で指定される配列読み出しが 1 回だけになっています。これにより、パフォーマンスを達成するためには、シングル ポート RAM だけが必要となります。
#include "array_mem_perform.h"
dout_t array_mem_perform(din_t mem[N]) {
din_t tmp0, tmp1, tmp2;
dout_t sum=0;
int i;
tmp0 = mem[0];
tmp1 = mem[1];
SUM_LOOP:for (i = 2; i < N; i++) {
tmp2 = mem[i];
sum += tmp2 + tmp1 + tmp0;
tmp0 = tmp1;
tmp1 = tmp2;
}
return sum;
}
Vitis HLS には、配列のインプリメントおよびアクセス方法を変更できる最適化指示子が多くあります。通常、このような指示子が使用される場合、コードを変更する必要はありません。配列は、ブロックまたは個々の要素に分割できます。Vitis HLS で配列が個々の要素に分割されることもあります。これは、自動分割のコンフィギュレーション設定を使用すると指定できます。
配列が複数のブロックに分割されると、1 つの配列は複数の RTL RAM ブロックとしてインプリメントされます。個々の要素に分割される場合、各要素は RTL でレジスタとしてインプリメントされます。どちらの場合も、分割によりさらに多くの要素に並列アクセスできるようになり、パフォーマンスが向上します。デザインのトレードオフは、パフォーマンスとそれを達成するために必要な RAM またはレジスタの数です。
FIFO アクセス
配列アクセスの特殊なケースに、配列が FIFO としてインプリメントされる場合があります。これは、データフロー最適化を使用する場合によく発生します。
FIFO へのアクセスは、位置 0 から順に実行される必要があります。また、1 つの配列が複数の位置で読み出される場合、コードで FIFO アクセスの順序を厳密に制御する必要があります。通常、ファンアウトが複数の配列は、アクセス順を指定するコードを追加しないと FIFO としてインプリメントできません。
インターフェイスの配列
Vivado IP フローでは、Vitis HLS により配列がデフォルトでメモリ エレメントに合成されます。配列を最上位関数への引数として使用した場合、Vitis HLS では次が想定されます。
- メモリはオフチップ。
Vitis HLS では、メモリにアクセスするためにインターフェイス ポートを合成。
- メモリはレイテンシ 1 の標準ブロック RAM。
データはアドレスの供給後 1 クロック サイクルで準備完了。
Vitis HLS でのこれらのポートの作成方法は、次のように設定します。
- INTERFACE プラグマまたは指示子を使用して、インターフェイスを RAM または FIFO インターフェイスとして指定します。
- INTERFACE プラグマまたは指示子の
storage_typeを使用して、RAM をシングル ポートまたはデュアル ポート RAM として指定します。 - INTERFACE プラグマまたは指示子の
latencyオプションを使用して、RAM のレイテンシを指定します。 - 配列最適化指示子 (ARRAY_PARTITION または ARRAY_RESHAPE) を使用すると、配列の構造を設定し直せ、I/O ポートの数も変更できます。
d_i[4] を d_i[] に変更すると、Vitis HLS でデザインが合成できないことを示すメッセージが表示されます。@E [SYNCHK-61] array_RAM.c:52: unsupported memory access on variable 'd_i' which is (or contains) an array with unknown size at compile time.配列インターフェイス
INTERFACE プラグマまたは指示子を使用すると、storage_type=<value> オプションで使用される RAM または ROM のタイプを明示的に指定できます。これにより、シングル ポートまたはデュアル ポートのどちらを作成するかを定義できます。storage_type を指定しない場合、Vitis HLS で次が使用されます。
- シングル ポート RAM (デフォルト)。
- 開始間隔やレイテンシが削減される場合はデュアル ポート RAM。
ARRAY_PARTITION および ARRAY_RESHAPE プラグマを使用すると、インターフェイスの配列をリコンフィギュレーションできます。配列は複数の小型の配列に分割でき、それぞれに別のインターフェイスを使用できます。これには、配列のすべての要素をスカラー要素に分割する機能も含まれます。関数インターフェイスの場合は、配列のすべての要素に対してそれぞれ別のポートが作成されます。これにより並列アクセスは最大になりますが、さらにポートが作成されるため、インプリメンテーション中に配線問題が発生することがあります。
小さい配列も 1 つの大きな配列にまとめられ、1 つのインターフェイスになることがあります。この場合、オフチップ ブロック RAM へのマップは改善されますが、パフォーマンスに障害が出る可能性があることに注意してください。これらのトレードオフは Vitis HLS の最適化指示子を使用すると発生するので、関数のコード記述には影響しません。
次のコード例に示す関数の配列引数は、デフォルトでシングル ポート RAM インターフェイスに合成されます。
#include "array_RAM.h"
void array_RAM (dout_t d_o[4], din_t d_i[4], didx_t idx[4]) {
int i;
For_Loop: for (i=0;i<4;i++) {
d_o[i] = d_i[idx[i]];
}
}
シングル ポート RAM インターフェイスが使用されるのは、for-loop により各クロック サイクルで読み出しおよび書き込みできるのは 1 要素のみであるため、デュアル ポート RAM インターフェイスを使用する利点がないからです。
for-loop が展開されると、Vitis HLS ではデュアル ポート RAM が使用されます。これにより、同時に複数の要素を読み出すことができ、開始間隔を改善できます。RAM インターフェイスのタイプは、INTERFACE プラグマまたは指示子を適用し、storage_type を指定すると明示的に設定できます。
インターフェイスで配列に関する問題がある場合は、通常スループットに関係しており、最適化指示子で処理できます。たとえば、上記の例の配列が個々の要素に分割され、for-loop が展開されていれば、各配列の 4 つの要素すべてが同時にアクセスされます。
また、INTERFACE プラグマまたは指示子で latency=<value> オプションを使用すると、RAM のレイテンシを指定できます。これにより、Vitis HLS によりインターフェイスで 1 を超えるレイテンシの外部 SPRAM をモデリングできます。
FIFO インターフェイス
Vitis HLS では、配列引数を RTL で FIFO ポートとしてインプリメントできます。FIFO ポートを使用する場合は、配列のアクセスがシーケンシャルであることを確認してください。Vitis HLS では、アクセスがシーケンシャルであるかどうかが判断されます。
| シーケンシャル アクセス | Vitis HLS の動作 |
|---|---|
| ○ | FIFO ポートをインプリメントします。 |
| × |
|
| 不定 |
|
次のコード例では、Vitis HLS でアクセスがシーケンシャルかどうかを判断できません。この例では、d_i と d_o の両方が合成中に FIFO インターフェイスを使用してインプリメントされるように指定されています。
#include "array_FIFO.h"
void array_FIFO (dout_t d_o[4], din_t d_i[4], didx_t idx[4]) {
int i;
#pragma HLS INTERFACE ap_fifo port=d_i
#pragma HLS INTERFACE ap_fifo port=d_o
// Breaks FIFO interface d_o[3] = d_i[2];
For_Loop: for (i=0;i<4;i++) {
d_o[i] = d_i[idx[i]];
}
}
この場合、変数 idx の動作により FIFO インターフェイスが正しく作成できるかどうかが決まります。
idxがシーケンシャルに増加すれば、FIFO インターフェイスを作成できます。idxにランダムな値が使用されると、FIFO インターフェイスが RTL にインプリメントされる際にエラーになります。
このインターフェイスは機能しない可能性があるので、Vitis HLS で合成中に次のようなメッセージが表示され、FIFO インターフェイスが作成されます。
@W [XFORM-124] Array 'd_i': may have improper streaming access(es).
//Breaks FIFO
interface を削除し、d_o[3] = d_i[2]; をコメントにせずに残すと、Vitis HLS で配列へのアクセスがシーケンシャルではないと判断され、FIFO インターフェイスが指定されている場合はエラー メッセージが表示されて停止します。次の一般的な規則は、FIFO インターフェイスではなくストリーミング インターフェイスでインプリメントされる配列に適用されます。
- 配列は、1 ループまたは関数でのみ読み出され、書き込まれる必要があります。これは FIFO リンクの特性と一致するポイント ツー ポイントの接続に変換される可能性があります。
- 配列の読み出しは、配列の書き込みと同じ順序で実行される必要があります。FIFO チャネルではランダム アクセスがサポートされないので、配列は先入れ先出し (First In First Out) 動作に従ったプログラムで使用される必要があります。
- FIFO からの読み出しおよび書き込みに使用されるインデックスは、コンパイル時に解析される必要があります。ランタイム計測に基づいた配列のアドレス指定は、FIFO 動作用には解析できないので、配列が FIFO に変換されなくなります。
最上位インターフェイスに配列をインプリメントまたは最適化するのに、コードを変更する必要は通常ありません。インターフェイスの配列のコードを変更する必要があるのは、配列が構造体 (struct) の一部である場合のみです。
配列の初期化
次のコード例では、配列は値のセットを使用して初期化されます。関数が実行されるたびに、coeff 配列にこれらの値が代入されます。合成後、coeff をインプリメントする RAM が実行されるたびに、これらの値が読み込まれます。シングル ポート RAM の場合は、8 クロック サイクルかかります。1024 の配列の場合、当然 1024 クロック サイクルかかります。この間、coeff によっては演算は実行されません。
int coeff[8] = {-2, 8, -4, 10, 14, 10, -4, 8, -2};
次のコードでは、static 修飾子を使用して coeff 配列を定義しています。配列は実行開始時に指定した値で初期化されます。関数が実行されるたびに、coeff 配列には前の実行からの値が記録されます。C コードでは、static 配列はメモリが RTL で動作するように動作します。
static int coeff[8] = {-2, 8, -4, 10, 14, 10, -4, 8, -2};
また、変数に static 修飾子が含まれる場合、Vitis HLS は RTL デザインおよび FPGA ビットストリームの変数を初期化します。これにより、メモリを初期化するのに複数クロック サイクルも必要なくなり、大容量メモリの初期化が使用オーバーヘッドにならなくなります。
リセットを適用した後に static 変数をその初期ステートに戻す (デフォルトではない) かどうかは、RTL コンフィギュレーション コマンドで指定できます。メモリをリセット後に初期ステートに戻す場合、これが演算上のオーバーヘッドとなり、値をリセットするのに複数サイクル必要となり、値を各メモリ アドレスに書き込む必要があります。
ROM のインプリメント
Vitis HLS では、メモリを合成するために配列に static 修飾子を指定したり、メモリを ROM に推論するために const 修飾子を使用したりすることは必須ではありません。Vitis HLS は、デザインを解析して最適なハードウェアを作成することを試みます。
static 修飾子を使用することをお勧めします。配列の初期化 で説明したように、static 型は RTL でメモリとほぼ同じように動作します。const 修飾子を使用することもお勧めします。ROM の自動推論の一般的な規則は、ローカルの (グローバルでない) static 配列は読み出しの前に書き込まれるようにするということです。次の例のように指定すると、ROM が推論されやすくなります。 - 配列をそれを使用する関数でできるだけ早い段階で初期化します。
- 書き込みをまとめます。
array(ROM)初期化書き込みを初期化コード以外のコードとインターリーブしないようにします。- 異なる値を同じ配列要素に格納しないようにします (すべての書き込みをコード内でグループ化)。
- 要素値の計算は、初期化ループ カウンター変数を除いて、定数以外 (コンパイル時) のデザイン変数に依存しないようにします。
ROM を初期化するのに複雑な代入が使用される場合 (math.h ライブラリからの関数など)、配列初期化を別の関数に配置すると、ROM が推論されるようになります。次の例では、配列 sin_table[256] がメモリとして推論され、RTL 合成後は ROM としてインプリメントされます。
#include "array_ROM_math_init.h"
#include <math.h>
void init_sin_table(din1_t sin_table[256])
{
int i;
for (i = 0; i < 256; i++) {
dint_t real_val = sin(M_PI * (dint_t)(i - 128) / 256.0);
sin_table[i] = (din1_t)(32768.0 * real_val);
}
}
dout_t array_ROM_math_init(din1_t inval, din2_t idx)
{
short sin_table[256];
init_sin_table(sin_table);
return (int)inval * (int)sin_table[idx];
}
sin() 関数の結果は定数値なので、sin() 関数をインプリメントするのに RTL デザインにコアは必要ありません。データ型
実行ファイルへコンパイルされる C 関数で使用されるデータ型は、結果の精度、メモリ要件、パフォーマンスに影響します。
- 32 ビット整数の int 型には、さらに多くのデータを保持できるので、8 ビットの char 型よりも精度が高くなりますが、さらに多くのストレージが必要となります。
- 64 ビットの
long long型が 32 ビット システムで使用されると、このような値を読み出しおよび書き込みするために通常複数アクセスが必要になるので、ランタイムに影響します。
同様に、C 関数が RTL インプリメンテーションに合成される場合も、データ型が RTL デザインの精度、エリア、パフォーマンスに影響します。変数に使用されるデータ型により、必要な演算子のサイズが決まるので、RTL のエリアおよびパフォーマンスも決まります。
Vitis HLS では、固定長整数型を含むすべての標準 C データ型の合成がサポートされます。
(unsigned) char、(unsigned) short、(unsigned) int(unsigned) long、(unsigned) long long(unsigned) intN_t(Nはstdint.hで定義される 8、16、32 および 64)float、double
固定長整数型を使用すると、デザインをシステムのすべてのデータ型間で移植できます。
C 規格では、整数型 (unsigned)long で 64 ビットが 64 ビット OS、32 ビットが 32 ビット OS としてインプリメントされます。合成ではこの動作に合わせて、Vitis HLS が実行される OS タイプによって、異なるサイズの演算子が生成されるので、異なる RTL デザインが生成されます。Windows OS の場合、Microsoft 社により OS に関係なく long 型が 32 ビットに定義されます。
- 32 ビットの場合、
(unsigned)long型の代わりに、((unsigned)intまたは(unsigned)int32_tを使用します。 - 64 ビットの場合、
(unsigned)long型の代わりに、((unsigned)long longまたは(unsigned)int64_tを使用します。
-m32 を使用すると、コードが C シミュレーション用にコンパイルされ、32 ビット アーキテクチャの仕様に合成でき、long 型が 32 ビット値でインプリメントされます。このオプションは、add_files コマンドに -CFLAGS オプションを使用して適用します。ザイリンクスでは、すべての変数のデータ型を 1 つの共通のヘッダー ファイルで定義することをお勧めしています。このファイルは、すべてのソース ファイルに含めることができます。
- 通常の Vitis HLS プロジェクト フロー中には、たとえばサイズを削減したり、ハードウェア インプリメンテーションをより効率的にできるようにするために、変更可能なデータ型があります。
- 抽象度の高いレベルで変更をしておく利点の 1 つは、新しいデザイン インプリメンテーションをすばやく作成できる点にあります。通常同じファイルが後のプロジェクトで使用されますが、別の (より小型、より大型、またはより正確な) データ型を使用することもできます。
これらのタスクはどちらも、データ型が 1 つの箇所で変更できる場合は、より簡単に達成できます。または、複数ファイルを編集します。
_TYPES_H という名前のマクロがヘッダー ファイルで定義されている場合、よくある名前なのでほかのファイルで定義されている可能性があり、ほかのコードをイネーブルまたはディスエーブルしてしまい、予期しない問題が発生することもあります。任意精度 (AP) 型
ネイティブ C データ型は、8 ビット境界 (8、16、32、64 ビット) に基づきますが、RTL バス (ハードウェアに対応) では、任意精度のデータ長がサポートされます。標準 C データ型を使用すると、効率の悪いハードウェア インプリメンテーションになることがあります。たとえば、FPGA での基本的な乗算単位は DSP モジュールなので、18*18 ビットの乗算器が提供されます。17 ビットの乗算が必要な場合は、32 ビットの C データ型でこれをインプリメントしないようにしてください。32 ビットの C データ型の場合、乗算器をインプリメントするのに DSP48 マクロが 1 つで十分なのに 3 つも必要となるからです。
任意精度 (AP) 型を使用すると、コードでビット幅の狭い変数を使用でき、C シミュレーションでその機能が同じであるか、許容できることを検証できます。ビット幅が狭い方が小型で高速なハードウェア演算になります。これにより、より多くのロジックを FPGA に配置できるようになり、より高速のクロック周波数で実行できるようになります。
AP 型は C++ 用に提供されており、1 ~ 1024 ビットまでの幅のデータ型を記述できます。任意精度型ライブラリ で説明するように、C++ ソース コードに AP ライブラリを使用することを指定する必要があります。
AP の例
たとえば、通信プロトコル用のフィルター関数を含むデザインがデータ送信要件を満たすのに 10 ビットの入力データと 18 ビットの出力データを必要とする場合、標準 C データ型を使用すると、入力データが少なくとも 16 ビット、出力データが少なくとも 32 ビット必要です。最終的なハードウェアでは、これにより入力と出力の間に必要よりもデータ幅が広いデータパスが作成されるので、リソースがより多く使用され、遅延も長くなり (32 ビット x 32 ビットの乗算は 18 ビット x 18 ビットの乗算よりも長くかかる)、完了するのにさらに多くのクロック サイクルが必要になります。
このデザインで任意精度型を使用すると、合成前にコードに必要なビット サイズを正確に指定して、アップデートされたコードをシミュレーションして、合成前に結果を検証できます。
AP 型の利点
次のコードは、基本的な算術演算を実行します。
#include "types.h"
void apint_arith(dinA_t inA, dinB_t inB, dinC_t inC, dinD_t inD,
dout1_t *out1, dout2_t *out2, dout3_t *out3, dout4_t *out4
) {
// Basic arithmetic operations
*out1 = inA * inB;
*out2 = inB + inA;
*out3 = inC / inA;
*out4 = inD % inA;
}
dinA_t や dinB_t
などのデータ型は、ヘッダー ファイル types.h で定義されます。types.h などのプロジェクト全体に適用されるヘッダー ファイルを使用すると、標準 C
型から任意精度型に簡単に移行でき、任意精度型を最適なサイズに調整できるようになります。
上記の例のデータ型は次のように定義されているとします。
typedef char dinA_t;
typedef short dinB_t;
typedef int dinC_t;
typedef long long dinD_t;
typedef int dout1_t;
typedef unsigned int dout2_t;
typedef int32_t dout3_t;
typedef int64_t dout4_t;
この場合、合成後の結果は次のようになります。
+ Timing (ns):
* Summary:
+---------+-------+----------+------------+
| Clock | Target| Estimated| Uncertainty|
+---------+-------+----------+------------+
|default | 4.00| 3.85| 0.50|
+---------+-------+----------+------------+
+ Latency (clock cycles):
* Summary:
+-----+-----+-----+-----+---------+
| Latency | Interval | Pipeline|
| min | max | min | max | Type |
+-----+-----+-----+-----+---------+
| 66| 66| 67| 67| none |
+-----+-----+-----+-----+---------+
* Summary:
+-----------------+---------+-------+--------+--------+
| Name | BRAM_18K| DSP48E| FF | LUT |
+-----------------+---------+-------+--------+--------+
|Expression | -| -| 0| 17|
|FIFO | -| -| -| -|
|Instance | -| 1| 17920| 17152|
|Memory | -| -| -| -|
|Multiplexer | -| -| -| -|
|Register | -| -| 7| -|
+-----------------+---------+-------+--------+--------+
|Total | 0| 1| 17927| 17169|
+-----------------+---------+-------+--------+--------+
|Available | 650| 600| 202800| 101400|
+-----------------+---------+-------+--------+--------+
|Utilization (%) | 0| ~0 | 8| 16|
+-----------------+---------+-------+--------+--------+
ただし、次のような例のように、データ幅をインプリメントするのに標準 C 型を使用する必要がなく、一部の幅は狭いが、次に小さい標準 C 型よりは広い場合があるとします。
typedef int6 dinA_t;
typedef int12 dinB_t;
typedef int22 dinC_t;
typedef int33 dinD_t;
typedef int18 dout1_t;
typedef uint13 dout2_t;
typedef int22 dout3_t;
typedef int6 dout4_t;
合成結果では、最大クロック周波数とレイテンシが改善し、エリア使用量が 75% 削減されています。
+ Timing (ns):
* Summary:
+---------+-------+----------+------------+
| Clock | Target| Estimated| Uncertainty|
+---------+-------+----------+------------+
|default | 4.00| 3.49| 0.50|
+---------+-------+----------+------------+
+ Latency (clock cycles):
* Summary:
+-----+-----+-----+-----+---------+
| Latency | Interval | Pipeline|
| min | max | min | max | Type |
+-----+-----+-----+-----+---------+
| 35| 35| 36| 36| none |
+-----+-----+-----+-----+---------+
* Summary:
+-----------------+---------+-------+--------+--------+
| Name | BRAM_18K| DSP48E| FF | LUT |
+-----------------+---------+-------+--------+--------+
|Expression | -| -| 0| 13|
|FIFO | -| -| -| -|
|Instance | -| 1| 4764| 4560|
|Memory | -| -| -| -|
|Multiplexer | -| -| -| -|
|Register | -| -| 6| -|
+-----------------+---------+-------+--------+--------+
|Total | 0| 1| 4770| 4573|
+-----------------+---------+-------+--------+--------+
|Available | 650| 600| 202800| 101400|
+-----------------+---------+-------+--------+--------+
|Utilization (%) | 0| ~0 | 2| 4|
+-----------------+---------+-------+--------+--------+
2 つのデザイン間でレイテンシが大きく異なるのは、除算および余りの計算に複数サイクルかかるからです。デザインを標準 C 型にフィットさせるよりも、AP 型を使用した方が、同じ精度でパフォーマンスが高く、リソース使用量の少ない高品質のハードウェア インプリメンテーションが得られます。
任意精度整数型の概要
Vitis HLS では、C++ の整数および固定小数点の任意精度型が提供されています。
| 言語 | 整数型 | 必要なヘッダー |
|---|---|---|
| C++ | ap_[u]int<W> (1024 ビット) 32K ビット幅までに拡張可能。 |
#include "ap_int.h" |
| C++ | ap_[u]fixed<W,I,Q,O,N> | #include "ap_fixed.h" |
Vitis HLS には、任意精度型を定義するヘッダー ファイルもスタンドアロン パッケージとして含まれており、ソース コードで使用できるようになっています。xilinx_hls_lib_<release_number>.tgz パッケージは、Vitis HLS インストール ディレクトリの include ディレクトリに含まれます。パッケージには、ap_cint.h で定義された C 任意精度型は含まれません。これらのデータ型は、標準 C コンパイラとは一緒に使用できず、Vitis HLS でのみ使用できるようになっています。
C++ 言語での任意精度型
C 言語では、ヘッダー ファイル ap_int.h で任意精度の整数型 ap_[u]int が定義されます。C++ 関数で任意精度の整数型を使用するには、次の手順に従います。
- ソース コードにヘッダー ファイル ap_int.h を追加します。
- ビット型を、
ap_int<N>またはap_uint<N>(N はビット サイズを表す 1 ~ 1024 の値) に変更します。
次の例に、ヘッダー ファイルの追加方法と、2 つの変数を 9 ビット整数型および 10 ビットの符号なし整数型を使用してインプリメントする方法を示します。
#include "ap_int.h"
void foo_top (
) {
ap_int<9> var1; // 9-bit
ap_uint<10> var2; // 10-bit unsigned
ap_[u]int 型に使用可能なデフォルトの最大幅は 1024 ビットです。このデフォルトは、ap_int.h ヘッダー ファイルを含める前に、32768 以下の正の整数値でマクロ AP_INT_MAX_W を定義すると上書きできます。
AP_INT_MAX_W の値を大きくしすぎると、ソフトウェアのコンパイルおよび実行に時間がかかる可能性があります。APFixed: を使用すると、ROM 合成に時間がかかることがあります。int に変更すると、合成は速くなります。次に例を示します。 static ap_fixed<32> a[32][depth] =
次のように変更できます。
static int a[32][depth] =
次に、AP_INT_MAX_W を上書きする例を示します。
#define AP_INT_MAX_W 4096 // Must be defined before next line
#include "ap_int.h"
ap_int<4096> very_wide_var;
任意精度固定小数点型の概要
固定小数点型では、データが整数ビットおよび小数ビットとして記述されます。次の例では、Vitis HLS の ap_fixed 型を使用して、18 ビット変数 (6 ビットが整数部、12 ビットが小数部) を定義しています。変数は、符号付きとして指定され、量子化モードは正の無限大の方向に丸めらるように設定されています。オーバーフロー モードは指定されていないので、オーバーフローにはデフォルトの折り返しモードが使用されます。
#include <ap_fixed.h>
...
ap_fixed<18,6,AP_RND > my_type;
...
ビット数または精度が異なる変数を含む計算を実行する場合は、2 進小数点が自動的に揃えられます。
固定小数点を使用して実行された C++ シミュレーションの動作は、最終的なハードウェアと同じになります。これにより、ビット精度、量子化、およびオーバーフロー ビヘイビアーを高速の C レベル シミュレーションで解析できるようになります。
固定小数点型は、終了するのに多くのクロック サイクルを必要とする浮動小数点型の代わりに使用できます。浮動小数点型の範囲がすべて必要な場合を除き、固定小数点型で同じ精度をインプリメントでき、より小型で高速なハードウェアにできることがよくあります。
次の表に、ap_fixed 型の識別子を示します。
| 識別子 | 説明 | ||
|---|---|---|---|
| W | ワード長のビット数 | ||
| I | 整数値をビット数で指定 (2 進小数点より上位のビット数) | ||
| Q |
量子化モード 結果の保存に使用される変数の最小の小数ビットで定義できるよりも大きい精度が生成された場合の動作を指定します。 |
||
| ap_fixed 型 | 説明 | ||
| AP_RND | 正の無限大への丸め | ||
| AP_RND_ZERO | 0 への丸め | ||
| AP_RND_MIN_INF | 負の無限大への丸め | ||
| AP_RND_INF | 無限大への丸め | ||
| AP_RND_CONV | 収束丸め | ||
| AP_TRN | 負の無限大への切り捨て (デフォルト) | ||
| AP_TRN_ZERO | 0 への切り捨て | ||
| O |
オーバーフロー モード。 演算結果が結果の変数に格納可能な最大値 (負の数値の場合は最小値) を超える場合の動作を指定します。 |
||
| ap_fixed 型 | 説明 | ||
| AP_SAT | 飽和 | ||
| AP_SAT_ZERO | 0 への飽和 | ||
| AP_SAT_SYM | 対称飽和 | ||
| AP_WRAP | 折り返し (デフォルト) | ||
| AP_WRAP_SM | 符号絶対値の折り返し | ||
| N | オーバーフロー折り返しモードでの飽和ビット数を定義。 | ||
ap_[u]fixed 型に使用可能なデフォルトの最大幅は 1024 ビットです。このデフォルトは、ap_int.h ヘッダー ファイルを含める前に、32768 以下の正の整数値でマクロ AP_INT_MAX_W を定義すると上書きできます。
AP_INT_MAX_W の値を大きくしすぎると、ソフトウェアのコンパイルおよび実行に時間がかかる可能性があります。static APFixed_2_2 CAcode_sat[32][CACODE_LEN] =
を使用すると ROM 合成に時間がかかることがあります。APFixed を int (static int CAcode_sat[32][CACODE_LEN] =) に変更すると、合成が速くなります。次に、AP_INT_MAX_W を上書きする例を示します。
#define AP_INT_MAX_W 4096 // Must be defined before next line
#include "ap_fixed.h"
ap_fixed<4096> very_wide_var;
Vitis HLS を使用する場合は、任意精度型の使用をお勧めします。前の例で示したように、任意精度型を使用すると、ハードウェア インプリメンテーションの質がかなり向上するという利点があります。
標準データ型
次のコード例に、基本的ないくつかの算術演算の実行を示します。
#include "types_standard.h"
void types_standard(din_A inA, din_B inB, din_C inC, din_D inD,
dout_1 *out1, dout_2 *out2, dout_3 *out3, dout_4 *out4
) {
// Basic arithmetic operations
*out1 = inA * inB;
*out2 = inB + inA;
*out3 = inC / inA;
*out4 = inD % inA;
}
上記の例のデータ型は、次のコード例に示すヘッダー ファイル types_standard.h で定義されています。これには、次のデータ型をどのように使用できるかが示されています。
- 標準符号付き型
- 符号なし型
- 固定長整数型 (
stdint.hヘッダー ファイルを含有)#include <stdio.h> #include <stdint.h> #define N 9 typedef char din_A; typedef short din_B; typedef int din_C; typedef long long din_D; typedef int dout_1; typedef unsigned char dout_2; typedef int32_t dout_3; typedef int64_t dout_4; void types_standard(din_A inA,din_B inB,din_C inC,din_D inD,dout_1 *out1,dout_2 *out2,dout_3 *out3,dout_4 *out4);
これらのデータ型は、合成後、次の演算子およびポート サイズになります。
out1結果を計算するために使用される乗算器は 24 ビット乗算器です。これは、8 ビットのchar型を 16 ビットのshortで乗算するには、24 ビット乗算器が必要だからです。結果は、出力ポート幅と一致するように 32 ビットまで符号拡張されます。out2に使用される加算器は 8 ビットです。出力が 8 ビットのunsigned char型なので、inB(16 ビットのshort) の下位 8 ビットのみが 8 ビットのchar型のinAに追加されます。out3(32 ビットの固定幅型) 出力では、8 ビットのchar型のinAが 32 ビット値に拡張され、32 ビット (int型) のinC入力を使用して 32 ビットの除算演算が実行されます。- 64 ビット モジュールの演算は 64 ビットの
long long型inDと 64 ビットに符号拡張された 8 ビットのchar型inAを使用して実行され、64 ビットの出力結果out4が作成されます。
out1 結果が示すように、Vitis HLS では可能な限り最小の演算子が使用され、必要な出力ビット幅に一致するように結果が拡張されます。結果 out2 では、入力の 1 つが 16 ビットですが、必要なのは 8 ビット出力なので、8 ビット加算器を使用できます。out3 および out4 結果が示すように、すべてのビットが必要であれば、フルサイズの演算子が合成されます。
float および double 型
Vitis HLS では、合成で float および double 型がサポートされます。どちらのデータ型も IEEE-754 規格に従って合成されます。
- 単精度 32 ビット
- 仮数部 24 ビット
- 指数部 8 ビット
- 単精度 64 ビット
- 仮数部 53 ビット
- 指数部 11 ビット
float 型および double 型は、標準演算 (+、-、* など) だけでなく、math.h (C++ の場合は cmath.h) にもよく使用されます。このセクションでは、標準演算のサポートについて説明されます。
次に、標準データ型 で使用されたヘッダー ファイルに double 型および float 型を定義したコード例を示します。
#include <stdio.h>
#include <stdint.h>
#include <math.h>
#define N 9
typedef double din_A;
typedef double din_B;
typedef double din_C;
typedef float din_D;
typedef double dout_1;
typedef double dout_2;
typedef double dout_3;
typedef float dout_4;
void types_float_double(din_A inA,din_B inB,din_C inC,din_D inD,dout_1
*out1,dout_2 *out2,dout_3 *out3,dout_4 *out4);
このアップデートされたヘッダー ファイルは、sqrtf() 関数が使用される次のコード例で使用します。
#include "types_float_double.h"
void types_float_double(
din_A inA,
din_B inB,
din_C inC,
din_D inD,
dout_1 *out1,
dout_2 *out2,
dout_3 *out3,
dout_4 *out4
) {
// Basic arithmetic & math.h sqrtf()
*out1 = inA * inB;
*out2 = inB + inA;
*out3 = inC / inA;
*out4 = sqrtf(inD);
}
上記の例を合成すると、64 ビットの倍精度乗算、加算、除算演算子が作成前述の例されます。これらの演算子は、適切な浮動小数点のザイリンクス IP カタログ コアでインプリメントされます。
sqrtf() を使用する平方根は、32 ビットの単精度浮動小数点コアを使用してインプリメントされます。
倍精度の平方根関数 sqrt() が使用されると、inD で使用される 32 ビットの単精度浮動単型の型変換のためにロジックが追加されます。sqrtf() は単精度 (float) 関数ですが、out4:
sqrt() は倍精度 (double) 関数です。
C 関数では、float-to-double および double-to-float 変換ユニットがハードウェアで推論されるので、float 型と double 型を混合する場合には注意が必要です。
float foo_f = 3.1459;
float var_f = sqrt(foo_f);
上記のコードは、次のようなハードウェアになります。
wire(foo_t)
-> Float-to-Double Converter unit
-> Double-Precision Square Root unit
-> Double-to-Float Converter unit
-> wire (var_f)
sqrtf() 関数を使用すると、次のようになります。
- ハードウェアで型コンバーターが不要になる。
- エリアが節約される。
- タイミングが改善される。
float および double 型を合成する際には、Vitis HLS が C コードで実行される演算順序を維持して、C シミュレーションと結果が同じになるようにします。飽和および切り捨てのため、次は単精度および倍精度演算で必ずしも同じになるわけではありません。
A=B*C; A=B*F;
D=E*F; D=E*C;
O1=A*D O2=A*D;
float および double 型を使用する場合、O1 と O2 は必ずしも同じになるわけではありません。
C++ デザインの場合は、Vitis HLS の最もよく使用される数学関数のビットを概算する機能を使用できます。
任意精度型
Vitis HLS では、任意精度 (AP) 型 で説明するように、任意精度型が提供されています。
複合型
Vitis HLS では、合成で複合型がサポートされます。
構造体 (struct)
インターフェイス上の構造体はデフォルトでまとめられます。内部変数およびグローバル変数などコード内の構造体はデフォルトで分割され、メンバー要素に分けられます。作成される要素の数とタイプは、その構造体の内容によって決まります。構造体の配列は、複数の配列 (構造体の各メンバーに別々の配列) としてインプリメントされます。
または、AGGREGATE プラグマや指示子を使用して、構造体のすべての要素を 1 つの幅の広いベクターにまとめるともできます。これにより、構造体のすべてのメンバーを同時に読み出しおよび書き込みできます。集められた構造体は、構造体のパディングとアライメント に説明するように、4 バイト境界に要素を揃えるため、必要に応じてパディングされます。構造体のメンバー要素は C コードに記述されている順序でベクターに配置されます (構造体の最初の要素がベクターの LSB に、最後の要素がベクターの MSB に配置される)。構造体の配列は個別の配列要素に分割され、下位から上位の順のベクターに配置されます。
int 型の要素が 4096 個含まれる場合、ベクター (およびポート) の幅は 4096*32=131072 ビットになります。Vitis HLS でこの RTL デザインは作成できますが、FPGA インプリメンテーション中に論理合成でこれが配線できることはほとんどありません。単一幅のベクターを AGGREGATE 指示子を使用して作成した場合、1 クロック サイクルでより多くのデータにアクセスできるようになります。データが 1 クロック サイクルでアクセスできると、このデータを消費するループを展開してスループットが改善される場合にのみ、Vitis HLS でこれらのループが自動的に展開されます。ループを完全または部分展開すると、1 クロック サイクルでこれらの追加データを消費するのに十分なハードウェアを作成できます。この機能は config_unroll コマンドと tripcount_threshold オプションを使用して制御されます。次の例では、展開するとスループットが改善される場合にのみトリップカウントが 16 未満のループが自動的に展開されます。
config_unroll -tripcount_threshold 16
構造体に配列が含まれる場合、AGGREGATE 指示子により ARRAY_RESHAPE プラグマと同様の処理が実行され、再形成された配列が構造体内のほかの要素とまとめられます。ただし、構造体は AGGREGATE を使用して最適化した後に分割または再形成することはできません。AGGREGATE、ARRAY_PARTITION、および ARRAY_RESHAPE 指示子は一緒には使用できません。
構造体のパディングとアライメント
Vitis HLS の構造体では、__attributes__ または #pragmas を使用して異なるタイプのパディングおよびアライメントを設定できます。次に、これらの機能を示します。
- 分割
-
デフォルトでは、コード内の内部変数としての構造体は分割されます。カーネル インターフェイス上で定義された構造体は分割されません。set_directive_disaggregate または pragma HLS disaggregate に説明されているように、分割された構造体は個々の要素に分けられます。これが構造体のデフォルトなので、DISAGGREGATE プラグマまたは指示子を適用する必要はありません。
図 1: 分割された構造体 - 集約
-
インターフェイスの構造体のデフォルトです (インターフェイス合成および構造体を参照)。Vitis HLS で構造体の要素がまとめられ、1 つのデータ ユニットに統合されます。これは pragma HLS aggregate に従って実行されます。これがインターフェイスの構造体のデフォルトなので、このプラグマを指定する必要はありません。集約プロセスでは、バイト構造をデフォルトの 4 バイト アライメントに揃えるため、要素がパディングされることがあります。
注記:-Wpaddedをコンパイラ フラグとして指定すると、構造体をパディングするためビットが追加された場合に警告が表示されます。 - アライメント
-
デフォルトでは、Vitis HLS で構造体の要素がパディングされ、4 バイト (32 ビット幅) に揃えられます。
__attribute__((aligned(X)))(X はバイト境界) を使用してバイト境界を指定し、構造体の要素をパディングすることもできます。次の図では、構造体を 2 バイト境界に揃えています。重要: X は、2 のべき乗で指定する必要があります。図 2: アライメントされた構造体のインプリメンテーション 使用されるパディングは、構造体の要素の順序とサイズによって異なります。次のコード例では、構造体が 4 バイトに揃えられ、Vitis HLS で最初の要素varAの後に 2 バイトのパディングが追加され、3 番目の要素varCの後に 2 バイトのパディングが追加されます。構造体の合計サイズは 96 ビットになります。struct data_t { short varA; int varB; short varC; };構造体を次のように記述し直すと、パディングは必要なくなり、構造体の合計サイズは 64 ビットになります。struct data_t { short varA; short varC; int varB; }; - パッキング
-
Vitis HLS で構造体が各要素の実際のサイズに基づいてパックされます。
__attribute__(packed(X))を使用して指定します。次の例では、構造体のサイズは 72 ビットになります。図 3: パッキングされた構造体のインプリメンテーション
struct __attribute__((packed)) data_t {
short varA;
int varB;
short varC;
};
任意精度型のデータ レイアウト (ap_int ライブラリ)
ap_int などのカスタム データ幅を持つ構造体内のデータ型は、2 のべき乗のサイズで割り当てられます。Vitis HLS は、データ型のサイズを 2 のべき乗に揃えるためにパディング ビットを追加します。
次の例では、サイズ varA の構造体が 5 ビットではなく 8 ビットにパディングされます。
struct example {
ap_int<5> varA;
unsigned short int varB;
unsigned short int varC;
int d;
};
bool 型をパディングして 8 ビットに揃えます。列挙型 (enum)
次のコード例のヘッダー ファイルでは、enum 型をいくつか定義し、それらを struct で使用しています。この struct は、別の struct で使用されます。これにより、複雑なデータ型がわかりやすくなります。
次のコード例は、複雑な定義文 (MAD_NSBSAMPLES) の指定および合成方法を示しています。
#include <stdio.h>
enum mad_layer {
MAD_LAYER_I = 1,
MAD_LAYER_II = 2,
MAD_LAYER_III = 3
};
enum mad_mode {
MAD_MODE_SINGLE_CHANNEL = 0,
MAD_MODE_DUAL_CHANNEL = 1,
MAD_MODE_JOINT_STEREO = 2,
MAD_MODE_STEREO = 3
};
enum mad_emphasis {
MAD_EMPHASIS_NONE = 0,
MAD_EMPHASIS_50_15_US = 1,
MAD_EMPHASIS_CCITT_J_17 = 3
};
typedef signed int mad_fixed_t;
typedef struct mad_header {
enum mad_layer layer;
enum mad_mode mode;
int mode_extension;
enum mad_emphasis emphasis;
unsigned long long bitrate;
unsigned int samplerate;
unsigned short crc_check;
unsigned short crc_target;
int flags;
int private_bits;
} header_t;
typedef struct mad_frame {
header_t header;
int options;
mad_fixed_t sbsample[2][36][32];
} frame_t;
# define MAD_NSBSAMPLES(header) \
((header)->layer == MAD_LAYER_I ? 12 : \
(((header)->layer == MAD_LAYER_III && \
((header)->flags & 17)) ? 18 : 36))
void types_composite(frame_t *frame);
次の例では、前の例で定義された struct および enum 型が使用されています。enum が最上位関数への引数に使用されると、標準の C コンパイルの動作に準拠するため、32 ビット値として合成されます。enum 型がデザイン内部に対して指定される場合、Vitis HLS で必要なビット数まで最適化で減らされます。
次のコード例は、合成中に printf 文が無視されるように記述されています。
#include "types_composite.h"
void types_composite(frame_t *frame)
{
if (frame->header.mode != MAD_MODE_SINGLE_CHANNEL) {
unsigned int ns, s, sb;
mad_fixed_t left, right;
ns = MAD_NSBSAMPLES(&frame->header);
printf("Samples from header %d \n", ns);
for (s = 0; s < ns; ++s) {
for (sb = 0; sb < 32; ++sb) {
left = frame->sbsample[0][s][sb];
right = frame->sbsample[1][s][sb];
frame->sbsample[0][s][sb] = (left + right) / 2;
}
}
frame->header.mode = MAD_MODE_SINGLE_CHANNEL;
}
}
共用体 (union)
次のコード例では、double および struct を使用して共用体を作成しています。C コンパイルと異なり、union のすべてのフィールドに合成で必ず同じメモリ (合成の場合、レジスタ) が使用されるとは限りません。Vitis HLS では、最も適したハードウェアを提供するように、最適化が実行されます。
#include "types_union.h"
dout_t types_union(din_t N, dinfp_t F)
{
union {
struct {int a; int b; } intval;
double fpval;
} intfp;
unsigned long long one, exp;
// Set a floating-point value in union intfp
intfp.fpval = F;
// Slice out lower bits and add to shifted input
one = intfp.intval.a;
exp = (N & 0x7FF);
return ((exp << 52) + one) & (0x7fffffffffffffffLL);
}
Vitis HLS では、次はサポートされません。
- 最上位関数インターフェイスの共用体。
- 合成でのポインター再変換。このため、共用体には別のデータ型へのポインターまたは別のデータ型の配列へのポインターは保持できません。
- 別の変数を介した共用体へのアクセス。同じ共用体を前の例のように使用する場合、次はサポートされません。
for (int i = 0; i < 6; ++i) if (i<3) A[i] = intfp.intval.a + B[i]; else A[i] = intfp.intval.b + B[i]; } -
ただし、次のように明示的に記述し直すことができます。
A[0] = intfp.intval.a + B[0]; A[1] = intfp.intval.a + B[1]; A[2] = intfp.intval.a + B[2]; A[3] = intfp.intval.b + B[3]; A[4] = intfp.intval.b + B[4]; A[5] = intfp.intval.b + B[5];
共用体の合成では、ネイティブ C 型とユーザー定義の型間の型変換はサポートされません。
Vitis HLS デザインでは、生のビットを 1 つのデータ型から別のデータ型に変換するために共用体が使用されることがよくあります。通常、この生ビットの変換は、最上位ポート インターフェイスで浮動小数点値を使用する場合に必要です。次にその例を示します。
typedef float T;
unsigned int value; // the "input" of the conversion
T myhalfvalue; // the "output" of the conversion
union
{
unsigned int as_uint32;
T as_floatingpoint;
} my_converter;
my_converter.as_uint32 = value;
myhalfvalue = my_converter. as_floatingpoint;
このタイプのコードは浮動小数点 C データ型では問題なく、変更すれば double 型でも問題ありません。half 型はクラスであり、共用体では使用できないので、typedef および int を short に変更することはできません。その代わりに、次のコードを使用できます。
typedef half T;
short value;
T myhalfvalue = static_cast<T>(value);
同様に、反対方向の変換では、value=static_cast<ap_uint<16> >(myhalfvalue) または static_cast< unsigned short >(myhalfvalue) を使用します。
ap_fixed<16,4> afix = 1.5;
ap_fixed<20,6> bfix = 1.25;
half ahlf = afix.to_half();
half bhlf = bfix.to_half();
また、ヘルパー クラスの fp_struct<half> を使用して、data() または to_int() を使用して変換することもできます。hls/utils/x_hls_utils.h ヘッダー ファイルを使用してください。
型修飾子
型修飾子は、高位合成で作成されるハードウェアに直接影響します。通常、修飾子は次に示すように合成結果に影響を与えますが (予測可能)、Vitis HLS は、修飾子の解釈によってのみ制限されます。これは、修飾子が関数の動作に影響を与え、最適化を実行してより最適なハードウェア デザインを作成できるからです。この例は、各修飾子の概要の後に示します。
揮発性 (volatile)
volatile 修飾子は、ポインターが関数インターフェイスで複数回アクセスされるときの、読み出しまたは書き込みの実行回数に影響します。volatile 修飾子は、階層内のすべての関数に影響します。volatile 修飾子については、最上位インターフェイスに関するセクションで主に説明しています。
- バースト アクセスなし
- ポート幅の拡張なし
- デッド コードの削除なし
任意精度型では、算術演算での volatile 修飾子はサポートされません。volatile 修飾子を使用した任意精度型は、演算式で使用する前に、volatile 以外のデータ型に割り当てる必要があります。
スタティック型 (static)
関数内の static 型は、関数呼び出し間の値を保持します。ハードウェア デザインの同等のビヘイビアーは、レジスタ付きの変数 (フリップフロップまたはメモリ) です。正しく実行されるために変数を C 関数の static 型にする必要がある場合、最終 RTL デザインでは確実にレジスタになります。値は、関数およびデザインの起動中維持されている必要があります。
static 型だけが合成後にレジスタになるというわけではありません。RTL デザインでどの変数がレジスタとしてインプリメントされる必要があるかは、Vitis HLS で判断されます。たとえば、変数引数が複数サイクル間保持される必要がある場合、C 関数の元の変数が static 型でなくても、Vitis HLS ではその値を保持するためにレジスタが作成されます。
Vitis HLS は static の初期化動作に従って、初期化中にレジスタへ値 0 を割り当てるか、指定された初期化値に割り当てます。つまり、static 変数は RTL コードと FPGA ビットストリームで初期化されます。リセット信号がアサートされるたびに変数が初期化し直されるわけではありません。
static 初期化値をシステム リセット時にインプリメントする方法については、RTL コンフィギュレーション (config_rtl コマンド) を参照してください。
定数型 (const)
const 型は、変数の値がアップデートされないことを指定します。変数は読み出されますが、書き込まれることはないので、初期化される必要があります。ほとんどの const 変数は、通常 RTL デザインでは定数になります。Vitis HLS は定数伝搬を実行して、不必要なハードウェアを削除します。
配列の場合、const 変数は最終 RTL デザインで ROM としてインプリメントされます (小さい配列では Vitis HLS で自動分割は実行されない)。const 修飾子で指定された配列は、static の場合と同様、RTL および FPGA ビットストリームで初期化されます。これらは書き込まれることがないため、リセットする必要はありません。
ROM 最適化
次のコードは、配列が static または const 修飾子で指定されていなくても、Vitis HLS で ROM がインプリメントされる例を示しています。これは、Vitis HLS でどのようにデザインが解析され、最適なインプリメンテーションが決定されるかを示しています。修飾子はツールをガイドするためのもので、最終的な RTL はこれによって決定はされません。
#include "array_ROM.h"
dout_t array_ROM(din1_t inval, din2_t idx)
{
din1_t lookup_table[256];
dint_t i;
for (i = 0; i < 256; i++) {
lookup_table[i] = 256 * (i - 128);
}
return (dout_t)inval * (dout_t)lookup_table[idx];
}
この例の場合、lookup_table 変数が最終 RTL でメモリ エレメントになるのが最適なインプリメンテーションであると判断されます。
グローバル変数
グローバル変数はコード内で自由に使用でき、完全に合成可能です。デフォルトではグローバル変数は RTL インターフェイスのポートとしては公開されません。
次のコード例では、グローバル変数のデフォルトの合成ビヘイビアーが示され、3 つのグローバル変数が使用されています。この例では配列が使用されますが、Vitis™ HLS ではすべてのタイプのグローバル変数タイプがサポートされています。
- 値は配列
Ainから読み出されます。 - 配列
AintはAinからの値を変換してAoutに渡すために使用されます。 - 出力は配列
Aoutに書き込まれます。din_t Ain[N]; din_t Aint[N]; dout_t Aout[N/2]; void types_global(din1_t idx) { int i,lidx; // Move elements in the input array for (i=0; i<N; ++i) { lidx=i; if(lidx+idx>N-1) lidx=i-N; Aint[lidx] = Ain[lidx+idx] + Ain[lidx]; } // Sum to half the elements for (i=0; i<(N/2); i++) { Aout[i] = (Aint[i] + Aint[i+1])/2; } }
デフォルトでは、合成後、RTL デザインのポートは idx だけになります。グローバル変数はデフォルトでは RTL ポートとしては使用可能になりません。デフォルトでは、次のようになります。
- 配列
Ainは読み出し元の内部 RAM。 - 配列
Aoutは書き込み先の内部 RAM。
ポインター
ポインターは C コードで広範囲に使用され、合成でもサポートされます。ポインターを使用する場合は、次に注意してください。
- ポインターが同じ関数内で複数回アクセス (読み出しまたは書き込み) される場合。
- ポインター配列を使用する場合、各ポインターが別のポインターではなく、スカラーまたはスカラー配列を指定する必要あり。
- ポインターの型変換は標準 C 型間の変換の場合にのみサポートあり。
次のコード例は、複数のオブジェクトをポイントするポインターの合成サポートを示しています。
#include "pointer_multi.h"
dout_t pointer_multi (sel_t sel, din_t pos) {
static const dout_t a[8] = {1, 2, 3, 4, 5, 6, 7, 8};
static const dout_t b[8] = {8, 7, 6, 5, 4, 3, 2, 1};
dout_t* ptr;
if (sel)
ptr = a;
else
ptr = b;
return ptr[pos];
}
Vitis™ HLS のポインター トゥ ポインターは合成ではサポートされますが、最上位インターフェイスでは (最上位関数への引数としては) サポートされません。ポインター トゥ ポインターを複数の関数で使用する場合、Vitis HLS でポインター トゥ ポインターを使用する関数すべてがインライン展開されます。複数の関数をインライン展開すると、ランタイムが増加します。
#include "pointer_double.h"
data_t sub(data_t ptr[10], data_t size, data_t**flagPtr)
{
data_t x, i;
x = 0;
// Sum x if AND of local index and pointer to pointer index is true
for(i=0; i<size; ++i)
if (**flagPtr & i)
x += *(ptr+i);
return x;
}
data_t pointer_double(data_t pos, data_t x, data_t* flag)
{
data_t array[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
data_t* ptrFlag;
data_t i;
ptrFlag = flag;
// Write x into index position pos
if (pos >=0 & pos < 10)
*(array+pos) = x;
// Pass same index (as pos) as pointer to another function
return sub(array, 10, &ptrFlag);
}
ポインターの配列も合成できます。次のコード例では、ポインターの配列がグローバル配列の 2 次元の開始位置を格納するために使用されています。ポインターの配列内のポインターは、スカラーまたはスカラーの配列のみを指定でき、ほかのポインターを指定することはできません。
#include "pointer_array.h"
data_t A[N][10];
data_t pointer_array(data_t B[N*10]) {
data_t i,j;
data_t sum1;
// Array of pointers
data_t* PtrA[N];
// Store global array locations in temp pointer array
for (i=0; i<N; ++i)
PtrA[i] = &(A[i][0]);
// Copy input array using pointers
for(i=0; i<N; ++i)
for(j=0; j<10; ++j)
*(PtrA[i]+j) = B[i*10 + j];
// Sum input array
sum1 = 0;
for(i=0; i<N; ++i)
for(j=0; j<10; ++j)
sum1 += *(PtrA[i] + j);
return sum1;
}
ポインターの型変換は、ネイティブ C 型が使用される場合に合成でサポートされます。次のコード例では、int 型が char 型に変換されています。
#define N 1024
typedef int data_t;
typedef char dint_t;
data_t pointer_cast_native (data_t index, data_t A[N]) {
dint_t* ptr;
data_t i =0, result = 0;
ptr = (dint_t*)(&A[index]);
// Sum from the indexed value as a different type
for (i = 0; i < 4*(N/10); ++i) {
result += *ptr;
ptr+=1;
}
return result;
}
Vitis HLS では、一般的なデータ型間のポインター型変換はサポートされません。たとえば、符号付きの値の struct 複合型が作成されると、ポインターを型変換して符号なしの値を代入することはできません。
struct {
short first;
short second;
} pair;
// Not supported for synthesis
*(unsigned*)(&pair) = -1U;
このような場合、値はネイティブ型を使用して代入する必要があります。
struct {
short first;
short second;
} pair;
// Assigned value
pair.first = -1U;
pair.second = -1U;
インターフェイスのポインター
ポインターは、最上位関数への引数として使用できます。ポインターは目標どおりの RTL インターフェイスおよびデザインを合成後に達成する際、問題の原因となることがあるので、合成中にポインターがどのようにインプリメントされるか理解するのが重要になります。
基本的なポインター
次のコード例のような、最上位インターフェイスに基本的なポインターを含む関数は、Vitis HLS では問題となりません。ポインターは単純なワイヤ インターフェイスかハンドシェイクを使用したインターフェイス プロトコルのいずれかに合成できます。
#include "pointer_basic.h"
void pointer_basic (dio_t *d) {
static dio_t acc = 0;
acc += *d;
*d = acc;
}
インターフェイスのポインターは、関数呼び出しごとに一度だけ読み出しまたは書き込みされます。テストベンチは次のコード例のようになります。
#include "pointer_basic.h"
int main () {
dio_t d;
int i, retval=0;
FILE *fp;
// Save the results to a file
fp=fopen(result.dat,w);
printf( Din Dout\n, i, d);
// Create input data
// Call the function to operate on the data
for (i=0;i<4;i++) {
d = i;
pointer_basic(&d);
fprintf(fp, %d \n, d);
printf( %d %d\n, i, d);
}
fclose(fp);
// Compare the results file with the golden results
retval = system(diff --brief -w result.dat result.golden.dat);
if (retval != 0) {
printf(Test failed!!!\n);
retval=1;
} else {
printf(Test passed!\n);
}
// Return 0 if the test
return retval;
}
C および RTL シミュレーションでは、この単純なデータ セットを使用して正しい操作が (可能性のある操作すべてではありませんが) 検証されます。
Din Dout
0 0
1 1
2 3
3 6
Test passed!
ポインター演算
ポインター演算 (pointer_arith) を使用すると、RTL に合成可能なインターフェイスが制限されます。次の例は同じコードですが、データ値を 2 つ目の値から累積するために、単純なポインター演算が使用されている点が異なります。
#include "pointer_arith.h"
void pointer_arith (dio_t *d) {
static int acc = 0;
int i;
for (i=0;i<4;i++) {
acc += *(d+i+1);
*(d+i) = acc;
}
}
次に、この例をサポートするテストベンチのコード例を示します。累積を実行するループが pointer_arith 関数内に含まれるようになったので、テストベンチにより配列 d[5] で指定されたアドレス空間に適切な値が指定されます。
#include "pointer_arith.h"
int main () {
dio_t d[5], ref[5];
int i, retval=0;
FILE *fp;
// Create input data
for (i=0;i<5;i++) {
d[i] = i;
ref[i] = i;
}
// Call the function to operate on the data
pointer_arith(d);
// Save the results to a file
fp=fopen(result.dat,w);
printf( Din Dout\n, i, d);
for (i=0;i<4;i++) {
fprintf(fp, %d \n, d[i]);
printf( %d %d\n, ref[i], d[i]);
}
fclose(fp);
// Compare the results file with the golden results
retval = system(diff --brief -w result.dat result.golden.dat);
if (retval != 0) {
printf(Test failed!!!\n);
retval=1;
} else {
printf(Test passed!\n);
}
// Return 0 if the test
return retval;
}
これは、シミュレーションすると、次のような出力になります。
Din Dout
0 1
1 3
2 6
3 10
Test passed!
ポインター演算では、ポインター データは順序どおりにアクセスされません。ワイヤ、ハンドシェイク、または FIFO インターフェイスでは、順序どおりにアクセスされます。
- ワイヤ インターフェイスは、デザインがデータを消費するか書き込む準備ができたときに、データを読み出します。
- ハンドシェイクおよび FIFO インターフェイスは、制御信号により処理が許可されたときに読み出しおよび書き込みを実行します。
どちらの場合も、データは順序どおりに要素 0 から到着し、書き込まれる必要があります。ポインター演算を使用したインターフェイスの例では、最初のデータ値がインデックス 1 から読み出されるように記述されています (i が 0 で開始され、0+1=1)。これは、テストベンチの配列 d[5] の 2 つ目の要素です。
これがハードウェアにインプリメントされる際には、何らかのデータ インデックス形式が必要になります。Vitis HLS では、ワイヤ、ハンドシェイクまたは FIFO インターフェイスを使用するとこれはサポートされません。ポインター演算を使用したインターフェイスの例のコードは、ap_bus インターフェイスを使用してのみ合成できます。このインターフェイスでは、データがアクセス (読み出しまたは書き込み) されたときにデータにインデックスを付けるためのアドレスが提供されます。
または、ポインターではなく、次の例のようにインターフェイスの配列を使用してコードを変更する必要があります。これは、RAM インターフェイス (ap_memory) を使用して合成できます。このインターフェイスは、アドレスを付けてデータのインデックスを作成できるので、順序どおりでなくても実行できます。
ワイヤ、ハンドシェイク、FIFO インターフェイスは、ストリーミング データでのみ使用でき、ポインター演算と一緒には使用できません (0 からデータのインデックスを作成して、順番に進める場合は例外)。
#include "array_arith.h"
void array_arith (dio_t d[5]) {
static int acc = 0;
int i;
for (i=0;i<4;i++) {
acc += d[i+1];
d[i] = acc;
}
}
マルチアクセス ポインター インターフェイス: ストリーミング データ
最上位関数の引数リストにポインターが使用されるデザインでは、ポインターを使用して複数回アクセスする場合に、特別な注意事項があります。複数回のアクセスは、同じ関数でポインターが複数回読み出されたり書き込まれたりすると発生します。
- 複数回アクセスされる関数の引数には、
volatile修飾子を使用する必要があります。 - Vitis HLS 内で協調シミュレーションを使用して RTL を検証する場合は、最上位関数でこのような引数のポート インターフェイスへのアクセス回数を指定する必要があります。
- 合成の前に C を検証し、目的を確認し、C 記述が正しいことを確認してください。
ザイリンクスでは、関数の引数に複数回アクセスする必要がある場合は、ストリームを使用してデザインを記述することを勧めします。このセクションで説明される問題が発生しないようにするため、ストリームを使用してください。
| サンプル デザイン | 説明 |
|---|---|
pointer_stream_bad |
同じ関数内でポインターに複数回アクセスする場合に volatile 修飾子が必要な理由を示します。 |
pointer_stream_better |
最上位インターフェイスにこのようなポインターが含まれるデザインでは、意図した動作が正しくモデリングされていることを確認するために、C テストベンチを使用してデザインを検証する必要があることを示します。 |
次のコード例では、入力ポインター d_i が 4 回読み出され、出力ポインター d_o が 2 回書き込まれます。アクセスは FIFO インターフェイスでインプリメントされ、データは最終的な RTL インプリメンテーションからストリーミングされることを意図しています。
#include "pointer_stream_bad.h"
void pointer_stream_bad ( dout_t *d_o, din_t *d_i) {
din_t acc = 0;
acc += *d_i;
acc += *d_i;
*d_o = acc;
acc += *d_i;
acc += *d_i;
*d_o = acc;
}
次に、このデザインを検証するテストベンチのコード例を示します。
#include "pointer_stream_bad.h"
int main () {
din_t d_i;
dout_t d_o;
int retval=0;
FILE *fp;
// Open a file for the output results
fp=fopen(result.dat,w);
// Call the function to operate on the data
for (d_i=0;d_i<4;d_i++) {
pointer_stream_bad(&d_o,&d_i);
fprintf(fp, %d %d\n, d_i, d_o);
}
fclose(fp);
// Compare the results file with the golden results
retval = system(diff --brief -w result.dat result.golden.dat);
if (retval != 0) {
printf(Test failed !!!\n);
retval=1;
} else {
printf(Test passed !\n);
}
// Return 0 if the test
return retval;
}
揮発性データの理解
マルチアクセス ポインター インターフェイス例のコードは、入力ポインター d_i および出力ポインター d_o を、RTL で FIFO (またはハンドシェイク) インターフェイスとしてインプリメントすることを意図して記述されています。これにより、次のことが確実になります。
- アップストリームのプロデューサー ブロックは、RTL ポート
d_iで読み出しが実行されるたびに新しいデータを供給します。 - ダウンストリームのコンシューマー ブロックは、RTL ポート
d_oで書き込みが実行されるたびに新しいデータを受信します。
このコードが標準 C コンパイラでコンパイルされる場合、各ポインターへの複数アクセスが 1 つのアクセスに削減されます。コンパイラについては、d_i 上のデータが関数の実行中に変化することが示されないので、関係あるのは d_o への最終の書き込みのみです。ほかの書き込みは、関数が完了するまでに上書きされます。
Vitis HLS での処理は、gcc コンパイラの動作と一致しており、複数の読み出しおよび書き込みは 1 つの読み出し操作および 1 つの書き込み操作に最適化されます。RTL が検証されると、各ポートでは 1 つの読み出しおよび書き込み操作のみが実行されます。
このデザインの基本的な問題は、テストベンチとデザインで RTL ポートが設計者の意図どおりにインプリメントされるように記述されていないことです。
- RTL ポートは、トランザクション中に複数回読み出しおよび書き込みされ、データ入力および出力をストリーミングすることが可能であるはずです。
- テストベンチは 1 つの入力値だけを供給し、1 つの出力値だけを返します。マルチアクセス ポインター インターフェイス: ストリーミング データ の C シミュレーションの結果は次のようになり、各入力が 4 回累積されていることを示しています。同じ値が 1 回読み出され、毎回累積されており、4 つの個別の読み出しが実行されるわけではありません。
Din Dout 0 0 1 4 2 8 3 12
-
このデザインで RTL ポートに対して読み出しおよび書き込みが複数回実行されるようにするには、
volatile修飾子を使用します。次にそのコード例を示します。volatileキーワードを使用すると、C コンパイラ (および Vitis HLS) でポインター アクセスに関して何も想定がされず、データが揮発性であり変化する可能性があると解釈されます。ヒント: ポインター アクセスは最適化しないでください。#include "pointer_stream_better.h" void pointer_stream_better ( volatile dout_t *d_o, volatile din_t *d_i) { din_t acc = 0; acc += *d_i; acc += *d_i; *d_o = acc; acc += *d_i; acc += *d_i; *d_o = acc; }上記の例は マルチアクセス ポインター インターフェイス: ストリーミング データ と同じようにシミュレーションされますが、
volatile修飾子により次のような状態になります。 - ポインター アクセスの最適化がされなくなります。
- 意図したとおり、入力ポート
d_iで読み出しが 4 回実行され、出力ポートd_oで書き込みが 2 回実行される RTL デザインになります。
volatile キーワードを使用しても、このコーディング スタイル (ポインターに複数回アクセスする) には、関数とテストベンチに読み出しおよび書き込みが明示的に記述されていないという問題がまだあります。
この場合、読み出しは 4 回実行されますが、同じデータが 4 回読み出されています。書き込みは 2 回、それぞれ正しいデータで実行されますが、テストベンチでは最後の書き込みのデータのみが取り込まれます。
cosim_design をイネーブルにして RTL シミュレーション中にトレース ファイルを作成し、適切なビューアーでそのトレース ファイルを確認します。上記のマルチアクセス volatile ポインター インターフェイスの例は、ワイヤ インターフェイスを使用してインプリメントできます。FIFO インターフェイスが指定される場合は、Vitis HLS で各読み出しごとに新しいデータをストリーミングする RTL テストベンチが作成されます。テストベンチから使用できる新しいデータはないので、RTL の検証はエラーになります。テストベンチでは、正しく読み出しおよび書き込みが記述されません。
ストリーミング データ インターフェイスの記述
ハードウェア システムには並列処理機能があるので、ソフトウェアと違ってストリーミング データの利点を生かすことができます。データはデザインに連続して提供され、デザインからデータは連続して出力されます。RTL デザインは既存データを処理し終わる前に新しいデータを受信できます。
揮発性データの理解 に示すように、ソフトウェアでのストリーミング データの記述は、特に既存のハードウェア インプリメンテーション (既に並列/ストリーミング処理機能が存在し、記述する必要あり) を表すソフトウェアを記述する際に重要です。
これには、次のような複数の方法があります。
- マルチアクセス
volatileポインター インターフェイスの例に示すように、volatile 修飾を追加します。テストベンチには固有の読み出しおよび書き込みが記述されていないので、元の C テストベンチを使用した RTL シミュレーションはエラーになりますが、トレース ファイルの波形を確認すると、正しい読み出しおよび書き込みが実行されています。 - 固有の読み出しおよび書き込みを明示的に記述するようにコードを変更します。次に例を示します。
- ストリーミング データ型を使用するようにコードを変更します。ストリーミング データ型を使用すると、ストリーミング データを使用してハードウェアが適切に記述されるようになります。
次に、テストベンチから 4 つの異なる値を読み出し、2 つの異なる値を書き込むようにアップデートしたコードを示します。ポインターのアクセスはシーケンシャルでロケーション 0 から開始するので、合成ではストリーミング インターフェイスが使用されます。
#include "pointer_stream_good.h"
void pointer_stream_good ( volatile dout_t *d_o, volatile din_t *d_i) {
din_t acc = 0;
acc += *d_i;
acc += *(d_i+1);
*d_o = acc;
acc += *(d_i+2);
acc += *(d_i+3);
*(d_o+1) = acc;
}
関数が各トランザクションで 4 つの固有の値を読み出すことを記述するように、テストベンチがアップデートされます。このテストベンチは、1 つのトランザクションのみを記述しています。複数のトランザクションを記述するには、入力データ セットを増加し、関数を複数回呼び出す必要があります。
#include "pointer_stream_good.h"
int main () {
din_t d_i[4];
dout_t d_o[4];
int i, retval=0;
FILE *fp;
// Create input data
for (i=0;i<4;i++) {
d_i[i] = i;
}
// Call the function to operate on the data
pointer_stream_good(d_o,d_i);
// Save the results to a file
fp=fopen(result.dat,w);
for (i=0;i<4;i++) {
if (i<2)
fprintf(fp, %d %d\n, d_i[i], d_o[i]);
else
fprintf(fp, %d \n, d_i[i]);
}
fclose(fp);
// Compare the results file with the golden results
retval = system(diff --brief -w result.dat result.golden.dat);
if (retval != 0) {
printf(Test failed !!!\n);
retval=1;
} else {
printf(Test passed !\n);
}
// Return 0 if the test
return retval;
}
このテストベンチにより、次のような結果のアルゴリズムが検証されます。
- 1 つのトランザクションから 2 つの出力が生成されます。
- 出力は最初の 2 つの入力読み出しが累算されたものと、次の 2 つの入力読み出しと最初の出力が累算されたものになります。
Din Dout 0 1 1 6 2 3
-
ポインターが関数インターフェイスで複数回アクセスされる際に注意すべき最後の問題は、RTL シミュレーションの記述です。
マルチアクセス ポインターおよび RTL シミュレーション
インターフェイスのポインターが複数回アクセスされた場合、Vitis HLS では関数インターフェイスから何回読み出しおよび書き込みが実行されたかを判断できません。値がいくつ読み出されるか、または書き込まれるかを Vitis HLS に示すような関数インターフェイスの引数はありません。
void pointer_stream_good (volatile dout_t *d_o, volatile din_t *d_i)
Vitis HLS に対して配列の最大サイズなど、インターフェイスに値がいくつ必要かを示すものがコードに含まれないと、1 つの値が想定されて、1 つの入力および 1 つの出力のみで C/RTL 協調シミュレーションを作成します。RTL ポートが実際には複数の値の読み出しまたは書き込みを実行する場合、RTL 協調シミュレーションが停止します。RTL 協調シミュレーションでは、ポート インターフェイスを介して RTL デザインに接続される外部プロデューサー ブロックとコンシューマー ブロックが記述されます。複数の値が必要な場合、複数の値の読み出しまたは書き込みが実行されると、読み出す値がないか書き込むスペースがないため、RTL デザインは停止します。
インターフェイスでマルチアクセス ポインターが使用される場合、インターフェイスでの読み出しまたは書き込みの必要回数を Vitis HLS に示す必要があります。ポインター インターフェイスに INTERFACE プラグマまたは指示子を手動で指定し、depth オプションを必要な深さに設定します。
たとえば、上記のコード例の引数 d_i に必要な FIFO の深さは 4 です。これにより、RTL 協調シミュレーションで RTL を正しく検証するのに十分な値が供給されます。
C++ クラスおよびテンプレート
C++ クラスは、Vitis HLS での合成で完全にサポートされています。合成の最上位は、関数である必要があります。クラスは、合成では最上位にできません。クラスのメンバー関数を合成するには、クラス自体を関数にインスタンシエートする必要があります。最上位クラスは、単にテストベンチにインスタンシエートしないようにしてください。次のコード例は、CFir クラス (次で説明するヘッダー ファイルで定義) がどのように最上位関数 cpp_FIR にインスタンシエートされ、FIR フィルターのインプリメントに使用されるかを示しています。
#include "cpp_FIR.h"
// Top-level function with class instantiated
data_t cpp_FIR(data_t x)
{
static CFir<coef_t, data_t, acc_t> fir1;
cout << fir1;
return fir1(x);
}
上記の C++ FIR フィルターの例で、デザインをインプリメントするために使用されるクラスを調べる前に、Vitis HLS では合成中に標準出力ストリームの cout が無視されることに注意してください。Vitis HLS で合成されると、次のような警告メッセージが表示されます。
INFO [SYNCHK-101] Discarding unsynthesizable system call:
'std::ostream::operator<<' (cpp_FIR.h:108)
INFO [SYNCHK-101] Discarding unsynthesizable system call:
'std::ostream::operator<<' (cpp_FIR.h:108)
INFO [SYNCHK-101] Discarding unsynthesizable system call: 'std::operator<<
<std::char_traits<char> >' (cpp_FIR.h:110)
次のコード例に示す cpp_FIR.h ヘッダー ファイルには、CFir クラスの定義およびそれに関連するメンバー関数が含まれています。この例では、演算子のメンバー関数 () および << はオーバーロードされる演算子です。どちらも main アルゴリズムを実行するために使用され、cout と共に使用すると C シミュレーション中に表示されるデータをフォーマットできます。
#include <fstream>
#include <iostream>
#include <iomanip>
#include <cstdlib>
using namespace std;
#define N 85
typedef int coef_t;
typedef int data_t;
typedef int acc_t;
// Class CFir definition
template<class coef_T, class data_T, class acc_T>
class CFir {
protected:
static const coef_T c[N];
data_T shift_reg[N-1];
private:
public:
data_T operator()(data_T x);
template<class coef_TT, class data_TT, class acc_TT>
friend ostream&
operator<<(ostream& o, const CFir<coef_TT, data_TT, acc_TT> &f);
};
// Load FIR coefficients
template<class coef_T, class data_T, class acc_T>
const coef_T CFir<coef_T, data_T, acc_T>::c[N] = {
#include "cpp_FIR.h"
};
// FIR main algorithm
template<class coef_T, class data_T, class acc_T>
data_T CFir<coef_T, data_T, acc_T>::operator()(data_T x) {
int i;
acc_t acc = 0;
data_t m;
loop: for (i = N-1; i >= 0; i--) {
if (i == 0) {
m = x;
shift_reg[0] = x;
} else {
m = shift_reg[i-1];
if (i != (N-1))
shift_reg[i] = shift_reg[i - 1];
}
acc += m * c[i];
}
return acc;
}
// Operator for displaying results
template<class coef_T, class data_T, class acc_T>
ostream& operator<<(ostream& o, const CFir<coef_T, data_T, acc_T> &f) {
for (int i = 0; i < (sizeof(f.shift_reg)/sizeof(data_T)); i++) {
o << shift_reg[ << i << ]= << f.shift_reg[i] << endl;
}
o << ______________ << endl;
return o;
}
data_t cpp_FIR(data_t x);
次のコード例に示される C++ FIR フィルターのテストベンチは、最上位関数 cpp_FIR がどのように呼び出されて検証されるか示しています。この例には、Vitis HLS 合成用にテストベンチの重要な属性が含まれます。
- 出力結果は、既知の良い値に対して比較されます。
- 結果が正しいと確認されれば、テストベンチは 0 を返します。
#include "cpp_FIR.h"
int main() {
ofstream result;
data_t output;
int retval=0;
// Open a file to saves the results
result.open(result.dat);
// Apply stimuli, call the top-level function and saves the results
for (int i = 0; i <= 250; i++)
{
output = cpp_FIR(i);
result << setw(10) << i;
result << setw(20) << output;
result << endl;
}
result.close();
// Compare the results file with the golden results
retval = system(diff --brief -w result.dat result.golden.dat);
if (retval != 0) {
printf(Test failed !!!\n);
retval=1;
} else {
printf(Test passed !\n);
}
// Return 0 if the test
return retval;
}
cpp_FIR の C++ テストベンチ
指示子をクラスで定義したオブジェクトに適用するには、次の手順に従ってください。
- クラスが定義されているファイル (通常はヘッダー ファイル) を開きます。
- Directives タブを使用して指示子を適用します。
関数と同様、1 つのクラスのインスタンスはすべて同じ最適化が適用されます。
コンストラクター、デストラクター、および仮想関数
クラス コンストラクターおよびデストラクターは、クラス オブジェクトが宣言されると含まれて合成されます。
Vitis HLS がエラボレーション中に関数を静的に決定できる場合、仮想関数は、抽象的なものも含め、合成でサポートされます。次の場合、Vitis HLS では仮想関数が合成でサポートされません。
- 仮想関数はマルチレイヤー インヘリタンス クラス階層で定義できますが、シングル インヘリタンスを使用してのみ定義できます。
- 動的なポリモーフィズムは、ポインター オブジェクトがコンパイル時に決定できる場合にのみサポートされます。たとえば、このようなポインターは if-else やループ文では使用できません。
- STL コンテナーは、オブジェクトのポインターを含め、ポリモーフィズム関数を呼び出すためには使用できません。次に例を示します。
vector<base *> base_ptrs(10); //Push_back some base ptrs to vector. for (int i = 0; i < base_ptrs.size(); ++i) { //Static elaboration cannot resolve base_ptrs[i] to actual data type. base_ptrs[i]->virtual_function(); } - Vitis HLS では、base オブジェクト ポインターがグローバル変数の場合はサポートされません。次に例を示します。
Base *base_ptr; void func() { ... base_prt->virtual_function(); ... } - base オブジェクト ポインターはクラス定義のメンバー変数にはできません。次に例を示します。
// Static elaboration cannot bind base object pointer with correct data type. class A { ... Base *base_ptr; void set_base(Base *base_ptr); void some_func(); ... }; void A::set_base(Base *ptr) { this.base_ptr = ptr; } void A::some_func() { â¦. base_ptr->virtual_function(); â¦. } - base オブジェクト ポインターまたはリファレンスがコンストラクターの関数パラメーター リストにある場合は、Vitis HLS でそれは変換されません。これについては、ISO C++ 規格のセクション 12.7 に記述されています。ビヘイビアーは定義されないこともあります。
class A { A(Base *b) { b-> virtual _ function (); } };
グローバル変数およびクラス
ザイリンクスでは、クラスでグローバル変数を使用することは推奨していません。使用すると、一部の最適化が実行されないことがあります。次のコード例では、フィルターのコンポーネントを作成するのにクラスが使用されています (polyd_cell クラスはシフト、乗算、累算を実行するコンポーネントとして使用されます)。
typedef long long acc_t;
typedef int mult_t;
typedef char data_t;
typedef char coef_t;
#define TAPS 3
#define PHASES 4
#define DATA_SAMPLES 256
#define CELL_SAMPLES 12
// Use k on line 73 static int k;
template <typename T0, typename T1, typename T2, typename T3, int N>
class polyd_cell {
private:
public:
T0 areg;
T0 breg;
T2 mreg;
T1 preg;
T0 shift[N];
int k; //line 73
T0 shift_output;
void exec(T1 *pcout, T0 *dataOut, T1 pcin, T3 coeff, T0 data, int col)
{
Function_label0:;
if (col==0) {
SHIFT:for (k = N-1; k >= 0; --k) {
if (k > 0)
shift[k] = shift[k-1];
else
shift[k] = data;
}
*dataOut = shift_output;
shift_output = shift[N-1];
}
*pcout = (shift[4*col]* coeff) + pcin;
}
};
// Top-level function with class instantiated
void cpp_class_data (
acc_t *dataOut,
coef_t coeff1[PHASES][TAPS],
coef_t coeff2[PHASES][TAPS],
data_t dataIn[DATA_SAMPLES],
int row
) {
acc_t pcin0 = 0;
acc_t pcout0, pcout1;
data_t dout0, dout1;
int col;
static acc_t accum=0;
static int sample_count = 0;
static polyd_cell<data_t, acc_t, mult_t, coef_t, CELL_SAMPLES>
polyd_cell0;
static polyd_cell<data_t, acc_t, mult_t, coef_t, CELL_SAMPLES>
polyd_cell1;
COL:for (col = 0; col <= TAPS-1; ++col) {
polyd_cell0.exec(&pcout0,&dout0,pcin0,coeff1[row][col],dataIn[sample_count],
col);
polyd_cell1.exec(&pcout1,&dout1,pcout0,coeff2[row][col],dout0,col);
if ((row==0) && (col==2)) {
*dataOut = accum;
accum = pcout1;
} else {
accum = pcout1 + accum;
}
}
sample_count++;
}
polyd_cell クラス内には、データをシフトするための SHIFT ループがあります。SHIFT ループで使用されるループ インデックスの k が削除され、k のグローバル インデックスに置換されると (前の例でも記述していましたが static int k と記述されてコメントアウトされていました)、Vitis HLS では polyd_cell クラスの使用されるループまたは関数がパイプライン処理できなくなります。Vitis HLS では、次のようなメッセージが表示されます。
@W [XFORM-503] Cannot unroll loop 'SHIFT' in function 'polyd_cell<char, long long,
int, char, 12>::exec' completely: variable loop bound.
ループ インデックスにはグローバル変数以外のローカル変数を使用すると、Vitis HLS ですべての最適化が実行されます。
テンプレート
Vitis HLS では合成用に C++ のテンプレートの使用がサポートされます。Vitis では、最上位関数のテンプレートはサポートされません。
テンプレートを使用した固有のインスタンスの作成
テンプレート関数のスタティック変数は、テンプレート引数の異なる値に対してそれぞれ複製されます。
template<int NC, int K>
void startK(int* dout) {
static int acc=0;
acc += K;
*dout = acc;
}
void foo(int* dout) {
startK<0,1> (dout);
}
void goo(int* dout) {
startK<1,1> (dout);
}
int main() {
int dout0,dout1;
for (int i=0;i<10;i++) {
foo(&dout0);
goo(&dout1);
cout <<"dout0/1 = "<<dout0<<" / "<<dout1<<endl;
}
return 0;
}
再帰関数でのテンプレートの使用
テンプレートは標準 C 合成ではサポートされない再帰関数をインプリメントするために使用することもできます。
次のコード例では、末尾再帰のフィボナッチ アルゴリズムをインプリメントするために、テンプレート化された struct が使用されています。合成を実行するためには、再帰の最終呼び出しをインプリメントするのに終端クラスを使用する必要があります。この場合、テンプレート サイズは 1 が使用されます。
//Tail recursive call
template<data_t N> struct fibon_s {
template<typename T>
static T fibon_f(T a, T b) {
return fibon_s<N-1>::fibon_f(b, (a+b));
}
};
// Termination condition
template<> struct fibon_s<1> {
template<typename T>
static T fibon_f(T a, T b) {
return b;
}
};
void cpp_template(data_t a, data_t b, data_t &dout){
dout = fibon_s<FIB_N>::fibon_f(a,b);
}
アサーション
C の assert マクロは、範囲情報をアサートする場合に合成でサポートされます。たとえば、変数とループ境界の上限を指定できます。
ループ境界が可変である場合、Vitis HLS ではそのループの反復すべてのレイテンシを判断できず、レイテンシがクエスチョン マーク (?) で表示されます。tripcount 指示子を使用して Vitis HLS にループ境界の情報を渡すことはできますが、この情報はレポート目的にのみ使用され、合成結果には影響しません (tripcount 指示子の有無に関係なく同じサイズのハードウェアが作成される)。
次のコード例は、assert を使用して Vitis HLS に変数の最大範囲を伝え、さらに適したハードウェアを作成するためにどのように assert を使用するかを示しています。
アサーションを使用する前に、assert マクロを定義するヘッダー ファイルを含める必要があります。この例では、これはヘッダー ファイルに含まれます。
#ifndef _loop_sequential_assert_H_
#define _loop_sequential_assert_H_
#include <stdio.h>
#include <assert.h>
#include ap_cint.h
#define N 32
typedef int8 din_t;
typedef int13 dout_t;
typedef uint8 dsel_t;
void loop_sequential_assert(din_t A[N], din_t B[N], dout_t X[N], dout_t Y[N], dsel_t
xlimit, dsel_t ylimit);
#endif
main コードの各ループの前に、次の 2 つの assert 文が記述されています。
assert(xlimit<32);
...
assert(ylimit<16);
...
これらのアサーションは、次を実行します。
- アサーションが偽で値が指定の値よりも大きい場合、C シミュレーションでエラーが発生します。このため、合成前に C コードをシミュレーションすることが重要です。合成前にデザインが有効であることを確認してください。
- この変数の範囲がこの値を超えないことを Vitis HLS に伝えます。この情報は、RTL の変数のサイズ (この場合はループの反復回数) を最適化するために使用できます。
次のコード例に、これらの assert 文を示します。
#include "loop_sequential_assert.h"
void loop_sequential_assert(din_t A[N], din_t B[N], dout_t X[N], dout_t Y[N], dsel_t
xlimit, dsel_t ylimit) {
dout_t X_accum=0;
dout_t Y_accum=0;
int i,j;
assert(xlimit<32);
SUM_X:for (i=0;i<=xlimit; i++) {
X_accum += A[i];
X[i] = X_accum;
}
assert(ylimit<16);
SUM_Y:for (i=0;i<=ylimit; i++) {
Y_accum += B[i];
Y[i] = Y_accum;
}
}
このコードは assert マクロを除けば ループの並列処理 と同じですが、合成後の合成レポートには 2 つの重要な相違点があります。
assert マクロを使用しない場合、レポートは次のようになります。ループ境界が d_sel 型の 8 ビット変数であるため、ループのトリップカウントは 1 ~ 256 の間で変化する可能性があります。
* Loop Latency:
+----------+-----------+----------+
|Target II |Trip Count |Pipelined |
+----------+-----------+----------+
|- SUM_X |1 ~ 256 |no |
|- SUM_Y |1 ~ 256 |no |
+----------+-----------+----------+
assert マクロを使用した場合、レポートには SUM_X および SUM_Y ループのトリップカウントが 32 および 16 であることが示されます。assert により値が 32 および 16 を超えることはないので、Vitis HLS でレポートにこれが使用されます。
* Loop Latency:
+----------+-----------+----------+
|Target II |Trip Count |Pipelined |
+----------+-----------+----------+
|- SUM_X |1 ~ 32 |no |
|- SUM_Y |1 ~ 16 |no |
+----------+-----------+----------+
また、tripcount 指示子を使用する場合と異なり、assert 文を使用すると最適なハードウェアが可能になります。assert を使用しない場合、最終的なハードウェアでループの最大反復回数 256 に合わせた変数およびカウンターが使用されます。
* Expression:
+----------+------------------------+-------+---+----+
|Operation |Variable Name |DSP48E |FF |LUT |
+----------+------------------------+-------+---+----+
|+ |X_accum_1_fu_182_p2 |0 |0 |13 |
|+ |Y_accum_1_fu_209_p2 |0 |0 |13 |
|+ |indvar_next6_fu_158_p2 |0 |0 |9 |
|+ |indvar_next_fu_194_p2 |0 |0 |9 |
|+ |tmp1_fu_172_p2 |0 |0 |9 |
|+ |tmp_fu_147_p2 |0 |0 |9 |
|icmp |exitcond1_fu_189_p2 |0 |0 |9 |
|icmp |exitcond_fu_153_p2 |0 |0 |9 |
+----------+------------------------+-------+---+----+
|Total | |0 |0 |80 |
+----------+------------------------+-------+---+----+
assert を使用して変数範囲を限定したコードでは、変数範囲を最大値よりも小さくできるので、より小型の RTL デザインが得られます。
* Expression:
+----------+------------------------+-------+---+----+
|Operation |Variable Name |DSP48E |FF |LUT |
+----------+------------------------+-------+---+----+
|+ |X_accum_1_fu_176_p2 |0 |0 |13 |
|+ |Y_accum_1_fu_207_p2 |0 |0 |13 |
|+ |i_2_fu_158_p2 |0 |0 |6 |
|+ |i_3_fu_192_p2 |0 |0 |5 |
|icmp |tmp_2_fu_153_p2 |0 |0 |7 |
|icmp |tmp_9_fu_187_p2 |0 |0 |6 |
+----------+------------------------+-------+---+----+
|Total | |0 |0 |50 |
+----------+------------------------+-------+---+----+
アサーションでは、デザインに含まれるどの変数の範囲でも指定できます。アサーションを使用する場合は、可能なすべての状況を想定した C シミュレーションを実行することが重要になります。これにより、Vitis HLS で使用されるアサーションが有効であることを確認できます。
ハードウェア効率の良い C コードの例
C コードが CPU 用にコンパイルされると、C コードが変換され、コンパイラで CPU マシン命令のセットに最適化されます。多くの場合、この段階で開発者の仕事は終わりです。ただし、次のいくつか、またはすべてを実行するためにパフォーマンスを改善する必要があることもあります。
- コンパイラによって実行可能な最適化があるかどうかを理解。
- プロセッサ アーキテクチャをより深く理解し、アーキテクチャ特有のビヘイビアー (たとえば、組み合わせ分岐の削減により命令パイプラインを改善など) を生かすようにコードを修正。
- 主な操作を並列で実行するための CPU 特有の組み込み機能 (例:Arm NEON 組み込みなど) を使用して C コードを変更してください。
同じ手法は、DSP または GPU 用に記述したコードや FPGA を使用した場合に適用できます (FPGA デバイスは単に別のターゲットです)。
Vitis HLS で合成された C コードが FPGA で実行され、同じ機能が C シミュレーションとして提供されます。この段階で開発者の仕事は終わりなこともあります。
ただし、通常は FPGA デバイスのパフォーマンスを向上するため、C コードをインプリメンする FPGA が選択されます。FPGA に大量の並列アーキテクチャを含めると、プロセッサのもともとのシーケンシャル操作よりもかなり速く操作を実行できるので、この利点を生かすことをお勧めします。
ここでは、C コードが達成可能な結果にどのように影響するかを理解し、C コードをどのように変更すれば、最初の 3 つのトピックの利点を最大限に引き出されるのかについて説明します。
典型的なたたみ込み関数の C コード
ここでは画像に適用される標準的なたたみ込み関数を使用して、FPGA では可能であったパフォーマンスが C コードによりどのように劣化するかをお見せします。この例では、データに対してまず水平たたみ込みが実行された後、垂直たたみ込みが実行されます。画像のエッジのデータはたたみ込み範囲外にあるので、最後に境界周囲のデータが処理されます。
アルゴリズムでの処理は、次の順で実行されます。
template<typename T, int K>
static void convolution_orig(
int width,
int height,
const T *src,
T *dst,
const T *hcoeff,
const T *vcoeff) {
T local[MAX_IMG_ROWS*MAX_IMG_COLS];
// Horizontal convolution
HconvH:for(int col = 0; col < height; col++){
HconvWfor(int row = border_width; row < width - border_width; row++){
Hconv:for(int i = - border_width; i <= border_width; i++){
}
}
// Vertical convolution
VconvH:for(int col = border_width; col < height - border_width; col++){
VconvW:for(int row = 0; row < width; row++){
Vconv:for(int i = - border_width; i <= border_width; i++){
}
}
// Border pixels
Top_Border:for(int col = 0; col < border_width; col++){
}
Side_Border:for(int col = border_width; col < height - border_width; col++){
}
Bottom_Border:for(int col = height - border_width; col < height; col++){
}
}
水平たたみ込み
最初に、次の図のように水平方向のたたみ込みを実行します。
たたみ込みは、K 個のデータ サンプルと K 個のたたみ込み係数を使用して実行されます。上の図では K の値は 5 ですが、この値はコードで定義されます。たたみ込みを実行するには、K 個以上のデータ サンプルが必要です。たたみ込みウィンドウは、画像外にあるピクセルを含む必要があるため、最初のピクセルでは開始できません。
対称たたみ込みを実行すると、src 入力からの最初の K 個のデータ サンプルが水平係数でたたみ込まれ、最初の出力が計算されます。2 つ目の出力を計算するには、次の K 個のデータ サンプルが使用されます。この計算は、最後の出力が書き込まれるまで各行に対して実行されます。
最終結果は、青色で示すようにより小さな画像になります。垂直境界沿いのピクセルは、後で処理されます。
この操作を実行する C コードは次のとおりです。
const int conv_size = K;
const int border_width = int(conv_size / 2);
#ifndef __SYNTHESIS__
T * const local = new T[MAX_IMG_ROWS*MAX_IMG_COLS];
#else // Static storage allocation for HLS, dynamic otherwise
T local[MAX_IMG_ROWS*MAX_IMG_COLS];
#endif
Clear_Local:for(int i = 0; i < height * width; i++){
local[i]=0;
}
// Horizontal convolution
HconvH:for(int col = 0; col < height; col++){
HconvWfor(int row = border_width; row < width - border_width; row++){
int pixel = col * width + row;
Hconv:for(int i = - border_width; i <= border_width; i++){
local[pixel] += src[pixel + i] * hcoeff[i + border_width];
}
}
}
このコードは簡単でわかりやすいものですが、問題がいくつかあり、次の 3 つの問題はハードウェア結果の質に悪影響を及ぼします。
最初の問題は、ストレージ要件が 2 つ別々にあることです。結果は内部の local 配列に格納されます。これには、HEIGHT*WIDTH の配列が必要で、標準ビデオ画像の 1920*1080 の場合は 2,073,600 の値が保持されます。Windows システムによっては、この量のローカル ストレージにより問題が発生することがあります。local 配列のデータはスタックに配置され、OS で管理されるヒープには含まれません。
このような問題を回避するには、__SYNTHESIS__ マクロを使用します。このマクロは、合成が実行されると自動的に定義されます。上記のコードでは、C シミュレーション中にダイナミック メモリ割り当てを使用してコンパイルの問題を回避しており、合成中はスタティック ストレージのみが使用されます。このマクロを使用する短所は、C シミュレーションで検証されたコードが合成されるコードとは異なるものになることです。この例の場合はコードは複雑ではないので、動作は同じになります。
FPGA インプリメンテーションの質の最初の問題は、local 配列にあります。これは配列なので、内部 FPGA ブロック RAM を使用してインプリメントされます。これは、FPGA 内にインプリメントするにはかなり大きいメモリであり、より大きくてコストのかかる FPGA デバイスが必要となる可能性があります。データフロー最適化を使用して、小型で効率的な FIFO を介してデータをストリーミングすると、ブロック RAM の使用を最小限に抑えることはできますが、データがストリーミングされるようにする必要があります。
次の問題は、local 配列の初期化です。Clear_Local ループは local 配列の値を 0 に設定するために使用されます。このループはパイプラインされていても、インプリメントには約 2 百万クロック サイクル (HEIGHT*WIDTH) が必要となります。これと同じデータの初期化は、HConv ループ内の一時的な変数を使用して、書き込み前に累積を初期化することにより実行できます。
最後の問題は、データのスループットがデータ アクセス パターンにより制限されることです。
- 最初の出力を作成するため、最初の K 個の値が入力から読み出されます。
- 2 つ目の出力を計算するには、同じ K-1 値がデータ入力ポートを介して再度読み出されます。
- このデータの再読み出しプロセスは、画像全体で繰り返されます。
パフォーマンスの優れた FPGA にするには、最上位関数引数へのアクセスを最小限に抑えることが重要課題の 1 つとなります。最上位関数の引数は、RTL ブロックのデータ ポートになります。上記のコードでは、データを何度も読み出す必要があるので、DMA 操作を使用してプロセッサから直接ストリーミングできません。入力を再度読み出すと、FPGA がサンプルを処理するレートも制限されます。
垂直たたみ込み
次の段階では、次の図に示す垂直たたみ込みを実行します。
垂直たたみ込みのプロセスは、水平たたみ込みと似ています。たたみ込み係数 (この場合は Vcoeff) を使用したたたみ込みには、K 個のデータ サンプルが必要です。垂直方向の最初の K 個のサンプルを使用して最初の出力が作成された後、次の K 個の値を使用して 2 つ目の出力が作成されます。この処理は、最後の出力が作成されるまで各列に対して実行されます。
水平および垂直境界の効果により、垂直たたみ込み後の画像はソース画像 src よりも小さくなります。
これらの操作を実行するコードは、次のとおりです。
Clear_Dst:for(int i = 0; i < height * width; i++){
dst[i]=0;
}
// Vertical convolution
VconvH:for(int col = border_width; col < height - border_width; col++){
VconvW:for(int row = 0; row < width; row++){
int pixel = col * width + row;
Vconv:for(int i = - border_width; i <= border_width; i++){
int offset = i * width;
dst[pixel] += local[pixel + offset] * vcoeff[i + border_width];
}
}
}
このコードには、水平たたみ込みコードを使用して既に説明した問題と同様の問題があります。
- 出力画像
dstの値を 0 に設定するのに、多数のクロック サイクルが費やされます。この場合、1920*1080 画像サイズに対してさらに約 2 百万サイクルが必要です。 local配列に格納されたデータを再度読み出すために、各ピクセルが複数回アクセスされます。- 出力配列/ポート
dstに対しても、各ピクセルが複数回書き込まれます。
上記のコードには、local 配列へのアクセス パターンの問題もあります。アルゴリズムでは、最初の計算を実行するために行 K のデータが使用可能になっていることが必要です。次の列に進む前に各行のデータを処理するため、画像全体がローカルに格納されている必要があります。また、データは local 配列外にはストリーミングされないので、データフロー最適化で作成されたメモリ チャネルをインプリメントするために FIFO を使用することはできません。このデザインにデータフロー最適化を使用すると、このメモリ チャネルにピンポン バッファーが必要で、インプリメンテーションに必要なメモリは倍の 4 百万データ サンプルになり、そのすべてを FPGA ローカルに格納する必要があります。
境界ピクセル
たたみ込みの最後の段階では、境界周辺のデータを作成します。これらのピクセルは、たたみ込み出力の最も近いピクセルを再利用することにより作成できます。次の図に、これをどのように達成するかを示します。
境界領域は、最も近い有効な値を使用して作成されます。図に示す操作は、次のコードで実行されます。
int border_width_offset = border_width * width;
int border_height_offset = (height - border_width - 1) * width;
// Border pixels
Top_Border:for(int col = 0; col < border_width; col++){
int offset = col * width;
for(int row = 0; row < border_width; row++){
int pixel = offset + row;
dst[pixel] = dst[border_width_offset + border_width];
}
for(int row = border_width; row < width - border_width; row++){
int pixel = offset + row;
dst[pixel] = dst[border_width_offset + row];
}
for(int row = width - border_width; row < width; row++){
int pixel = offset + row;
dst[pixel] = dst[border_width_offset + width - border_width - 1];
}
}
Side_Border:for(int col = border_width; col < height - border_width; col++){
int offset = col * width;
for(int row = 0; row < border_width; row++){
int pixel = offset + row;
dst[pixel] = dst[offset + border_width];
}
for(int row = width - border_width; row < width; row++){
int pixel = offset + row;
dst[pixel] = dst[offset + width - border_width - 1];
}
}
Bottom_Border:for(int col = height - border_width; col < height; col++){
int offset = col * width;
for(int row = 0; row < border_width; row++){
int pixel = offset + row;
dst[pixel] = dst[border_height_offset + border_width];
}
for(int row = border_width; row < width - border_width; row++){
int pixel = offset + row;
dst[pixel] = dst[border_height_offset + row];
}
for(int row = width - border_width; row < width; row++){
int pixel = offset + row;
dst[pixel] = dst[border_height_offset + width - border_width - 1];
}
}
このコードには、データに繰り返しアクセスするという同じ問題があります。FPGA 外の dst 配列に格納されたデータは、入力データとして複数回読み出されることが可能になっている必要があります。最初のループでも、dst[border_width_offset + border_width] が複数回読み出されますが、border_width_offset および border_width の値は変更されません。
コーディング スタイルがパフォーマンスと FPGA インプリメンテーションの質に悪影響を与える問題の最後は、どのように異なる条件を指定するかの構造です。for-loop で各条件 (top-left、top-row など) ごとに操作が処理される場合は、次のように最適化します。
この場合、変数境界 (width 入力の値に基づく) を使用するサブループがあるため、最上位ループ (Top_Border、Side_Border、Bottom_Border) はパイプライン処理できません。この場合、サブループをパイプライン処理し、パイプライン処理されたループの各セットを順に実行する必要があります。
最上位ループをパイプライン処理してサブループを展開するか、サブループを個別にパイプライン処理するのかは、ループの制限と FPGA デバイスで使用可能なリソース数によって決まります。最上位ループの制限が小さい場合は、ループを展開してハードウェアを複製することによりパフォーマンスを満たします。最上位ループの制限が大きい場合は、サブループをパイプライン処理して、それらをループ (Top_Border、Side_Border、Bottom_Border) 内で順に実行することによりパフォーマンスを落とします。
この標準的なたたみ込みアルゴリズムの説明に示すように、次のコーディング スタイルを使用すると、FPGA インプリメンテーションのパフォーマンスおよびサイズに悪影響を及ぼします。
- 配列のデフォルト値を設定すると、クロック サイクル数が多くなり、パフォーマンスが低下します。
- データを複数回読み出すと、クロック サイクルが増加し、パフォーマンスも劣化します。
- 任意またはランダム アクセス方法でデータにアクセスする場合は、データを配列にローカルに格納する必要があり、リソースが費やされてしまいます。
データおよびデータ再利用の続行フロー
前のセクションで説明したたたみ込みの例 (パフォーマンスの優れたリソース使用量が最小のデザイン) をインプリメントする際には、システム全体で FPGA インプリメンテーションがどのように使用されるのかについて考慮する必要があります。理想的なビヘイビアーは、データ サンプルが FPGA 全体を一定して流れることです。
- システム中のデータフローを最大限にします。データフローを制限するようなコーディング手法やアルゴリズム ビヘイビアーは避けてください。
- データの再利用を最大限にします。ローカル キャッシュを使用して、同じデータを何回も読み出す必要がないようにし、入力データのフローが途切れないようにします。
最初の段階では、FPGA 内外に対する I/O 操作が最適になるようにします。たたみ込みアルゴリズムは、画像に対して実行されます。画像からのデータは、次の図に示すような標準のラスター走査順序で転送されます。
データが CPU またはシステム メモリから FPGA に転送されると、通常はこのストリーミング方法で転送されます。FPGA から転送されたデータをシステムに戻す場合も、この方法で実行する必要があります。
ストリーミング データに対する HLS ストリームの使用
前述のコードを改善する方法の 1 つは、通常 hls::stream と呼ばれる HLS ストリーム コンストラクトを使用することです。hls::stream オブジェクトは、配列と同じ方法でデータ サンプルを格納するために使用できます。hls::stream のデータには、シーケンシャルでのみアクセスできます。C コードでは、hls::stream が無限の深さの FIFO のように動作します。
hls::stream を使用してコードを記述すると、通常 FPGA にパフォーマンスに優れたリソース使用量の少ないデザインが作成できます。これは、hls::stream により、FPGA でのインプリメンテーションに理想的なコーディング スタイルが使用されるからです。
hls::stream から同じデータを何度も読み出すことはできません。データは hls::stream から読み出されると、ストリームからはなくなります。この結果、このようなコーディング方法は使用されなくなります。
hls::stream からのデータが再び必要な場合は、キャッシュに入力する必要があります。これも FPGA に合成されるようにコードを記述するのに推奨される方法の 1 つです。
hls::stream を使用すると、FPGA インプリメンテーションに理想的な方法で C コードが開発されるようになります。
hls::stream が合成されると、1 要素の深さの FIFO チャネルとして自動的にインプリメントされます。これは、パイプラインされたタスクの接続には理想的なハードウェアです。
hls::stream を必ず使用する必要はありません。同じインプリメンテーションは C コードの配列を使用すると実行できます。hls::stream コンストラクトを使用した方がコーディング方法としては優れています。
hls::stream コンストラクトを使用して新しく最適化されたコードの概要は次のようになります。
template<typename T, int K>
static void convolution_strm(
int width,
int height,
hls::stream<T> &src,
hls::stream<T> &dst,
const T *hcoeff,
const T *vcoeff)
{
hls::stream<T> hconv("hconv");
hls::stream<T> vconv("vconv");
// These assertions let HLS know the upper bounds of loops
assert(height < MAX_IMG_ROWS);
assert(width < MAX_IMG_COLS);
assert(vconv_xlim < MAX_IMG_COLS - (K - 1));
// Horizontal convolution
HConvH:for(int col = 0; col < height; col++) {
HConvW:for(int row = 0; row < width; row++) {
HConv:for(int i = 0; i < K; i++) {
}
}
}
// Vertical convolution
VConvH:for(int col = 0; col < height; col++) {
VConvW:for(int row = 0; row < vconv_xlim; row++) {
VConv:for(int i = 0; i < K; i++) {
}
}
Border:for (int i = 0; i < height; i++) {
for (int j = 0; j < width; j++) {
}
}
前述のコードと比較すると、このコードには明らかな違いがいくつかあります。
- 入力および出力データは
hls::streamと記述されるようになりました。 - HEIGHT*WDITH サイズの単一の local 配列の代わりに、2 つの内部
hls::streamが使用され、水平および垂直たたみ込みの出力が保存されています。
また、assert 文がいくつか使用され、ループ境界の最大値が指定されています。これにより、Vivado HLS で可変の境界付きループのレイテンシが自動的にレポートされ、そのループ境界が最適化されるようになるので、この方法はお勧めです。
水平たたみ込み
FPGA インプリメンテーションで効率的な方法で計算を実行するため、水平たたみ込みは次の図に示すように計算されます。
hls::stream を使用すると、データへのランダム アクセスを実行するのではなく、最初のサンプルを最初に読み出す優れたアルゴリズムになります。このアルゴリズムでは、前の K サンプルを使用してたたみ込み結果を計算する必要があるので、サンプルが一時キャッシュ hwin にコピーされます。最初の計算では、hwin に結果を計算するのに十分な値が含まれていないので、出力値は書き込まれません。
アルゴリズムは入力サンプルを継続的に読み出し、hwin キャッシュに格納します。新しいサンプルが読み出されるたびに、不要なサンプルが hwin から排出されます。K 番目の入力が読み込まれると、最初の出力値を書き込むことができるようになります。
このように、最後のサンプルが読み込まれるまで行ごとに処理されます。この段階で hwin に格納されているのは最後の K 個のサンプルだけであり、そのすべてがたたみ込み計算に必要となります。
これらの処理を実行するコードは、次のとおりです。
// Horizontal convolution
HConvW:for(int row = 0; row < width; row++) {
HconvW:for(int row = border_width; row < width - border_width; row++){
T in_val = src.read();
T out_val = 0;
HConv:for(int i = 0; i < K; i++) {
hwin[i] = i < K - 1 ? hwin[i + 1] : in_val;
out_val += hwin[i] * hcoeff[i];
}
if (row >= K - 1)
hconv << out_val;
}
}
上記のコードでは、一時変数 out_val を使用してたたみ込み計算が実行されています。この変数は、計算の実行前に 0 に設定されるので、前の例で示したように、値をリセットするために 2 百万クロック サイクルを費やす必要はありません。
プロセス全体を通して、src 入力のサンプルはラスター ストリーミング方法で処理されます。すべてのサンプルが順番に読み込まれます。タスクからの出力は破棄または使用されますが、タスクは常に計算を実行し続けます。この点が、CPU で実行するために記述されたコードと異なります。
CPU アーキテクチャの場合、条件文または分岐文はよく回避されます。プログラムが分岐を必要とする場合は、CPU フェッチ パイプラインに格納された命令が失われます。FPGA アーキテクチャの場合、各条件分岐に対してハードウェアに別のパスが既に存在するので、パイプラインされたタスク内の分岐のためにパフォーマンスが落ちることはなく、単にどの分岐を使用するかといった問題だけです。
出力は、垂直たたみ込みループで使用するため、hls::stream hconv に格納されます。
垂直たたみ込み
垂直たたみ込みでは、FPGA 向けのストリーミング データ モデルを記述するのが困難です。データには列ごとにアクセスする必要がありますが、画像全体を格納するのは望ましくありません。ソリューションは、次の図に示すようにライン バッファーを使用することです。
サンプルはストリーミング方式で読み込まれますが、この場合は hls::stream hconv から読み込まれます。このアルゴリズムでは、最初のサンプルを処理するのに少なくとも K-1 行のデータが必要です。これより前に実行された計算はすべて削除されます。
ライン バッファーには、K-1 行のデータを格納できます。新しいサンプルが読み込まれるたびに、別のサンプルがライン バッファーから排出されます。つまり、最新のサンプルが計算に使用されると、そのサンプルがライン バッファーに格納され、古いサンプルが排出されます。この結果、キャッシュされる必要があるのは K 行ではなく、K-1 行のみになります。ライン バッファーにはローカルで格納するために複数行が必要ですが、たたみ込みのカーネル サイズ K はフル ビデオ画像の 1080 行よりもかなり小さくなります。
最初の計算は、K 行目にある最初のサンプルが読み込まれると実行されます。その後、最後のピクセルが読み込まれるまで値が出力されます。
// Vertical convolution
VConvH:for(int col = 0; col < height; col++) {
VConvW:for(int row = 0; row < vconv_xlim; row++) {
#pragma HLS DEPENDENCE variable=linebuf inter false
#pragma HLS PIPELINE
T in_val = hconv.read();
T out_val = 0;
VConv:for(int i = 0; i < K; i++) {
T vwin_val = i < K - 1 ? linebuf[i][row] : in_val;
out_val += vwin_val * vcoeff[i];
if (i > 0)
linebuf[i - 1][row] = vwin_val;
}
if (col >= K - 1)
vconv << out_val;
}
}
上記のコードでは、デザインのサンプルがすべてストリーミング方式で処理されます。タスクは、継続して実行されます。hls::stream コンストラクトを使用すると、データがローカルにキャッシュされます。これは、FPGA をターゲットにする場合の理想的なストラテジです。
境界ピクセル
このアルゴリズムの最後には、エッジ ピクセルを境界領域に複製します。一定したフローまたはデータ/データ再利用を実現するには、アルゴリズムで hls::stream とキャッシュが使用されるようにします。
次の図に、境界サンプルがどのように画像に組み込まれるかを示します。
- 各サンプルが垂直たたみ込みからの
vconv出力から読み込まれます。 - 次に、サンプルが 4 つのピクセル タイプのいずれかとしてキャッシュに格納されます。
- サンプルが出力ストリームに書き出されます。
次は、境界ピクセルの位置を決定するコードです。
Border:for (int i = 0; i < height; i++) {
for (int j = 0; j < width; j++) {
T pix_in, l_edge_pix, r_edge_pix, pix_out;
#pragma HLS PIPELINE
if (i == 0 || (i > border_width && i < height - border_width)) {
if (j < width - (K - 1)) {
pix_in = vconv.read();
borderbuf[j] = pix_in;
}
if (j == 0) {
l_edge_pix = pix_in;
}
if (j == width - K) {
r_edge_pix = pix_in;
}
}
if (j <= border_width) {
pix_out = l_edge_pix;
} else if (j >= width - border_width - 1) {
pix_out = r_edge_pix;
} else {
pix_out = borderbuf[j - border_width];
}
dst << pix_out;
}
}
}
このコードの明らかな違いは、タスク内に条件文が多く使用されている点です。これにより、タスクはパイプラインされたら、データを続けて処理し、条件文の結果によりパイプラインの実行が影響を受けることはありません。結果は出力値には影響しますが、パイプラインは入力サンプルが使用できる限り、処理され続けます。
この FPGA に最適なアルゴリズムを含む最終的なコードには、次の最適化指示子が使用されます。
template<typename T, int K>
static void convolution_strm(
int width,
int height,
hls::stream<T> &src,
hls::stream<T> &dst,
const T *hcoeff,
const T *vcoeff)
{
#pragma HLS DATAFLOW
#pragma HLS ARRAY_PARTITION variable=linebuf dim=1 complete
hls::stream<T> hconv("hconv");
hls::stream<T> vconv("vconv");
// These assertions let HLS know the upper bounds of loops
assert(height < MAX_IMG_ROWS);
assert(width < MAX_IMG_COLS);
assert(vconv_xlim < MAX_IMG_COLS - (K - 1));
// Horizontal convolution
HConvH:for(int col = 0; col < height; col++) {
HConvW:for(int row = 0; row < width; row++) {
#pragma HLS PIPELINE
HConv:for(int i = 0; i < K; i++) {
}
}
}
// Vertical convolution
VConvH:for(int col = 0; col < height; col++) {
VConvW:for(int row = 0; row < vconv_xlim; row++) {
#pragma HLS PIPELINE
#pragma HLS DEPENDENCE variable=linebuf inter false
VConv:for(int i = 0; i < K; i++) {
}
}
Border:for (int i = 0; i < height; i++) {
for (int j = 0; j < width; j++) {
#pragma HLS PIPELINE
}
}
各タスクは、同レベルでパイプラインされます。ライン バッファーはレジスタに完全に分割され、ブロック RAM ポートの不足によって読み出しまたは書き込みが制限されることはありません。ライン バッファーには、依存指示子も必要です。すべてのタスクがデータフロー領域で実行され、タスクが同時処理されるようになります。hls::streams は 1 要素を含む FIFO として自動的にインプリメントされます。
まとめ: 効率的なハードウェアのための C 言語
データ入力の読み込みを最小限にします。データがブロックに読み込まれると、多くの並列パスに簡単に供給できますが、入力ポートがパフォーマンスのボトルネックになることがあります。データを読み込んだ後、再利用する必要がある場合は、ローカル キャッシュを使用します。
配列、特に大型の配列へのアクセスを最小限に抑えます。配列はブロック RAM にインプリメントされますが、ブロック RAM では I/O ポートと同様ポート数が限られるので、パフォーマンスのボトルネックになることがあります。配列は小型の配列および個別のレジスタに分割できますが、大型の配列を分割すると使用されるレジスタ数が多くなります。小型のローカル キャッシュを使用して累積などの結果を保持してから、最終結果を配列に書き出すようにします。
パイプライン処理されたタスクであっても、タスクを条件で実行するのではなく、パイプライン処理されたタスク内で条件分岐を実行するようにします。条件文は、パイプラインで別々のパスとしてインプリメントされます。データが 1 つのタスクから次のタスク内で実行される条件に流れるようにすると、システムのパフォーマンスが向上します。
入力の読み出しと同様に、ポートはボトルネックになるので、出力の書き込みも最低限にします。追加ポートを複製すると、単に問題がシステムに先送りになるだけです。
ストリーミング方式でデータを処理する C コードの場合、優れたコード記述になるので、hls::streams を使用することをお勧めします。なぜ FPGA が必要なパフォーマンスで動作しないのかデバッグするよりも、優れたパフォーマンスの FPGA インプリメンテーションになる C のアルゴリズムを設計する方が生産的です。
インターフェイス合成の管理
C ベース デザインでは、すべての入力および出力操作がフォーマル関数引数を使用して 0 時間で実行されます。RTL デザインでは、同じ入力および出力操作をデザイン インターフェイスのポートを介して実行する必要があり、通常は特定の I/O (入力/出力) プロトコルを使用して実行されます。Vitis HLS は、使用した I/O プロトコルを指定するのに業界標準を使用し、このポート インターフェイスを自動的に定義します。
最上位関数が合成されると、その関数への引数が RTL ポートに合成されます。このプロセスは、「インターフェイス合成」と呼ばれます。
Vitis HLS は、ターゲット フローに基づいてポート インターフェイスと I/O プロトコルを選択し、データ型と関数引数の方向を選択し、デフォルトでインターフェイス合成を実行します。ターゲット フローは、新規 Vitis HLS プロジェクトの作成 に示すように GUI を使用して指定するか、open_solution コマンドでターゲットを指定すると定義できます。
open_solution -flow_target
vitis を指定するか、GUI で Vitis Kernel
Flow をオンにすると、Vitis HLS は ポート レベル I/O: AXI4 インターフェイス プロトコル および AXI4 インターフェイスの使用 で説明するように、AXI 規格を使用してインターフェイス ポートをインプリメントします。
インターフェイス合成の概要
次のコードは、sum_io 関数でインターフェイス合成の概要を示しています。
#include "sum_io.h"
dout_t sum_io(din_t in1, din_t in2, dio_t *sum) {
dout_t temp;
*sum = in1 + in2 + *sum;
temp = in1 + in2;
return temp;
}
sum_io の例には、次が含まれます。
- 2 つの値渡し入力:
in1およびin2。 - ポインター: 読み出しおよび書き込み両方の
sum。 tempの値を割り当てる関数return。
Vitis HLS で使用されるデフォルトのインターフェイス合成設定では、デザインは次の図に示すように、ポートを含む RTL ブロックに合成されます。
Vitis HLS は、RTL デザインに 3 つのタイプのインターフェイス ポートを作成して、データおよび制御の両方のフローを処理します。
- クロックおよびリセット ポート:
ap_clkおよびap_rst。 - ブロック レベルのインターフェイス プロトコル: これらは前の図で展開されている
ap_ctrlインターフェイスに含まれます (ap_start、ap_done、ap_ready、およびap_idle)。 - ポート レベルのインターフェイス プロトコル: 最上位関数の各引数および関数の戻り値 (関数が値を返す場合) に対して作成されます。上記の例では、
in1、in2、sum_i、sum_o、sum_o_ap_vldおよびap_returnなどです。
クロックおよびリセット ポート
デザインが完了するのにかかるクロック サイクルが 1 を超える場合、 および config_interface コマンドを使用してブロック全体にチップ イネーブル ポートをオプションで追加できます。
リセットの動作は、config_rtl コンフィギュレーションで制御されます。
ブロックおよびポートのインターフェイス プロトコル
ブロック レベルのインターフェイス プロトコルは、RTL モジュールの別のモジュールおよびソフトウェア アプリケーションからの操作を制御するためのメカニズムを提供します。ポート レベルのインターフェイス プロトコルは、同じ制御を RTL モジュールの個々のポートに適用するのに使用できます。
デフォルトでは、ブロックを制御するため、ブロック レベルのインターフェイス プロトコルがデザインに追加されます。ブロック レベルのインターフェイスのポートは、ブロックのデータ処理の開始 (ap_start)、新しい入力の受け入れ準備完了 (ap_ready)、デザインのアイドル状態 (ap_idle)、操作の終了 (ap_done) を制御します。これらの詳細は、ブロック レベル I/O プロトコル を参照してください。
ポート レベルの I/O プロトコルは、RTL モジュールのデータ ポートに割り当てる制御信号です。作成される I/O プロトコルは、C 引数のタイプとデフォルトによって異なります。ブロック レベルのプロトコルが使用されてブロックの演算が開始したら、ポート レベルの I/O プロトコルを使用して、データをブロック内外に順に送信できます。詳細は、ポート レベル I/O プロトコル を参照してください。
インターフェイス合成の I/O プロトコル
インターフェイス合成で作成されるインターフェイスのタイプは、C 引数のデータ型、ターゲット フロー、デフォルトのインターフェイス モードおよび INTERFACE 最適化指示子によって異なります。次の図は、各 C 引数のデータ型で指定できるインターフェイス プロトコル モードを示しています。この図では、次の省略語が使用されています。
- D: 各データ型のデフォルトのインターフェイス モード。注記: 不適切なインターフェイスが指定されると、Vitis HLS でメッセージが表示され、デフォルトのインターフェイス モードがインプリメントされます。
- I: 読み出しのみの入力引数。
- O: 書き込みのみの出力引数。
- I/O: 読み出しおよび書き込みの両方に使用される入力/出力引数。
Vitis カーネル フローの open_solution -flow_target vitis では、コードに INTERFACE プラグマがない場合は、次のインターフェイスが自動的に適用されます。
| 引数のタイプ | スカラー | 配列を指すポインター | hls::stream | |||
|---|---|---|---|---|---|---|
| インターフェイス モード | 入力 | 戻り値 | I | I/O | O | I および O |
| ap_ctrl_none | ||||||
| ap_ctrl_hs | ||||||
| ap_ctrl_chain | D | |||||
| axis | D | |||||
| m_axi | D | D | D | |||
| s_axi_lite | D | D | D | D | D | |
s_axi_lite インターフェイスは、スカラー入力のデフォルト インターフェイスで、ストリーミング以外のすべてのポートの I/O ポート制御インターフェイスでもあります。次のセクションでは、各インターフェイス モードの内容と、インターフェイス プロトコルの詳細を波形図も含めて説明します。
ブロック レベル I/O プロトコル
Vitis HLS では、ap_ctrl_none、ap_ctrl_hs、および ap_ctrl_chain インターフェイスを使用して、RTL をブロック レベルのハンドシェイク信号を含めてインプリメントするかどうかを指定します。ブロック レベルのハンドシェイク信号では、次が指定されます。
- デザインが演算の実行を開始するタイミング
- 演算が終了するタイミング
- デザインがアイドル状態になって新しい入力に対する準備ができるタイミング
ap_return 出力ポートを作成します。 s_axilite) として指定されている場合は、ブロック レベル インターフェイスのポートはすべて、デフォルトで control と呼ばれる AXI4-Lite インターフェイスにまとめられます。これは、CPU などの別のデバイスがブロックの動作の開始および停止を設定および制御する場合によく使用される方法です。デフォルトは ap_ctrl_hs ブロック レベル I/O プロトコルです。次の図に、Vitis HLS で関数に ap_ctrl_hs がインプリメントされる場合の結果の RTL ポートと動作を示します。この例では、関数に return 文を使用して値を返しており、Vitis HLS で RTL デザインに ap_return 出力ポートが作成されます。関数の return 文が C コードに含まれていない場合は、このポートは作成されません。
ap_ctrl_chain インターフェイス モードは ap_ctrl_hs と似ていますが、バック プレッシャーを適用するために入力信号 ap_continue が追加されている点が異なります。Vitis HLS ブロックをチェーン接続する場合は、ap_ctrl_chain ブロックレベル I/O プロトコルを使用することをお勧めします。
ap_ctrl_none
ap_ctrl_none ブロック レベル I/O プロトコルを指定すると、上の図に示すハンドシェイク信号ポート (ap_start、ap_idle、ap_ready、および ap_done) は作成されません。このプロトコルを使用すると、フリーランニング カーネルなどで使用される制御信号のないブロックを作成できます。
ap_none ブロック レベル I/O プロトコルを使用する場合は、RTL デザインを検証するため、インターフェイス合成要件 に説明されている C/RTL 協調シミュレーションの条件の少なくとも 1 つを満たす必要があります。ap_ctrl_hs
次の図に、パイプライン処理されていないデザインの ap_ctrl_hs I/O プロトコルで作成されるブロック レベル ハンドシェイク信号の動作を示します。
リセット後は、次にようになります。
ap_startが High になるとブロックが操作を開始します。ap_idle出力は即座に Low になり、デザインがアイドル状態でなくなったことを示します。ap_start信号はap_readyが High になるまで High のままである必要があります。ap_readyが High になると、次のようになります。ap_startが High のままの場合、次のトランザクションを開始します。ap_startが Low になると、現在のトランザクションが完了して、操作を停止します。
- 入力ポートからデータを読み出せるようになります。
- 出力ポートにデータを書き込めるようになります。注記: 入力ポートおよび出力ポートでは、このブロック レベル I/O プロトコルからは独立したポート レベル I/O プロトコルも指定できます。詳細は、ポート レベル I/O プロトコルを参照してください。
- ブロックの操作が完了すると、
ap_done出力が High になります。注記:ap_returnポートがある場合、このポートの値はap_doneが High なると有効になります。つまり、ap_done信号はap_return出力のデータが有効であることも示します。 - デザインが新しい入力を受信可能な状態になると、
ap_ready信号が High になります。次に、ap_ready信号に関する追加情報を示します。ap_ready信号はデザインが動作を始めるまで非アクティブ (Low) です。- パイプライン処理されていないデザインでは、
ap_ready信号はap_doneと同時にアサートされます。 - パイプライン処理されたデザインでは、
ap_startが High になった後のどのサイクルでもap_ready信号が High になる可能性があります。これは、デザインがどのようにパイプライン処理されたかによって異なります。 ap_readyが High のときにap_startが Low になると、デザインはap_doneが High になるまで実行されてから停止します。ap_readyが High のときにap_startが High になると、次のトランザクションが即座に開始され、デザインが動作を続行します。
ap_idleは、デザインがアイドル状態で動作していないことを示します。次に、ap_idle信号に関する追加情報を示します。ap_readyが High のときにap_startが Low になると、デザインは動作を停止し、ap_idleはap_doneの 1 サイクル後に High になります。ap_readyが High のときにap_startが High になると、デザインは動作を継続し、ap_idleは Low のままになります。
ap_ctrl_chain
ap_ctrl_chain ブロック レベル I/O プロトコルは ap_ctrl_hs と似ていますが、ap_continue ポートが追加されている点が異なります。ap_continue 信号がアクティブ High の場合、出力データを使用するダウンストリーム ブロックが新しいデータ入力を読み出す準備ができたことを示します。ダウンストリーム ブロックで新しいデータ入力を消費できない場合は、ap_continue が Low になり、アップストリームで追加データが生成されなくなります。
ダウンストリーム ブロックの ap_ready ポートは ap_continue ポートを直接駆動できます。次に、ap_continue ポートに関する追加情報を示します。
ap_doneが High のときにap_continueが High の場合、デザインは動作を継続します。それ以外のブロック レベル I/O 信号の動作は、ap_ctrl_hsブロック レベル I/O プロトコルと同じです。ap_doneが High のときにap_continue信号が Low の場合、ap_done信号は High のままになり、ap_returnポートがある場合はap_returnポートのデータは有効なままになります。
次の図では、ap_done が High で ap_continue が High なので、最初のトランザクションの終了直後に、2 つ目のトランザクションが開始しますが、ap_continue が High にアサートされなければ、2 つ目のトランザクションが終了すると停止します。
ポート レベル I/O プロトコル
デフォルトでは、入力ポインターおよび値渡し引数は、ハンドシェイク信号のない単純なワイヤ ポートとしてインプリメントされます。たとえば、インターフェイス合成の概要 で説明した sum_io 関数では、入力ポートは I/O プロトコルなしのデータ ポートとしてインプリメントされます。ポートに I/O プロトコルがない場合 (デフォルトまたはそのように設計されている場合)、入力データは読み出されるまで安定させる必要があります。
sum_io 関数の例では、出力ポートは、ポートのデータがいつ有効になって読み出し可能になったかを示す出力 Valid ポート (sum_o_ap_vld) を関連付けてインプリメントされます。出力ポートに関連付けられた I/O ポートがない場合は、データを読み出すタイミングを判断するのは困難です。 読み出しおよび書き込みの両方が実行される関数引数は、入力ポートと出力ポートに分割されます。sum_io 関数の例では、sum 引数は入力ポート sum_i および出力ポート sum_o の両方としてインプリメントされ、I/O プロトコル ポート sum_o_ap_vld が関連付けられます。
関数に戻り値がある場合、その戻り値を示す出力ポート ap_return がインプリメントされます。RTL デザインが 1 つのトランザクション (C 関数の 1 回の実行) で終了する場合、ブロック レベルのプロトコルで ap_done 信号を使用して関数が終了したことが示されます。これは、ap_return ポートのデータが有効で読み出し可能になったことも示します。
たとえば、次のコード例はタイミング ビヘイビアーを示しています (そのターゲット テクノロジとクロック周波数でクロック サイクルごとに 1 つの加算が可能)。
- デザインは
ap_startがHighにアサートされると開始します。 ap_idle信号がLowにアサートされ、デザインが動作していることを示します。- 入力データは最初のサイクル後のクロックで読み出されます。読み出されるタイミングは Vitis HLS で自動的にスケジューリングされます。すべての入力が読み出されると、
ap_ready信号が High にアサートされます。 - 出力
sumが計算されると、関連する出力ハンドシェイク (sum_o_ap_vld) でデータが有効なことが示されます。 - 関数が終了すると、
ap_doneがアサートされます。これは、ap_returnのデータが有効であることも示します。 ap_idleポートがHighにアサートされ、デザインが再開を待機していることを示します。
ポート レベル I/O: AXI4 インターフェイス プロトコル
Vitis HLS でサポートされる AXI4 インターフェイスには、AXI4-Stream (axis)、AXI4-Lite (s_axilite)、および AXI4 マスター (m_axi) インターフェイスがあります。
open_solution -flow_target vitis)、Vitis HLS で使用されるデフォルトのポート レベル インターフェイスは AXI4 インターフェイスです。タイミングおよびポートを含む AXI4 インターフェイスの詳細は、『Vivado Design Suite: AXI リファレンス ガイド』 (UG1037: 英語版、日本語版) を参照してください。この I/O プロトコルの機能すべてについては、AXI4 インターフェイスの使用を参照してください。
axis
axis モードでは、AXI4-Stream I/O プロトコルが指定されます。このプロトコルは、入力/出力引数ではなく、入力引数または出力引数のみに指定します。
s_axilite
s_axilite モードでは、AXI4-Lite スレーブ I/O プロトコルが指定されます。このプロトコルは、ストリーム以外のどのタイプの引数にでも指定できます。
m_axi
m_axi モードでは、AXI4 マスター I/O プロトコルが指定されます。配列およびポインター (C++ ではリファレンスも) のみに指定します。
ポート レベル I/O: プロトコルなし
ap_none および ap_stable モードは、ポートに追加する I/O ポートがないことを指定します。これらのモードを指定すると、引数が関連信号のないデータ ポートとしてインプリメントされます。ap_none モードは、スカラー入力のデフォルトです。ap_stable モードは、デバイスがリセット モードのときにのみ変化するコンフィギュレーション入力用です。
ap_none
ap_none ポート レベル I/O プロトコルは最も単純なインターフェイス タイプで、ほかの信号は関連付けられていません。入力データ信号にも出力データ信号にも、データの読み出しまたは書き込みがいつ実行されているかを示す制御ポートはありません。RTL デザインに含まれるポートは、ソース コードで指定されているものだけです。
ap_none インターフェイスに追加のハードウェア オーバーヘッドは必要ありません。ただし、ap_none インターフェイスには次が必要です。
- 次の 1 つを実行するプロデューサー ブロック:
- 正しい時間に入力ポートにデータを供給
- デザインが終了するまでトランザクションの長さの間データを保持
- 正しい時間に出力ポートを読み出すコンシューマー ブロック
ap_none インターフェイスを配列引数には使用できません。ap_stable
ap_none と同様に、ap_stable ポート レベル I/O プロトコルではインターフェイス制御ポートは追加されません。ap_stable タイプは、コンフィギュレーション データを供給するポートなど、変化することはあるが通常の動作中は変化しないデータに使用されます。ap_stable タイプを使用すると、Vitis HLS に次の情報が伝えられます。
- ポートに適用されるデータが通常の動作中は安定していて変化しませんが、最適化可能な定数値ではありません。
- このポートからのファンアウトにはレジスタを付ける必要はありません。
ap_stable タイプは、入力ポートのみに適用できます。入出力ポートに適用すると、ポートの入力のみが安定していると想定されます。ポート レベル I/O: ワイヤ ハンドシェイク
インターフェイス モード ap_hs には、データ ポートの付いた 2 方向のハンドシェイク信号が含まれます。業界標準の有効 (valid) と承認 (acknowledge) ハンドシェイクに使用されます。ap_vld モードの場合は valid ポートしかなく、ap_ack モードの場合は acknowledge ポートしかありません。
入出力引数には、ap_ovld モードを使用します。入出力が入力ポートと出力ポートに分割される場合は、ap_none モードが入力ポートに、ap_vld モードが出力ポートに使用されます。これが、読み出しと書き込みの両方に使用されるポインター引数のデフォルトです。
ap_hs モードは、順に読み出しまたは書き込みが実行される配列に使用できます。Vitis HLS では読み出しまたは書き込みアクセスが順に実行されない場合はそれが検出され、エラー メッセージが表示されて合成が停止します。アクセス順序が決定できない場合は、Vitis HLS で警告メッセージが表示されます。
ap_hs (ap_ack、ap_vld、および ap_ovld)
ap_hs ポート レベル I/O プロトコルを使用すると、開発プロセス中の柔軟性が最大となり、ボトムアップおよびトップダウン デザイン フローの両方が可能になります。2 方向ハンドシェイクによりすべてのブロック間の通信が安全に実行されるので、正しい動作に手動の介入や想定は必要ありません。ap_hs ポート レベル I/O プロトコルには、次の信号が含まれます。
- データ ポート
- データが受信されたことを示す承認 (ACK) 信号。
- データが読み出されたことを示す Valid 信号。
次の図に、入力ポートと出力ポートでの ap_hs インターフェイスの動作を示します。この例では、入力ポート名は in、出力ポート名は out です。
in の valid ポートの名前は in_vld です。入力では、次のようになります。
- ap_start が High になると、ブロックが通常の動作を開始します。
- デザインが入力データを読み出す準備ができても、valid 入力が Low の場合デザインは停止し、valid 入力が High になって新しい入力値が有効であることが示されるまで待機します。注記: 前の図は、この動作を示しています。この例では、デザインはクロック サイクル 4 でデータ入力を読み出す準備ができ、データを読み出す前に valid 入力を待ちます。
- valid 入力が High になると、承認出力が High になり、データが読み出されたことが示されます。
出力では、次のようになります。
- ap_start が High になると、ブロックが通常の動作を開始します。
- 出力ポートへの書き込みが実行されると、同時に valid 出力信号が High になり、ポートに valid データが存在することが示されます。
- in_ack 入力が Low の場合、デザインが停止し、in_ack 入力が High になるまで待機します。
- in_ack 入力が High になると、次のクロック エッジで valid 出力が Low になります。
ap_ack
ap_ack ポート レベル I/O プロトコルは ap_hs インターフェイス タイプのサブセットです。ap_ack ポート レベル I/O プロトコルには、次の信号が含まれます。
- データ ポート
- データが受信されたことを示す承認 (ACK) 信号
- 入力引数に対しては、出力 ACK ポートが生成され、入力が読み出されるサイクルで High になります。
- 出力引数に対しては、Vitis HLS で入力 ACK ポートがインプリメントされ、出力が読み出されたことが確認されます。
注記: 書き込み操作後、デザインが停止し、ACK 入力が High になって出力がコンシューマー ブロックにより読み出されたことが示されるまで待機します。ただし、データが使用可能であることを示す出力ポートはありません。
ap_ack を使用するデザインを検証できません。ap_vld
ap_vld は ap_hs インターフェイス タイプのサブセットです。ap_vld ポート レベル I/O プロトコルには、次の信号が含まれます。
- データ ポート
- データが読み出されたことを示す valid 信号
- 入力引数に対しては、デザインは valid 信号が High になった直後にデータ ポートを読み出します。デザインが新しいデータを読み出す準備ができていなくても、データ ポートがサンプリングされ、必要になるまでデータが内部で保持されます。
- 出力引数に対しては、Vitis HLS で出力 valid ポートがインプリメントされ、出力ポートのデータが valid になったことを示します。
ap_ovld
ap_ovld は ap_hs インターフェイス タイプのサブセットです。ap_ovld ポート レベル I/O プロトコルには、次の信号が含まれます。
- データ ポート
- データが読み出されたことを示す valid 信号
- 入力引数と、入出力引数の入力部分に対しては、デザインはデフォルトで
ap_noneタイプになります。 - 出力引数と、入出力引数の出力部分に対しては、デザインは
ap_vldタイプをインプリメントします。
- 入力引数と、入出力引数の入力部分に対しては、デザインはデフォルトで
ポート レベル I/O: メモリ インターフェイス プロトコル
配列引数は、デフォルトでは ap_memory インターフェイスとしてインプリメントされます。これは、データ ポート、アドレス ポート、チップイネーブル ポート、ライトイネーブル ポートを含む標準ブロック RAM インターフェイスです。
ap_memory インターフェイスは、デュアル ポート インターフェイスのシングル ポートとしてインプリメントされる可能性があります。Vitis HLS でデュアル ポート インターフェイスを使用することで開始間隔 (II) が削減されると判断された場合は、デュアル ポート インターフェイスが自動的にインプリメントされます。BIND_STORAGE プラグマまたは指示子を使用してメモリ リソースを指定し、シングル ポート ブロック RAM を含む配列に指定した場合は、シングル ポート インターフェイスがインプリメントされます。また、BIND_STORAGE プラグマでデュアル ポート インターフェイスを指定していても、このインターフェイスを使用しても利点がないと Vitis HLS で判断された場合は、シングル ポート インターフェイスが自動的にインプリメントされます。
bram インターフェイス モードは、ap_memory と機能的には同じですが、デザインが Vitis IP インテグレーターで使用される際のポートのインプリメント方法が異なります。
ap_memoryインターフェイスは複数の別々のポートとして表示されます。bramインターフェイスは 1 つのまとまったポートとして表示され、1 つのポイント ツー ポイント接続を使用してザイリンクス ブロック RAM に接続できます。
配列がシーケンシャルにアクセスされる場合は、ap_fifo インターフェイスを使用できます。ap_hs インターフェイスを使用した場合と同様、データ アクセスがシーケンシャルでないと判断されたら Vitis HLS が停止します。アクセスがシーケンシャルかどうか判別できなかった場合は警告メッセージが表示され、シーケンシャルであると判断された場合はメッセージは表示されません。ap_fifo は、読み出しまたは書き込みのいずれかにのみ使用可能で、両方には使用できません。
ap_memory、bram
ap_memory および bram インターフェイス ポート レベル I/O プロトコルは、配列引数をインプリメントするために使用されます。このタイプのポート レベル I/O プロトコルは、インプリメンテーションでメモリのアドレス ロケーションにランダム アクセスが必要な場合に、メモリ エレメント (RAM、ROM など) と通信するために使用されます。
ap_fifo インターフェイスを使用してください。ap_fifo インターフェイスを使用すると、アドレス生成は実行されないので、ハードウェア オーバーヘッドが削減します。ap_memory と bram インターフェイス ポート レベル I/O プロトコルは同じですが、Vivado IP インテグレーターでのブロックの表示方法だけが異なります。
ap_memoryインターフェイスは個別のポートとして表示される。bramインターフェイスは 1 つのポートとして表示される。IP インテグレーターでは、1 つの接続を使用して、すべてのポートへの接続を作成できます。
ap_memory インターフェイスを使用する場合は、BIND_STORAGE 指示子を使用して配列ターゲットを指定してください。配列に対してターゲットを指定しない場合は、Vitis HLS でシングル ポートまたはデュアル ポート RAM インターフェイスのいずれかが指定されます。
次の図では、d という配列がシングル ポート ブロック RAM として指定されています。ポート名は、C 関数引数に基づいています。たとえば、C 関数が d の場合、チップ イネーブルは d_ce、入力データは BRAM の output/q ポートに基づいて d_q0 になります。
リセット後は、次にようになります。
- ap_start が High になると、ブロックが通常の動作を開始します。
- 出力信号
d_ceをアサートしたときに出力アドレス ポートにアドレスを供給することにより、読み出しが実行されます。注記: デフォルトのブロック RAM では、入力データd_q0が次のクロック サイクルで有効になると想定されます。BIND_STORAGE プラグマを使用すると、RAM の読み出しレイテンシを長くできます。 - 書き込み操作は、出力ポート
d_ceとd_weがアサートされ、同時にアドレスと出力データd_d0が供給されると実行されます。
ap_fifo
出力ポートが書き込まれると、デザインがメモリ エレメントにアクセスする必要があり、そのアクセスが常に順番に実行される、つまりランダム アクセスが必要ない場合、ap_fifo に関連する出力 valid 信号インターフェイスを使用するのが最もハードウェア効率の良いアプローチです。ap_fifo ポート レベル I/O プロトコルでは、次がサポートされます。
- ポートを FIFO に接続できるようになる
- 完全な 2 方向の
empty-full通信を可能にする - 配列、ポインター、参照渡し引数タイプに使用できる
ap_fifo インターフェイスを使用可能な関数では、ポインターがよく使用され、同じ変数に複数回アクセスする可能性があります。マルチアクセス ポインター インターフェイス: ストリーミング データを参照し、このコーディング スタイルを使用する場合の volatile 修飾子の重要性を理解してください。次の例では、in1 は現在のアドレスにアクセスし、その後現在のアドレスの上 2 つのアドレスにアクセスして、最後に 1 つ下のアドレスにアクセスするポインターです。
void foo(int* in1, ...) {
int data1, data2, data3;
...
data1= *in1;
data2= *(in1+2);
data3= *(in1-1);
...
}
in1 を ap_fifo インターフェイスとして指定すると、Vitis HLS でアクセスがチェックされ、アクセスが順次でない場合はエラー メッセージが表示され、停止します。順次アドレスではないアドレス位置から読み出すには、ap_memory または bram インターフェイスを使用します。
読み出しおよび書き込みの両方に使用される場合、引数に ap_fifo インターフェイスは指定できません。ap_fifo インターフェイスは、入力引数または出力引数にのみ指定可能です。入力引数 in、出力引数 out を ap_fifo インターフェイスとして指定したデザインは、次の図のように動作します。
入力では、次のようになります。
- ap_start が High になると、ブロックが通常の動作を開始します。
- 入力ポートで読み出し準備ができたのに FIFO が空の場合 (
in_empty_n入力ポートが Low)、デザインは停止し、データを読み出せるようになるまで待機します。 in_empty_n入力ポートが High になって FIFO にデータが含まれていることが示されると、出力 ACK のin_readが High にアサートされてデータがこのサイクルで読み出されたことが示されます。
出力では、次のようになります。
- ap_start が High になると、ブロックが通常の動作を開始します。
- 出力ポートで書き込み準備ができても、FIFO がフルの場合 (
out_full_nが Low)、データは出力ポートに配置されますが、デザインは停止し、FIFO に書き込むスペースができるまで待機します。 - FIFO に書き込むスペースができると (
out_full_n入力が High)、出力 ACK 信号のout_writeが High になり、出力データが valid であることが示されます。 -rewindオプションで最上位関数または最上位ループがパイプライン処理されると、Vitis HLS では_lwrが接尾語に付いた追加の出力ポートが作成されます。FIFO インターフェイスへの最後の書き込みが終了すると、_lwrポートがアクティブ High になります。
インターフェイス合成および構造体
Vitis HLS では、デフォルトでインターフェイスの構造体が集められ、構造体内のすべての要素が 1 つの幅の広いベクターにまとめられます。これにより、構造体のすべてのメンバーを同時に読み出しおよび書き込みできます。構造体のパディングとアライメント で説明するように、集合処理では構造体の要素が 4 バイトで揃えられますが、そのためにビット パディングが必要とされることもあります。
__attribute__(packed) を使用する必要があります。構造体のメンバー要素は C コードに記述されている順序でベクターに配置されます (構造体の最初の要素がベクターの LSB に、最後の要素がベクターの MSB に配置される)。これにより、1 クロック サイクルでより多くのデータにアクセスできるようになります。構造体の配列は個別の配列要素に分割され、下位から上位の順のベクターに配置されます。
次の例では、struct data_t がヘッダー ファイルで定義されています。構造体には、2 つのデータ メンバーが含まれます。
short型 (16 ビット) の符号なしベクターvarA。- 4 つの
unsigned char型 (8 ビット) の配列varB。typedef struct { unsigned short varA; unsigned char varB[4]; } data_t; data_t struct_port(data_t i_val, data_t *i_pt, data_t *o_pt);
構造体をインターフェイスにまとめると、16 ビットの varA と 8 ビットの
varB を含む 1 つの 24 ビット ポートになります。
構造体に配列が含まれる場合、構造体をまとめることにより、ARRAY_RESHAPE プラグマと同様の処理が実行され、再形成された配列が構造体内のほかの要素と結合されます。パックされた構造体は、ARRAY_PARTITION や ARRAY_RESHAPE を使用して最適化できません。
Vitis HLS で合成できる構造体のサイズや複雑さに制限はありません。構造体には必要なだけの配列次元およびメンバーを含めることができます。構造体のインプリメンテーションでの唯一の制限は、ストリーミングとして (たとえば配列が FIFO インターフェイスとして) インプリメントされる場合にあります。この場合、そのインターフェイスの配列に適用するのと同じ一般規則に従う必要があります。
インターフェイス合成およびマルチアクセス ポインター
複数回アクセスされるポインターを使用すると、合成後に予測しない動作になることがあります。次の例では、ポインター d_i が 4 回読み出され、ポインター d_o に 2 回書き込まれます。
#include "pointer_stream_bad.h"
void pointer_stream_bad ( dout_t *d_o, din_t *d_i) {
din_t acc = 0;
acc += *d_i;
acc += *d_i;
*d_o = acc;
acc += *d_i;
acc += *d_i;
*d_o = acc;
}
このコードを合成すると、入力ポートが 1 回読み出され、出力ポートに 1 回書き込まれる RTL になります。標準的な C コンパイラと同様、Vitis HLS では最適化で不必要なポインター アクセスが削除されます。上記のコードを d_i で 4 回読み出し、d_o に 2 回書き込むようにインプリメントするには、次の例のようにポインターを volatile と指定する必要があります。
#include "pointer_stream_better.h"
void pointer_stream_better ( volatile dout_t *d_o, volatile din_t *d_i) {
din_t acc = 0;
acc += *d_i;
acc += *d_i;
*d_o = acc;
acc += *d_i;
acc += *d_i;
*d_o = acc;
}
この C コードでも問題となる可能性があります。テストベンチを使用した場合、d_i に複数の値を提供する方法はなく、最後の書き込み以外の d_o への書き込みを検証する方法もありません。マルチアクセス ポインターはサポートされていますが、hls::stream クラスを使用して必要な動作をインプリメントすることをお勧めします。hls::stream クラスの詳細は、HLS ストリーム ライブラリを参照してください。
インターフェイスの指定
デフォルトでは、インターフェイスの合成は、config_interface で定義されたコンフィギュレーション設定と、最上位関数の引数のタイプによって制御されます。コンフィギュレーション設定で定義されているデフォルトを変更するには、コンフィギュレーション オプションの設定 で説明されているように、Solution Settings ダイアログ ボックスを開きます。
INTERFACE プラグマまたは指示子を使用すると、特定の関数引数またはインターフェイス ポートの詳細を指定することにより、必要に応じてデフォルト設定に補足したり、別の設定を使用したりできます。ポートのインターフェイス モードを指定するには、Vitis HLS GUI の Directives タブで最上位ポートまたは引数を右クリックして Insert Directive をクリックし、次の図に示す Vitis HLS Directive Editor を開きます。
- mode
ドロップダウン リストからインターフェイス モードを選択します。[Directive Editor] ダイアログ ボックスに表示される選択肢は、ポートに選択したインターフェイス モードによって異なります。
- register
すべての値渡し読み出しを演算の最初のサイクルで実行します。出力ポートには、レジスタを付けるオプションを選択すると、出力に必ずレジスタが付くようになります。このレジスタ オプションはデザインのどのファンクションにも適用できます。メモリ、FIFO、および AXI4 インターフェイスの場合は、レジスタ オプションを設定しても効果はありません。
- port
このオプションは必須です。デフォルトでは、Vitis HLS はポートにレジスタは付けません。
- depthテストベンチからデザインに供給されるサンプル数と、テストベンチで格納する必要のある出力値の数を指定します。大きい方の数値を使用してください。注記: 1 つのトランザクションでポインターの読み出しまたは書き込みが複数回実行される場合は、C/RTL 協調シミュレーションのために depth オプションが必要です。depth オプションは、配列の場合および
hls::streamコンストラクトを使用する場合は不要です。インターフェイスでポインターを使用する場合にのみ必要になります。depth オプションの値が小さすぎると、C/RTL 協調シミュレーションが次のようなデッドロック状態になる可能性があります。
- 入力読み出しが、テストベンチでは提供できないデータを待機中に停止する。
- ストレージがフルであるため、データを書き込もうとすると、出力の書き込みが停止する。
- offset
このオプションは AXI4 インターフェイスの場合に使用します。
AXI4 インターフェイスの使用
AXI4-Stream インターフェイス
AXI4-Stream インターフェイスは、どの入力引数でも、どの配列またはポインター出力引数にでも使用できます。AXI4-Stream インターフェイスはデータをシーケンシャル ストリーミングで送信するので、読み込みと書き出しの両方を実行する引数とは併用できません。AXI4-Stream インターフェイスは常に次のバイトに符号拡張されます。たとえば、12 ビットのデータ値は 16 ビットに符号拡張されます。
AXI4-Stream インターフェイスは、複数の HLS IP ブロックと AXI-Stream インターフェイスがより大きなデザインに統合される際に、組み合わせフィードバック パスが作成されないように、常にレジスタ付きインターフェイスとしてインプリメントされます。AXI-Stream インターフェイスには、AXI-Stream インターフェイスのレジスタのインプリメント方法を制御する 4 つのレジスタ モードが含まれます。
- Forward:
TDATAおよびTVALID信号のみがレジスタに送信されます。 - Reverse:
TREADY信号のみがレジスタに送信されます。 - Both: すべての信号 (
TDATA、TREADY、TVALID) がレジスタに送信されます。これがデフォルトです。 - Off: どのポート信号もレジスタに送信されません。
AXI-Stream サイドチャネル信号はデータ信号と考慮され、TDATA にレジスタが付けられるとレジスタが付けられます。
デザインで AXI4-Stream を使用するには、基本的に次の 2 つの方法があります。
- サイドチャネルなしで AXI4-Stream を使用します。
- サイドチャネルありで AXI4-Stream を使用します。
この 2 つ目のユース ケースの場合は、AXI4-Stream 規格の一部であるオプションのサイドチャネルが C コードで直接使用できるという機能もあります。
サイドチャネルなしの AXI4-Stream インターフェイス
AXI4-Stream は、関数引数 ap_axis 型に AXI4 サイドチャネル エレメントが含まれない場合は、サイドチャネルなしで使用されます。この例では、両方のインターフェイスが AXI4-Stream を使用してインプリメントされます。
#include "ap_axi_sdata.h"
#include "hls_stream.h"
void example(hls::stream< ap_axis<32>> &A,
hls::stream< ap_axis<32>> &B)
{
#pragma HLS INTERFACE axis port=A
#pragma HLS INTERFACE axis port=B
ap_axis<32> tmp;
A.read(tmp);
tmp.data = tmp + 5;
B.write(tmp);
}
合成後は、次の図に示すように、どちらの引数もデータ ポートと標準 AXI4-Stream の TVALID および TREADY ポートを使用してインプリメントされます。
構造体を使用して複数の変数を 1 つの AXI4-Stream インターフェイスにまとめることができ、Vitis HLS によりデフォルトで統合されます。構造体の要素を 1 つの幅の広いベクターに統合すると、構造体のすべての要素を同じ AXI4-Stream インターフェイスにインプリメントできます。
サイドチャネルありの AXI4-Stream インターフェイス
次の例は、サイドチャネルを C コードで直接使用する方法とインターフェイスへのインプリメント方法を示しています。このコードは、API 用に ap_axi_sdata.h インクルード ファイルを使用して、AXI4-Stream インターフェイスのサイドチャネルを処理します。この例の場合、符号付き 32 ビットのデータ型が使用されています。
#include "ap_axi_sdata.h"
#include "ap_int.h"
#include "hkls_stream.h"
#define DWIDTH 32
typedef ap_axiu<DWIDTH, 0, 0, 0> trans_pkt;
extern "C"{
void krnl_stream_vmult(hls::stream<trans+pkt> &b, hls::stream<trans_pkt> &output) {
#pragma HLS INTERFACE axis port=b
#pragma HLS INTERFACE axis port=output
#pragma HLS INTERFACE s_axilite port=return bundle=control
bool eos = false;
vmult:
do {
#pragma HLS PIPLINE II=1
trans_pkt t2 = b.read();
//Patcket for Output
trans_pkt t_out;
// Reading data from input packet
ap_uint<DWIDTH> in2 = t2.data;
ap_uint<DWIDTH> tmpOut = in2 * 5;
// Setting data and configuration to output packet
t_out.data = tmpOut;
t_out.last = t1.get_last();
t_out.keep = -1; //Enabling all bytes
//Writing packet to output stream
output.write(t_out);
if(t.get_last()) {
eos = true;
}
} while (eos == false);
}
}
合成後は、どちらの引数も、データ ポート、標準 AXI4-Stream の TVALID および TREADY プロトコル ポート、および構造体で記述されたオプションのポートすべてを使用してインプリメントされます。
構造体の AXI4-Stream インターフェイスへのパック
AXI4-Stream インターフェイスと一緒に構造体を使用する場合は、デフォルトの合成動作が異なります。構造体のデフォルトの合成動作については、インターフェイス合成および構造体を参照してください。
サイドチャネルありまたはなしの AXI4-Stream インターフェイスを使用していて、関数引数が構造体の場合、Vitis HLS でその構造体のすべての要素が単一幅のデータ ベクターにまとめられます。インターフェイスは、関連する TVALID および TREADY 信号を使用して単一幅のデータ ベクターとしてインプリメントされます。
サイドチャネルありの AXI4-Stream インターフェイスを使用する場合、Vitis HLS ツールでは RGBPixel などの構造体を axis インターフェイスを介して送信できません。独自の構造体を定義するために ap_axiu 構造体をオーバーロードすることはできません。この制限を回避するには、構造体初期化関数を使用して意図する結果を取得します。
struct RGBPixel
{
unsigned char r;
unsigned char g;
unsigned char b;
unsigned char a;
RGBPixel(ap_int<32> d) : r(d.range(7,0)), g(d.range(15,8)), b(d.range(23,16)), a(d.range(31,24)) {
#pragma HLS INLINE
}
operator ap_int<32>() {
#pragma HLS INLINE
return (ap_int<8>(a), ap_int<8>(b), ap_int<8>(g), ap_int<8>(r));
}
}__attribute__((aligned(4)));
AXI4-Lite インターフェイス
AXI4-Lite インターフェイスを使用すると、デザインを CPU やマイクロプロセッサで制御できるようになります。Vitis HLS の AXI4-Lite インターフェイスを使用すると、次が実行できるようになります。
- 複数のポートを同じ AXI4-Lite インターフェイスにまとめることができます。
- プロセッサで実行されるコードと一緒に使用できる C ドライバー ファイルが出力できます。注記: これにより、C アプリケーション プログラム インターフェイス (API) 関数のセットが提供されるので、ソフトウェアからハードウェアを簡単に制御できるようになります。これは、デザインを IP カタログにエクスポートする場合に便利です。
次は、Vitis HLS で関数リターンを含む複数の引数が AXI4-Lite インターフェイスとしてインプリメントされる例を示しています。各インターフェイスで bundle オプションに同じ名前が使用されるので、各ポートが同じ AXI4-Lite インターフェイスにまとめられます。
void example(char *a, char *b, char *c)
{
#pragma HLS INTERFACE s_axilite port=return bundle=BUS_A
#pragma HLS INTERFACE s_axilite port=a bundle=BUS_A
#pragma HLS INTERFACE s_axilite port=b bundle=BUS_A
#pragma HLS INTERFACE s_axilite port=c bundle=BUS_A offset=0x0400
#pragma HLS INTERFACE ap_vld port=b
*c += *a + *b;
}
bundle オプションを使用しない場合、Vitis HLS は AXI4-Lite インターフェイスを使用して、指定されたすべての引数を同じデフォルトのバンドルにまとめ、自動的にポートに名前を付けます。また、I/O プロトコルも AXI4-Lite インターフェイスにまとめられたポートへ割り当てることができます。上記の例の場合、Vitis HLS はポート b を ap_vld インターフェイスとしてインプリメントしてから、ポート b を AXI4-Lite インターフェイスにまとめます。このため、AXI4-Lite インターフェイスには、ポート b のデータ用のレジスタ、ポート b が読み出されたことを承認する出力用のレジスタ、ポート b の入力有効信号用のレジスタが含まれます。
ポート b が読み出されるたびに、Vitis HLS は自動的に入力有効信号をクリアにして、ロジック 0 にレジスタをリセットします。入力 valid レジスタがロジック 1 に設定されない場合、b データ レジスタのデータは有効だと判断されないので、デザインが停止し、valid レジスタの設定を待機する状態になります。
return ポートに関連するブロック レベルの I/O プロトコルは含めることを勧めしています。配列は、bram インターフェイスを使用して AXI4-Lite インターフェイスに割り当てることができません。配列は、デフォルトの ap_memory インターフェイスを使用してのみ AXI4-Lite インターフェイスに割り当てることができます。I/O プロトコルの ap_stable で指定した引数も AXI4-Lite インターフェイスには割り当てることができません。
AXI4-Lite インターフェイスにまとめられた変数は関数引数であり、それ自体は C コードでデフォルト値を割り当てることができないので、AXI4-Lite インターフェイスのどのレジスタもデフォルト値を割り当てられない可能性があります。レジスタは config_rtl コマンドを使用するとリセットを付けてインプリメントできますが、それ以外のデフォルト値を割り当てることはできません。
デフォルトでは、Vitis HLS で AXI4-Lite インターフェイスにまとめられる各ポートのアドレスが自動的に割り当てられます。Vitis HLS からは、C ドライバー ファイルに割り当てられたアドレスが提供されます。詳細は、C ドライバー ファイル を参照してください。アドレスを明示的に定義するには、上記の例の引数 c で示したように、offset オプションを使用します。
合成後、Vitis HLS は次の図に示すように AXI4-Lite インターフェイスにポートをインプリメントします。Vitis HLS は、AXI4-Lite インターフェイスに関数リターンを含めて、割り込みポートを作成します。割り込みは、AXI4-Lite インターフェイスを使用してプログラムできます。割り込みは、次のブロック レベル プロトコルから駆動することもできます。
ap_done: 関数がすべての操作を終了したことを示します。ap_ready: 関数が新しい入力データを受信する準備ができたことを示します。
インターフェイスは、C ドライバー ファイルを使用してプログラムできます。
AXI4-Lite インターフェイスの制御クロックおよびリセット
デフォルトでは、Vitis HLS では AXI4-Lite インターフェイスと合成済みデザインに同じクロックが使用されます。また、Vitis HLS では、AXI4-Lite インターフェイスのすべてのレジスタが合成されたロジック (ap_clk) に使用されるクロックに接続されます。
INTERFACE 指示子に clock オプションを使用すると、AXI4-Lite ポートごとに別々のクロックを指定できます (オプション)。クロックを AXI4-Lite インターフェイスに接続する場合は、次のプロトコルを使用する必要があります。
- AXI4-Lite インターフェイス クロックは、合成されたロジック (
ap_clk) に使用されるクロックと同期している必要があります。つまり、どちらのクロックも同じマスター ジェネレーター クロックから派生している必要があります。 - AXI4-Lite インターフェイス クロックの周波数は、合成されたロジック (
ap_clk) に使用されるクロックの周波数以下にする必要があります。
インターフェイス指示子に clock オプションを使用する場合、clock オプションは各バンドルの 1 つの関数引数に指定するだけです。Vitis HLS では、バンドル内のその他すべての関数引数が同じクロックとリセットを使用してインプリメントされます。Vitis HLS では、リセット信号が ap_rst_ の後にクロック名が付いた名前で生成されます。生成されるリセット信号は、config_rtl コマンドとは関係なくアクティブ Low になります。
次の例では、Vitis HLS で関数引数 a および b が AXI_clk1 というクロックと関連するリセット ポートと一緒に AXI4-Lite ポートにまとめられるところを示しています。
// Default AXI-Lite interface implemented with independent clock called AXI_clk1
#pragma HLS interface s_axilite port=a clock=AXI_clk1
#pragma HLS interface s_axilite port=b
次の例では、Vitis HLS で関数引数 c および d が CTRL1 というクロックと関連するリセット ポートを使用して AXI4-Lite ポートの AXI_clk2 にまとめられるところを示しています。
// CTRL1 AXI-Lite bundle implemented with a separate clock (called AXI_clk2)
#pragma HLS interface s_axilite port=c bundle=CTRL1 clock=AXI_clk2
#pragma HLS interface s_axilite port=d bundle=CTRL1
C ドライバー ファイル
AXI4-Lite スレーブ インターフェイスがインプリメントされると、C ドライバー ファイルのセットが自動的に作成されます。これらの C ドライバー ファイルは、CPU で実行されるどのソフトウェアにも統合できる API を含んでおり、AXI4-Lite スレーブ インターフェイスを介してデバイスとの通信に使用されます。
C ドライバー ファイルは、デザインが IP カタログで IP としてパッケージされると作成されます。
ドライバー ファイルは、スタンドアロン モードと Linux モード用に作成されます。スタンドアロン モードの場合、ドライバーがその他のザイリンクス スタンドアロン ドライバーと同じように使用されます。Linux モードの場合、すべての C ファイル (.c) とヘッダー ファイル (.h) がソフトウェア プロジェクトにコピーされます。
合成では、ドライバー ファイルと API 関数に最上位関数からの名前が一部使用されます。上記の例の場合、最上位関数の名前は example です。最上位関数が DUT であれば、次の説明に使用されている「example」は「DUT」になります。ドライバー ファイルがパッケージされた IP (solution フォルダー内の impl ディレクトリ) に作成されます。
| ファイル パス | 使用モード | 説明 |
|---|---|---|
| data/example.mdd | スタンドアロン | ドライバー定義ファイル。 |
| data/example.tcl | スタンドアロン | ソフトウェアを SDK プロジェクトに統合するために SDK で使用。 |
| src/xexample_hw.h | 両方 | すべての内部レジスタのアドレス オフセットを定義。 |
| src/xexample.h | 両方 | API 定義 |
| src/xexample.c | 両方 | 標準 API インプリメンテーション |
| src/xexample_sinit.c | スタンドアロン | 初期化 API インプリメンテーション |
| src/xexample_linux.c | Linux | 初期化 API インプリメンテーション |
| src/Makefile | スタンドアロン | Makefile |
xexample.h ファイルでは、次の 2 つの構造体が定義されています。
- XExample_Config
- IP インスタンスのコンフィギュレーション情報 (各 AXI4-Lite スレーブ インターフェイスのベース アドレス) を保持するのに使用されます。
- XExample
- IP インスタンスのポインターを保持するのに使用されます。ほとんどの API ではこのインスタンス ポインターが最初の引数として認識されます。
標準 API インプリメンテーションは、xexample.c、xexample_sinit.c、xexample_linux.c ファイルで提供されており、次の操作を実行する関数が含まれます。
- デバイスを初期化
- デバイスを制御し、そのステータスをクエリ
- レジスタの読み出し/書き込み
- 割り込みの設定、監視および制御
C ドライバー ファイルで提供される API 関数の詳細は、AXI4-Lite スレーブの C ドライバーのリファレンス を参照してください。
C ドライバー ファイルと float 型
C ドライバー ファイルは、常にデータ転送に 32 ビット符号なし整数 (U32) を使用します。次の例では、関数で float 型の引数 a と r1 が使用されています。a の値が設定され、r1 の値が返されます。
float caculate(float a, float *r1)
{
#pragma HLS INTERFACE ap_vld register port=r1
#pragma HLS INTERFACE s_axilite port=a
#pragma HLS INTERFACE s_axilite port=r1
#pragma HLS INTERFACE s_axilite port=return
*r1 = 0.5f*a;
return (a>0);
}
合成後、Vitis HLS ですべてのポートがデフォルトの AXI4-Lite インターフェイスにまとめられ、C ドライバー ファイルが作成されますが、次の図に示すように、ドライバー ファイルでは U32 型が使用されています。
// API to set the value of A
void XCaculate_SetA(XCaculate *InstancePtr, u32 Data) {
Xil_AssertVoid(InstancePtr != NULL);
Xil_AssertVoid(InstancePtr->IsReady == XIL_COMPONENT_IS_READY);
XCaculate_WriteReg(InstancePtr->Hls_periph_bus_BaseAddress,
XCACULATE_HLS_PERIPH_BUS_ADDR_A_DATA, Data);
}
// API to get the value of R1
u32 XCaculate_GetR1(XCaculate *InstancePtr) {
u32 Data;
Xil_AssertNonvoid(InstancePtr != NULL);
Xil_AssertNonvoid(InstancePtr->IsReady == XIL_COMPONENT_IS_READY);
Data = XCaculate_ReadReg(InstancePtr->Hls_periph_bus_BaseAddress,
XCACULATE_HLS_PERIPH_BUS_ADDR_R1_DATA);
return Data;
}
これらの関数が float 型で直接使用できる場合は、書き込み値および読み出し値が予測される float 型と一貫しません。これらの関数をソフトウェアで使用する場合は、次を使用して型を変換します。
float a=3.0f,r1;
u32 ua,ur1;
// cast float “a” to type U32
XCaculate_SetA(&calculate,*((u32*)&a));
ur1=XCaculate_GetR1(&caculate);
// cast return type U32 to float type for “r1”
r1=*((float*)&ur1);
ハードウェアの制御
ハードウェア ヘッダー ファイル (この例の場合 xexample_hw.h) には、AXI4-Lite スレーブ インターフェイスにまとめられたポートのメモリ マップド ロケーションのリストが含まれます。
// 0x00 : Control signals
// bit 0 - ap_start (Read/Write/SC)
// bit 1 - ap_done (Read/COR)
// bit 2 - ap_idle (Read)
// bit 3 - ap_ready (Read)
// bit 7 - auto_restart (Read/Write)
// others - reserved
// 0x04 : Global Interrupt Enable Register
// bit 0 - Global Interrupt Enable (Read/Write)
// others - reserved
// 0x08 : IP Interrupt Enable Register (Read/Write)
// bit 0 - Channel 0 (ap_done)
// bit 1 - Channel 1 (ap_ready)
// 0x0c : IP Interrupt Status Register (Read/TOW)
// bit 0 - Channel 0 (ap_done)
// others - reserved
// 0x10 : Data signal of a
// bit 7~0 - a[7:0] (Read/Write)
// others - reserved
// 0x14 : reserved
// 0x18 : Data signal of b
// bit 7~0 - b[7:0] (Read/Write)
// others - reserved
// 0x1c : reserved
// 0x20 : Data signal of c_i
// bit 7~0 - c_i[7:0] (Read/Write)
// others - reserved
// 0x24 : reserved
// 0x28 : Data signal of c_o
// bit 7~0 - c_o[7:0] (Read)
// others - reserved
// 0x2c : Control signal of c_o
// bit 0 - c_o_ap_vld (Read/COR)
// others - reserved
// (SC = Self Clear, COR = Clear on Read, TOW = Toggle on Write, COH = Clear on
Handshake)
AXI4-Lite スレーブ インターフェイスのレジスタを正しくプログラムするには、ハードウェア ポートがどのように動作するのかを理解しておく必要があります。インターフェイス合成の I/O プロトコルで説明したように、ブロックは同じポート プロトコルで操作されます。
たとえば、ブロック操作を開始するには ap_start レジスタを 1 に設定する必要があります。次にデバイスがレジスタから AXI4-Lite スレーブ インターフェイスにまとめられた入力を読み出します。ブロックの演算が終了すると、ハードウェアの出力ポートにより ap_done、ap_idle、および ap_ready が設定され、AXI4-Lite スレーブ インターフェイスにまとめられた出力ポートの結果が適切なレジスタから読み出されます。
上記の例の関数引数 c のインプリメンテーションは、ハードウェア ポートの動作をある程度理解しておくことが重要であることも示しています。関数引数 c は読み出しおよび書き込みの両方に使用されるので、インターフェイス合成の概要に示すように、入力ポート c_i および出力ポート c_o として別々にインプリメントされます。
AXI4-Lite スレーブ インターフェイスをプログラムするのに最初に推奨されるフローは、関数を一度だけ実行するフローです。
- 割り込み関数を使用して、割り込みの動作方法を決定します。
- ブロックの入力ポートにレジスタ値を読み込みます。上記の例では、これは API 関数の
XExample_Set_a、XExample_Set_bおよびXExample_Set_c_iを使用して実行されます。 XExample_Startを使用してap_startビットを 1 に設定し、関数を開始します。このレジスタには、上記のヘッダー ファイルに記述されているように、セルフ クリーニング機能があります。トランザクションが 1 つ終了すると、ブロックは動作を一時停止します。- 関数が実行されます。生成された割り込みが処理されます。
- 出力レジスタが読み出されます。上記の例では、これは API 関数の
XExample_Get_c_o_vld(データが有効なことを確認) とXExample_Get_c_oを使用して実行されます。注記: AXI4-Lite スレーブ インターフェイスのレジスタは、ポートと同じ I/O プロトコルに従います。この場合、出力 valid がロジック 1 に設定されて、データが有効かどうかが示されます。 - 次のトランザクションでも繰り返します。
2 つ目に推奨されるフローは、ブロックの継続実行です。このモードの場合、AXI4-Lite スレーブ インターフェイスに含まれる入力ポートは、コンフィギュレーションを実行するポートのみであるべきです。ブロックは通常 CPU よりも速く実行される必要があります。ブロックが入力を待つ必要がある場合、ブロックはほとんどの時間を待機に費やすことになります。
- 割り込み関数を使用して、割り込みの動作方法を決定します。
- ブロックの入力ポートにレジスタ値を読み込みます。上記の例では、これは API 関数の
XExample_Set_a、XExample_Set_aおよびXExample_Set_c_iを使用して実行されます。 - API
XExample_EnableAutoRestartを使用して自動開始関数を設定します。 - 関数が実行されます。各ポートの I/O プロトコルにより、ブロックを介して処理されるデータが同期されます。
- 生成された割り込みが処理されます。出力レジスタにはこの操作中アクセスできますが、データが頻繁に変更される可能性があります。
- API 関数
XExample_DisableAutoRestartを使用して、これ以上実行されないようにします。 - 出力レジスタが読み出されます。上記の例では、これは API 関数の
XExample_Get_c_oおよびXExample_Set_c_o_vldを使用して実行されます。
ソフトウェアの制御
API 関数は、CPU で実行されるソフトウェアで使用すると、ハードウェア ブロックを制御できます。プロセスの概要は次のとおりです。
- ハードウェア インスタンスのインスタンスを作成
- デバイス コンフィギュレーションを検索
- デバイスを初期化
- HLS ブロックの入力パラメーターを設定
- デバイスを開始して結果を読み出し
このプロセスを抽象化すると、次のようになります。ソフトウェア制御のすべての例は、Zynq-7000 SoC のチュートリアルに含まれます。
#include "xexample.h" // Device driver for HLS HW block
#include "xparameters.h"
// HLS HW instance
XExample HlsExample;
XExample_Config *ExamplePtr
int main() {
int res_hw;
// Look Up the device configuration
ExamplePtr = XExample_LookupConfig(XPAR_XEXAMPLE_0_DEVICE_ID);
if (!ExamplePtr) {
print("ERROR: Lookup of accelerator configuration failed.\n\r");
return XST_FAILURE;
}
// Initialize the Device
status = XExample_CfgInitialize(&HlsExample, ExamplePtr);
if (status != XST_SUCCESS) {
print("ERROR: Could not initialize accelerator.\n\r");
exit(-1);
}
//Set the input parameters of the HLS block
XExample_Set_a(&HlsExample, 42);
XExample_Set_b(&HlsExample, 12);
XExample_Set_c_i(&HlsExample, 1);
// Start the device and read the results
XExample_Start(&HlsExample);
do {
res_hw = XExample_Get_c_o(&HlsExample);
} while (XExample_Get_c_o(&HlsExample) == 0); // wait for valid data output
print("Detected HLS peripheral complete. Result received.\n\r");
}
IP インテグレーターでの AXI4-Lite スレーブ インターフェイスのカスタマイズ
AXI4-Lite スレーブ インターフェイスを使用した HLS の RTL デザインが Vitis IP インテグレーターのデザインに統合されると、ブロックをカスタマイズできるようになります。IP インテグレーターのブロック図で HLS ブロックを選択し、右クリックして Customize Block をクリックします。
アドレス幅はデフォルトで必要最小限のサイズに設定されます。これを変更して、アドレス サイズが 32 ビット未満のブロックに接続します。
AXI4 マスター インターフェイス
AXI4 マスター インターフェイスは配列またはポインター/リファレンス引数で使用でき、Vitis HLS では次のいずれかのモードでインプリメントされます。
- 個別のデータ転送
- バースト モードのデータ転送
個別のデータ転送では、Vitis HLS で各アドレスのデータの 1 つの要素が読み出されるか書き込まれます。次の例に、1 つの読み出しおよび 1 つの書き込み操作を示します。Vitis HLS で AXI インターフェイスにアドレスが生成され、1 つのデータ値とアドレスが読み出され、1 つのデータ値が書き込まれます。インターフェイスは、アドレスごとに 1 つのデータ値を転送します。
void bus (int *d) {
static int acc = 0;
acc += *d;
*d = acc;
}
バースト モード転送では、Vitis HLS で 1 つのベース アドレスを使用してデータが読み出しまたは書き込みされ、その後に複数のシーケンシャル データ サンプルが続くので、より高いデータ スループットを達成できます。バースト モードは、C の memcpy 関数を使用しテイル場合、または for ループをパイプライン処理している場合に使用できます。
memcpy 関数は、AXI4 マスター インターフェイスで指定された最上位関数の引数のデータ転送に使用された場合にのみ、合成でサポートされます。次の例に、memcpy 関数を使用したバースト モードのコピーを示します。最上位関数の引数 a は、AXI4 マスター インターフェイスとして指定されています。
void example(volatile int *a){
#pragma HLS INTERFACE m_axi depth=50 port=a
#pragma HLS INTERFACE s_axilite port=return
//Port a is assigned to an AXI4 master interface
int i;
int buff[50];
//memcpy creates a burst access to memory
memcpy(buff,(const int*)a,50*sizeof(int));
for(i=0; i < 50; i++){
buff[i] = buff[i] + 100;
}
memcpy((int *)a,buff,50*sizeof(int));
}
この例が合成されると、次の図のようなインターフェイスになります。
次の例は前の例と同じコードですが、データ出力をコピーするのに for ループを使用しています。
void example(volatile int *a){
#pragma HLS INTERFACE m_axi depth=50 port=a
#pragma HLS INTERFACE s_axilite port=return
//Port a is assigned to an AXI4 master interface
int i;
int buff[50];
//memcpy creates a burst access to memory
memcpy(buff,(const int*)a,50*sizeof(int));
for(i=0; i < 50; i++){
buff[i] = buff[i] + 100;
}
for(i=0; i < 50; i++){
#pragma HLS PIPELINE
a[i] = buff[i];
}
}
バースト読み出しまたは書き込みをインプリメントするのに for ループを使用する場合は、次の要件に従ってさい。
- ループをパイプライン処理する
- 昇順でアドレスにアクセスする
- 条件文内にアクセスを記述しない
- 入れ子状態のループの場合、バースト動作が抑制されてしまわないよう、ループを平坦にしない
for ループ内で使用できるのは読み出し 1 つと書き込み 1 つのみです。次の例では、別の AXI インターフェイスを使用して、2 つの読み出しをバースト モードで実行するところを示します。次の例では、Vitis HLS でポート読み出しがバースト転送としてインプリメントされます。ポート a は bundle オプションを使用せずに指定され、デフォルトの AXI インターフェイスにインプリメントされます。b ポートにはバンドル名が指定され、d2_port という別の AXI インターフェイスにインプリメントされます。
void example(volatile int *a, int *b){
#pragma HLS INTERFACE s_axilite port=return
#pragma HLS INTERFACE m_axi depth=50 port=a
#pragma HLS INTERFACE m_axi depth=50 port=b bundle=d2_port
int i;
int buff[50];
//copy data in
for(i=0; i < 50; i++){
#pragma HLS PIPELINE
buff[i] = a[i] + b[i];
}
...
}
ポート幅の自動変更
Vitis HLS には、カーネル インターフェイス ポートのサイズを Vitis ツール フローでのバースト アクセス用に自動的に 512 に変更する機能があります。これはつまり、バースト可能なアクセスは 512 ビット幅にしか変更されないということです。バーストするには、バースト転送の最適化 で説明する前提条件にコードが従っている必要があります。Vitis HLS では、次の 2 つのコマンドを使用してポート幅の自動変更を制御できます。
config_interface -m_axi_max_widen_bitwidth <N>: M-AXI インターフェイスのバーストを指定した幅まで自動的に広げます。<N> の値には、0 ~ 1024 の 2 のべき乗を指定する必要があります。バースト幅を広げる場合は、バーストだけでなく、強力なアライメント プロパティが必要となります。<N> のデフォルト値は 512 ビットです。config_interface -m_axi_alignment_byte_size <N>: M-AXI インターフェイスにマップされる最上位関数ポインターが少なくとも指定したバイト幅 (2 のべき乗) に揃えられるとします。これにより、自動バースト拡張が実行されやすくなります。デフォルトのアライメントは 64 バイトです。
自動最適化の場合、カーネルが次の前提条件を満たす必要があります。
- アクセスするメモリのロケーションおよび時間が単調に増加する必要があります。以前にアクセスされた 2 つのメモリ ロケーション間のメモリ ロケーションにアクセスすることはできません (オーバーラップなしということ)。
- バーストの前提条件は、すべてポート幅のサイズ変更にも適用されます。
- グローバル メモリからのアクセス パターンはシーケンシャル順で、次の追加要件に従っている必要があります。
- シーケンシャル アクセスは、ベクター以外のタイプにする必要があります。
- シーケンシャル アクセスの開始は、拡張されたワード サイズにアラインメントする必要があります。
- シーケンシャル アクセスの長さはその拡張係数で割り切れるようにする必要があります。
次のコード例は、これを示しています。
vadd_pipeline:
for (int i = 0; i < iterations; i++) {
#pragma HLS LOOP_TRIPCOUNT min = c_len/c_n max = c_len/c_n
// Pipelining loops that access only one variable is the ideal way to
// increase the global memory bandwidth.
read_a:
for (int x = 0; x < N; ++x) {
#pragma HLS LOOP_TRIPCOUNT min = c_n max = c_n
#pragma HLS PIPELINE II = 1
result[x] = a[i * N + x];
}
read_b:
for (int x = 0; x < N; ++x) {
#pragma HLS LOOP_TRIPCOUNT min = c_n max = c_n
#pragma HLS PIPELINE II = 1
result[x] += b[i * N + x];
}
write_c:
for (int x = 0; x < N; ++x) {
#pragma HLS LOOP_TRIPCOUNT min = c_n max = c_n
#pragma HLS PIPELINE II = 1
c[i * N + x] = result[x];
}
}
}
}
上記のコードの自動最適化の幅は、次の 3 つの段階で実行されます。
- まず、read_a ループのアクセス パターン数がチェックされます。1 つのループ反復中に 1 アクセスなので、最適化ではインターフェイスのビット幅が 32= 32 *1 (int 変数のビット幅 * アクセス) と決定されます。
- 次の式の項を使用して、
config_interface m_axi_max_widen_bitwidth 512で指定されたデフォルトの最大値を到達しようとします。length = (ceil((loop-bound of index inner loops) * (loop-bound of index - outer loops)) * #(of access-patterns))- 上記のコードでは外側のループが不完全なループなので、外側のループのバースト転送はなくなります。このため、長さには内側のループのみが含まれるので、計算式は次のように短縮できます。
length = (ceil((loop-bound of index inner loops)) * #(of access-patterns))または、長さ = ceil(128) *32 = 4096 となります。
- 上記のコードでは外側のループが不完全なループなので、外側のループのバースト転送はなくなります。このため、長さには内側のループのみが含まれるので、計算式は次のように短縮できます。
- 最後に、計算された長さが 2 のべき乗かどうか確認され、2 のべき乗の場合は長さが
m_axi_max_widen_bitwidthで指定された幅に制限されます。
ポート幅の自動変更機能を使用する前に、その利点と問題点について考慮してください。この機能は、データ型サイズではなく大きなベクターを読み出すので、DDR からの読み出しレイテンシを改善します。大きなベクターをバッファリングして、データをデータパス サイズに合わせてシフトする必要があるので、リソースも増えます。ただし、自動ポート幅変更では標準 C データ型のみがサポートされ、ap_int、ap_uint、構造体、配列などの集合体でないデータ型はサポートされません。
AXI4 バースト動作の制御
最適な AXI4 インターフェイスとは、バスへのアクセス待機中にデザインが停止せず、バス アクセスが承認された後の読み出し/書き込み待機中にバスが停止しないようなインターフェイスです。最適な AXI4 インターフェイスを作成するには、INTERFACE プラグマまたは指示子に次のオプションを使用してバーストの動作を指定し、AXI4 インターフェイスの効率を最適化します。バースト転送の詳細は、バースト転送の最適化 を参照してください。
これらのオプションの中には、内部ストレージを使用してデータをバッファリングするため、エリアおよびリソースへの影響があるものもあります。
latency: AXI4 インターフェイスのレイテンシを指定し、読み出しまたは書き込みの指定サイクル (レイテンシ) 前にバス要求を開始できるようにします。この値が小さすぎると、デザインが準備完了になるのが早すぎ、バスを待つために停止する可能性があります。この値が大きすぎると、バス アクセスが承認されても、デザインがアクセスを開始するのを待つためにバスが停止する可能性があります。max_read_burst_length: バースト転送中に読み出されるデータ値の最大数を指定します。num_read_outstanding: デザインが停止する前に、AXI4 バスに対して応答なしで送信できる読み出し要求の数を指定します。これにより、デザインの内部ストレージである FIFO のサイズ (num_read_outstanding*max_read_burst_length*word_size) が決まります。max_write_burst_length: バースト転送中に書き込まれるデータ値の最大数を指定します。num_write_outstanding: デザインが停止する前に、AXI4 バスに対して応答なしで送信できる書き込み要求の数を指定します。これにより、デザインの内部ストレージである FIFO のサイズ (num_read_outstanding*max_read_burst_length*word_size) が決まります。
次に、これらのオプションを使用した例を示します。
#pragma HLS interface m_axi port=input offset=slave bundle=gmem0
depth=1024*1024*16/(512/8)
latency=100
num_read_outstanding=32
num_write_outstanding=32
max_read_burst_length=16
max_write_burst_length=16
インターフェイスは、レイテンシ 100 になるように指定されています。Vitis HLS では、デザインが AXI4 バスにアクセスできるようになる 100 クロック サイクル前に、バースト アクセス要求をスケジューリングしようとします。バスの効率をさらに向上するには、num_write_outstanding および num_read_outstanding オプションを使用して、デザインに最大 32 個の読み出しおよび書き込みアクセスを格納できるバッファー容量が含まれるようにします。これで、バス要求が送信されるまでデザインの処理を続行できるようになります。最後に、max_read_burst_length および max_write_burst_length オプションを使用することで、最大バースト サイズが 16 になり、AXI4 インターフェイスがそれを超える時間バスを保持しないようにします。
これらのオプションを使用すると、AXI4 インターフェイスの動作をそれが実行されるシステム用に最適化できます。操作の効率は、これらの値が正しく設定されているかどうかによって異なります。
32 ビット アドレス機能のある AXI4 インターフェイスを作成
m_axi_addr64 インターフェイス コンフィギュレーション オプションをディスエーブルにすると、32 ビット アドレス バスの AXI4 インターフェイスをインプリメントできます。- をクリックします。
- [Solution Settings] ダイアログ ボックスで General カテゴリをクリックし、Edit で既存の
config_interfaceコマンドを変更するか、Add で追加します。 - [Edit] または [Add] ダイアログ ボックスで config_interface を選択して m_axi_addr64 をオフにします。
AXI4 インターフェイスのアドレス オフセットの制御
デフォルトでは、AXI4 マスター インターフェイスはすべての読み出しおよび書き込み操作をアドレス 0x00000000 から開始します。たとえば、次のコードの場合、50 個のアドレス値を表すアドレス 0x00000000 ~ 0x000000c7 (32 ビット ワード 50 個で 200 バイトを提供) からデータを読み出してから、同じアドレスにデータを再び書き込みます。
void example(volatile int *a){
#pragma HLS INTERFACE m_axi depth=50 port=a
#pragma HLS INTERFACE s_axilite port=return bundle=AXILiteS
int i;
int buff[50];
memcpy(buff,(const int*)a,50*sizeof(int));
for(i=0; i < 50; i++){
buff[i] = buff[i] + 100;
}
memcpy((int *)a,buff,50*sizeof(int));
}
アドレス オフセットを使用するには、INTERFACE 指示子に -offset を付けて、次のいずれかを指定します。
off: オフセット アドレスは使用しません。これがデフォルトです。direct: デザインにアドレス オフセットを適用するための 32 ビット ポートが追加されます。slave: AXI4-Lite インターフェイス内にアドレス オフセットを適用するための 32 ビット レジスタが追加されます。
Vitis HLS では、最終的な RTL にアドレス オフセットは AXI4 マスター インターフェイスで生成される読み出しまたは書き込みアドレスに直接適用され、システムのアドレス位置にアクセスできるようになります。
AXI インターフェイスで slave オプションを使用する場合は、デザイン インターフェイスに AXI4-Lite ポートを使用する必要があります。ザイリンクスでは、次のプラグマを使用して AXI4-Lite インターフェイスをインプリメントすることを勧めしています。
#pragma HLS INTERFACE s_axilite port=return
また、slave オプションを使用する際に複数の AXI4-Lite インターフェイスを使用した場合は、AXI マスター ポートのオフセット レジスタが正しい AXI4-Lite インターフェイスにまとめられているようにする必要もあります。次の例では、a ポートが、オフセットと AXI4-Lite インターフェイス (AXI_Lite_1 および AXI_Lite_2) と一緒に AXI マスター インターフェイスとしてインプリメントされています。
#pragma HLS INTERFACE m_axi port=a depth=50 offset=slave
#pragma HLS INTERFACE s_axilite port=return bundle=AXI_Lite_1
#pragma HLS INTERFACE s_axilite port=b bundle=AXI_Lite_2
a ポートのオフセット レジスタが AXI_Lite_1 という AXI4-Lite インターフェイスにまとめられるようにするには、次の INTERFACE 指示子が必要です。
#pragma HLS INTERFACE s_axilite port=a bundle=AXI_Lite_1
IP インテグレーターでの AXI4 Master インターフェイスのカスタマイズ
AXI4 マスター インターフェイスを使用した HLS の RTL デザインを Vivado IP インテグレーターのデザインに組み込む際には、ブロックをカスタマイズできます。IP インテグレーターのブロック図で HLS ブロックを選択し、右クリックして Customize Block をクリックし、設定をカスタマイズします。AXI4 パラメーターのすべての説明については、『Vivado Design Suite: AXI リファレンス ガイド』 (UG1037) のこのセクションを参照してください。
次の図は、次のデザインの [Re-Customise IP] ダイアログ ボックスを示しています。このデザインには、AXI4-Lite ポートが含まれています。
SSI テクノロジ デバイスのインターフェイスの管理
一部のザイリンクス デバイスには、スタックド シリコン インターフェイス (SSI) テクノロジが使用されています。これらのデバイスでは、使用可能なリソースが複数の SLR (Super Logic Region) に分割されます。SLR 間の接続には、スーパー ロング ライン (SSL) 配線が使用されます。SSL 配線の場合、通常標準の FPGA 配線よりも遅延の発生する可能性が高くなります。最大パフォーマンスでデザインが動作するようにするには、次のガイドラインに従ってください。
- SLR 出力と SLR 入力両方の SLR 間をまたがるすべての信号をレジスタに入力。
- I/O バッファーを介して SLR に入力または SLR から出力する場合は、信号をレジスタ入力する必要なし。
- Vitis HLS で作成されたロジックが 1 つの SLR 内にフィットするようにする。
ロジックが 1 つの SLR デバイス内に含まれる場合、Vitis HLS で config_interface コマンドに register_io オプションが使用できるようになります。このオプションを使用すると、すべてのブロック入力か出力、または両方ともが自動的にレジスタに入力されるようにできます。このオプションは、スカラーの場合にのみ必要です。配列ポートはすべて自動的にレジスタ入力されます。
register_io オプションの設定は、次のとおりです。
off: どの入力も出力もレジスタには送信されません。scalar_in: すべての入力がレジスタに送信されます。scalar_out: すべての出力がレジスタに送信されます。scalar_all: すべての入力および出力がレジスタに送信されます。
register_io オプションを使用すると、SSI テクノロジ デバイスをターゲットにしたロジックが最大クロック レートで実行されるようになります。Vitis HLS での最適化手法
このセクションでは、Vitis HLS でパフォーマンスおよびエリアの目標を満たすマイクロアーキテクチャを生成するための、さまざまな最適化手法を説明します。Vitis HLS を使用すると、次を含むさまざまな最適化指示子をデザインに適用できます。
- タスクをパイプライン処理し、現在のタスクが終了する前に次のタスクを開始できるよう指定。
- 関数、ループ、および領域を完了するまでのターゲット レイテンシを指定。
- 使用されるリソース数を制限。
- コードから継承または暗示された依存を無効にし、特定の演算を実行できるよう指定。たとえば、ビデオ ストリームのように初期データ値を削除または無視できる場合、パフォーマンスが改善できるのであれば、メモリの書き込み前に読み出しできるようにします。
- I/O プロトコルを指定して、関数引数を同じ I/O プロトコルのほかのハードウェア ブロックに接続。注記: サブ関数で使用される I/O プロトコルは Vitis HLS により自動的に決定されます。ポートにレジスタを付けるかどうかを指定することを除き、これらのポートを制御することはできません。
-
スループットの最適化: 最適化を一般的に使用される順序で説明します。タスクのパイプライン処理によるパフォーマンスの向上、タスク間のデータフローの向上、パフォーマンスを制限する可能性のある問題を解決するための構造の最適化を示します。
-
レイテンシの最適化: レイテンシ制約とループ遷移の削除を使用して、演算を終了するのに必要なクロック サイクル数を削減します。
-
エリアの最適化: 演算のインプリメント方法に焦点を置き、演算数を制御して、演算をハードウェアにインプリメントする方法を指定することにより、エリアを向上する手法を説明します。
- ロジックの最適化: RTL のインプリメンテーションに影響する最適化について説明します。
最適化指示子をコンパイラ コンパイラとしてソース コードに直接追加するか、HLS プロジェクトの最適化 に説明されているように、set_directive Tcl コマンドを使用して Tcl スクリプトに最適化指示子を含めてコンパイル中にソリューションで使用されるようにすることができます。次の表に、Vitis HLS でプラグマまたは Tcl 指示子として使用可能な最適化指示子を示します。
| 指示子 | 説明 |
|---|---|
ALLOCATION |
使用される演算、インプリメンテーション、または関数の数を制限します。これによりハードウェア リソースが共有されるので、レイテンシが増加する可能性があります。 |
ARRAY_PARTITION |
大型の配列を複数の配列または個別のレジスタに分割し、データへのアクセスを改善し、ブロック RAM のボトルネックを削除します。 |
ARRAY_RESHAPE |
配列を多数の要素を含むものからワード幅の広いものに変更します。多数のブロック RAM を使用せずにブロック RAM アクセスを向上するのに有益です。 |
BIND_OP |
RTL で演算に特定のインプリメンテーションを指定します。 |
BIND_STORAGE |
RTL でストレージ エレメント、メモリに特定のインプリメンテーションを指定します。 |
DATAFLOW |
タスク レベルのパイプライン処理を有効にし、関数およびループが同時に実行されるようにします。スループットおよびレイテンシの最適化に使用します。 |
DEPENDENCE |
ループ キャリー依存を克服し、ループをパイプライン処理できるようにする (またはより短い間隔でパイプラインできるようにする) 追加情報を提供します。 |
DISAGGREGATE |
構造体を個別の要素に分割します。 |
EXPRESSION_BALANCE |
自動演算調整をオフにできます。 |
INLINE |
関数をインライン展開し、このレベルの関数の階層を削除します。関数の境界を超えたロジック最適化をイネーブルにし、関数呼び出しのオーバーヘッドを削減することにより、レイテンシ/間隔を改善します。 |
INTERFACE |
関数記述から RTL ポートをどのように作成するかを指定します。 |
LATENCY |
最小および最大レイテンシ制約を指定します。 |
LOOP_FLATTEN |
入れ子のループを 1 つのループに展開し、レイテンシを改善します。 |
LOOP_MERGE |
連続するループを結合して、全体的なレイテンシを削減し、共有を増やして最適化を向上します。 |
LOOP_TRIPCOUNT |
範囲が可変のループに使用されます。ループの反復回数の見積もりを指定します。これは合成には影響がなく、レポートにのみ影響します。 |
OCCURRENCE |
関数またはループをパイプライン処理する際に、あるロケーションのコードがそれを含む関数またはループのコードよりも低速で実行されることを指定します。 |
PIPELINE |
ループまたは関数内で演算をオーバーラップできるようにして、開始間隔を削減します。 |
RESET |
特定のステート変数 (グローバルまたはスタティック) のリセットを追加または削除するために使用します。 |
SHARED |
グローバル変数または関数引数配列が、複数のデータフロー プロセスで同期化の必要なしで共有されることを指定します。 |
STABLE |
データフロー領域の開始と終了に同期化を生成する場合に、データフロー領域の変数入力または出力を無視できることを指定します。 |
STREAM |
データフロー最適化中に特定の配列を FIFO または RAM メモリ チャネルとしてインプリメントするよう指定します。hls::stream を使用している場合に、STREAM 最適化指示子を使用して hls::stream の設定を無効にします。 |
TOP |
合成の最上位関数はプロジェクト設定で指定します。この指示子は、関数を合成の最上位として指定するために使用できます。これにより、新しくプロジェクトを作成しなくても、同じプロジェクト内の別のソリューションを合成の最上位関数として指定できます。 |
UNROLL |
for ループを展開して、ループ本体のインスタンスを複数作成し、その命令が別々にスケジュールできるようにします。 |
Vitis HLS では、最適化指示子に加え、合成結果のパフォーマンスに影響するコンフィギュレーション コマンドも多数提供されています。コンフィギュレーション コマンドの使用方法は、コンフィギュレーション オプションの設定 を参照してください。次の表に、これらのコマンドの一部を示します。
| GUI 指示子 | 説明 |
|---|---|
| Config Array Partition | グローバル配列を含めた配列の分割方法と、分割が配列ポートに影響するかどうかを指定します。 |
| Config Compile | 自動ループ パイプラインおよび浮動小数点の math 最適化など、合成特有の最適化を制御します。 |
| Config Dataflow | データフロー最適化でのデフォルトのメモリ チャネルと FIFO の深さを指定します。 |
| Config Interface | 最上位関数の引数に関連付けられていない I/O ポートを制御し、最終的な RTL から未使用のポートが削除されるようにします。 |
| Config Op | 指定した演算のデフォルト レイテンシとインプリメンテーションを設定します。 |
| Config RTL | ファイルおよびモジュールの命名、リセット形式、FSM エンコーディングを含めた出力 RTL を制御できます。 |
| Config Schedule | 合成のスケジューリング段階中に使用するエフォート レベルおよび出力メッセージの詳細度合いを決定します。 |
| Config Storage | 指定したストレージ タイプのデフォルト レイテンシとインプリメンテーションを設定します。 |
| Config Unroll | ループの展開に使用されるデフォルトのトリップカウントしきい値を設定します。 |
リセット動作の制御
リセット ポートは、リセット信号が適用されたときにリセット ポートに接続されたレジスタおよびブロック RAM を初期値に戻すために FPGA で使用されます。RTL コンフィギュレーションで最も重要なのは、通常リセット動作の選択です。
RTL リセット ポートの存在と動作は、次の図に示すように config_rtl コマンドで制御されます。このコマンドには、 からアクセスできます。
リセット設定では、リセットの極性を設定したり、リセットが同期か非同期かを設定できますが、reset オプションを使用すると、リセット信号が適用されたときにどのレジスタをリセットするかを制御することもできます。
config_rtl コンフィギュレーションの設定に関係なく、リセット極性は自動的にアクティブ Low に変更されます。これは、AXI4 規格の要件です。reset オプションには、次の 4 つの設定があります。
- none: デザインにリセットは追加されません。
- control: すべての制御レジスタがリセットされるようになります (デフォルト)。制御レジスタは、ステート マシンに使用されるレジスタで、I/O プロトコル信号を生成するために使用されます。この設定により、デザインはその動作ステートを即座に開始できるようになります。
- state: 制御レジスタにリセットが (control 設定と同様) 追加されるほか、C コードのスタティックおよびグローバル変数から派生するレジスタまたはメモリにリセットが追加されます。この設定により、リセットが適用されたら、C コードで初期化されたスタティックおよびグローバル変数がそれらの初期化値にリセットされます。
- all: デザイン内のレジスタおよびメモリがすべてリセットされます。
RESET プラグマまたは指示子を使用すると、さらに詳細にリセットを制御できます。スタティックおよびグローバル変数には、RESET 指示子を使用してリセットを追加できます。変数のリセット設定は、RESET 指示子の off オプションを使用すると、削除することもできます。
初期化動作
C では、static 修飾子で定義された変数およびグローバルに定義された変数はデフォルトで 0 に初期化されますが、オプションでこれらの変数に特定の初期値を指定することも可能です。変数の初期値を指定した場合、C コードでその値がコンパイル時 (時間 0) に割り当てられ、その後再び割り当てられることはありません。どちらの場合も初期値が RTL にインプリメントされます。
- RTL シミュレーション中、変数は C コードと同じ値で初期化されます。
- 変数は、FPGA をプログラムするために使用されるビットストリームでも初期化されます。デバイスが電源投入されると、変数が初期化ステートで開始されます。
RTL では、変数が C コードと同じ初期値で開始していても、その変数をこの初期ステートに強制的に戻すことはできません。初期ステートに戻すには、リセット信号を使用して変数をインプリメントする必要があります。
配列の初期化およびリセット
state または all が使用されると、すべての要素がブロック RAM としてインプリメントされ、リセット後に初期値に戻ります。これにより、RTL デザインでかなり不適切な条件が 2 つできてしまうことがあります。- 電源投入時の初期化とは異なり、明示的なリセットでは RTL デザインでブロック RAM 内の各アドレスに反復的に値を設定する必要あり。N が大きい場合は多数のクロック サイクルがかかることがあり、リセットをインプリメントするのにより多くのエリア リソースが必要。
- リセットがデザインのすべての配列に追加される。
このようなブロック RAM すべてにリセット ロジックが追加されて RAM のすべての要素をリセットするためのサイクル オーバーヘッドが発生しないようにするには、デフォルトの control リセット モードを指定し、RESET 指示子でスタティックまたはグローバル変数をそれぞれ識別します。
または、デフォルトの state リセット モードを使用し、RESET 指示子に off オプションを使用して、スタティックまたはグローバル変数を個別にリセットに指定します。
スループットの最適化
次の最適化を使用すると、スループットを改善できたり、開始間隔を削減したりできます。
関数およびループのパイプライン処理
パイプライン処理を実行すると、演算を同時に実行できるようになります。各実行ステップをすべての演算を完了しなくても、次の演算を開始できます。パイプライン処理は、関数およびループに対して実行できます。次の図は、関数のパイプラインによるスループットの改善を示しています。
パイプライン処理を実行しない場合、上記の例では、関数で入力が 3 クロック サイクルごとにこの例で読み出され、値が 2 サイクル後に出力されます。関数の開始間隔 (II) は 3 で、レイテンシは 3 です。パイプライン処理を実行すると、出力レイテンシの変更なしで、新しい入力が各サイクルで読み出されるようになります (II=1)。
ループのパイプライン処理を使用すると、ループ内の演算がオーバーラップ方式でインプリメントできるようになります。次の図の (A) はデフォルトの順次演算を示しています。各入力は 3 クロック サイクルごとに処理され (II=3)、最後の出力が書き出されるまでに 8 クロック サイクルかかっています。
(B) に示すパイプライン処理されたループでは、入力サンプルが各クロック サイクルで読み出され、最終的な出力は 4 クロック サイクル後に書き込まれるようになり、同じハードウェア リソースを使用して、開始間隔 (II) とレイテンシの両方を向上できます。
関数またはループをパイプライン処理するには、PIPELINE 指示子を使用します。関数またはループ本体に指定します。指定しなければ開始間隔 (II) はデフォルトで 1 ですが、明示的に指定することもできます。
パイプライン処理は指定した領域のみに適用され、その下の階層には適用されません。ただし、その階層内にあるすべてのループが自動的に展開されます。指定した関数の下の階層にあるサブ関数は、個別にパイプライン処理する必要があります。サブ関数をパイプライン処理すると、その上のパイプライン処理された関数でパイプライン処理による最大限にパフォーマンスを改善できます。パイプライン処理されている最上位関数の下にあるサブ関数がパイプライン処理されていないと、パイプライン処理によるパフォーマンスの改善が制限される可能性があります。
パイプライン処理された関数とループのビヘイビアーには違いがあります。
- 関数の場合、パイプラインは永遠に実行されて、終了しません。
- ループの場合、パイプラインはループのすべての反復が終了するまで実行されます。
このビヘイビアーの違いについては、次の図を参照してください。
ビヘイビアーの違いは、パイプラインへの入力および出力の処理方法に影響します。上の図に示すように、パイプライン処理された関数は新しい入力を継続して読み出し、新しい出力を継続して書き込みます。反対に、ループは次のループの開始前にまずそのループ内の演算をすべて終了する必要があるので、パイプライン処理されたループによりデータ ストリームに「バブル」 (ループが最後の反復の実行を終了したときに新しい入力が読み出されないポイントと、ループが新しいループ反復を開始するときに新しい出力が書き込まれないポイント) が発生します。
パイプライン処理されたループを巻き戻してパフォーマンスを改善
前の図に示す問題を回避するため、PIPELINE プラグマには rewind というオプションのコマンドがあります。このコマンドを使用すると、rewind ループが最上位関数またはデータフロー プロセスの一番外側で、データフロー領域が複数回呼び出される場合に、このループへの連続する呼び出しの反復をオーバーラップできます。
次の図に、ループをパイプラインする際に rewind オプションを使用した場合の動作を示します。ループは、ループ反復カウントが終了すると再実行されます。通常は直後に再実行されますが、遅延があることもあり、それは GUI で表示されるようになっています。
パイプラインのフラッシュ
パイプライン処理は、データがパイプラインの入力に入ってくる限り実行され続けます。処理するデータがなければ、パイプラインは一時停止します。これを次の図に示します。入力データの valid 信号が Low になり、データがないことが示されています。処理するデータが新しく入ってくれば、パイプラインは動作を再開します。
場合によっては、空にできる (フラッシュできる) パイプラインが必要なこともあります。このために、flush オプションが提供されています。パイプラインをフラッシュすると、パイプラインの開始時にデータ valid 信号によりデータがないことが示されたときに、新しい入力の読み出しが停止し、最後の入力が処理されてパイプラインから出力されるまで、処理が継続されてその後の各パイプライン段が閉鎖されます。
Vitis HLS でインプリメントされるパイプラインのデフォルト スタイルは、config_compile -pipeline_style コマンドで定義されます。デザインで使用するパイプラインは、ストール パイプライン (stp) またはフリーランニング フラッシュ パイプライン (frp) に指定できます。enable_flush オプションを使用すると、3 つ目のタイプのフラッシュ可能パイプライン (flp) を PIPELINE プラグマまたは指示子を使用して定義することもできます。このオプションは、特定のスコープのプラグマまたは指示子にのみ適用され、config_compile で割り当てられたグローバル デフォルト設定は変更されません。
| 名前 | ストール パイプライン (デフォルト) | フリーランニング/フラッシュ可能パイプライン | フラッシュ可能パイプライン |
|---|---|---|---|
| グローバル設定 | config_compile -pipeline_style stp (デフォルト) | config_compile -pipeline_style frp | なし |
| プラグマ/指示子 | #HLS pragma pipeline | なし | #HLS pragma pipeline enable_flush |
| 利点 |
|
|
|
| 欠点 |
|
|
|
| ユース ケース |
|
|
|
ループの自動パイプライン
config_compile を使用すると、反復回数に基づくループの自動パイプライン処理をイネーブルにできます。この設定にアクセスするには、 をクリックします。
反復回数は、pipeline_loops オプションで設定します。反復回数がこの設定値よりも少ないループは、すべて自動的にパイプライン処理されます。デフォルトは 0 で、ループの自動パイプライン処理は実行されません。
次のようなコードがあるとします。
for (y = 0; y < 480; y++) {
for (x = 0; x < 640; x++) {
for (i = 0; i < 5; i++) {
// do something 5 times
...
}
}
}
pipeline_loops オプションを 6 に設定すると、上記のコードの最内 for ループは自動的にパイプライン処理されます。これは、次のコードと同じです。
for (y = 0; y < 480; y++) {
for (x = 0; x < 640; x++) {
for (i = 0; i < 5; i++) {
#pragma HLS PIPELINE II=1
// do something 5 times
...
}
}
}
自動パイプライン処理を適用するべきでないループがデザインに含まれる場合は、そのループに PIPELINE 指示子を off 設定で適用します。この指示子を off に設定すると、ループの自動パイプライン処理は実行されなくなります。
config_compile pipeline_loops オプションが適用されます。たとえば、Vitis HLS でループにユーザー指定の UNROLL 指示子が適用される場合、このループがまず展開されるので、自動ループ パイプラインは適用できません。パイプライン エラーの対処方法
関数がパイプライン処理されると、その階層内にあるすべてのループが自動的に展開されます。これはパイプライン処理のために必要です。ループに変数境界があると展開できず、関数がパイプライン処理されなくなります。
スタティック変数
スタティック変数は、ループの反復間でデータを維持するために使用される変数で、最終的なインプリメンテーションでレジスタになることがよくあります。これがパイプライン処理された関数に使用されていると、Vitis HLS でデザインを十分に最適化できず、開始間隔 (II) が要件を超えることがあります。
次に、この状況の典型的な例を示します。
function_foo()
{
static bool change = 0
if (condition_xyz){
change = x; // store
}
y = change; // load
}
Vitis HLS でこのコードを最適化できない場合、ストア操作に 1 サイクル、ロード操作に 1 サイクル必要となります。この関数がパイプラインの一部である場合は、スタティック変更変数によりループ運搬依存が作成されるので、パイプラインを最小限の開始間隔である 2 を使用してインプリメントする必要があります。
これを回避する方法の 1 つは、コードを次のように書き直すことです。これにより、ループの反復ごとに read または write のみが存在するようになるので、デザインが II=1 でスケジュールできるようになります。
function_readstream()
{
static bool change = 0
bool change_temp = 0;
if (condition_xyz)
{
change = x; // store
change_temp = x;
}
else
{
change_temp = change; // load
}
y = change_temp;
}
配列の分割によるパイプラインの改善
関数をパイプライン処理すると、次のようなエラー メッセージが表示されることがあります。
INFO: [SCHED 204-61] Pipelining loop 'SUM_LOOP'.
WARNING: [SCHED 204-69] Unable to schedule 'load' operation ('mem_load_2',
bottleneck.c:62) on array 'mem' due to limited memory ports.
WARNING: [SCHED 204-69] The resource limit of core:RAM:mem:p0 is 1, current
assignments:
WARNING: [SCHED 204-69] 'load' operation ('mem_load', bottleneck.c:62) on array
'mem',
WARNING: [SCHED 204-69] The resource limit of core:RAM:mem:p1 is 1, current
assignments:
WARNING: [SCHED 204-69] 'load' operation ('mem_load_1', bottleneck.c:62) on array
'mem',
INFO: [SCHED 204-61] Pipelining result: Target II: 1, Final II: 2, Depth: 3.
上記の Vitis HLS のメッセージは、メモリ ポートが限られているため、メモリへの load (読み込み) 操作 (mem_load_2) をスケジューリングできず、指定された開始間隔 (II) 1 を達成できないことを示しています。「The resource limit for core:RAM:mem:p0 is 1」と記述されているように core:RAM:mem:p0 のリソース制限は 1 で、62 行目の mem_load 演算で使用されてしまっています。ブロック RAM の 2 つ目のポートにもリソースが 1 つしかなく、これも mem_load_1 演算で使用されています。Vitis HLS は、このメモリ ポートの競合により、最終的な II が指定した 1 ではなく 2 になったことをレポートします。
この問題は、通常配列が原因で発生します。配列は、ブロック RAM (最大でも 2 つのデータ ポートしかない) としてインプリメントされます。これにより、read/write (または load/store) 集中型アルゴリズムのスループットが制限される可能性があります。配列 (1 つのブロック RAM リソース) を複数の小型の配列 (複数のブロック RAM) に分割してポート数を増加することにより、バンド幅を向上できることがあります。
配列を分割するには、ARRAY_PARTITION 指示子を使用します。Vitis HLS には、次の図に示すように 3 つの配列分割方法があります。これらの分割方法は、次のとおりです。
block- 元の配列の連続した要素が同じサイズのに分割されます。
cyclic- 元の配列の要素がインターリーブされて同じサイズのブロックに分割されます。
complete- デフォルトでは配列が個別エレメントに分割されます。これは、メモリの複数のレジスタへの分解に相当します。
block および cyclic 分割では、factor オプションを使用して作成する配列の数を指定します。前の図では、factor オプション 2 が使用され、配列が 2 つの配列に分割されています。配列の要素数がこの係数の整数倍ではない場合、最後の配列に含まれる要素数は少なくなります。
多次元配列を分割する際は、dimension オプションを使用して、分割する次元を指定できます。次の図に、次のコード例を異なる dimension オプションを使用して分割した場合を示します。
void foo (...) {
int my_array[10][6][4];
...
}
dimension を 3 に設定すると 4 つの配列に、dimension を 1 に設定すると 10 個の配列に分割されます。dimension を 0 に設定すると、すべての次元が分割されます。
配列の自動分割
config_array_partition は、配列を要素数に基づいて自動的に分割する方法を指定します。この設定にアクセスするには、 をクリックします。
throughput_driven オプションを使用すると、分割のしきい値を調整し、分割を完全に自動化できます。throughput_driven オプションを選択すると、指定したスループットを達成するために Vitis HLS で配列が自動的に分割されます。
Vitis HLS での依存
Vitis HLS では、C ソース コードに対応するハードウェア データパスが作成されます。
パイプライン指示子がない場合、実行がシーケンシャルになるので、考慮する依存はありませんが、デザインがパイプライン処理される場合は、Vitis HLS で生成されるハードウェアでプロセッサ アーキテクチャと同じ依存を処理する必要があります。
データ依存またはメモリ依存は通常、読み出しまたは書き込みの後に読み出しまたは書き込みが実行される場合に発生します。
- RAW (read-after-write) は true 依存とも呼ばれ、命令 (および命令で読み出させる/使用されるデータ) は前の演算の結果に依存します。
- I1: t = a * b;
- I2: c = t + 1;
I2 の読み出しは、I1 の t の書き込みに依存します。命令の順序を変えると、前の t の値が使用されます。
- WAR (write-after-read) は anti 依存とも呼ばれ、前の命令がデータを読み出すまで、現在の命令の書き込みでレジスタまたはメモリをアップデートできません。
- I1: b = t + a;
- I2: t = 3;
I2 の書き込みを I1 の前に実行することはできません。I1 の前に実行すると、b の結果が無効になりす。
- WAW (write-after-write) は、レジスタまたはメモリを特定の順序で書き込む必要がある場合の依存です。この順序に従わないと、ほかの命令が破損することがあります。
- I1: t = a * b;
- I2: c = t + 1;
- I3: t = 1;
I3 の書き込みは、I1 の書き込み後に実行する必要があります。そうしないと、I2 の結果が無効になります。
- read-after-read では、変数が揮発性と宣言されていない場合は、命令の順序を自由に並べ替えることができるので、依存はありません。変数が揮発性と宣言されている場合は、命令の順序を保持する必要があります。
たとえば、パイプラインが生成される場合、後の段階で読み出されるレジスタまたはメモリの位置が前の書き込みで変更されないようにする必要があります。これが true 依存または RAW (read-after-write) 依存です。次はその具体例です。
int top(int a, int b) {
int t,c;
I1: t = a * b;
I2: c = t + 1;
return c;
}
変数 t に依存があるので、I2 は I1 が完了するまで評価できません。ハードウェアでは、乗算に 3 クロックかかる場合、I2 がその分だけ遅延されます。上記の関数がパイプライン処理されると、Vivado HLS でこれが true 依存として検出されて、演算が適切にスケジューリングされます。データ転送最適化を使用して RAW 依存が削除されて、関数が II=1 で動作できるようになります。
メモリ依存はこの例が変数だけでなく、配列に適用される場合に発生します。
int top(int a) {
int r=1,rnext,m,i,out;
static int mem[256];
L1: for(i=0;i<=254;i++) {
#pragma HLS PIPELINE II=1
I1: m = r * a; mem[i+1] = m; // line 7
I2: rnext = mem[i]; r = rnext; // line 8
}
return r;
}
上記の例では、ループ L1 のスケジューリングにより、次のようなスケジューリング警告メッセージが表示されます。
WARNING: [SCHED 204-68] Unable to enforce a carried dependency constraint (II = 1,
distance = 1)
between 'store' operation (top.cpp:7) of variable 'm', top.cpp:7 on array 'mem' and
'load' operation ('rnext', top.cpp:8) on array 'mem'.
INFO: [SCHED 204-61] Pipelining result: Target II: 1, Final II: 2, Depth: 3.
ユーザーがインデックスを書き込んで別のインデックスを読み出しているので、このループの同じ反復内には問題はありません。2 つの命令は同時に並列で実行可能ですが、2、3 回の反復間は、読み出しと書き込みを監視するようにしてください。
// Iteration for i=0
I1: m = r * a; mem[1] = m; // line 7
I2: rnext = mem[0]; r = rnext; // line 8
// Iteration for i=1
I1: m = r * a; mem[2] = m; // line 7
I2: rnext = mem[1]; r = rnext; // line 8
// Iteration for i=2
I1: m = r * a; mem[3] = m; // line 7
I2: rnext = mem[2]; r = rnext; // line 8
2 つの連続した反復を見てみると、I1 からの乗算結果 m (レイテンシ=2) がループの次の反復の I2 で読み出され、rnext に代入されます。この場合、次の反復は前の計算の書き込みが終了する前に mem[i] の読み出しを開始できないので、RAW 依存があります。
クロック周波数が増加すると、乗算器でより多くのパイプライン段が必要になり、レイテンシが増加します。これにより、II も増加します。
次のコードでは、演算がスワップされ、機能が変更されています。
int top(int a) {
int r,m,i;
static int mem[256];
L1: for(i=0;i<=254;i++) {
#pragma HLS PIPELINE II=1
I1: r = mem[i]; // line 7
I2: m = r * a , mem[i+1]=m; // line 8
}
return r;
}
次のようなスケジューリング警告が表示されます。
INFO: [SCHED 204-61] Pipelining loop 'L1'.
WARNING: [SCHED 204-68] Unable to enforce a carried dependency constraint (II = 1,
distance = 1)
between 'store' operation (top.cpp:8) of variable 'm', top.cpp:8 on array 'mem'
and 'load' operation ('r', top.cpp:7) on array 'mem'.
WARNING: [SCHED 204-68] Unable to enforce a carried dependency constraint (II = 2,
distance = 1)
between 'store' operation (top.cpp:8) of variable 'm', top.cpp:8 on array 'mem'
and 'load' operation ('r', top.cpp:7) on array 'mem'.
WARNING: [SCHED 204-68] Unable to enforce a carried dependency constraint (II = 3,
distance = 1)
between 'store' operation (top.cpp:8) of variable 'm', top.cpp:8 on array 'mem'
and 'load' operation ('r', top.cpp:7) on array 'mem'.
INFO: [SCHED 204-61] Pipelining result: Target II: 1, Final II: 4, Depth: 4.
この後の読み出しと書き込みを数反復間監視してください。
Iteration with i=0
I1: r = mem[0]; // line 7
I2: m = r * a , mem[1]=m; // line 8
Iteration with i=1
I1: r = mem[1]; // line 7
I2: m = r * a , mem[2]=m; // line 8
Iteration with i=2
I1: r = mem[2]; // line 7
I2: m = r * a , mem[3]=m; // line 8
II が長くなるのは、WAR 依存では mem[i] から r が読み出され、乗算が実行されてから、mem[i+1] に書き込まれるからです。
false 依存の削除によるループのパイプライン改善
false 依存とは、コンパイラが保守的すぎる場合に発生する依存のことです。これらの依存は、実際のコードにはありませんが、コンパイラでは判断できません。これらの依存があると、ループ パイプラインがされないことがあります。
次の例は、フォルス依存を示しています。この例では、読み出しおよび書き込みが同じループ反復内の 2 つの異なるアドレスにアクセスします。これらのアドレスはどちらも入力データに依存し、hist 配列の個別エレメントを指定できます。このため、Vitis HLS ではこれらのアクセスのどちらも同じ位置にアクセスできると想定されます。この結果、配列への読み出しと書き込みが交互のサイクルでスケジュールされ、ループ II が 2 になります。ただし、このコードは、hist[old] および hist[val] が if(old
== val) 条件の else 分岐に含まれるため、これらが同じ位置にアクセスすることがないことを示しています。
void histogram(int in[INPUT SIZE], int hist[VALUE SIZE]) f
int acc = 0;
int i, val;
int old = in[0];
for(i = 0; i < INPUT SIZE; i++)
{
#pragma HLS PIPELINE II=1
val = in[i];
if(old == val)
{
acc = acc + 1;
}
else
{
hist[old] = acc;
acc = hist[val] + 1;
}
old = val;
}
hist[old] = acc;
この非効率性を克服するには、DEPENDENCE 指示子を指定して、Vitis HLS に依存に関する情報を入力します。
void histogram(int in[INPUT SIZE], int hist[VALUE SIZE]) {
int acc = 0;
int i, val;
int old = in[0];
#pragma HLS DEPENDENCE variable=hist intra RAW false
for(i = 0; i < INPUT SIZE; i++)
{
#pragma HLS PIPELINE II=1
val = in[i];
if(old == val)
{
acc = acc + 1;
}
else
{
hist[old] = acc;
acc = hist[val] + 1;
}
old = val;
}
hist[old] = acc;
依存には、主に二種類あります。
- Inter
- 依存が同じループ内の別の反復間にあることを指定します。
Vitis HLS でこのタイプの依存を FALSE に設定すると、ループが展開されていない場合または部分的に展開されていない場合に並列実行が可能になり、TRUE に設定すると並列実行が不可能になります。
- Intra
- 同じ反復の開始と終了でアクセスされる配列など、ループ内の同じ反復内の依存を指定します。
このタイプの依存を FALSE に設定すると、Vitis HLS によりループ内で演算を自由に移動でき、パフォーマンスまたはエリアを向上できる可能性が高くなります。TRUE に設定すると、操作は指定の順序で実行する必要があります。
スカラーの依存
スカラーの依存は解消するのがより困難で、通常ソース コードの変更が必要になります。スカラー データの依存は、次のようになります。
while (a != b) {
if (a > b) a -= b;
else b -= a;
}
このループの次の反復は、次の図に示すように、現在の反復が計算されて、a および b の値がアップデートされるまで開始しません。
ループの反復を始めるのに前のループ反復の結果が必要な場合、ループのパイプライン処理は不可能です。Vitis HLS で指定した開始間隔 (II) でパイプライン処理できない場合は、開始間隔が増加されます。パイプライン処理がまったく不可能な場合は、パイプライン処理が停止され、パイプライン処理されないデザインが出力されます。
最適なループ展開によるパイプラインの改善
Vitis HLS ではデフォルトで、ループが非展開のままになります。これらの非展開ループがループの各反復で使用されるハードウェア リソースを生成します。これにより、リソース効率の高いブロックが作成されますが、パフォーマンスのボトルネックとなることもあります。
Vitis HLS では、UNROLL を使用すると、ループを展開または一部展開できます。
次の図は、ループ展開の利点と、ループを展開する際の考慮事項を示しています。この例では、配列 a[i], b[i], and c[i] がブロック RAM にマップされると想定されます。この例は、ループの展開を適用しただけで多数のインプリメンテーションを作成できるところを示しています。
- Rolled Loop (非展開ループ)
- ループが非展開の場合、各反復は別々のクロック サイクルで実行されます。このインプリメンテーションは 4 クロック サイクルかかり、必要なのは乗算器 1 つだけで、各ブロック RAM はシングル ポート RAM にできます。
- Partially Unrolled Loop (部分展開されたループ)
- この例では、ループは係数 2 で部分的に展開されます。各 RAM に対して 2 つの読み出しまたは書き込みを同じクロック サイクルで実行するために、このインプリメンテーションには乗算器 2 つとデュアル ポート RAM が必要となりますが、完了するのに 2 クロック サイクルしかかからず、展開されていないループと比較すると、開始間隔もレイテンシも半分ですみます。
- Unrolled loop (展開ループ)
- 完全に展開されたバージョンでは、すべてのループ動作が 1 クロック サイクルで実行できます。乗算器が 4 つ必要になります。このインプリメンテーションでは、同じクロック サイクルで 4 つの読み出しと 4 つの書き込みが実行される必要があります。ブロック RAM には最大で 2 つのポートしか含めることができないので、このインプリメンテーションでは配列を分割する必要があります。
ループ展開は、デザインのループごとに UNROLL 指示子を適用すると実行できます。または、関数に UNROLL 指示子を適用して、その関数のスコープ内のすべてのループが展開されるようにします。
ループが完全に展開される場合、データ依存およびリソースが許容される限り、すべての操作が並列処理で実行されます。ループの 1 回の反復での演算に前の反復からの結果が必要な場合、それらは並列では実行されませんが、データが使用可能になるとすぐに実行されます。ループを完全に展開して最適化すると、通常ループ ボディにロジックのコピーが複数含まれるようになります。
次のコード例は、ループ展開を使用して最適化されたデザインを作成する方法を示しています。この例では、データが配列にインターリーブされたチャネルとして格納されています。ループが II=1 でパイプライン処理される場合は、各チャネルは 8 ブロック サイクルごとにのみ読み出されて書き込まれます。
// Array Order : 0 1 2 3 4 5 6 7 8 9 10 etc. 16 etc...
// Sample Order: A0 B0 C0 D0 E0 F0 G0 H0 A1 B1 C2 etc. A2 etc...
// Output Order: A0 B0 C0 D0 E0 F0 G0 H0 A0+A1 B0+B1 C0+C2 etc. A0+A1+A2 etc...
#define CHANNELS 8
#define SAMPLES 400
#define N CHANNELS * SAMPLES
void foo (dout_t d_out[N], din_t d_in[N]) {
int i, rem;
// Store accumulated data
static dacc_t acc[CHANNELS];
// Accumulate each channel
For_Loop: for (i=0;i<N;i++) {
rem=i%CHANNELS;
acc[rem] = acc[rem] + d_in[i];
d_out[i] = acc[rem];
}
}
factor を 8 に設定してループを部分的に展開すると、チャネルごと (8 つ目のサンプルごと) に並列処理できるようになります (入力配列と出力配列も cyclic で分割し、クロック サイクルごとに複数アクセスを可能にした場合)。ループが rewind オプションでパイプラインされても、最上位またはデータフロー領域内のいずれかで並列で呼び出されていれば、すべての 8 チャネルが並列で継続して処理されます。
void foo (dout_t d_out[N], din_t d_in[N]) {
#pragma HLS ARRAY_PARTITION variable=d_i cyclic factor=8 dim=1 partition
#pragma HLS ARRAY_PARTITION variable=d_o cyclic factor=8 dim=1 partition
int i, rem;
// Store accumulated data
static dacc_t acc[CHANNELS];
// Accumulate each channel
For_Loop: for (i=0;i<N;i++) {
#pragma HLS PIPELINE rewind
#pragma HLS UNROLL factor=8
rem=i%CHANNELS;
acc[rem] = acc[rem] + d_in[i];
d_out[i] = acc[rem];
}
}
ループを部分的に展開する場合、展開係数を最大反復回数の整数倍にする必要はありません。Vitis HLS では、部分的に展開されたループが元のループと同じように動作することを確認する exit チェックが追加されます。たとえば、次のようなコードがあるとします。
for(int i = 0; i < N; i++) {
a[i] = b[i] + c[i];
}
ループを係数 2 で展開すると、次の例のようにコードが変更されます。このコードでは、機能が同じになるように、break コンストラクトが使用されます。
for(int i = 0; i < N; i += 2) {
a[i] = b[i] + c[i];
if (i+1 >= N) break;
a[i+1] = b[i+1] + c[i+1];
}
N は変数なので、Vitis HLS で最大値を特定できない場合があります (入力ポートで駆動されている可能性あり)。展開係数 (この場合は 2) が最大反復カウント N の整数係数だとわかっている場合は、skip_exit_check オプションを使用すると、exit チェックと関連ロジックが削除されます。展開の結果は、次のようになります。
for(int i = 0; i < N; i += 2) {
a[i] = b[i] + c[i];
a[i+1] = b[i+1] + c[i+1];
}
これによりエリアが最小限に抑えられ、制御ロジックが単純になります。
タスク レベルの同時処理: データフロー最適化
データフロー最適化は、次の図に示すように、一連の順次タスク (関数やループなど) のセットに使用するのが便利です。
上記の図は、3 つの一連のタスクを示していますが、通信構造は表示されているよりも複雑である可能性があります。
この一連の順次タスクを使用すると、データフロー最適化で次の図に示すような同時処理プロセスのアーキテクチャが作成されます。データフロー最適化は、デザインのスループットとレイテンシを向上する優れた方法です。
次の図に、データフロー最適化によりタスクの実行をオーバーラップさせることにより、デザイン全体のスループットが向上し、レイテンシが削減することを示します。
次の図では、(A) はデータフロー最適化を適用していない場合を示しています。インプリメンテーションで新しい入力が func_A で処理できるようになるまでに 8 サイクル、出力が func_C に書き込まれるまでに 8 サイクル必要です。
(B) は同じ例でデータフロー最適化を適用した場合を示しています。func_A が 3 クロック サイクルごとに新しい入力の処理を開始できるので (開始間隔が短くなり)、最終値を出力するまでに必要なのは 5 クロック サイクルだけに (レイテンシが短く) なっています。
このタイプの並列処理では、ハードウェアに多少のオーバーヘッドが発生します。関数本体やループ本体などの特定の領域がデータフロー最適化を適用するよう指定されている場合、Vitis HLS で関数本体またはループ本体が解析され、データフローをモデリングする個別のチャネルが作成され、データフロー領域の各タスクの結果が保存されます。これらのチャネルは、スカラー変数の場合は単純な FIFO、配列のなどのスカラー変数以外のものの場合はピンポン (PIPO) バッファーです。これらの各チャネルには、FIFO またはピンポン バッファーがフルまたは空であることを示す信号も含まれます。これらの信号は、完全にデータ ドリブンのハンドシェイク インターフェイスです。FIFO またはピンポン バッファーが存在することにより、Vitis HLS で各タスクを自由に実行でき、スループットは入力および出力バッファーが使用可能かどうかでのみ制限されます。この手法では、通常のパイプライン インプリメンテーションよりもタスクの実行をより良くインターリーブできますが、追加の FIFO またはピンポン バッファー用のブロック RAM が使用されます。前の図は、次の図の同じ例をデータフロー領域で実現した構造を示しています。
データフロー最適化では、スタティックにパイプライン処理されたソリューションよりもパフォーマンスを向上できる可能性があります。厳密な中央制御型のパイプライン ストールが、FIFO またはピンポン バッファー (PIPO) を使用した配布ハンドシェイク アーキテクチャに置き換えられます。中央制御型構造を分散型に置き換えても、制御信号、たとえばレジスタ イネーブルなどが個別プロセスの制御構造間で分散され、ファンアウトが改善されます。
データフロー最適化は、一連のプロセスに制限されず、どの有向非巡回グラフ (DAG) 構造にも使用できます。プロセスが FIFO に接続されていると 1 つの反復内でオーバーラップする場合と、PIPO および FIFO を介して異なる反復間でオーバーラップする場合の、2 種類のオーバーラップが生成される可能性もあります。
正規形式
Vitis HLS は、DATAFLOW 最適化を適用するために領域を変更します。ザイリンクスでは、正規形式を使用してこの領域内 (正規領域と呼ぶ) でコードを記述することをお勧めしています。データフロー最適化を適用するには、2 つの正規形式があります。
- インライン展開されていない関数の正規形式。
void dataflow(Input0, Input1, Output0, Output1) { #pragma HLS dataflow UserDataType C0, C1, C2; func1(read Input0, read Input1, write C0, write C1); func2(read C0, read C1, write C2); func3(read C2, write Output0, write Output1); } - ループ本体内のデータフロー。
ループ (関数内でインラインなし) の場合、積分ループ変数は次のように設定する必要があります。
- ループ ヘッダーで宣言され 0 に設定される初期値。
- ループ条件は正の定数または定数の関数引数。
- 1 ずつインクリメント。
- データフロー プラグマはループ内にある必要あり。
void dataflow(Input0, Input1, Output0, Output1) { for (int i = 0; i < N; i++) { #pragma HLS dataflow UserDataType C0, C1, C2; func1(read Input0, read Input1, write C0, write C1); func2(read C0, read C0, read C1, write C2); func3(read C2, write Output0, write Output1); } }
正規本体
正規領域内では、正規本体は次のガイドラインにしたがっている必要があります。
- ローカルの非スタティック スカラーまたは配列/ポインター変数、もしくはローカルのスタティック ストリーム変数を使用します。ローカル変数は、関数本体内 (関数内のデータフローの場合) またはループ本体 (ループ内のデータフローの場合) で宣言します。
- データをフィードバックなしで、1 つの関数から別の関数に後に渡される関数呼び出しシーケンスの条件は次のとおりです。
- 変数 (スカラー以外) に含めることができるのは、1 つの読み出しプロセスと 1 つの書き込みプロセスのみです。
- ローカル変数を使用する場合は write before read (受信前に送信) を使用します。
- 関数引数を使用する場合、read before write (送信前に受信) を使用します。本体内の反依存はデザインで保持される必要があります。
- 関数の戻り値型は void である必要があります。
- 変数を介した異なるプロセス間のループ キャリー依存はないようにします。
- 正規ループ内 (1 つの反復で書き出されて次の反復で読み込まれる値など)。
- 最上位関数への連続呼び出し内 (1 つの反復で書き出されて次の反復で読み込まれる入力引数など)。
データフロー チェック
Vitis HLS には、データフロー チェッカーが含まれており、オンにすると、コードが推奨される標準形式に従っているかどうかが確認され、問題があると、エラーや警告メッセージを表示します。デフォルトでは、このチェッカーは warning に設定されています。チェッカーは config_dataflow Tcl コマンドの strict モードで error に設定したり、off を選択してディスエーブルにしたりできます。
config_dataflow -strict_mode (off | error | warning)
データフロー最適化の制限
DATAFLOW 最適化では、タスク (関数およびループ) 間、および最大限のパフォーマンスを得るため理想的にはパイプライン処理された関数およびループ間のデータの流れが最適化されます。そのためタスクを順につなげる必要はありませんが、データの転送方法にいくつかの制限があります。
次のような条件下では、Vitis HLS で DATAFLOW 最適化が実行されなかったり、設定が重複してしまう可能性があります。
- シングル プロデューサー コンシューマー違反
- タスクのバイパス
- タスク間のフィードバック
- タスクの条件付き実行
- 複数の exit 条件を持つループ
DATAFLOW 指示子が適用された場合の構造を表示できます。シングル プロデューサー コンシューマー違反
Vitis HLS で DATAFLOW 最適化が実行されるようにするには、タスク間で渡される要素すべてがシングル プロデューサー コンシューマー モデルに従っている必要があります。変数はそれぞれ 1 つのタスクから駆動され、1 つのタスクでのみ消費される必要があります。次のコード例では、temp1 がファンアウトして、Loop2 および Loop3 の両方に入力されています。これは、シングル プロデューサー コンシューマー モデルに違反しています。
void foo(int data_in[N< N; i++) {
temp1[i] = data_in[i] * scale;
temp2[i] = data_in[i] >> scale;
}
Loop2: for(int j = 0; j < N; j++) {
temp3[j] = temp1[j] + 123;
}
Loop3: for(int k = 0; k < N; k++) {
data_out[k] = temp2[k] + temp3[k];
}
}
これを修正したコードでは、Split 関数を使用して、シングル プロデューサー コンシューマー デザインを作成しています。void foo(int data_in[N], int scale, int data_out1[N], int data_out2[N]) { int temp1[N]; Loop1: for(int i = 0; i < N; i++) { temp1[i] = data_in[i] * scale; } Loop2: for(int j = 0; j < N; j++) { data_out1[j] = temp1[j] * 123; } Loop3: for(int k = 0; k < N; k++) { data_out2[k] = temp1[k] * 456; } }
この場合、データは Loop1 から Split 関数へ流れ、Loop2 と Loop3 に流れます。4 つのタスクすべての間でデータが流れるようになったので、Vitis HLS で DATAFLOW 最適化を実行できるようになります。
void Split (in[N], out1[N], out2[N]) {
// Duplicated data
L1:for(int i=1;i<N;i++) {
out1[i] = in[i];
out2[i] = in[i];
}
}
void foo(int data_in[N], int scale, int data_out1[N], int data_out2[N]) {
int temp1[N], temp2[N]. temp3[N];
Loop1: for(int i = 0; i < N; i++) {
temp1[i] = data_in[i] * scale;
}
Split(temp1, temp2, temp3);
Loop2: for(int j = 0; j < N; j++) {
data_out1[j] = temp2[j] * 123;
}
Loop3: for(int k = 0; k < N; k++) {
data_out2[k] = temp3[k] * 456;
}
}
タスクのバイパス
また、データは通常 1 つのタスクから次のタスクに流れる必要があります。タスクをバイパスすると、DATAFLOW 最適化のパフォーマンスが落ちる可能性があります。次の例の場合、Loop1 で temp1 および temp2 の値が生成されますが、次のタスク Loop2 では temp1 の値しか使用されません。temp2 の値は Loop2 の後まで使用されません。このため、temp2 はシーケンスの次のタスクをバイパスすることになり、DATAFLOW 最適化のパフォーマンスが制限されることがあります。
void foo(int data_in[N], int scale, int data_out1[N], int data_out2[N]) {
int temp1[N], temp2[N
temp2 を格納するのに使用する PIPO バッファーの深さをデフォルトの 2 から 3 に増やす必要があります。これで Loop2 が実行された状態で、バッファーに Loop3 の値が格納できるようになります。同様に、2 つのプロセスをバイパスする PIPO の深さは 4 にする必要があります。バッファーの深さは、次のように STREAM プラグマまたは指示子で設定します。#pragma HLS STREAM off variable=temp2 depth=3タスク間のフィードバック
フィードバックは、DATAFLOW 領域で 1 つのタスクからの出力が前のタスクで消費される場合に起こります。タスク間のフィードバックは、DATAFLOW 領域内では使用できません。このため、Vitis HLS でフィードバックが検出されると、状況によって警告メッセージが表示され、DATAFLOW 最適化は実行されません。
タスクの条件付き実行
DATAFLOW 最適化では、条件付きで実行されるタスクは最適化されません。次は、この制限を示す例です。この例では、Loop1 および Loop2 の条件を実行すると、データが次のループに流れなくなり、Vitis HLS でこれらのループ間のデータフローが最適化されなくなります。
void foo(int data_in1[N], int data_out[N], int sel) {
int temp1[N], temp2[N];
if (sel) {
Loop1: for(int i = 0; i < N; i++) {
temp1[i] = data_in[i] * 123;
temp2[i] = data_in[i];
}
} else {
Loop2: for(int j = 0; j < N; j++) {
temp1[j] = data_in[j] * 321;
temp2[j] = data_in[j];
}
}
Loop3: for(int k = 0; k < N; k++) {
data_out[k] = temp1[k] * temp2[k];
}
}
どの場合にも各ループが実行されるようにするには、コードを次のように変更する必要があります。この例の場合、条件文を最初のループに移動します。これで、両方のループが常に実行され、データが常に次のループに流れるようになります。
void foo(int data_in[N], int data_out[N], int sel) {
int temp1[N], temp2[N];
Loop1: for(int i = 0; i < N; i++) {
if (sel) {
temp1[i] = data_in[i] * 123;
} else {
temp1[i] = data_in[i] * 321;
}
}
Loop2: for(int j = 0; j < N; j++) {
temp2[j] = data_in[j];
}
Loop3: for(int k = 0; k < N; k++) {
data_out[k] = temp1[k] * temp2[k];
}
}
複数の exit 条件を持つループ
複数の exit ポイントのあるループは、DATAFLOW 領域では使用できません。次の例の場合、Loop2 には exit 条件が 3 つあります。
Nの値で定義される場合 (例:k>=Nで exit)。break文で定義される場合。continue文で定義される場合。#include "ap_cint.h" #define N 16 typedef int8 din_t; typedef int15 dout_t; typedef uint8 dsc_t; typedef uint1 dsel_t; void multi_exit(din_t data_in[N], dsc_t scale, dsel_t select, dout_t data_out[N]) { dout_t temp1[N], temp2[N]; int i,k; Loop1: for(i = 0; i < N; i++) { temp1[i] = data_in[i] * scale; temp2[i] = data_in[i] >> scale; } Loop2: for(k = 0; k < N; k++) { switch(select) { case 0: data_out[k] = temp1[k] + temp2[k]; case 1: continue; default: break; } } }ループの exit 条件は常にループ境界で定義されるので、
breakまたはcontinue文を使用すると、ループの使用がDATAFLOW領域で禁止されます。最後に、
DATAFLOW最適化には階層インプリメンテーションはありません。サブ関数またはループにDATAFLOWデータフロー最適が有益となる可能性のあるタスクが含まれる場合、DATAFLOW最適化をそのループまたはサブ関数に適用するか、サブ関数をインライン展開する必要があります。
DATAFLOW 領域内で std::complex を使用することもできます。ただし、次の例に示すように __attribute__((no_ctor)) を使用する必要があります。void proc_1(std::complex<float> (&buffer)[50], const std::complex<float> *in);
void proc_2(hls::Stream<std::complex<float>> &fifo, const std::complex<float> (&buffer)[50], std::complex<float> &acc);
void proc_3(std::complex<float> *out, hls::Stream<std::complex<float>> &fifo, const std::complex<float> acc);
void top(std::complex<float> *out, const std::complex<float> *in) {
#pragma HLS DATAFLOW
std::complex<float> acc __attribute((no_ctor)); // Here
std::complex<float> buffer[50] __attribute__((no_ctor)); // Here
hls::Stream<std::complex<float>, 5> fifo; // Not here
proc_1(buffer, in);
proc_2(fifo, buffer, acc);
proc_3(out, fifo, acc);
}
データフロー メモリ チャネルの設定
Vitis HLS では、タスク間のチャネルが、データのプロデューサーとコンシューマーのアクセス パターンによって、ピンポン バッファーまたは FIFO のいずれかでインプリメントされます。
- スカラー、ポインター、リファレンス パラメーターの場合、Vitis HLS はチャネルを FIFO としてインプリメントします。
- パラメーター (プロデューサーまたはコンシューマー) が配列の場合、Vitis HLS はチャネルをピンポン バッファーまたは FIFO としてインプリメントします。
- Vitis HLS でデータが順番どおり (シーケンシャルに) アクセスされると決定されると、Vitis HLS はメモリ チャネルを深さ 2 の FIFO としてインプリメントします。
- Vitis HLS でデータが順にアクセスされるかを判断できない場合、または任意の順序でアクセスされていると判断された場合、Vitis HLS はメモリ チャネルをピンポン バッファー (プロデューサーまたはコンシューマーの配列の最大サイズで定義された 2 つのブロック RAM) としてインプリメントします。注記: ピンポン バッファーが使用されると、チャネルに常に損失なしにサンプルすべてを維持できるようになりますが、場合によっては、これが控えめすぎる設定であることもあります。
タスク間で使用されるデフォルトのチャネルを明示的に指定するには、config_dataflow を使用します。これを使用すると、デザイン内のすべてのチャネルにデフォルト チャネルが設定されます。チャネルで使用されるメモリ サイズを削減し、反復内で重複できるようにするには、FIFO を使用します。FIFO 内の深さ (要素数) を明示的に設定する場合は、-fifo_depth オプションを使用します。
FIFO チャネルのサイズを指定すると、デフォルトの設定が上書きされます。デザイン内のタスクが指定した FIFO サイズよりも速いレートでサンプルを入力または出力する場合、FIFO が空またはフルになることがあります。この場合、読み出しまたは書き込みはできないので、デザインの動作が停止し、デッドロック状態になってしまうこともあります。
ザイリンクスでは、FIFO の深さを設定する場合は、最初は深さを転送されるデータの最大値 (タスク間で渡される配列のサイズ) に設定して C/RTL 協調シミュレーションで問題がないことを確認し、その後 FIFO のサイズを削減してそれでも問題がないことを C/RTL 協調シミュレーションで再度確認することを勧めします。RTL 協調シミュレーションがエラーになる場合は、FIFO のサイズが小さすぎて、停止やデッドロック状態を回避できなかった可能性があります。
RTL 協調シミュレーションの実行後に、Vitis HLS GUI に時間経過に伴う各 FIFO/PIPO バッファーの占有率のヒストグラムが表示されます。これは、各バッファーの最適な深さを判断するのに役立ちます。
配列をピンポン バッファーまたは FIFO として指定
デフォルトでは、ランダム アクセスを可能にするためすべての配列がピンポン バッファーとしてインプリメントされます。これらのバッファーは、必要に応じてサイズ指定可能です。たとえば、タスクをバイパスする場合など、パフォーマンスが落ちることがあります。パフォーマンスへの影響を少なくするには、次のように STREAM 指示子を使用してこれらのバッファーのサイズを増加し、プロデューサーとコンシューマーにスラックを追加します。
void top ( ... ) {
#pragma HLS dataflow
int A[1024];
#pragma HLS stream off variable=A depth=3
producer(A, B, …); // producer writes A and B
middle(B, C, ...); // middle reads B and writes C
consumer(A, C, …); // consumer reads A and C
最上位関数インターフェイスの配列を ap_fifo、axis、または ap_hs インターフェイス タイプに設定すると、自動的にストリーミングとして設定されます。
FIFO がインプリメンテーションに必要な場合は、STREAM 指示子を使用してデザイン内ですべての配列をストリーミングとして指定する必要があります。
-depth オプションを使用します。STREAM 指示子を使用すると、config_dataflow コンフィギュレーションで指定したデフォルトのインプリメンテーションから DATAFLOW 領域の配列を変更することもできます。
config_dataflowdefault_channelがピンポンとして設定される場合、配列に STREAM 指示子を適用すると、どの配列も FIFO としてインプリメントできます。注記: FIFO インプリメンテーションを使用するためには、配列はストリーミング手法でアクセスされる必要があります。config_dataflowdefault_channelが FIFO に設定されるか、Vitis HLS で DATAFLOW 領域のデータがストリーミング手法でアクセスされると自動的に判断された場合、STREAM 指示子に-offオプションを付けて配列に適用すると、どの配列もピンポン インプリメンテーションとしてインプリメントできます。
DATAFLOW 領域の配列がストリーミングとして指定され、FIFO としてインプリメントされる場合、FIFO に元の配列と同じ数の要素を含める必要は通常ありません。DATAFLOW 領域のタスクでは、各データ サンプルは使用可能になるとすぐに使用されます。config_dataflow コマンドに -fifo_depth オプションを指定するか、STREAM 指示子に -depth を指定すると、FIFO のサイズを最低限必要な要素数に設定して、データが一時停止しないようにできます。-off オプションを選択する場合は、-off オプションでピンポンの深さ (ブロック数) を設定します。深さは少なくとも 2 にする必要があります。
コンパイラ FIFO の深さの指定
開始の伝搬
コンパイラが自動的に開始 FIFO を作成して、開始トークンを内部プロセスまで伝搬することがあります。このような FIFO がパフォーマンスのボトルネックになることがあるので、その場合は次のコマンドを使用してデフォルト サイズの 2 を増加します。
config_dataflow -start_fifo_depth <value>
#pragma HLS DATAFLOW disable_start_propagationスカラーの伝搬
コンパイラは、C/C++ コードからのスカラーの一部をスカラー FIFO を介して自動的にプロセス間を伝搬します。このような FIFO がパフォーマンスのボトルネックになったり、デッドロックの原因となることがあるので、その場合は次のコマンドを使用してサイズを設定します (デフォルト値は -fifo_depth に設定)。
config_dataflow -scalar_fifo_depth <value>
stable 配列
stable プラグマを使用すると、データフロー領域の入力または出力変数をマークして、それらの該当する同期を削除できます (ユーザーがこの削除を本当に正しいと確認した場合のみ)。
void dataflow_region(int A[...], ...
#pragma HLS stable variable=A
#pragma HLS dataflow
proc1(...);
proc2(A, ...);
stable プラグマがなく、A が proc2 で読み出される場合、proc2 は、それが含まれるデータフロー領域で (ap_start を使用して) 初期同期の一部になります。つまり、proc1 は proc2 も再開可能な状態になるまでは再開されないので、データフロー反復が重複されなくなり、パフォーマンスが落ちてしまうことがあります。stable プラグマは、この同期が必ずしも正確性を保持するわけではないことを示しています。
前の例では、stable プラグマがない場合、A が proc2 で読み出されるとすると、proc2 がタスクをバイパスするので、パフォーマンスが落ちます。
stable プラグマがある場合、コンパイラは次のような判断をします。Aがproc2で読み出される場合、dataflow_regionの実行中に、読み出されるメモリ位置はほかのプロセスや呼び出したコンテキストで上書きされません。Aがproc2で書き込まれる場合、書き込まれたメモリ位置はdataflow_regionの実行中に、それらの定義前にほかのプロセスや呼び出したコンテキストによって読み出されません。
これは、データフロー領域がまだ開始していない場合や実行を終了していない場合にのみ、呼び出し元がこれらの変数をアップデートまたは読み出す場合によく使用されます。
データフロー内での ap_ctrl_none の使用
ap_ctrl_none ブロックレベルの I/O プロトコルを使用すると、ap_ctrl_hs および ap_ctrl_chain プロトコルから設定される柔軟性の少ない同期スキームを使用せずにすみます。これらのプロトコルでは、領域内のプロセスすべてが同じ回数分実行され、その C ビヘイビアーとより一致するようになります。
ただし、たとえば、作業を複数のより遅い作業に分散させてより頻繁に実行することで、プロセスを速めることを目的とすることがあります。
#pragma HLS interface ap_ctrl_none port=returnこれは、あくまでも次の条件が整った場合にのみ可能です。
-
含まれる領域とすべてのプロセスが FIFO (hls::stream、ストリーム配列、AXIS などのメモリ以外) を介してしか通信しない場合。
- その領域の親領域すべてが、最上位デザインまで、次の要件に従っている場合。
- すべてがデータフロー領域 (ループ内のデータフロー以外) に含まれる。
- すべてが
ap_ctrl_noneを指定する。
つまり、次のいずれも階層内の ap_ctrl_none を使用するデータフロー領域の親にはなりません。
- シーケンシャルまたはパイプライン FSM
- for ループ (ループ内のデータフロー) 内のデータフロー領域
このプラグマでは、その領域内のプロセスを同期するのに ap_ctrl_chain が使用されなくなります。これらは入力 FIFO のデータが使用できるかどうか、および出力 FIFO のスペースによって実行されたり、停止したりします。次に例を示します。
void region(...) {
#pragma HLS dataflow
#pragma HLS interface ap_ctrl_none port=return
hls::stream<int> outStream1, outStream2;
demux(inStream, outStream1, outStream2);
worker1(outStream1, ...);
worker2(outStream2, ....);
この例の場合、demux は worker1 および worker2 の 2 倍の頻度で実行できます。たとえば、II=1 の設定で、worker1 およ worker2 に II=2 を設定しても、グローバル II=1 を達成できます。
- C シミュレーションが動作するようにするには、ノンブロッキング読み出しを少ない頻度で実効されるプロセス内で注意して使用する必要があることがあります。
- プラグマは領域内の個別プロセスではなく、領域に適用されます。
- デッドロック検出は協調シミュレーションでディスエーブルにする必要があります。これは、cosim_design で説明する
-disable_deadlock_detectionオプションを使用すると設定できます。
レイテンシの最適化
レイテンシ制約の使用
Vitis HLS では、どのスコープでもレイテンシ制約の使用がサポートされます。レイテンシ制約は LATENCY 指示子を使用して指定します。
最大または最小 LATENCY 制約をスコープに設定すると、Vitis HLS で関数のすべての演算が指定のクロック サイクル内に完了するようになります。
ループに適用されたレイテンシ指示子は、ループの 1 回の反復に必要なレイテンシを指定します。つまり、次の例のように、ループ ボディのレイテンシを指定します。
Loop_A: for (i=0; i<N; i++) {
#pragma HLS latency max=10
..Loop Body...
}
すべてのループ反復のレイテンシ合計を制限するのが目的の場合は、次の例のようにループ全体を含む領域にレイテンシ指示子を指定する必要があります。
Region_All_Loop_A: {
#pragma HLS latency max=10
Loop_A: for (i=0; i<N; i++)
{
..Loop Body...
}
}
この場合、ループが展開されていたとしても、レイテンシ指示子ですべてのループ演算の最大制限が設定されます。
Vitis HLS で最大レイテンシ制約を満たすことができない場合、レイテンシ制約が緩和され、できるだけ最適な結果が達成されます。
最小レイテンシ制約を設定し、Vitis HLS で必要とされる最小値よりも短いレイテンシでデザインが生成される場合は、最小レイテンシが満たされるようにダミー クロック サイクルが挿入されます。
レイテンシを削減するためのシーケンシャル ループの結合
非展開ループからは、少なくとも 1 つのステートを持つ有限ステート マシン (FSM) が作成されます。複数のシーケンシャル ループがある場合、余分なクロック サイクルが追加されるので、これ以上の最適化ができなくなります。
次の図は、簡潔に見えるコーディング スタイルが RTL デザインのパフォーマンスに悪影響を与えている例を示しています。
前の図の (A) は、デザインの非展開ループそれぞれで少なくとも FSM の 1 ステートが作成されるところを示しています。このようなステート間の移行では、クロック サイクルが浪費されます。各ループの反復に 1 クロック サイクルかかるとすると、両方のループを実行するのに 11 サイクルかかります。
- Add ループに入るのに 1 クロック サイクル。
- Add ループを実行するのに 4 クロック サイクル。
- Add ループを抜けて Sub ループに入るのに 1 クロック サイクル。
- Sub ループを実行するのに 4 クロック サイクル。
- Sub ループを抜けるのに 1 クロック サイクル。
- 合計 11 クロック サイクル。
この単純な例の場合は、Add ループの else 分岐でも問題を解決できることは明らかですが、より複雑なコードでは簡単にはわからないことありるので、よりわかりやすいコーディング スタイルを使用した方が有益です。
LOOP_MERGE 最適化指示子を使用すると、ループが自動的に結合されるようにできます。LOOP_MERGE 指示子は、適用されたスコープ内のループすべてを結合しようとします。上記の例の場合、ループの結合により、終了するのに 6 クロックしか必要のない前の図の (B) のような制御構造が作成されます。
ループを結合すると、ループ内のロジックが一緒に最適化されるようになります。上記の例の場合、デュアル ポート ブロック RAM を使用すると、加算および乗算が並列で実行できるようになります。
現時点では、Vitis HLS でのループ統合には、次のような制限があります。
- ループの範囲がすべて変数である場合は、同じ値である必要があります。
- ループの境界が定数の場合、最大定数値が結合されたループの境界として使用されます。
- 境界が変数のループと定数のループを結合することはできません。
- 結合されるループの間のコードによる悪影響がないことを確認し、このコードを複数回実行したときに同じ結果になるようにします (たとえば、a=b は使用できますが、a=a+1 は使用できません)。
- ループに FIFO アクセスが含まれている場合は、ループどうしを結合できません。ループを結合すると、FIFO からの読み出しおよび書き込みの順序が変更されることがあります。
入れ子のループの平坦化によるレイテンシの改善
前のセクションで説明した連続するループと同じように、展開されていないネスト (入れ子) になったループ間を移動するためには追加のクロック サイクルが必要になります。外側のループから内側のループに、内側のループから外側のループに移動するのに 1 クロック サイクルかかります。
ここに示す小さい例の場合、これはループ Outer を実行するのに余分のクロック サイクルが 200 かかることを意味します。
void foo_top { a, b, c, d} {
...
Outer: while(j<100)
Inner: while(i<6) // 1 cycle to enter inner
...
LOOP_BODY
...
} // 1 cycle to exit inner
}
...
}
Vitis HLS で提供されている set_directive_loop_flatten コマンドを使用すると、ラベル付きの完全な入れ子のループとほぼ完全な入れ子のループを平坦にできるので、最適なハードウェア パフォーマンスにするためにコードを記述し直さなくても、ループ内の演算を実行するのにかかるサイクル数を削減できます。
- 完全ループ ネスト
- 一番内側のループのみにループ ボディがあり、ループ文の間にロジックは指定されておらず、すべてのループ範囲が定数。
- 半完全ループ ネスト
- 一番内側のループのみにループ ボディがあり、ループ文の間にロジックは指定されていないが、最外側ループの範囲が変数。
内側のループの範囲が可変であったり、ループ本体が一番内側のループにのみ含まれているとは限らないような不完全なループ ネストの場合は、コードの構造を変更するか、ループ ボディのループを展開して、完全なループ ネストを作成してみてください。
入れ子のループに指示子を適用する際は、ループ ボディを含む一番内側のループに適用する必要があります。
set_directive_loop_flatten top/Inner
ループの平坦化は、GUI の [Vivado HLS Directive Editor] ダイアログ ボックスを使用しても指定できます。個々のループに設定できるほか、関数レベルで指示子を適用して関数に含まれるすべてのループに設定することもできます。
エリアの最適化
データ型およびビット幅
C 関数での変数のビット幅は、RTL インプリメンテーションで使用されるストレージ エレメントのサイズと演算子に直接影響します。変数に 12 ビットのみが必要なのに整数型 (32 ビット) で指定されていると、大型で低速の 32 ビット演算子が使用され、1 クロック サイクルで実行できる演算の数が削減し、開始間隔 (II) およびレイテンシが増加する可能性があります。
- データ型には、適切な精度を使用してください。
- RAM またはレジスタとしてインプリメントされる配列のサイズを確認します。大きすぎるエレメントがあると、ハードウェア リソースでエリアが無駄になります。
- 乗算、除算、対数演算、その他の複雑な算術演算に特に注目します。これらの変数が必要以上に大きい場合、エリアおよびパフォーマンスの両方に悪影響を与えます。
関数のインライン展開
関数をインライン展開すると、関数階層が削除されます。関数をインライン展開するには、INLINE 指示子を使用します。関数をインライン展開すると、関数内でコンポーネントをより共有できたり、呼び出し関数のロジックで最適化できるので、エリアが削減されることがあります。このタイプの関数のインライン展開も Vitis HLS で自動的に実行されます。小型の関数は自動的にインライン展開されます。
インライン展開により、関数の共有を制御しやすくなります。関数が共有されるには、それらが同じ階層レベル内で使用される必要があります。このコード例の場合、foo_top 関数により foo が 2 回呼び出され、foo_sub 関数が呼び出されます。
foo_sub (p, q) {
int q1 = q + 10;
foo(p1,q); // foo_3
...
}
void foo_top { a, b, c, d} {
...
foo(a,b); //foo_1
foo(a,c); //foo_2
foo_sub(a,d);
...
}
foo_sub 関数をインライン展開し、ALLOCATION 指示子を使用して foo 関数の 1 つのインスタンスのみを指定すると、foo 関数の 1 つのインスタンスのみを含むデザイン (上記の例の 1/3 のエリア) になります。
foo_sub (p, q) {
#pragma HLS INLINE
int q1 = q + 10;
foo(p1,q); // foo_3
...
}
void foo_top { a, b, c, d} {
#pragma HLS ALLOCATION instances=foo limit=1 function
...
foo(a,b); //foo_1
foo(a,c); //foo_2
foo_sub(a,d);
...
}
INLINE 指示子を recursive オプションを指定して使用すると、指定した関数の下の関数がすべてインライン展開されます。recursive オプションを最上位関数に使用すると、デザイン内のすべての関数階層が削除されます。
INLINE の off オプションを使用すると、関数がインライン展開されないようにできます。このオプションを使用して、Vitis HLS で関数が自動的にインライン展開されるのを回避できます。
INLINE 指示子は、ソース コードに変更を加えずにコードの構造を大きく変更できるので、最適なアーキテクチャを探す効果的な手法として使用できます。
配列の再形成
ARRAY_RESHAPE 指示子は、リマッピングの垂直モードと ARRAY_PARTITIONING を組み合わせ、データに並列アクセスできる分割属性の利点を活かしながら、ブロック RAM 数を削減できます。
次のようなコードがあるとします。
void foo (...) {
int array1[N];
int array2[N];
int array3[N];
#pragma HLS ARRAY_RESHAPE variable=array1 block factor=2 dim=1
#pragma HLS ARRAY_RESHAPE variable=array2 cycle factor=2 dim=1
#pragma HLS ARRAY_RESHAPE variable=array3 complete dim=1
...
}
ARRAY_RESHAPE 指示子では配列が次の図に示すように変形されます。
ARRAY_RESHAPE 指示子を使用すると、1 クロック サイクルでより多くのデータにアクセスできるようになります。より多くのデータが 1 クロック サイクルでアクセスできる場合、このデータを消費するループを展開することでスループットが改善される場合にのみ、Vitis HLS でこれらのループが自動的に展開されます。ループを完全または部分展開すると、1 クロック サイクルでこれらの追加データを消費するのに十分なハードウェアを作成できます。この機能は config_unroll コマンドと tripcount_threshold オプションを使用して制御されます。次の例では、展開するとスループットが改善される場合にのみトリップカウントが 16 未満のループが自動的に展開されます。
config_unroll -tripcount_threshold 16
ハードウェア リソースの制御
Vitis HLS の合成は、次の基本的なタスクを実行します。
- C、C++ ソース コードを、加算、乗算、配列の読み出しおよび書き込みなどの C コードの演算子を含む内部データベースにエラボレートします。
- 演算子をハードウェアのインプリメンテーションにマップします。
インプリメンテーションは、デザイン作成のために使用される特定のハードウェア コンポーネント (加算器、乗算器、パイプライン乗算器、ブロック RAM など) です。
コマンド、プラグマ、指示子などで各段階ごとに制御ができるので、ハードウェア インプリメンテーションをより粒度の精度が高いレベルで制御できます。
演算子数の制限
デフォルトでは Vitis HLS はまずパフォーマンスを最大限にしようとするため、場合によっては、明示的に演算子の数を制限してエリア削減する必要があります。デザインの演算子数を制限する方法はエリア削減には効率的な方法で、演算を共有させるとエリアが削減しやすくなりますが、パフォーマンスが落ちる可能性があります。
ALLOCATION 指示子を使用すると、デザインで使用される演算子の数を制限できます。たとえば、foo というデザインには 317 個の乗算が含まれるのに、FPGA には 256 個の乗算器リソース (DSP48) しかないとします。Vitis HLS では、次の ALLOCATION プラグマを使用すると、最大 256 個の乗算 (mul) 演算子を含むデザインが作成されます。
dout_t array_arith (dio_t d[317]) {
static int acc;
int i;
#pragma HLS ALLOCATION instances=mul limit=256 operation
for (i=0;i<317;i++) {
#pragma HLS UNROLL
acc += acc * d[i];
}
rerun acc;
}
ALLOCATION 制限を必要な数よりも大きな値に指定すると、Vitis HLS ではその制限で指定されたリソース数を使用するか、必要な最大数を使用しようとするので、共有される量が減ります。type オプションを使用すると、ALLOCATION 指示子で演算、インプリメンテーション、または関数を制限するかどうかを指定できます。次の表に、ALLOCATION 指示子を使用して制御可能なすべての演算をリストします。
| 演算子 | 説明 |
|---|---|
| add | 整数の加算 |
| ashr | 四則演算右シフト |
| dadd | 倍精度浮動小数点の加算 |
| dcmp | 倍精度浮動小数点の比較 |
| ddiv | 倍精度浮動小数点の除算 |
| dmul | 倍精度浮動小数点の乗算 |
| drecip | 倍精度浮動小数点の逆数 |
| drem | 倍精度浮動小数点の剰余 |
| drsqrt | 倍精度浮動小数点の逆数平方根 |
| dsub | 倍精度浮動小数点の減算 |
| dsqrt | 倍精度浮動小数点の平方根 |
| fadd | 単精度浮動小数点の加算 |
| fcmp | 単精度浮動小数点の比較 |
| fdiv | 単精度浮動小数点の除算 |
| fmul | 単精度浮動小数点の乗算 |
| frecip | 単精度浮動小数点の逆数 |
| frem | 単精度浮動小数点の剰余 |
| frsqrt | 単精度浮動小数点の逆数平方根 |
| fsub | 単精度浮動小数点の減算 |
| fsqrt | 単精度浮動小数点の平方根 |
| icmp | Integer Compare |
| lshr | 論理演算右シフト |
| mul | 乗算 |
| sdiv | 符号付き除算 |
| shl | 左シフト |
| srem | 符号付き剰余 |
| sub | 減算 |
| udiv | 符号なし除算 |
| urem | 符号なし剰余 |
ハードウェア インプリメンテーションの制御
合成を実行すると、クロックで指定されたタイミング制約、ターゲット デバイスで指定された遅延、ユーザーが指定した指示子に基づいて、Vitis HLS でコードのさまざまな演算子をインプリメントするのに使用するハードウェア インプリメンテーションが決定されます。たとえば、乗算をインプリメントする場合、Vitis HLS で組み合わせ乗算器が使用されることもあれば、パイプライン乗算器が使用されることもあります。
合成中に演算子にマップされるインプリメンテーションは、演算子と同様に、ALLOCATION プラグマまたは指示子を指定して制限できます。乗算演算子の総数を制限する代わりに、組み合わせ乗算器コアの数を制限して、残りの乗算がパイプライン乗算器を使用して実行されるようにすることもできます (逆も可能)。
BIND_OP または BIND_STORAGE プラグマまたは指示子を使用すると、特定の演算またはストレージ タイプにどのインプリメンテーションを使用するかを明示的に指定できます。次のコマンドは、Vitis HLS に変数 c に 2 段パイプライン乗算器を使用するよう指示します。変数 d にどのインプリメンテーションを使用するかは、Vitis HLS で決定されます。
int foo (int a, int b) {
int c, d;
#pragma HLS BIND_OP variable=c op=mul latency=2
c = a*b;
d = a*c;
return d;
}
次の例は、BIND_OP プラグマを使用して、変数 temp の加算を dsp インプリメンテーションを使用してインプリメントすることを指定します。これにより、最終的なデザインでこの演算が DSP モジュール プリミティブを使用してインプリメントされるようになります。デフォルトでは、加算は LUT を使用してインプリメントされます。
void apint_arith(dinA_t inA, dinB_t inB,
dout1_t *out1
) {
dout2_t temp;
#pragma HLS BIND_OP variable=temp op=add impl=dsp
temp = inB + inA;
*out1 = temp;
}
演算またはストレージ タイプに指定可能なインプリメンテーションの詳細は、BIND_OP または BIND_STORAGE プラグマまたは指示子を参照してください。
次の例は、BIND_OP プラグマを使用して、out1 の乗算を 3 段パイプライン乗算器を使用してインプリメントすることを指定します。
void foo(...) {
#pragma HLS BIND_OP variable=out1 op=mul latency=3
// Basic arithmetic operations
*out1 = inA * inB;
*out2 = inB + inA;
*out3 = inC / inA;
*out4 = inD % inA;
}
代入で複数の同じ演算子が指定されている場合、各演算子に対し変数は 1 つとなるようにコードを変更する必要があります。たとえば次のコードで、最初の乗算 (inA * inB) のみをパイプライン乗算器を使用してインプリメントするとします。
*out1 = inA * inB * inC;
この場合、このコードを次のように変更し、Result_tmp 変数にプラグマを指定する必要があります。
#pragma HLS BIND_OP variable=Result_tmp op=mul latency=3
Result_tmp = inA * inB;
*out1 = Result_tmp * inC;
ロジックの最適化
シフト レジスタの推論
次のコードが使用されている場合、Vitis HLS でシフト レジスタが推論されるようになりました。
int A[N]; // This will be replaced by a shift register
for(...) {
// The loop below is the shift operation
for (int i = 0; i < N-1; ++i)
A[i] = A[i+1];
A[N] = ...;
// This is an access to the shift register
... A[x] ...
}
シフト レジスタはシフト/サイクルを実行できるので、パフォーマンスが大幅に向上し、またシフト レジスタのどこででもサイクルごとにランダムな読み出しアクセスを実行できるので、FIFO よりも柔軟性があります。
演算子のパイプライン処理の制御
Vitis HLS では、内部演算に使用するパイプライン レベルが自動的に決定されます。-latency オプションと共に BIND_OP または BIND_STORAGE を使用すると、Vitis HLS で指定された数を無効にしてパイプライン段の数を明示的に指定できます。
RTL 合成では、配置配線後に発生する可能性のあるタイミング問題を改善しやすいように、追加でパイプライン レジスタが使用されることがあります。通常は演算の出力にレジスタを追加すると、出力データパスのタイミングが改善しやすくなります。演算の入力にレジスタを追加すると、入力データパスと FSM からの制御ロジックの両方のタイミングが改善しやすくなります。
config_op コマンドを使用すると、デザインで使用されている特定の動作のすべてのインスタンスを同じパイプライン段数にパイプライン処理できます。詳細は、config_op を参照してください。
論理演算式の最適化
合成中は、駆動電流の削減やビット幅の最小化など、複数の最適化が実行されます。このような自動最適化の中には、演算式のバランス調整も含まれます。
演算式のバランス調整では、演算子を並べ替えてバランスの取れたツリーを構築することによりレイテンシを削減します。
- 整数演算の演算式のバランス調整はデフォルトでオンになっていますが、オフになっていることもあります。
- 浮動小数点演算では、演算式のバランス調整はデフォルトでオフになっていますが、オンになっていることもあります。
たとえば、+= および *= などの代入演算子を使用した次のようなシーケンシャル コードがあるとします。
data_t foo_top (data_t a, data_t b, data_t c, data_t d)
{
data_t sum;
sum = 0;
sum += a;
sum += b;
sum += c;
sum += d;
return sum;
}
演算式のバランス調整が実行されない場合、各加算に 1 クロック サイクルが必要だとすると、次の図に示すように sum の計算に 4 クロック サイクルかかります。
ただし、加算 a+b および c+d は並列実行できるので、レイテンシを削減できます。このように計算のバランスが調整されると、計算は 2 クロック サイクルで終了します。演算式のバランス調整を使用すると共有はできず、エリアは増加します。
整数に対しては、EXPRESSION_BALANCE 最適化指示子を off に設定すると演算式のバランス調整を無効にできます。デフォルトでは、Vitis HLS で float または double 型の演算に対しては EXPRESSION_BALANCE 最適化は実行されません。float および double 型を合成する際には、C シミュレーションと結果が同じになるように、Vitis HLS で C コードで実行される演算順序が維持されます。たとえば、次のコード例では、すべての変数が float または double 型です。O1 および O2 は、同じ基本的な計算を実行しているように見えますが、値は同じではありません。
A=B*C; A=B*F;
D=E*F; D=E*C;
O1=A*D O2=A*D;
これは、float または double 型の演算における C 標準の飽和および丸めの結果です。このため、Vitis HLS では float または double 型の変数が使用され、デフォルトで演算式のバランス調整が実行されない場合、演算の順序が常に維持されます。
float および double 型で演算式のバランス調整を有効にするには、config_compile コンフィギュレーション オプションを次のように使用します。
- をクリックします。
- [Solution Settings] ダイアログ ボックスで General カテゴリをクリックし、Add をクリックします。
- [Add Command] ダイアログ ボックスで config_compile を選択して unsafe_math_optimizations をオンにします。
この設定をオンにすると、Vitis HLS でさらに最適なデザインが生成されるように演算の順序が変更されることがありますが、C/RTL 協調シミュレーションの結果が C シミュレーションと異なるものになる可能性があります。
unsafe_math_optimizations を使用すると、no_signed_zeros 最適化も有効になります。no_signed_zeros 最適化により、float および double 型を使用した次の演算式が同一になります。
x - 0.0 = x;
x + 0.0 = x;
0.0 - x = -x;
x - x = 0.0;
x*0.0 = 0.0;
no_signed_zeros 最適化が使用されない場合は、丸めが実行されるので、上記の演算式は同一になりません。この最適化は、config_compile でこのオプションのみをオンにすると、演算式のバランス調整なしで使用できます。
unsafe_math_optimizations および no_signed_zero 最適化が使用されると、RTL インプリメンテーションが C シミュレーションとは異なる結果になります。テストベンチでは、結果の小さな違いは無視できるので、範囲を確認し、厳密な比較は実行しないでください。バースト転送の最適化
バースト転送の概要
最適化手法では、バースト最適化は、HLS カーネルを最適化するのにすべての通常オプションを使用しつくした後にのみ実行することが推奨されます。バーストとは、メモリ アクセスを DDR に知的に集めることで、スループット帯域幅を最大にしたり、レイテンシを最小限に抑えようとする最適化です。バーストは、多くのカーネルに対する最適化の 1 つです。通常、パフォーマンスは 4 ~ 5 倍改善しますが、ほかの最適化 (アクセスの幅を広くしたり、DDR を介した依存性がないようにしたり) を使用した方がさらにパフォーマンスを改善できます。バーストは、通常複数のカーネルが競争して DDR ポートで競合がある場合に使用すると役立ちます。
上記の図は、AXI プロトコルの動作を示しています。HLS カーネルは、長さ 8 のバーストの読み出し要求を送信してから、長さ 8 のバーストの書き込み要求を送信します。読み出しレイテンシとは、読み出し要求バーストが送信されてから、バースト内の最初の読み出し要求のデータがカーネルで受信されるまでの時間です。同様に、書き込みレイテンシとは、書き込みバースの最後の書き込みデータが送信されてから、書き込み承認がカーネルで受信されるまでの時間です。読み出し要求は、通常最初に送信可能になった時点で送信され、書き込み要求はそのバースト内の各書き込みデータが使用可能になるまでキューに格納されます。
次の図は、システムに含めることのできるさまざまなレイテンシを理解しやすくするため、HLS カーネルが DDR にバーストを送信する際に何が起こるかを示しています。
システム内のレイテンシを確認するには、次の方法もあります。たとえば、インターコネクトの平均 II が 2 で、要求時の DDR コントローラーの平均 II が 4 ~ 5 サイクルだとします (データ上は両方とも II=1)。インターコネクト アービトレーション ストラテジは、読み出し/書き込み要求のサイズに基づくので、バースト長が長いほど、短いバーストの要求よりも優先され、競合があると、広いチャネル帯域幅が長いバーストに割り当てられます。バースト要求が大きいと、ほかのユーザーが DDR にアクセスできないという影響があるので、バースト長と DDR ポートの競合削減との間で妥協点を見つける必要があります。レイテンシを長くすると、このポートの競合の一部が発生しなくなり、要求を効率的にパイプラインすることで、システム内で使用可能な帯域幅スループットがかなり改善するという利点もあります。
バースト セマンティクス
カーネルによっては、HLS コンパイラがバースト解析最適化をマルチパス最適化としてインプリメントすることもありますが、これは関数ベースでインプリメントされます。バーストは、関数に対してのみインプリメントでき、複数の関数をまたがるバーストはサポートされません。
まず、HLS コンパイラは、関数の基本ブロック内でメモリ アクセス (関数内の文のシーケンシャル セットでのメモリ アクセスなど) を検索します。バーストの前提条件が満たされると、これらの基本ブロックで推論された各バーストは、「リージョン バースト」と呼ばれます。コンパイラは自動的にこの基本ブロックをスキャンして、アクセス内の一番長いシーケンスを 1 つのリージョン バーストにビルドします。
この後、コンパイラはループを確認し、「ループ バースト」と呼ばれるバーストを推論しようとします。ループ バーストとは、ループ イテレーション中の読み出し/書き込みのシーケンスのことです。コンパイラは、ループ帰納変数とループのトリップカウントを解析して、バーストの長さを推論しようとします。解析に問題がなければ、コンパイラはループ内の各イテレーションの読み出し/書き込みシーケンスを 1 つの長いループ バーストにチェーンします。近年のコンパイラは、ループ バーストまたはリージョン バーストを自動的に推論しますが、ループ バーストかリージョン バーストを特定して要求する方法はありませんので、ツールでループ バーストかリージョン バーストが推論されるように、コードを記述する必要があります。
バーストのセマンティクスを理解するには、次のコード部分について考慮してみてください。
for(size_t i = 0; i < size; i+=4) {
out[4*i+0] = f(in[4*i+0]);
out[4*i+1] = f(in[4*i+1]);
out[4*i+2] = f(in[4*i+2]);
out[4*i+3] = f(in[4*i+3]);
}
上記のコードは、通常配列からの一連の読み出しと、ループ内からの配列への一連の書き込みを実行するために使用されるものです。次は、Vitis HLS がバースト解析最適化を実行した後に推論するであろうコードです。コンパイラは、実際の配列アクセスと並行して、追加でユーザーの選択した AXI プロトコルに必要な読み出しおよび書き込み要求をします。
ループ バースト
/* requests can move anywhere in func */
rb = ReadReq(in, size);
wb = WriteReq(out, size);
for(size_t i = 0; i < size; i+=4) {
Write(wb, 4*i+0) = f(Read(rb, 4*i+0));
Write(wb, 4*i+1) = f(Read(rb, 4*i+1));
Write(wb, 4*i+2) = f(Read(rb, 4*i+2));
Write(wb, 4*i+3) = f(Read(rb, 4*i+3));
}
WriteResp(wb);
コンパイラで帰納変数 (size) とループのトリップカウントからバースト長を問題なく推定できた場合は、Loop Burst の例に示すように、1 つの大きなループ バーストが推論され、ReadReq、WriteReq、および WriteResp 呼び出しがループ外に移動します。このため、すべてのループ イテレーションの読み出し要求は 1 つの読み出し要求にまとめられ、すべての書き込み要求は 1 つの書き込み要求にまとめられます。すべての読み出し要求は通常即座に送信されますが、書き込み要求はデータが使用可能になった後にのみ送信されることに注意してください。
ただし、バースト転送の前提条件と制限 に示すバーストの前提条件のいずれかが満たされていない場合は、ループ バーストが推論されず、その代わりにリージョン バーストが推論されることがあります。この場合、Region
Burst の例に示すように、ReadReq、WriteReg および WriteResp は読み出し/書き込みアクセスと共にバースト最適化されます。この場合、各ループ イテレーションの読み出しおよび書き込み要求が 1 つの読み出しまたは書き込み要求にまとめられます。
for(size_t i = 0; i < size; i+=4) {
rb = ReadReq(in+4*i, 4);
wb = WriteReq(out+4*i, 4);
Write(wb, 0) = f(Read(rb, 0));
Write(wb, 1) = f(Read(rb, 1));
Write(wb, 2) = f(Read(rb, 2));
Write(wb, 3) = f(Read(rb, 3));
WriteResp(wb);
}
バースト転送の前提条件と制限
バーストの前提条件
バーストは、連続するメモリ アクセス要求をまとめることです。バースト最適化が正しく実行されるようにするには、連続するアクセスが次の前提条件を満たしている必要があります。
- すべてが読み出しまたはすべて書き込みである必要があります。読み出しと書き込みをバースト転送することはできません。
- アクセスするメモリのロケーションおよび時間が単調に増加する必要があります。以前にアクセスされたメモリ ロケーションの間のメモリ ロケーションにアクセスすることはできません。
- メモリ内でギャップまたはオーバーラップなしで進行順に連続している必要があります。
- 読み出し/書き込みアクセスの数 (バースト長) が要求の送信前に決定されている必要があります。これは、バースト長がパラメーターで指定されている場合は、読み出し/書き込み要求の送信前に計算される必要があるということです。
- 2 つの配列を同じ MAXI ポートにまとめる場合、一度に 1 方向に 1 つの配列にしかバーストを実行できません。
- バースト要求が発行されてから終了するまでに依存の問題がないことが必要です。
メモリ アクセスのオーバーラップのため外側のループのバースト推論は不可
次の例では、ループ L1 の反復 0 と反復 1 が配列 a および b の同じ要素にアクセスするので、外側のループにバーストは推論されません。現在のところ、バーストの推論はすべてかすべてなしかの最適化であり、一部のみをバースト推論することはできません。これは、バースト長を最適化しようとする貪欲アルゴリズムです。この場合、内側のループに長さ 9 のバーストが推論されます。
L1: for (int i = 0; i < 8; ++i)
L2: for (int j = 0; j < 9; ++j)
b[i*8 + j] = a[i*8 + j];
itr 0: |0 1 2 3 4 5 6 7 8|
itr 1: | 8 9 10 11 12 13 14 15 16|
ap_int/ap_uint 型をループ帰納変数として使用
バーストの推論はループ帰納変数およびトリップカウントによるので、ネイティブでないデータ型を使用すると、最適化が実行されない可能性があります。ループ帰納変数には、常に符号なし整数型を使用することをお勧めします。
1 回以上ループに入る必要がある
場合によって、コンパイラでループ帰納変数の最大値は 0 にならないということが推論されない (常にループに入るかどうかを判断できない) ことがあります。このような場合は、assert 文を使用するとコンパイラでこれを推論できるようになることがあります。
assert (N > 0);
L1: for(int a = 0; a < N; ++a) { … }
配列でのループ間またはループ内依存
配列内のあるロケーションに書き込み、同じ反復または次の反復でそれを読み出す場合、最適化でこのような配列依存を解釈するのが困難になる可能性があります。このような場合、書き込みが読み出しの前に実行されることを確実にできないので、最適化を実行できません。
メモリへの条件付きアクセス
メモリ アクセスを条件付きにすると、条件文を解釈できないので、バースト推論アルゴリズムが機能しません。この場合、コンパイラで条件が単純化されるか削除されます。そのため、メモリ アクセスには条件文を使用しないことをお勧めします。
ループから呼び出された関数内からの M-AXI アクセス
関数間の配列アクセスは、バースト推論などのコンパイラによる変換ではうまく解析されません。このような場合は、INLINE プラグマまたは指示子を使用して関数をインライン展開すると、バーストが推論されない問題を回避できます。
void my_function(hls::stream<T> &out_pkt, int *din, int input_idx) {
T v;
v.data = din[input_idx];
out_pkt.write(v);
}
void my_kernel(hls::stream<T> &out_pkt,
int *din,
int num_512_bytes,
int num_times) {
#pragma HLS INTERFACE m_axi port = din offset=slave bundle=gmem0
#pragma HLS INTERFACE axis port=out_pkt
#pragma HLS INTERFACE s_axilite port=din bundle=control
#pragma HLS INTERFACE s_axilite port=num_512_bytes bundle=control
#pragma HLS INTERFACE s_axilite port=num_times bundle=control
#pragma HLS INTERFACE s_axilite port=return bundle=control
unsigned int idx = 0;
L0: for (int i = 0; i < ntimes; ++i) {
L1: for (int j = 0; j < num_512_bytes; ++j) {
#pragma HLS PIPELINE
my_function(out_pkt, din, idx++);
}
}
バーストが推論されないのは、メモリ アクセスが呼び出し元の関数から実行されていることが原因です。バーストが推論されるようにするには、M-AXI メモリにアクセスする関数をインライン展開することをお勧めします。
この例でバーストが推論されないもう 1 つの理由は、my_function の din を介してアクセスされるメモリが変数 (idx) で定義されていますが、この変数はループ帰納変数 i および j の関数でないため、順次または単調でない可能性があることです。idx を渡す代わりに、(i*num_512_bytes+j) を使用してください。
データフロー ループでのループ バーストの推論
DATAFLOW プラグマまたは指示子が指定されているループでは、バーストの推論はサポートされません。ただし、DATAFLOW プラグマまたは指示子が指定されているループ内の各プロセス/タスクはバースト可能です。また、データフロー領域ではタスクを並列実行できるので、M-AXI ポートの共有もサポートされません。
AXI4 バースト ビヘイビアーを制御するオプション
最適な AXI4 インターフェイスとは、バスへのアクセス待機中にデザインが停止せず、バス アクセスが承認された後の読み出し/書き込み待機中にバスが停止しないようなインターフェイスです。最適な AXI4 インターフェイスを作成するには、INTERFACE 指示子に次のコマンドオプションを使用して、バースト ビヘイビアーを指定し、AXI4 インターフェイスの効率を改善します。
これらのオプションの中には、内部ストレージを使用してデータをバッファリングすることがあるので、エリアおよびリソースへの影響があるものもあります。
- latency
- AXI4 インターフェイスのレイテンシを指定して、読み出しまたは書き込みの前にバスが複数サイクル (レイテンシ) の要求を送信するようにできます。このレイテンシ値が小さすぎると、デザインが準備完了になるのが早すぎ、バスを待つために停止する可能性があります。レイテンシ値が大きすぎると、バス アクセスは承認されても、デザインがアクセスを開始するのを待つためにバスが停止する可能性があります。Vitis HLS のデフォルト レイテンシは 64 です。
- max_read_burst_length
- バースト転送中に読み出されるデータ値の最大数を指定します。デフォルト値は 16 です。
- num_read_outstanding
- デザインが停止する前に、AXI4 バスに対して応答なしで送信できる読み出し要求の数を指定します。これにより、デザインの内部ストレージである FIFO のサイズ (
num_read_outstanding*max_read_burst_length*word_size) が決まります。デフォルト値は 16 です。 - max_write_burst_length
- バースト転送中に書き込まれるデータ値の最大数を指定します。デフォルト値は 16 です。
- num_write_outstanding
- デザインが停止する前に、応答なしで AXI4 バスに何回書き込み要求を送信できるかを指定します。これにより、デザインの内部ストレージである FIFO のサイズ (
num_read_outstanding*max_read_burst_length*word_size) が決まります。デフォルト値は 16 です。
#pragma HLS interface m_axi port=input offset=slave bundle=gmem0
depth=1024*1024*16/(512/8) latency=100 num_read_outstanding=32 num_write_outstanding=32
max_read_burst_length=16 max_write_burst_length=16インターフェイスは、レイテンシ 100 になるように指定されています。HLS コンパイラでは、デザインが AXI4 バスにアクセスできるようになる 100 クロック サイクル前に、バースト アクセス要求をスケジューリングしようとします。バスの効率をさらに向上するには、num_write_outstanding および num_read_outstanding オプションを使用して、デザインに最大 32 個の読み出しおよび書き込みアクセスを格納できるバッファー容量が含まれるようにします。要求ごとにバッファーが必要になります。これで、バス要求が送信されるまでデザインの処理を続行できるようになります。最後に、max_read_burst_length および max_write_burst_length オプションを使用することで、最大バースト サイズが 16 になり、AXI4 インターフェイスがそれを超える時間バスを保持しないようにします。これらのオプションを使用すると、AXI4 インターフェイスの動作をそれが実行されるシステム用に最適化できます。操作の効率は、これらの値が正しく設定されているかどうかによって異なります。デフォルト値を保守的な境界として指定した場合は、アクセラレーションされるデザインのメモリ アクセス プロファイルによっては変更する必要があることがあります。
| Vitis HLS コマンド | 値 | 説明 |
|---|---|---|
| config_rtl -m_axi_conservative_mode |
ブール値 デフォルト = false |
書き込みデータがすべて揃うまで (通常はアダプターに格納されるか、既に発行済み)、書き込み要求ごとに M-AXI が遅延されます。これにより書き込みレイテンシが多少長くなりますが、メモリ サブシステムに対する同時要求 (読み出しまたは書き込み) により発生するデッドロックを解決できます。 |
| config_interface -m_axi_latency |
uint 0 は auto -flow_target が vivado に指定されている場合はデフォルトは 0 -flow_target が vitis に指定されている場合はデフォルトは 64 |
M-AXI アクセスの予測レイテンシを含めてスケジューラに表示します。レイテンシは、読み出し要求と最初の読み出しデータ間、または最後の書き込みデータと書き込み応答間の遅延です。この数値は正確である必要はありません。過小評価するとレイテンシの短いスケジューリングができますが、ダイナミックな停止が長くなる点に注意してください。スケジューラは、追加のアダプター レイテンシを把握して、数サイクル追加します。 |
| config_interface -m_axi_min_bitiwidth |
uint デフォルト = 8 |
M-AXI インターフェイスのデータ チャネルの最小ビット幅。8 ~ 1024 の 2 のべき乗を指定する必要があります。実際のアクセスが必要なインターフェイスよりも少ない場合は、このオプションでスループットが必ずしも増加するというわけではありません。 |
| config_interface -m_axi_max_bitwidth |
uint デフォルト = 1024 |
M-AXI インターフェイスのデータ チャネルの最小ビット幅。8 ~ 1024 の 2 のべき乗を指定する必要があります。実際のアクセスが必要なインターフェイスよりも多い場合、アクセスがマルチサイクル バーストに分割されるので、スループットが落ちます。 |
| config_interface -m_axi_max_widen_bitwidth |
uint -flow_target が vivado に指定されている場合はデフォルトは 0 -flow_target が vitis に指定されている場合はデフォルトは 512 |
M-AXI インターフェイスのバーストを選択した幅まで自動的に広げるようになります。8 ~ 1024 の 2 のべき乗を指定する必要があります。バースト幅を広げる場合は、バーストだけでなく、強力なアライメント プロパティが必要となります。 |
| config_interface -m_axi_auto_max_ports |
bool デフォルト = false |
オプションが false の場合、明示的にバンドルされていない M-AXI インターフェイスすべてが 1 つの共通のインターフェイスにまとめられるので、リソース使用量が少なくなります (アダプターは 1 つ)。オプションが true の場合、明示的にバンドルされていない M-AXI インターフェイスはすべて個別のインターフェイスにマップされるので、リソース使用量が増加します (アダプターは複数)。 |
| config_interface -m_axi_alignment_byte_size |
uint -flow_target が vivado に指定されている場合はデフォルトは 0 -flow_target が vitis に指定されている場合はデフォルトは 64 |
M-AXI インターフェイスにマップされる最上位関数ポインターが少なくとも指定したバイト幅 (2 のべき乗) に揃えられるとするとします。これにより、自動バースト拡張が実行されやすくなります。警告: ポインターが実際に実行時にアライメントされない場合、動作が正しくなりません。 |
| config_interface -m_axi_num_read_outstanding |
uint デフォルト = 16 |
M-AXI インターフェイスの num_read_outstanding パラメーターのデフォルト値。 |
| config_interface -m_axi_num_write_outstanding |
uint デフォルト = 16 |
M-AXI インターフェイスの num_write_outstanding パラメーターのデフォルト値。 |
| config_interface -m_axi_max_read_burst_length |
uint デフォルト = 16 |
M-AXI インターフェイスの max_read_burst_length パラメーターのデフォルト値。 |
| config_interface -m_axi_max_write_burst_length |
uint デフォルト = 16 |
M-AXI インターフェイスの max_write_burst_length パラメーターのデフォルト値。 |
推奨されるコーディング スタイルの例
Vitis HLS では、バーストが可能な部分が見つかると情報メッセージが表示されます。バーストが不可能な場合は、メッセージの数が多くなりすぎるので、メッセージは表示されません。可能な場合は、バースト長とバーストのビット幅の統計もレポートされます。可変長のバーストが実行された場合は、可変長のバーストが推論されたことがメッセージに示されます。これらのメッセージは、コンパイラ ログ vitis_hls.log の始めの方で、スケジューリング段階の前に表示されます。
単純な読み出し/書き込みの推論
INFO: [HLS 214-115] Burst read of variable length and bit width 32 has been inferred on port 'gmem'
INFO: [HLS 214-115] Burst write of variable length and bit width 32 has been inferred on port 'gmem' (./src/vadd.cpp:75:9). この例のコードは次のとおりです。
/****** BEGIN EXAMPLE *******/
#define DATA_SIZE 2048
// Define internal buffer max size
#define BURSTBUFFERSIZE 256
//TRIPCOUNT identifiers
const unsigned int c_min = 1;
const unsigned int c__max = BURSTBUFFERSIZE;
const unsigned int c_chunk_sz = DATA_SIZE;
extern "C" {
void vadd(int *a, int size, int inc_value) {
// Map pointer a to AXI4-master interface for global memory access
#pragma HLS INTERFACE m_axi port=a offset=slave bundle=gmem max_read_burst_length=256 max_write_burst_length=256
// We also need to map a and return to a bundled axilite slave interface
#pragma HLS INTERFACE s_axilite port=a bundle=control
#pragma HLS INTERFACE s_axilite port=size bundle=control
#pragma HLS INTERFACE s_axilite port=inc_value bundle=control
#pragma HLS INTERFACE s_axilite port=return bundle=control
int burstbuffer[BURSTBUFFERSIZE];
// Per iteration of this loop perform BURSTBUFFERSIZE vector addition
for (int i = 0; i < size; i += BURSTBUFFERSIZE) {
#pragma HLS LOOP_TRIPCOUNT min=c_min*c_min max=c_chunk_sz*c_chunk_sz/(c_max*c_max)
int chunk_size = BURSTBUFFERSIZE;
//boundary checks
if ((i + BURSTBUFFERSIZE) > size)
chunk_size = size - i;
// memcpy creates a burst access to memory
// multiple calls of memcpy cannot be pipelined and will be scheduled sequentially
// memcpy requires a local buffer to store the results of the memory transaction
memcpy(burstbuffer, &a[i], chunk_size * sizeof(int));
// Calculate and write results to global memory, the sequential write in a for loop can be
// inferred as a memory burst access
calc_write:
for (int j = 0; j < chunk_size; j++) {
#pragma HLS LOOP_TRIPCOUNT min=c_size_max max=c_chunk_sz
#pragma HLS PIPELINE II=1
burstbuffer[j] = burstbuffer[j] + inc_value;
a[i + j] = burstbuffer[j];
}
}
}
2 次元配列の行データのアクセス
INFO: [HLS 214-115] Burst read of length 256 and bit width 512 has been inferred on port 'gmem' (./src/row_array_2d.cpp:43:5)
INFO: [HLS 214-115] Burst write of length 256 and bit width 512 has been inferred on port 'gmem' (./src/row_array_2d.cpp:56:5)
この例のコードは次のとおりです。
/****** BEGIN EXAMPLE *******/
// Parameters Description:
// NUM_ROWS: matrix height
// WORD_PER_ROW: number of words in a row
// BLOCK_SIZE: number of words in an array
#define NUM_ROWS 64
#define WORD_PER_ROW 64
#define BLOCK_SIZE (WORD_PER_ROW*NUM_ROWS)
// Default datatype is integer
typedef int DTYPE;
typedef hls::stream<DTYPE> my_data_fifo;
// Read data function: reads data from global memory
void read_data(DTYPE *inx, my_data_fifo &inFifo) {
read_loop_i:
for (int i = 0; i < NUM_ROWS; ++i) {
read_loop_jj:
for (int jj = 0; jj < WORD_PER_ROW; ++jj) {
#pragma HLS PIPELINE II=1
inFifo << inx[WORD_PER_ROW * i + jj];
;
}
}
}
// Write data function - writes results to global memory
void write_data(DTYPE *outx, my_data_fifo &outFifo) {
write_loop_i:
for (int i = 0; i < NUM_ROWS; ++i) {
write_loop_jj:
for (int jj = 0; jj < WORD_PER_ROW; ++jj) {
#pragma HLS PIPELINE II=1
outFifo >> outx[WORD_PER_ROW * i + jj];
}
}
}
// Compute function is pretty simple because this example is focused on efficient
// memory access pattern.
void compute(my_data_fifo &inFifo, my_data_fifo &outFifo, int alpha) {
compute_loop_i:
for (int i = 0; i < NUM_ROWS; ++i) {
compute_loop_jj:
for (int jj = 0; jj < WORD_PER_ROW; ++jj) {
#pragma HLS PIPELINE II=1
DTYPE inTmp;
inFifo >> inTmp;
DTYPE outTmp = inTmp * alpha;
outFifo << outTmp;
}
}
}
extern "C" {
void row_array_2d(DTYPE *inx, DTYPE *outx, int alpha) {
// AXI master interface
#pragma HLS INTERFACE m_axi port = inx offset = slave bundle = gmem
#pragma HLS INTERFACE m_axi port = outx offset = slave bundle = gmem
// AXI slave interface
#pragma HLS INTERFACE s_axilite port = inx bundle = control
#pragma HLS INTERFACE s_axilite port = outx bundle = control
#pragma HLS INTERFACE s_axilite port = alpha bundle = control
#pragma HLS INTERFACE s_axilite port = return bundle = control
my_data_fifo inFifo;
// By default the FIFO depth is 2, user can change the depth by using
// #pragma HLS stream variable=inFifo depth=256
my_data_fifo outFifo;
// Dataflow enables task level pipelining, allowing functions and loops to execute
// concurrently. For more details please refer to UG902.
#pragma HLS DATAFLOW
// Read data from each row of 2D array
read_data(inx, inFifo);
// Do computation with the acquired data
compute(inFifo, outFifo, alpha);
// Write data to each row of 2D array
write_data(outx, outFifo);
return;
}
}
まとめ
バーストが推論可能なようにコードを記述します。前提条件に違反がないようにします。
バーストは、すべてのデータを一気に取得できるということではなく、結果を 1 つにまとめることです。データは 1 つずつ順番に到着します。
バースト長は 16 が理想的ですが、8 でも十分です。バーストが長いとレイテンシが長くなり、バーストが短いとパイプライン処理できます。バーストとパイプラインを混同しないようにしてください。ただし、バーストはその他のバーストを使用してパイプライン処理可能なことに注意してください。
バーストが長いと、AXI インターコネクトを使用した場合に優先度が高くなります。カーネル内ではダイナミック アービトレーションは実行されません。
同じ DDR に 2 つの m_axi ポートを接続してカーネル内で一緒にアクセスしないように記述することはできますが、カーネル内で競争する要求はカーネル外の AXI インターコネクトで調整されます。
順不同アクセスの制限を回避するには、BRAM にバッファーを作成し、このバッファーにバーストを格納してから、このバッファーを使用して順不同アクセスを実行します。これは、通常ライン バッファーと呼ばれ、ビデオ プロセッシングでよく使用される最適化です。
RTL ブラック ボックスの追加
RTL ブラック ボックスは、HLS プロジェクト内の既存の RTL IP を使用できるようにします。これにより、RTL コードを C コードに追加して、Vitis HLS でプロジェクトを合成できます。RTL IP は、シーケンシャル、パイプライン、またはデータフロー領域で使用できます。
- RTL コードの C 関数シグネチャ。これは、ヘッダー ファイル (.h) に配置されます。
- ブラック ボックスの JSON 記述ファイル (RTL ブラック ボックスの JSON ファイル を参照)。
- RTL IP ファイル。
HLS プロジェクトで RTL ブラック ボックスを使用するには、次の手順に従います。
- Vitis HLS プロジェクトで最上位関数内から C 関数シグネチャまたはサブ関数を呼び出します。
- 新規 Vitis HLS プロジェクトの作成 で説明するように、Vitis HLS GUI から Add Files コマンドを使用するか、
add_filesコマンドを使用して、ブラック ボックスの JSON 記述ファイルを HLS プロジェクトに追加します。add_files –blackbox my_file.jsonヒント: 次のセクションで説明する新しい RTL Blackbox Wizard を使用すると、JSON ファイルを生成し、プロジェクトに RTL IP を簡単に追加できます。 - Vitis HLS デザイン フローでシミュレーション、合成、協調シミュレーションを通常どおり実行します。
要件および制限
- Verilog (.v) コードである必要があります。
- 固有のクロック信号と固有のリセット信号 (アクティブ High) が含まれている必要があります。
- RTL IP をイネーブルまたは停止する CE 信号が含まれている必要があります。
- ブロック レベル I/O プロトコル で説明するように、
ap_ctrl_chainプロトコルを使用している必要があります。
Vitis HLS の RTL ブラック ボックスの機能は、次のとおりです。
- C++ のみがサポートされます。
- 最上位インターフェイスの I/O 信号には接続できません。
- テスト対象デザイン (DUT) としては直接使用できません。
structまたはclass型のインターフェイスはサポートされません。- RTL ブラック ボックスの JSON ファイル で説明される次のインターフェイス プロトコルがサポートされます。
- hls::stream
- RTL ブラック ボックス IP では
hls::streamインターフェイスがサポートされます。このデータ型が C 関数で使用される場合は、RTL ブラック ボックス IP でこの引数にFIFORTL ポート プロトコルを使用します。 - 配列
- RTL ブラック ボックス IP では、配列に対して RAM インターフェイス インターフェイスがサポートされます。C 関数の配列引数では、次の RTL ポート プロトコルのいずれかを RTL ブラック ボックス IP の該当する引数に使用します。
- シングル ポート RAM – RAM_1P
- デュアル ポート RAM – RAM_T2P
- スカラーおよび入力ポインター
- RTL ブラック ボックス IP では、C スカラーと入力ポインターがシーケンシャルおよびパイプライン領域でのみサポートされます。データフロー領域ではサポートされません。このコンストラクトが C 関数で使用される場合は、RTL IP で
wireポート プロトコルを使用します。
- 入出力および出力ポインター
- RTL ブラック ボックス IP では、入出力および出力ポインターがシーケンシャルおよびパイプライン領域でのみサポートされます。データフロー領域ではサポートされません。このコンストラクトが C 関数で使用される場合は、RTL IP で出力ポインターに
ap_vldを、入出力ポインターにap_ovldを使用する必要があります。
RTL Blackbox Wizard の使用
このウィザードは、JSON ファイルを作成するプロセスがページ別に分類されます。ページ間は [Next] および [Back] をクリックして移動します。最終的なオプションが決定したら、[OK] をクリックして JSON ファイルを生成します。次のセクションでは、それぞれのページとその入力オプションについて説明します。
C++ モデルとヘッダー ファイル
[Blackbox C/C++ Files] ダイアログ ボックスには、RTL IP の機能モデルを記述する C++ ファイルを指定します。この C++ モデルは、C シミュレーションおよび C/RTL 協調シミュレーション中にのみ使用されます。RTL IP は Vitis HLS の結果と統合されて、合成出力が作成されます。
このダイアログ ボックスでは、次を実行できます。
- Add Files をクリックすると、ファイルを追加できます。
- Edit CFLAGS をクリックすると、機能 C モデルにリンカー フラグを指定できます。
- Next をクリックします。
[C File Wizard] ページでは、RTL IP の C++ 機能モデルに使用する値を指定できます。このフィールドには、次が含まれます。
- [C Function]
- RTL IP の C++ 関数の名前を指定します。
- [C Argument Name]
- 関数引数の名前を指定します。これらは、IP のポートに関連付ける必要があります。
- [C Argument Type]
- 各引数に使用するデータ型を指定します。
- [C Port Direction]
- IP のポートに対応して、引数のポート方向を指定します。
- [RAM Type]
- インターフェイスで使用される RAM タイプを指定します。
- [RTL Group Configuration]
- 該当する RTL 信号名を指定します。
Next をクリックします。
RTL IP 定義
RTL Blackbox Wizard のページでは、IP の RTL ソースを定義できます。定義するフィールドは、次のとおりです。
- [RTL Files]
- このオプションは、既存の RTL IP ファイル追加するか削除するのに使用します。
- [RTL Module Name]
- 最上位 RTL IP モジュール名を指定します。
- [Performance]
- IP のパフォーマンス目標を指定します。
- [Latency]
- レイテンシは、デザインが終了するのに必要な時間です。このフィールドには、レイテンシ情報を指定します。
- [II]
- 目標とする II (開始間隔) を定義します。これは、新規入力が適用可能になるまでのクロック サイクル数です。
- [Resource]
- RTL IP のデバイス リソース使用率を指定します。ここに指定するリソース情報は、合成からの使用率と統合され、全体のデザイン リソース使用率がレポートされます。この情報は、Vivado Design Suite から抽出できます。
Next をクリックし、次の図に示す [RTL Common Signal] ページへ進みます。
- module_clock
- RTL IP で使用されるクロックの名前を指定します。
- module_reset
- RTL IP で使用されるリセット信号の名前を指定します。
- module_clock_enable
- RTL IP で使用されるクロック イネーブルの名前を指定します。
- ap_ctrl_chain_protocol_start
- RTL IP で使用されるブロック制御 start 信号の名前を指定します。
- ap_ctrl_chain_protocol_ready
- RTL IP で使用されるブロック制御 ready 信号の名前を指定します。
- ap_ctrl_chain_protocol_done
- RTL IP で使用されるブロック制御 done 信号の名前を指定します。
- ap_ctrl_chain_protocol_continue
- RTL IP で使用されるブロック制御 continue 信号の名前を指定します。
Finish をクリックすると、指定した IP の JSON ファイルが自動的に生成されます。これは、次に示すようにログ メッセージで確認できます。
ログ メッセージ: "[2019-08-29 16:51:10] RTL Blackbox Wizard
Information: the "foo.json" file has been created in the rtl_blackbox/Source
folder."
JSON ファイルは、[Source] ファイル フォルダーからアクセスでき、次のセクションに示すように生成されます。
RTL ブラック ボックスの JSON ファイル
JSON ファイルのフォーマット
次の表に、JSON ファイルのフォーマットを示します。
| 項目 | 属性 | 説明 |
|---|---|---|
| c_function_name | ブラック ボックスの C++ 関数名。c_function name フィールドは、C 関数シミュレーション モデルと同じにする必要があります。 | |
| rtl_top_module_name | ブラック ボックスの RTL 関数名。rtl_top_module_name は c_function_name と同じにする必要があります。 | |
| c_files | c_file | ブラック ボックス モジュールに使用される C ファイルを指定。 |
| cflag | 該当する C ファイルに必要なコンパイル オプションを提供。 | |
| rtl_files | ブラック ボックス モジュールの RTL ファイルを指定。 | |
| c_parameters | c_name |
ブラック ボックス C++ 関数に使用される引数の名前を指定。 未使用の c_parameters フィールドは、テンプレートから削除する必要があります。 |
| c_port_direction | 該当する C 引数のアクセス方向。
|
|
| RAM_type | 対応する C 引数が RTL の RAM プロトコルを使用する場合に使用する RAM タイプを指定します。次の 2 つのタイプの RAM を使用可能です。
|
|
| rtl_ports |
対応する C 引数 ( 次の 5 つのタイプの RTL ポート プロトコルを使用できます。詳細は、「RTL ポート プロトコル」の表を参照してください。
|
|
| c_return | c_port_direction | out にする必要があります。 |
| rtl_ports | RTL ブラック ボックス IP で使用される該当する RTL ポート名を指定。 | |
| rtl_common_signal | module_clock | RTL ブラック ボックス モジュールの一意のクロック信号。 |
| module_reset | RTL ブラック ボックス モジュールのリセット信号を指定。リセット信号は、アクティブ High のまたは正の valid にする必要があります。 | |
| module_clock_enable | RTL ブラック ボックス モジュールのクロック イネーブル信号を指定します。イネーブル信号は、アクティブ High にする必要があります。 | |
| ap_ctrl_chain_protocol_idle | RTL ブラック ボックス モジュール用の ap_ctrl_chain プロトコルの ap_idle 信号。 |
|
| ap_ctrl_chain_protocol_start | RTL ブラック ボックス モジュール用の ap_ctrl_chain プロトコルの ap_start 信号。 |
|
| ap_ctrl_chain_protocol_ready | RTL ブラック ボックス IP 用の ap_ctrl_chain プロトコルの ap_ready 信号。 |
|
| ap_ctrl_chain_protocol_done | ブラック ボックス RTL モジュール用の ap_ctrl_chain プロトコルの ap_done 信号。 | |
| ap_ctrl_chain_protocol_continue | RTL ブラック ボックス モジュール用の ap_ctrl_chain プロトコルの ap_continue 信号。 |
|
| rtl_performance | latency | RTL ブラック ボックス モジュールのレイテンシを指定。負以外の整数値にする必要があります。組み合わせ RTL IP の場合は 0 を、それ以外の場合はその RTL モジュールのレイテンシをそのまま指定します。 |
| II | 関数が新しい入力データを受信できるようになるまでのクロック サイクル数。負以外の整数値にする必要があります。0 は、ブラック ボックスがパイプライン処理できないことを意味します。それ以外の場合、ブラック ボックス モジュールはパイプライン処理されます。 |
|
| rtl_resource_usage | FF | RTL ブラック ボックス モジュールのレジスタ使用率を指定。 |
| LUT | RTL ブラック ボックス モジュールの LUT 使用率を指定。 | |
| BRAM | RTL ブラック ボックス モジュールのブロック RAM 使用率を指定。 | |
| URAM | RTL ブラック ボックス モジュールの URAM 使用率を指定。 | |
| DSP | RTL ブラック ボックス モジュールの DSP 使用率を指定。 |
| RTL ポート プロトコル | RAM タイプ | C ポートの方向 | 属性 | ユーザー定義の名前 | 注記 |
|---|---|---|---|---|---|
| wire | in | data_read_in | RTL ブラック ボックス IP で使用されるユーザー定義の名前を指定。たとえば、wire の場合、RTL ポート名が flag であれば、JSON ファイルのフォーマットは "data_read-in" : "flag" になります。 | ||
| ap_vld | out | data_write_out | |||
| data_write_valid | |||||
| ap_ovld | inout | data_read_in | |||
| data_write_out | |||||
| data_write_valid | |||||
| FIFO | in | FIFO_empty_flag | 負の valid にする必要あり。 | ||
| FIFO_read_enable | |||||
| FIFO_data_read_in | |||||
| out | FIFO_full_flag | 負の valid にする必要あり。 | |||
| FIFO_write_enable | |||||
| FIFO_data_write_out | |||||
| RAM | RAM_1P | in | RAM_address | ||
| RAM_clock_enable | |||||
| RAM_data_read_in | |||||
| out | RAM_address | ||||
| RAM_clock_enable | |||||
| RAM_write_enable | |||||
| RAM_data_write_out | |||||
| inout | RAM_address | ||||
| RAM_clock_enable | |||||
| RAM_write_enable | |||||
| RAM_data_write_out | |||||
| RAM_data_read_in | |||||
| RAM | RAM_T2P | in | RAM_address | RTL ブラック ボックス IP で使用されるユーザー定義の名前を指定。たとえば、wire の場合、RTL ポート名が flag であれば、JSON ファイルのフォーマットは "data_read-in" : "flag" になります。 | _snd の付いた信号は RAM の 2 つ目のポートに属します。_snd の付いていない信号は 1 つ目のポートに属します。 |
| RAM_clock_enable | |||||
| RAM_data_read_in | |||||
| RAM_address_snd | |||||
| RAM_clock_enable_snd | |||||
| RAM_data_read_in_snd | |||||
| out | RAM_address | ||||
| RAM_clock_enable | |||||
| RAM_write_enable | |||||
| RAM_data_write_out | |||||
| RAM_address_snd | |||||
| RAM_clock_enable_snd | |||||
| RAM_write_enable_snd | |||||
| RAM_data_write_out_snd | |||||
| inout | RAM_address | ||||
| RAM_clock_enable | |||||
| RAM_write_enable | |||||
| RAM_data_write_out | |||||
| RAM_data_read_in | |||||
| RAM_address_snd | |||||
| RAM_clock_enable_snd | |||||
| RAM_write_enable_snd | |||||
| RAM_data_write_out_snd | |||||
| RAM_data_read_in_snd |
JSON ファイルの例
このセクションでは、RTL ブラック ボックスに必要な JSON ファイルを手動で書き出す方法について説明します。次は JSON ファイルの例です。
{
"c_function_name" : "foo",
"rtl_top_module_name" : "foo",
"c_files" :
[
{
"c_file" : "../../a/top.cpp",
"cflag" : ""
},
{
"c_file" : "xx.cpp",
"cflag" : "-D KF"
}
],
"rtl_files" : [
"../../foo.v",
"xx.v"
],
"c_parameters" : [{
"c_name" : "a",
"c_port_direction" : "in",
"rtl_ports" : {
"data_read_in" : "a"
}
},
{
"c_name" : "b",
"c_port_direction" : "in",
"rtl_ports" : {
"data_read_in" : "b"
}
},
{
"c_name" : "c",
"c_port_direction" : "out",
"rtl_ports" : {
"data_write_out" : "c",
"data_write_valid" : "c_ap_vld"
}
},
{
"c_name" : "d",
"c_port_direction" : "inout",
"rtl_ports" : {
"data_read_in" : "d_i",
"data_write_out" : "d_o",
"data_write_valid" : "d_o_ap_vld"
}
},
{
"c_name" : "e",
"c_port_direction" : "in",
"rtl_ports" : {
"FIFO_empty_flag" : "e_empty_n",
"FIFO_read_enable" : "e_read",
"FIFO_data_read_in" : "e"
}
},
{
"c_name" : "f",
"c_port_direction" : "out",
"rtl_ports" : {
"FIFO_full_flag" : "f_full_n",
"FIFO_write_enable" : "f_write",
"FIFO_data_write_out" : "f"
}
},
{
"c_name" : "g",
"c_port_direction" : "in",
"RAM_type" : "RAM_1P",
"rtl_ports" : {
"RAM_address" : "g_address0",
"RAM_clock_enable" : "g_ce0",
"RAM_data_read_in" : "g_q0"
}
},
{
"c_name" : "h",
"c_port_direction" : "out",
"RAM_type" : "RAM_1P",
"rtl_ports" : {
"RAM_address" : "h_address0",
"RAM_clock_enable" : "h_ce0",
"RAM_write_enable" : "h_we0",
"RAM_data_write_out" : "h_d0"
}
},
{
"c_name" : "i",
"c_port_direction" : "inout",
"RAM_type" : "RAM_1P",
"rtl_ports" : {
"RAM_address" : "i_address0",
"RAM_clock_enable" : "i_ce0",
"RAM_write_enable" : "i_we0",
"RAM_data_write_out" : "i_d0",
"RAM_data_read_in" : "i_q0"
}
},
{
"c_name" : "j",
"c_port_direction" : "in",
"RAM_type" : "RAM_T2P",
"rtl_ports" : {
"RAM_address" : "j_address0",
"RAM_clock_enable" : "j_ce0",
"RAM_data_read_in" : "j_q0",
"RAM_address_snd" : "j_address1",
"RAM_clock_enable_snd" : "j_ce1",
"RAM_data_read_in_snd" : "j_q1"
}
},
{
"c_name" : "k",
"c_port_direction" : "out",
"RAM_type" : "RAM_T2P",
"rtl_ports" : {
"RAM_address" : "k_address0",
"RAM_clock_enable" : "k_ce0",
"RAM_write_enable" : "k_we0",
"RAM_data_write_out" : "k_d0",
"RAM_address_snd" : "k_address1",
"RAM_clock_enable_snd" : "k_ce1",
"RAM_write_enable_snd" : "k_we1",
"RAM_data_write_out_snd" : "k_d1"
}
},
{
"c_name" : "l",
"c_port_direction" : "inout",
"RAM_type" : "RAM_T2P",
"rtl_ports" : {
"RAM_address" : "l_address0",
"RAM_clock_enable" : "l_ce0",
"RAM_write_enable" : "l_we0",
"RAM_data_write_out" : "l_d0",
"RAM_data_read_in" : "l_q0",
"RAM_address_snd" : "l_address1",
"RAM_clock_enable_snd" : "l_ce1",
"RAM_write_enable_snd" : "l_we1",
"RAM_data_write_out_snd" : "l_d1",
"RAM_data_read_in_snd" : "l_q1"
}
}],
"c_return" : {
"c_port_direction" : "out",
"rtl_ports" : {
"data_write_out" : "ap_return"
}
},
"rtl_common_signal" : {
"module_clock" : "ap_clk",
"module_reset" : "ap_rst",
"module_clock_enable" : "ap_ce",
"ap_ctrl_chain_protocol_idle" : "ap_idle",
"ap_ctrl_chain_protocol_start" : "ap_start",
"ap_ctrl_chain_protocol_ready" : "ap_ready",
"ap_ctrl_chain_protocol_done" : "ap_done",
"ap_ctrl_chain_protocol_continue" : "ap_continue"
},
"rtl_performance" : {
"latency" : "6",
"II" : "2"
},
"rtl_resource_usage" : {
"FF" : "0",
"LUT" : "0",
"BRAM" : "0",
"URAM" : "0",
"DSP" : "0"
}
}