FPGA をもっと活用するために IP コアを使ってみよう (5)

みなさんこんにちは。この「FPGA をもっと活用するために IP コアを使ってみよう」のシリーズでは、全5回を通じて FPGA を使って実用的なアプリケーションを実装するために必要不可欠な IP コアの使い方を紹介していきます。

最終回となる第5回の今回は、ボトムアップ的な FPGA アプリケーション開発で役に立つ FIFO の使い方を紹介します。FIFO を使うと、いくつかのモジュールでデータを受け渡す時に、それぞれの細かい動作タイミングを気にする必要がなくなります。ソフトウェア・プログラミングであれば Producer-Consumer パターンの実装が楽になる、と言えば通りがよいでしょうか。

Producer-Consumer パターンを考えてみよう

プログラミングのデザインパターンに Producer-Consumer パターンがありますね。複数のオブジェクトのデータのやりとりを、データを生成する者 (Producer) とデータを使う者 (Consumer) に見立ててプログラムに落とし込む方法です。

Producer-Consumer パターンのよいところは、それぞれの動作タイミングを両者で細かく合わせる必要がないところにあります。Producer 側は自身の都合でデータを生成すればよく、Consumer 側もデータがあればそれを使えばよいだけなので、それぞれのモジュールをボトムアップ的に開発し、後でつなぎあわせることが容易です。

FPGA 開発でも Producer-Consumer パターンで考えてみる

FPGA 上に実装するロジックのモジュール同士の関係も、Producer と Consumer に見立てることができます。例えば、第4回の記事で紹介したサンプルで、bram_to_uartserial_send の関係は、

  • bram_to_uart – BRAM からデータを読み出す (Producer)
  • serial_send – 受け取ったデータを UART に変換する (Consumer)

と、Producer-Consumer パターンであると見ることができます。

もちろん一つのモジュールが Producer や Consumer のどちらかだけということはなく serial_send は UART 信号を生成する Producer でもあります。なお、この場合には、Consumer は FPGA ボード上の USB-UART 変換チップです。

FIFO で Producer-Consumer パターン

ソフトウェアで Producer-Consumer パタンを実装する方法はいろいろありますが、FIFO あるいはキューがよく使われるデータモデルです。FIFO は、先に書き込まれたデータが先に読み出されるデータモデルで、Producer でデータを書き込み Consumerでデータを読み出すことで Producer-Consumer パタンを自然に実装できます。

ハードウェアでも FIFO は良く使われるので IP コアとして用意されています。

前回紹介した BRAM を Simple Dual Port や True Dual Port で使い、読み書きそれぞれのアドレスを管理すれば、自分で同等の機能を実現できます。しかし、有効なデータの有無や容量を越えた場合のフラグ管理などの実装が面倒ではあるので、特に理由がなければ IP コアを利用するのが便利です。

使ってみよう

前置きはこまでにして実際に FIFO IP を使う方法を紹介します。

今回は Xilinx の FPGA を搭載する Digilent の Arty を使うことを想定します。また、Vivado の FIFO Generator を使って FIFO IP を利用する方法を説明します。

プロジェクトの準備

Arty に搭載されている XC7A35TICSG324-1L 用にプロジェクトを作成します。プロジェクト名は fifo_example としました。

IP Catalog からメモリを選んで設定

メモリを使うには、IP Catalog から起動できるウィザードでモジュールを用意するのが楽です。具体的な手順と共に紹介します。ウィザードは、Flow Navigator から IP Catalog を選択し、検索ウィンドウに fifo と入力して表示される FIFO Generator をダブルクリックして開きます。

Basic タブでの設定

FIFO Generator の生成ウィザードを開いて最初に表示されるタブです。FIFO Generator では、次のオプションを有効/無効にした構成の IP コアを生成できます。

  • Non-symmetric aspect ratios: 読み書きのビット幅を変更できる
  • First-Word Fall-Through: メモリの読み出しモードの機能。First-Word Fall-Through では常に最新のデータが出力される。
  • ECC support: ECC (Error-correcting code) を使用する
  • Dynamic Error Inection: エラーを注入する

FIFO の実装に使うメモリコンポーネント (LUT または BRAM) によって利用できる機能が異なります。最初のタブでは、コンポーネントに何を使うかを決定します。ここでは、上記機能のすべてを利用可能な BRAM を使った FIFO である Independent Clock Block RAM を選択します。このコンポーネントで構成する FIFO では、独立した二つのクロックを供給できるのもポイントです。

Native Ports タブ

FIFO の幅や深さ、読み出しモードなどの基本設定ができます。メモリの幅は 8bit、深さは1024にセットしました。また、読み出しモード (Read Mode) では、Standard FIFO と First Word Fall Through の二つのモードの動作を比較してみましょう。まずは、Standard を選択して設定ステップをすすめます。

Status Flags タブ

FIFO の状態に関するステータス信号を外に引き出すかどうかの設定です。今回は一通り引き出してみましょう。

Data Counts タブ

FIFO の内部に投入されているデータの個数を外からモニタするかどうかの設定です。今回は一通り引き出してみましょう。読み出し側と書き込み側でクロックが異なる場合には、それぞれのクロックで個数を読み出せるように独立に2ポート生成されます。

Summary タブ

設定の確認です。深さ 1024 で FIFO を設定しましたが、実際には 1023 で生成されることに注意してください。また、Native Ports で選択した Read Mode がStandard Mode の 場合 Read Latency が 1 であることを覚えておいてください。設定内容を確認したら OK でウィザードを閉じます。

ウィザードを閉じると、作成したメモリモジュールに関連するファイルを作成するか尋ねられます。今回は早速シミュレーションして動作を確認したいので Generate をクリックしてダイアログを閉じます。

First Word Fall Through の FIFO も作ってみよう

同じ手順で、ウィザードを開き Native Ports の設定で、First Word Fall Through を選択した FIFO も作ってみましょう。モジュール名は、fifo_generator_0_ft としました。

Data Counts タブでの設定で、More Accurate Data Counts にもチェックを入れておきます。これにチェックを入れておかないと個数が正しく読み出せません。

サマリで設定を確認してウィザードを閉じましょう。First Word Fall Through の場合、深さ 1024 に設定した FIFO モジュールは、深さ 1025 で生成されることに注目してください。また Read Latency が 0 になっていることも重要です。シミュレーションでの動確認のときに注意してみてみましょう。

テンプレートの確認

作成したモジュールのインスタンスを作るためのテンプレートは IP Sources タブのツリーを展開して veo ファイルを開くことで参照できます。

シミュレーションで動作を確認

実機で動かしてみる前にメモリの動作の様子をシミュレーションで見てみましょう。Vivado を使ったシミュレーションの詳しい実行方法については、シリアル通信で Hello, FPGA (3) を参考にしてください。

動作を確認するために次のテストベンチを用意しました。メモリモジュールに供給するクロックを変えてみています。コードの全容は GitHub Gist にアップロードした fifo_tb.sv を参照してください。

    // 書き込み側のクロックを生成 (100MHz)
    always begin
        fifo_wr_clk <= 1'b0;
        fifo_ft_wr_clk <= 1'b0;
        #5;
        fifo_wr_clk <= 1'b1;
        fifo_ft_wr_clk <= 1'b1; 
        #5;
    end

    // 読み出し側のクロックを生成 (33MHz)
    always begin
        fifo_rd_clk <= 1'b0;
        fifo_ft_rd_clk <= 1'b0;
        #15;
        fifo_rd_clk <= 1'b1;
        fifo_ft_rd_clk <= 1'b1; 
        #15;
    end

    // リセット制御
    initial begin
        fifo_rst <= 1'b1; // リセット
        fifo_ft_rst <= 1'b1;
        #40; // 40ns間 リセットを保持
        fifo_rst <= 1'b0; // リセット解除
        fifo_ft_rst <= 1'b0;
    end

    integer i;
    logic [7:0] d;
    initial begin
        fifo_din <= 8'h00;
        fifo_wr_en <= 1'b0;
        fifo_ft_din <= 8'h00;
        fifo_ft_wr_en <= 1'b0;
        d <= 8'h61;
        #473; // リセット解除を適当に待つ。本来なら fifo_wr_rst_busy を見るべき。
        // 2048回 'a'から'z'をFIFOに書き込む
        for (i = 0; i < 2048; i = i + 1) begin
            // fifo_generator_0用
            fifo_din <= d;
            fifo_wr_en <= 1'b1;
            // fifo_generator_0_ft用(同じデータを同じタイミングで書く)
            fifo_ft_din <= d;
            fifo_ft_wr_en <= 1'b1;
            d <= d == 8'h7a ? 8'h61 : d + 1; // 'a'〜'z'を用意
            #10; // 10n秒待って、次の書き込みを実行
        end
        fifo_wr_en <= 1'b0;
        fifo_ft_wr_en <= 1'b0;
        #1000;
        $finish;
    end

    integer j;
    initial begin
        fifo_rd_en <= 1'b0;
        fifo_ft_rd_en <= 1'b0;
        d <= 8'h61;
        #723; // リセット解除を適当に待つ。本来なら fifo_rd_rst_busy を見るべき。
        // 1024回 FIFOからデータを読見出す
        for (j = 0; j < 1024; j = j + 1) begin
            fifo_rd_en <= 1'b1;
            fifo_ft_rd_en <= 1'b1;
            #30; // 30n秒待って、次の読み出しを実行
        end
        fifo_rd_en <= 1'b0;
        fifo_ft_rd_en <= 1'b0;
    end

シミュレーション結果は次の通りです。

実機でも動作確認

実際に Arty の上で FIFO を使ってみましょう。サンプルは、前回と同じように、メモリの 0 から 255 までのデータを 8 バイト毎に改行を挿入して UART から出力する、いわば 8×8 のアスキーアート表示ロジックを考えます。メモリへのデータ書き込みには VIO を使いましょう。

bram_to_fifo (ソースコード) が 各行のデータを BRAM から読み出し末尾に CRLF を追加して出力するモジュールです。メインのステートマシンは次のように記述してみました。

    always_comb begin
        n_state = state;
        n_x_count = x_count;
        n_y_count = y_count;
        n_wait_counter = wait_counter;
        n_we = 1'b0;
        n_a = A;
        n_data_in = DATA_IN;
        case(state)

            STATE_IDLE: begin
                n_state = STATE_SEND_PRE_0;
                n_a = 0;
            end

            STATE_SEND_PRE_0: begin
                n_a = A + 1;
                n_state = STATE_SEND_PRE_1;
            end

            STATE_SEND_PRE_1: begin
                n_a = A + 1;
                n_state = STATE_SEND;
            end

            // bram_to_uartでは serial_send の状況を見てSTATE_SEND_WAITに遷移していたが、
            // 間にFIFOがあることで serial_send を考慮せず、連続でデータを読み出して出力できる
            STATE_SEND: begin
                n_we = 1'b1;
                n_data_in = Q == 0 ? 8'h5f : Q; // replace NULL to '_'
                n_a = (x_count >= 6) ? A : (A == 8*8-1) ? 0 : A + 1;
                n_state = (x_count == 7) ? STATE_SEND_CR : STATE_SEND;
                n_x_count = (x_count == 7) ? 0 : x_count + 1;
            end

            // bram_to_uartでは CR の出力の後、serial_send の状況を見て待つ必要があったが、
            // 間にFIFOがあることで serial_send を考慮せず、すぐにLFの出力ができる
            STATE_SEND_CR: begin
                n_we = 1'b1;
                n_a = (A == 8*8-1) ? 0 : A + 1;
                n_data_in = 8'h0d; // CR
                n_state = STATE_SEND_LF;
            end

            // bram_to_uartでは LF の出力の後、serial_send の状況を見て待つ必要があったが、
            // 間にFIFOがあることで serial_send を考慮せず、すぐに次の状態に遷移できる
            STATE_SEND_LF: begin
                n_we = 1'b1;
                n_a = (A == 8*8-1) ? 0 : A + 1;
                n_data_in = 8'h0a; // LF
                n_state = (y_count == 7) ? STATE_WAIT : STATE_SEND;
                n_y_count = (y_count == 7) ? 0 : y_count + 1;
            end

            STATE_WAIT: begin
                n_a = 0;
                if(wait_counter < 100000000) begin
                    n_wait_counter <= wait_counter + 1;
                end else begin
                    n_wait_counter <= 0;
                    n_state <= STATE_SEND_PRE_0;
                end
            end

        endcase // case (state)
    end

    always_ff @ (posedge CLK) begin
        if (RST) begin
            state <= STATE_IDLE;
            x_count <= 0;
            y_count <= 0;
            A <= 0;
            WE <= 0;
            wait_counter <= 0;
        end else begin
            state <= n_state;
            x_count <= n_x_count;
            y_count <= n_y_count;
            wait_counter <= n_wait_counter;
            A <= n_a;
            WE <= n_we;
            DATA_IN <= n_data_in;
        end
    end // always_ff @ (posedge CLK)

前回の bram_to_uart (ソースコード) では、BRAM からデータを読み出すたびに serial_send の都合を考慮して待っていたのに対して、今回の bram_to_fifo は、serial_send の都合を見ることなくメモリからデータを読みだして出力できます。

UART で文字を送信するための serial_send モジュールは、シリアル通信で Hello, FPGA (2) の流用です。ただし BUSY フラグを WE リクエスト受理直後に立てて欲しかったので、一部変更しています(ソースコード)。

トップモジュール fifo_exmaple (ソースコード) で、FIFO の出力を Consumer である serial_send に送り込んでいます。次のように、有効なデータが FIFO にあって(fifo_ft_valid == 1)、serial_send が送信可能状態 (WE == 0 && BUSY == 0) の場合に、データを serial_send に供給して UART で出力します。

    always_ff @(posedge CLK) begin
        if(system_ready == 1 && WE == 0 && BUSY == 0 && fifo_ft_valid == 1) begin
            WE <= 1;
            fifo_ft_rd_en <= 1;
            DATA_IN <= fifo_ft_dout;
        end else begin
            WE <= 0;
            fifo_ft_rd_en <= 0;
        end
    end

プロジェクトにソースコードを登録して合成、ビットストリームを作成したら動作を確認してみます。たとえば、VIO で対角線上に相当するアドレス (0x0000, 0x0009, …0x003F) に 0 から 7 (0x60 から 0x67) を書き込んでみます。

次のように UART でメモリに書き込んだ値が対角線上に表示されることが確認できました。第3回のおまけで紹介したように tcl スクリプトを使って VIO 経由で値をセットすると、手軽に出力結果を変えて楽しむことができます。

いろいろな FIFO の使い方

BRAM と同じように、FIFO も二つの独立したクロックの緩衝材や、データ幅を変更する際に利用されます。

二つの独立したクロックの緩衝材として

埋め込まれているメモリは二つの独立したクロックでアクセスできるようになっています。一般に、異なるクロックで動作するロジックを接続する際には十分タイミングに気をつけて制約を指定する必要があり、できるだけ避けたいものです。クロックが異なるロジックの間にメモリを挟むことで緩衝材として利用できます。

データの幅を変える

二つのポートの幅を変えることもできます。たとえば外からは 32bit 幅のデータが入ってくるけどアプリケーションロジックでは 8bit ずつ処理したいという場合にも、それぞれのビット幅にあわせることで簡潔にロジックを設計することができます。

おわりに

FPGA に実装するモジュールをボトムアップで開発し接続するのに便利な FIFO とその使い方を紹介しました。FIFO を挟んだモジュール同士の内部動作を気にすることなくデータの受け渡しができます。FPGA では、好きな深さと幅の FIFO を柔軟に利用できるのでアプリケーションロジックの好きなところに FIFO を配置して、たくさんのモジュールを数珠つなぎにすることができます。

FIFOは大規模なFPGAアプリケーションを見通しよく開発するには必須とも言えるIPコアです。どんどん使ってみてください。

わさらぼ・みよしたけふみ

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