みなさんこんにちは。この「Synthesijer と高位合成ツールの作り方」のシリーズでは、全5回を通じて Synthesijer をベースに FPGA 向けの簡単な高位合成処理系を作る方法を紹介していきます。例は Java ですが、お気に入りの言語向けの処理系を開発する足がかりとして利用できるように書いていくもりです。
前回は、高位合成ツールとは何かについて説明し、また、このシリーズのベースとする Synthesijer を簡単に紹介をしました。今回は、Synthesijer のコンセプトを紹介し、Synthesijer が Java をハードウェア・ロジックに変換する処理の流れを紹介します。
Synthesijer のコンセプト
Synthesijer は、Java プログラムからハードウェアを合成する高位合成処理系です。Java プログラムをソフトウェアとしてコンピュータ上で実行してアルゴリズムのレベルのデバッグを行った後、デバッグ済みのプログラムをハードウェア化することで RTL 設計以降でのアルゴリズムレベルのデバッグが不要になることを目指しています。
Java は、広く普及しているプログラミング言語の一つです。オブジェクト指向で設計できること、及び、並列処理を記述可能な Thread を言語仕様として含むことが、ハードウェア設計に用いる言語に向いているのではないかと考えて開発をはじめました。
また、Vivado HLS や Intel HLS など多くの高位合成処理系がサポートする C や C++ では通常のソフトウェアの実装では、ポインタが明示的に利用されます。そのため、高位合成処理系ではポインタをサポートしないか、何かしらの方法でポインタを取り扱う方法を考えて、その方法を利用者に理解してもらう必要があります。一方で、Java の場合は、ポインタが処理系内部に隠蔽されています。そのため、処理系でポインタの扱いを自由に実装できます。このことも、高位合成処理系に Java を利用するメリットだと考えています。
パソコンでソフトウェアとして動作確認する
ハードウェアを生成するために Synthesijer が受理する Java プログラムは、ソフトウェアとして PC の上で実行することができます。
たとえば、2次元画像処理をするプログラムのハードウェア化を考えます。ソフトウェアとしてパソコン上で実装する場合には、Swing などの描画ライブラリを利用して、画像処理の動作を確認できます。ハードウェア化する場合には、画像処理ルーチンは Synthesijer でロジック化し、描画機能に相当する部分を VGA や HDMI 出力するモジュールに置き換えることで、FPGA 上で動かせます。
あまり本質でない部分のハードウェア化に利用
FPGA を使ったアプリケーションシステムを構築する場合には、FPGA で処理させたい本質的ではない部分、たとえばターミナル向けのフロントエンドや実行前の初期化ルーチンなどの実装を避けることはできません。これらのモジュールは、速度は必要ないものの必要不可欠であり、また、時として RTL での実装が面倒です。
高速な処理が不要であれば、ARM 内蔵 FPGA を利用する場合は ARM プロセッサで、あるいは、MicroBlaze や NiosII などのソフトプロセッサを利用してソフトウェアで処理を実装することも考えられます。その場合、それらのコアが想定する入出力インターフェースでのつなぎ込みや、ハードウェアと独立したソフトウェアの実装・デバッグが必要になります。
Synthesijer を使って Java から変換した HDL コードを利用する場合、処理の制御やデータ入出力がシンプルな入出力ポートとして生成されます。そのためモジュール同士を直接結びつけることが容易です。また、実装・デバッグの統合作業が少なくてすみます。
そのため、モジュール同士が密に結合するようなシステムでも、システム全体のプロトタイプを Java で実装した後で、必要に応じて RTL で実装したモジュールにおきかえておくという開発スタイルと高い親和性を持ちます。また、処理によってはソフトコアを利用する場合に比べてリソースの使用量を抑えることもできるでしょう。もちろん、余計にリソースを使用する場合もあります。
頻繁に変更する処理の手軽なハードウェア化
Synthesijer を使うことで、HDL では記述するのが煩雑になってしまう複雑な制御構造の処理も、Java の制御構文で比較的簡単に記述することができます。
たとえば、多重ループや複雑な条件分岐を含むような数値演算を RTL で実装すると、計算式や実装方針を変更したい場合のやりなおしが億劫ですが、Java で記述していれば比較的容易に変更できます。
Synthesijer でできないこと
Synthesijer は Java プログラムをハードウェア化しますが、すべての Java プログラムをハードウェア化できるわけではありません。特に、動的なインスタンス生成・削除やインスタンスの引数渡し、インスタンスの配列などの「大規模な Java プログラムでは一般的に利用される機能の多く」はまだ利用できません。これらの機能は、今後の開発で実現を目指しています。
Synthesijer における HDL コード生成
Synthesijer は、Java で記述されたプログラムを RTL の VHDL あるいは Verilog HDL に変換します。Java プログラムをどのように HDL に対応づけるか説明します。
クラス
クラスは Java プログラムの設計単位です。Synthesijer では、クラス毎にハードウェアモジュール (VHDLの entity
、Verilog HDL の module
) を生成します。なお、現在の Synthesijer では1つのファイルには、1つのクラス (つまり public
なクラス) のみを定義できます。
メソッド
メソッドは、引数と返り値を持つ処理の記述単位です。1つのクラスには複数のメソッドを記述することができます。Synthesijer では、それぞれのメソッドをステートマシンに変換します。ステートマシンはメソッドが呼び出された時点で動作を開始します。
public
修飾子の付いたメソッドでは、メソッド名_req
信号とメソッド名_busy
信号がモジュールの外に引出されます。メソッド名_req
信号を 1
にアサートすることでステートマシンは動作を開始し、動作中にはメソッド名_busy
信号が 1
にアサートされます。また、メソッドの引数と返り値は、それぞれメソッド名_引数名
信号とメソッド名_return
信号として引き出されます。
ソフトウェアプログラムでは、メソッドを再帰的に呼び出すことがあります。Synthesijer では末尾再帰の構造になっている再帰呼び出しはループに変換します。そうでない場合にはスタックを生成して (深さは自動あるいはアノテーションで指定する) ハードウェアロジックに変換します。
変数
変数は実行中に値を保存しておくために使います。Java には int や short などのプリミティブ型の変数と、クラスのインスタンスを指す参照型の変数があります。
Synthesijer では、プリミティブ型の変数をクラスで定義するメンバ変数とメソッド内で定義するローカル変数のどちらにも利用できます。変数は VHDL の signal
あるいは Verilog HDL の reg
に変換されます。メンバ変数のうち、public
修飾子を付けた変数は、クラスの外部から読み書きできるように、変数名_in
、変数名_we
、および変数名_out
の信号がモジュールの外に引き出されます。
現在の Synthesijer では、参照型の変数はメンバ変数としてしか利用できません。これは、インスタンスを動的に生成できないことによります。参照型の変数は宣言時にインスタンスを生成しなければいけません。インスタンスは、VHDL や Verilog HDL のサブモジュールに変換されます。
配列について説明を加えておきます。Java プログラムでは、プリミティブ型および参照型の変数の配列を利用できます。Synthesijer では、プリミティブ型の変数の配列を、メンバ変数に限って利用できます。配列は宣言時に固定サイズで生成する必要があります。VHDL でも Verilog HDL でも配列はブロック RAM のインスタンスに変換します。public
修飾子の付いた配列はデュアルポートのブロック RAM (dualportram.vhd または dualportram.v )、private
の配列はシングルポートのブロック RAM (singleportram.vhd または singleportram.v ) を利用して実体化されます。RTL シミュレーションや FPGA ベンダのツールで合成する場合には、これらのファイルをプロジェクトに含めて利用してください。
なお、現状の Synthesijer では、残念ながら多次元配列を含む参照型の変数の配列は利用できません。
式と文
Synthesijer では、Javaで利用可能な制御構文や演算子の多くをサポートしています。整数の加減算、定数シフト、比較などの多くは VHDL や Verilog HDL の組み合わせ回路に変換されます。一方で、除算や浮動小数点数演算など組合せ回路で実現すると大きなハードウェアになってしまう演算は、外部モジュールを使って複数サイクルをかけて処理します。
Java でのプログラミングと同様に構造化されたコードの利用、すなわち、クラス内のメソッドやインスタンスのメソッドの呼び出しやインスタンスのメンバ変数へのアクセスが利用できます。ただし、残念ながら、多段のメンバ変数アクセスはできません。
どのような演算が利用できるかは、 sample/test 以下のコードを参照してください。
継承
Synthesijer は簡易的に Java の継承をサポートしています。あるクラスを継承するクラスを設計した場合には、親クラスで定義されているメソッドやメンバ変数を含むモジュールを生成します。
多くの Java のクラスライブラリを継承したクラスは作れませんが、Thread クラスを継承したクラスは作成できます。Java の Thread を継承したクラスは、ソフトウェアでもハードウェアでも並列に実行可能です。Thread を利用した並列動作については前回紹介した通りです。
アノテーション
Java ではクラスや変数、メソッドにアノテーションを付与することで、処理系にコンパイル時や実行時の振舞いを指示できます。Synthesijer では、ハードウェア開発を少し便利にするために、メソッドや変数に対するアノテーションという形で拡張機能を導入しています。よく利用する2つのアノテーションを説明します。
変換の対象外にする – @unsynthesizable
メソッドに @unsynthesizable
は付与することで、そのメソッドを合成の対象外に指定できます。このアノテーションのついたメソッドは合成時に無視されるため、 System.out.println
など合成不可能な処理を記述することができます。テスト用のコードを同じクラスの中に記述できるためデバッグや動作検証に便利です。たとえば、sample/test/Test001.java などで利用しています。
メソッドを自動起動する – @auto
メソッドに相当するステートマシンはメソッド名_req
信号が 1
にアサートされることで開始します。ただし、@auto
というアノテーションが付いたメソッドはリセット解除と同時に動作を開始します。そのため、ソフトウェア実行時の main メソッドのように利用できます。ただし、@auto
のついた複数のメソッドがある場合、それらのメソッド同士の同期は考慮されません。プログラマが考慮する必要があります。
たとえば、sample/quickstart/Top.java では、@auto
を main()
と flag()
に付与することで、それぞれのメソッドを自動的に実行しています。
HDL で書いた部品を使う
デバイスを制御するような「タイミングを強く意識して設計するモジュール」や、性能を引き出すために「パイプライン動作するモジュール」の設計は、Java で記述するより HDL で直接記述する方が楽です。
Synthesijer では、HDL で記述したモジュールをあたかも Java のクラスのように使用する方法を提供しています。Synthesijer で Java プログラムからパイプライン並列性を活用したハードウェアを生成すればいいのに、というつっこみはもっともですが。
HDLで書いた部品を利用する例
sample/serial_echo は、VHDL で実装した UART 送受信モジュールを Java プログラムで利用する例です。送受信を行なうモジュールの実装は rs232c_tx.vhd/rs232c_rx.vhd です。
このモジュールを Java で利用できるようにしたラッパークラスが、RS232C_TX_Wrapper.javaと RS232C_RX_Wrapper.java です。ラッパークラスでは、利用するモジュールの持つ Java 向けのインターフェースと RTL が持つ入出力ポートのペアを定義します。EchoTest.java のように、ラッパークラスのインスタンスを生成することで Java プログラムから HDL で実装したモジュールを利用できます。
ラッパークラスはソフトウェアとしては動作しません。プログラムをソフトウェアとして実行する場合には、同等の機能を提供するクラスを実装してクラスパスを切り替えて呼び出せるようにするとよいでしょう。たとえば、sample/bf は、BF.java をハードウェア・ロジックとして動作する場合には IO.java と組み合せ、ソフトウェアとして動作させる場合には sim/IO.java と組み合わせて実行するようにクラスパスで切り替えて利用することでハードウェアとソフトウェアで同じプログラムを動作させています。
Synthesijer での VGA 出力ライブラリの実装例では、RTL で実装した VGA 出力モジュールを Java プログラムから呼び出して利用する事例を紹介しています。
提供している HDL モジュール
HDL で記述された拡張モジュールとして、プリミティブ型に相当するデータを外部のモジュールとやりとりする INPUT
、OUTPUT
関係のクラスを標準モジュールとして用意しています。sample/test/Test007.java のように利用できます。
また、少し複雑なライブラリとして AXI バスにマスタ/スレーブとして接続するためのクラスなども用意しています。これらのライブラリでは、応用事例として、配列やメソッド呼び出しの形で HDL モジュールにアクセスする手法も利用しています。
コンパイルフロー
Synthesijer のコンパイルフローは次の図の通りです。
フロントエンドは Java コンパイラ (javac) のプラグインとして実装しています。Java コンパイラのプラグインではコンパイル中に作られる AST を好きに走査することができます。Synthesijer では走査した AST から Synthesijer 用の簡易な AST を生成します。この段階ではクラス、メソッドなどの Java プログラムの構造や、if
、while
、switch
などの Java の制御構造がそのまま写し取られています。
写し取った AST からスケジューラ情報 (SchdulerInfo
) と呼ぶ内部情報を生成します。SchedulerInfo
は Java のクラスに対応し、クラスで定義されたメンバ変数およびメソッドに対応するステートマシン (SchedulerBoard
) を持ちます。ステートマシンはステートに対応する SchedulerSlot
で構成され、各 SchedulerSlot
内に二項演算程度の演算処理 (4つ組程度の演算処理) に相当する SchedulerItem
を持ちます。
各 SchedulerBoard
に対して、SchedulerSlot
あるいは SchedulerItem
をノードとした制御フローグラフやデータフローグラフを作ることができます。現在の Synthesijer では、SSA 化した後で簡易的な並列化や演算のチェイニング (複数の演算処理を複合した組み合わせ回路にまとめる) などの最適化を施しています。
内部情報はS式で入出力することができます。そのため一部の最適化処理を外部のプログラムにまかせたり、他のプログラムで生成した SchedulerInfo
を Synthesijer に投入できます。go2ir は Go で記述されたプログラムをフロントエンドとして利用しようとした試みです。
SchedulerInfoCompiler
で、SchedulerInfo
から HDLModule
をルートに持つ RTL に対応した HDL ツリーを生成します。HDL ツリーは、VHDL や Verilog HDL とほぼ一対一に対応します。ステートマシン (SchedulerBoard
) のステートレジスタを生成し、ステート (SchedulerSlot
) 遷移および各ステートでのレジスタアップデートを生成します。
最後に HDLツリーから VHDL や Verilog HDL を出力します。
まとめ
今回は、Synthesijer のコンセプトを紹介し、Synthesijer が Java をハードウェア・ロジックに変換する処理の流れを紹介しました。
次回以降は、実際にプログラムをハードウェア・ロジックに変換するフローの実装事例を説明する予定です。
わさらぼ・みよしたけふみ