シリアル通信で Hello, FPGA (3)

実用的な回路の設計・実装と動作確認を通じて,ハードウェア記述言語 (HDL) を使った FPGA 上のディジタル回路の設計について学ぶコースの第3回です。

前回は、シリアル通信 (UART) による文字送信回路を設計し、HDL 記述を作成しました。この回路を実際に動かす前に、まずは論理シミュレーションを使って、正しく動作しそうであるかチェックしてみましょう。今回は、テストベンチの作成方法、および Vivado シミュレータで回路の動作を確認する方法を解説していきます。

テストベンチとは

テストベンチの構造

これは半加算器ですか?

ここに、多分半加算器だと思われるディジタル回路があります。回路の外見を見るに、入力端子は A と B、出力端子は C と S であるようですが、その中身 (入力と出力の関係) が正しいかどうかは、よくわかりません。この回路が本当に半加算器であるかどうかを確かめるには、何をすればよいでしょうか?

回路の検証には、回路そのものと、入力のパターン、そして出力のチェックが必要。

当たり前ですが、検証対象の回路そのもの (Unit Under Test, UUT などとよばれます) 、この例の場合は半加算器そのものが必要です。回路の入出力の端子から信号線を引き出したら、入力には適当なパターンを与えてやる必要があります。また、出力は必要に応じてチェックする必要があります (単に出力の波形を観察するだけでも良いかもしれません)。

HDL では、こういった検証環境も回路と同じ文法で記述していきます。このような回路検証のためのモジュールのことを、テストベンチといいます。

テストベンチの例

例として半加算器のテストベンチを示しながら、テストベンチの構造を把握していきましょう。半加算器のテストベンチを SystemVerilog で記述すると、典型的には次の記述になります。ただし、半加算器はあらかじめ half_adder という回路名で定義されているものとします。

module half_adder_tb ();
    logic A, B, S, C;

    half_adder ha (.A(A), .B(B), .S(S), .C(C));

    initial begin
        A <= 1'b0; B <= 1'b0; #50;
        A <= 1'b0; B <= 1'b1; #50;
        A <= 1'b1; B <= 1'b0; #50;
        A <= 1'b1; B <= 1'b1; #50;
        $finish;
    end
endmodule

まず、テストベンチは回路と同じように記述しますが、全ての信号は内部信号であり、外部への入出力信号はもっていません。そのため、テストベンチは入出力のない module として宣言します (1行)。

次に、検証対象の回路の入出力に接続する内部信号を宣言 (2行) してから、その回路を貼り付けます (4行)。回路を貼り付けるには、回路名と回路の識別子の後にカッコを書き、その中に入出力と内部信号との接続関係をカンマ区切りで記述していきます。接続関係を記述するときは、ドットの後に入出力の信号名、カッコの中にそれを接続する内部信号を記載します。この例では入出力と内部信号にはいずれも同じ名前をつけています。

そして入力のパターンを与える部分 (6~12行) が続きます。繰り返す必要のないパターンは initial 文で、繰り返しのパターンは always 文で、それぞれ定義します。

この中では、テストベンチ専用の記法がいくつかみられます。 まず #(数字) ですが、これはその時間だけ待つという意味になります。 Vivado シミュレータで特に指定をしなかった場合、時間の単位は ns になります。 つまり7行目の全体を一言で言うと、「A と B を両方 ‘0’ にして 50 ns 待つ」という意味になります。 その後の記述 (8~10行) と併せて、考えられる4通りの入力の全てのパターンを 200 ns かけて試しているわけです。 最後 (11行) の $finish は、そこでシミュレーションを終了する手続きです。

出力のチェックはここでは行っていません。テストベンチ専用の記法は様々で、出力が想定通りでない場合にメッセージを表示させたり、ファイルから入出力パターンを読み書きすることもできます。目視で出力波形を確認するだけであれば記述は不要ですが、複雑な回路を検証する場合には、こうした機能を使って効率的に検証を進めていくことになります。

文字送信回路のテストベンチ

ソースコード全文

それでは、前回記述した文字送信回路のテストベンチを見ていきましょう。ソースコードの全文を以下に示します。

module serial_test ();
    logic       CLK, RST;
    logic [7:0] DATA_IN;
    logic       WE;
    logic       DATA_OUT;
    logic       BUSY;

    serial_send # (
            .WAIT_DIV(5))
        ser (
            .CLK(CLK),
            .RST(RST),
            .DATA_IN(DATA_IN),
            .WE(WE),
            .DATA_OUT(DATA_OUT),
            .BUSY(BUSY));

    always begin
        CLK <= 1'b1; #10;
        CLK <= 1'b0; #10;
    end

    initial begin
        RST <= 1'b1; #30;
        RST <= 1'b0;
    end

    assign DATA_IN = 8'h41;

    initial begin
        WE <= 1'b0; #90;
        WE <= 1'b1; #20;
        WE <= 1'b0;
        wait (BUSY == 1'b0); #100;
        $finish;
    end
endmodule

解説

基本的には先ほどのテストベンチの例と同じ構造を持っています。内部信号を宣言し (2~6行)、検証対象の回路を貼り付け (8~16行)、入力のパターンを initlal 文や always 文で指定しています (18~36行)。

検証対象の回路の中で parameter 文で宣言した定数については、その回路を貼り付けるときに変更できます。回路名と回路の識別子との間に # ( ) をはさみ、信号のときと同じ要領で、カッコ内に定数名と変更後の値を記述します。今回は、データを送出する時の待ち時間があまり長いと結果の確認が大変ですから、待ち時間を示す WAIT_DIV の値を5と小さく設定しています。

クロック入力は周期的な信号ですから、always 文で一定時間おきに ‘1’ と ‘0’ を繰り返すようにしています (18~21行)。一方で、リセット入力は最初に1度だけ ‘1’ となればよい信号ですから、initial 文で最初のある程度の時間だけ ‘1’ とし、その後はずっと ‘0’ となるようにしています (23~26行)。

今回のテストベンチでは、’A’ という文字をシリアル通信で正しく送出できるかどうかを見ていきます。’A’ に対応する ASCII コードは 0x41 なので、データ入力にはその値を固定で与えています (28行)。また、書き込み有効入力はシミュレーション開始から一定時間後に1度だけ ‘1’ となるように設定しています (31~33行)。

シミュレーションの終了は、文字の送出が完了してから一定時間後とします。ある条件が満たされるまで待つには、wait 文が使えます (34行) 。

順序回路のシミュレーションでは、入力を切り替えるタイミングにも注意が必要です。具体的には、クロック入力の立上りと同時に他の入力を切り替えることは避けるべきです。そうでないと、フリップフロップが切り替え前後のどちらの値を使えばいいのかが、あいまいになってしまいます。

論理シミュレーション

Vivado シミュレータの使い方

では、文字送信回路とテストベンチをシミュレートし、出力波形を見てみましょう。Vivado では、回路は Design Source(s) として、テストベンチは Simulation Source(s) として、それぞれプロジェクトに追加します。Simulation Source(s) として追加したファイルは、論理合成では利用されず、シミュレーションでのみ利用されます。

Simulation Sources の正しい階層構造。

ファイルが適切にプロジェクトに追加されていれば、Simulation Sources の階層関係は、上図のようにテストベンチをトップに、文字送信回路はその下にぶら下がる形で表示されるはずです。階層関係が正しいことを確認したら、Flow Navigator から Run Simulation → Run Behavioral Simulation をクリックするとシミュレータが起動し、(記述にエラーがなければ) 画面がシミュレータの波形画面に切り替わります。

シミュレーションは、開始から一定時間 (既定では 1 μs) 実行されたところで一旦停止します。最後まで ($finish に到達するまで) シミュレーションを行うには、Run All ボタン (ツールバーの再生ボタン) をクリックします。

$finish でシミュレーションが終了したときの様子。

画面がエディタに切り替わり、$finish が実行された場所がハイライトされますので、Untitled (数字) をクリックして元の波形画面に戻します。拡大率は波形画面の上部のアイコンから調整します。

文字送信回路のシミュレーション結果

文字送信回路のシミュレーション結果。

出力波形を確認します。今回送信したのは ‘A’ (0x41) なので、2進数で最下位ビットから並べると、1 0 0 0 0 0 1 0 です。 これにスタートビットとストップビットを加えた 0 1 0 0 0 0 0 1 0 1 が、送出されるべきビット列となります。またテストベンチでは、クロック周期を 20 ns、待ち時間のカウンタを5進 (WAIT_DIV を5) に設定していますので、1ビットあたりの送出時間は 100 ns となるはずです。WE が ‘1’ になった後の DATA_OUT の波形を見てみると……確かにその通りになっていますね。

定数 WAIT_DIV を別の値にしても正しく動作するでしょうか。テストベンチ9行目の WAIT_DIV に与える値を10に変更してみます。ソースファイルを更新した場合には、Relaunch ボタン (ツールバーの更新ボタン) をクリックし、シミュレータを再起動してください。

定数 WAIT_DIV を10に変更した場合のシミュレーション結果。

送出されたビット列は変わりませんが、1ビットあたりの送出時間が 200 ns (20 × 10) に変わっています。 どうやら文字送信回路は正しく動作しているようです。

内部信号を観察する

シミュレーションでは、内部信号も波形画面に追加し、観察できる。

更に細かく検証 (あるいはデバッグ) をしたい場合、入出力だけではなく内部信号を観察することもできます。波形画面の左側にモジュールの選択画面と信号の選択画面があります。モジュールを選択し、観察したい信号をドラッグし波形画面にドロップすると、その信号が波形画面に追加されます。このあと、シミュレーションを先頭に戻す Restart ボタン (ツールバーの巻き戻しボタン) を押してから Run All ボタンを押すと、内部信号の波形が表示されます。

カウンタの値を観察するようにした場合のシミュレーション結果。

今回は2つのカウンタの値 (wait_cnt, bit_cnt) を表示させてみました。待ち時間のカウンタ (wait_cnt) が0に戻ると同時に、送信ビット数のカウンタ (bit_cnt) がインクリメントされ、次のビットの送出が始まることが確認できます。

まとめ

今回は、シミュレーションで回路の動作を検証するための、テストベンチの作成方法と Vivado シミュレータの基本的な使い方を解説しました。

本当なら、より多くの状況 (他の文字も正しく送信できるか? 2回以上続けて文字を送信できるか? 文字を送信中に書き込み有効入力を ‘1’ にしても無視されるか? などなど……) を考慮して検証するところですが、今回は説明の都合から省略しました。どんなに注意深く設計してもバグはつきものです。十分な検証を心がけましょう。

次回はいよいよ文字送信回路を FPGA に書き込み、シリアル通信で PC に文字を送信します。

愛知工業大学 藤枝直輝

タイトルとURLをコピーしました