動作合成とC/C++/SystemC/SystemVerilogの協調設計 ―― Cynthesizerの活用事例
4.実装 ~動作合成~
ここではCコードをハードウェア化する際に,SystemCを用いてハードウェア・イメージを記述し,そのコードを動作合成ツールを用いて,最終的にRTLコードまで作成する方法を説明します.動作合成ツールは米国Forte Design Systems社の「Cynthesizer」を使っています.リスト1のCコードを例に具体的に説明していきます.
リスト1 Cコードをハードウェア化する説明のためのサンプル
● 動作合成の作業工程
まずは元のCコードをどのようにしてRTLにするか,その作業工程について見てみます.
(1) Cコードからハードウェア・イメージを考える
Cコードをハードウェアにするには,まずそのハードウェアと外部とのインターフェースを決定しなければなりません.図3に筆者らが主に採用しているスタイルのハードウェア・イメージを示します.
図3 リスト1に示したCコードと外部入出力のインターフェース
この図に示されるように,クロック・リセット信号のほかに,このCコードの起動と完了を知らせる信号として,start/done信号を設けています.そのほか元のCコードにあった引き数a,b,cと出力であるdを設けます.これで,ハードウェアとして最低限必要なインターフェースがそろいます.
(2) ハードウェア・イメージからSystemCコードを記述
リスト2にヘッダ・ファイルを,リスト3にソース・ファイルを示します.モジュール名はalgとしました.ハードウェア・イメージ図と比較してみてください.後ほど詳しく説明します.
リスト2 ハードウェア・イメージから記述したSystemCコード:ヘッダ・ファイル alg.h
リスト3 ハードウェア・イメージから記述したSystemCコード:ソース・ファイル alg.cpp
(3) 動作合成ツールでSystemCコードからRTLを生成
SystemCコード内に動作合成内容を指定して,より品質の高い回路を生成します.内容を指定するには,動作合成ツールCynthesizerが提供するディレクティブを用います.主にコード内のレイテンシ(遅延)を指定し,性能と面積の品質向上を目指します.
● 動作合成向けSystemCコードの説明
リスト1とリスト2のSystemCコードについて,詳しく説明します.
リスト2の(1)では動作合成ディレクティブを使えるようにするため,Cynthesizerが提供しているcynthhl.hをインクルードします.Cynthesizerが提供するディレクティブはこの中で宣言されていますが,#ifdefでCYNTHESIZERが宣言されなければ有効にならないようになっています.このため動作合成時には有効となり,シミュレーション時には,ダミー宣言となるように切り替えられる機構になっています.
リスト2の(2)はCコードを起動するために必要なリセット処理,前処理,後処理です.その定義をalg::main()で行います.クロックに同期することを宣言します.
リスト2の(3)は非同期リセットの定義です.reset_signal_is(resetn, false)で,resetn信号が"L"アクティブであることを定義します.C_ASYNCディレクティブは,Cynthesizerに非同期リセットであることを伝えます.そのほかalg.hでは必要な信号をsc_in/sc_outにて宣言・定義しています.
リスト2の(4)において,このモジュール内で使用する関数を宣言します.ここで,元のコードf()にあった,引き数と戻り値が消えています.これは,外部とのインターフェースのため,sc_in/sc_out信号に置き換えたためです.信号のビット幅は,>などといった記述で指定できますが,記述の簡素化のため,そのままの型で記述しています.
リスト3の(5)はリセット時の処理です.resetn信号が'0'のとき,この記述部分を実行するため,ここに初期化処理を記述します.出力信号は,すべて初期値をライトします.
● アンタイムド(untimed)とサイクル精度
ここで,C_PROTOCOLディレクティブが出てきました.動作合成では何も指定しない場合は,アンタイムド(時間概念を持たない)の扱いでRTLコードを生成します.しかし外部とのインターフェースなどはサイクル精度で生成しなければならないため,このディレクティブで指定します.このディレクティブを用いるのは,リセット処理,入出力信号のリード/ライトが主になります.このディレクティブで囲まれた内容はすべてwait()を記述したぶん,サイクルを保証します.逆にC_PROTOCOL内でなければ,wait()は無視されます.
リスト3の(6)は起動前処理です.今回のハードウェア・イメージはstart信号が'1'になったときに処理を開始するようにしました.ここでstart信号が1になるまで待つ処理を記述しています.またstart信号が'1'のときには,a,b,cの信号が外部から与えられていることを前提としています.
リスト3の(7)でやっと元のCコードを起動できます.元コードにあった,a,b,cの引き数は,既にstart信号が'1'になった時点で入力されています.従ってf()を起動するだけです.
リスト3の(8)は起動後の処理です.元のCコードの出力値dが求まり,出力信号d_oにライトします.このとき,このモジュールの完了を示すためのdone信号もあわせてライトします.この部分もC_PROTOCOLで囲み,サイクル保証をします.
● 動作合成の最適化
以上の内容で最低限,動作合成できるまでを作成しました.最低限というのは,外部とのインターフェースをサイクル保証しただけで,肝心の元コードが全く手付かずの状態だからです.Cynthesizerでは,より良い品質のRTLコードを生成するためにはディレクティブを適切に指定して,コードの生成を制御する必要があります.
アンタイムド部分については,いろいろなディレクティブ指定を行い動作合成してみることで,速度と面積が目標に達するようにします.以下に主に使用するディレクティブを紹介します.ディレクティブの有効範囲は,中カッコ{}内に影響します.
ディレクティブ (directive)は直訳すると「命令」を意味し,プログラミング言語において処理系に対し何らかの操作を指示します.ここでは動作合成ツールに対し「動作合成により回路生成をどのようにしたいか」を指定する記述を指します.
● CYN_LATENCY
どれだけサイクルを消費するか指定します.サイクルを小さく設定すれば,演算器を複数用いるため面積が増える傾向にあります.逆に面積を優先するのであればサイクルを大きめに設定します.どれだけ消費するように生成したかはログ・レポートにより確認できます.引き数でサイクル指定し,その間に収まるよう生成を指定します.例をリスト4に示します.
リスト4 ディレクティブを加えたコードの例
● CYN_FLATTEN
配列はアドレス信号とデータ信号のメモリ構造で展開されるため,アクセスに1サイクル消費するRTLコードとして生成されます.このアクセスのオーバヘッドをなくすため,CYN_FLATTENディレクティブを使用し,配列を個別のレジスタに展開し,速度を優先できます.展開しても面積に問題のないサイズの配列に指定します.
● CYN_UNROLL
for文などのループを展開します.具体的にはfor文内の記述部分の回路を複数生成し,並列実行するRTLコード生成を指示します.並列化によって速度を優先できます.ループ回数が比較的少ない記述に対して有効です.
そのほか,有効なディレクティブが用意されていますが,今回は入門としてよく使用するディレクティブに限って紹介しました.
以上で一通りの動作合成手順を説明しました.外部とのインターフェースをサイクル保証して,処理内部をディレクティブ指定することにより,RTLコードを自動生成する流れをご理解いただけたと思います.