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

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

第4回の今回は、FPGA の中に埋めこまれているメモリを利用する IP コアを紹介します。メモリは実用的なアプリケーションの作成には必須の機能です。すぐには使わないかなという人も、道具の一つとして覚えておくだけでも損はありません。

メモリってなんだっけ?

ソフトウェアでプログラミングをするときにも、メモリは馴染み深いハードウェアだと思います。メモリはアドレスを指定して値を読み書きするコンポーネントですね。FPGA 上にアプリケーションに実装する時にもデータをアドレスと紐づけて保存・利用できるメモリは便利です。

メモリのロジックイメージ

FPGA では、LUT や FF といった FPGA の基本要素を使って好きな論理回路を作ることができます。もちろんメモリも例外ではありません。しかし、メモリはたくさんの記憶素子からアドレスで指定した箇所を選んで読み書きしなければならないため、意外と大きなロジックになってしまいます。

FPGA にはメモリが埋め込まれている

多くの FPGA には専用回路としてメモリが埋め込まれています。埋め込まれているメモリをブロックメモリあるいは略して BRAM と呼んだりします。

基本的なメモリのサイズは FPGA 製造時に決められていますが、複数のメモリを束ねて大容量のメモリやデータ幅の広いメモリとして利用するなどの柔軟な使い方ができます。

FPGA内部のBRAMを利用しているデザインの例

FPGA 内の配線リソースを使うことでロジックの好きな箇所にメモリを接続できるのも FPGA 内のメモリのメリットです。たとえばディープニューラルネットワークを FPGA に実装したいという場合に、レイヤ毎に必要なメモリをそれぞれのロジックに接続するといった使い方ができます。それぞれのメモリアクセスは並行して動作できます。

使ってみよう

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

今回は Xilinx の FPGA を搭載した Digilent の Arty を使うことを想定して、Vivado のメモリジェネレータウィザードを使ってメモリ IP を利用する方法を説明します。なお、Intel や Lattice の FPGA を利用する場合にも同じようウィザードを使って好きなサイズ・幅のメモリモジュールを用意して使うことができます。

プロジェクトの準備

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

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

メモリを使うには、IP Catalog から起動できるウィザードでモジュールを用意するのが楽です。具体的な手順と共に紹介します。

まずは、Flow Navigator から IP Catalog を選択し、メモリモジュールを生成するためのウィザードを開きます。

メモリモジュールの生成ウィザードを開いたところです。構成するメモリのタイプや各種パラメタを設定できます。

メモリは大きく次の5つのタイプから選択して構成できます。

  • Simple Port RAM – 読み書きポートが一つのメモリ
  • Simple Dual Port RAM – 読み出し専用と書き込み専用ポートを一つずつ持つメモリ
  • True Dual Port RAM – 読み書きできる二つのポートを持つメモリ
  • Single Port ROM – 読み出しポートが一つの ROM (リードオンリーメモリ)
  • Dual Port RAM – 読み出しポートが二つの ROM

ポート数が多い方が柔軟に利用できますが、その一方で回路が複雑になり、使用するロジックの量が増加することがあります。今回は、リソースの制約を気にするほどのアプリケーションは考えないので、独立した二つの読み書きポートを持つ True Dual Port RAM を選択しました。

メモリのビット幅やサイズもこのウィザードで設定できます。幅は1から 4096bit の範囲で自由にセットすることができます。

True Dual Port RAM のもう一つのポートの設定です。読み書きポートの幅には二つのポートでそれぞれ異なるサイズを指定できます。

一通りの設定を終えたらサマリで設定内容を確認しましょう。今回の設定は 8bit 幅で 16384 エントリ、つまり 16KB のメモリを作っています。これは FPGA 内部にある 36K bit (4KB) の BRAM を4つ組み合わせて構成されるようです。

また、メモリの読み出しに2サイクルかかることもこのサマリで確認できます。読み出しにかかるサイクル数については後でシミュレーションの結果でも確認してみましょう。

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

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

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

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

ここでは、次のように各アドレスにアドレスの下位 8bit を値として書き込むロジックを使ってメモリが動作する様子を確認してみることにします。

module bram_example (
    input logic CLKA,
    input logic CLKB,
    input logic RST,
    input logic [13:0] A,
    output logic [7:0] Q
);
    logic        ena, enb;
    logic [0:0]  wea, web;
    logic [13:0] addra, addrb;
    logic [7:0]  dina, dinb;
    logic [7:0]  douta, doutb;

    blk_mem_gen_0 blk_mem_gen_0_i ( // メモリモジュールのインスタンスを作成
        .clka(CLKA), // ポートA(一つ目のポート)への接続
        .ena(ena),
        .wea(wea),
        .addra(addra),
        .dina(dina),
        .douta(douta),
        .clkb(CLKB), // ポートB(二つ目のポート)への接続
        .enb(enb),
        .web(web),
        .addrb(addrb),
        .dinb(dinb),
        .doutb(doutb)
    );

    // ポートAには,アドレスの下位8ビットをデータとして格納する
    assign ena  = 1'b1;
    logic [13:0] waddr;
    always_ff @(posedge CLKA) begin
        if(RST == 1) begin
            waddr <= 14'd0;
            wea   <= 1'b0;
        end else begin
            addra <= waddr;
            dina  <= waddr[7:0];
            if(waddr[0] == 1) begin // 奇数アドレスにだけデータを書いてみる
                wea   <= 1'b1;
            end else begin
                wea   <= 1'b0;
            end
            waddr <= waddr + 1;
        end
    end

    // ポートBはリードオンリーで利用する
    assign enb  = 1'b1;
    assign web  = 1'b0; // read-only
    assign dinb = 8'h0;
    assign Q = doutb;
    always_ff @(posedge CLKB) begin
        if(RST == 1) begin
        end else begin
            addrb <= A; // ポートBはアドレスを指定するだけ
        end
    end

endmodule

動作を確認するために次のようなテストベンチを用意しました。メモリモジュールに供給するクロックとして、異なる動作周波数の CLKA と CLKB を用いていることにも注目してください。

module bram_example_tb ();

logic CLKA, CLKB, RST;
logic [13:0] A;
logic [7:0] Q;

bram_example bram_example_i (
    .CLKA(CLKA),
    .CLKB(CLKB),
    .RST(RST),
    .A(A),
    .Q(Q)
);

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

always begin
    CLKB <= 1'b1; #15;
    CLKB <= 1'b0; #15;
end

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

initial begin
    #75;
    A <= 14'd1;
    #30;
    A <= 14'd2;
    #30;
    A <= 14'd3;
    #30;
    A <= 14'd4;
    #30;
    A <= 14'd5;
    #10000;
    $finish;
end

endmodule // bram_example_tb

シミュレーション結果は次の通りです。ポート A で書き込んだデータ (アドレス 0x0001 に書き込まれた 0x010x0003 に書き込まれた 0x03) を,ポート B から読み出していることがわかります。データを書き込んでいないアドレス 0x00020x0004 の読み出し結果は 0x00 ですね。

少し詳しく見てみてると、ウィザードのサマリで見たようにアドレスが確定した2サイクル後にデータが読み出されていることがみて取れます。

実機でも動作確認

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

ソースコードは GitHub Gist を参照してください。UART で文字を送信するための serial_send モジュールは、シリアル通信で Hello, FPGA (2) の流用です。VIO はメモリを読み書きするためのポートを接続します。

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

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

いろいろなメモリの使い方

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

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

データの幅を変える

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

おわりに

FPGA の中に埋め込まれているメモリとその使い方を紹介しました。FPGA では好きなサイズと幅のメモリを柔軟に利用できます。ロジックの好きなところにメモリを配置することで実質的なデータアクセス帯域を増やすことができるのも魅力的です。

紹介した構成以外にも、動作周波数向上のために出力に一段レジスタを挟んだり、LUT を使って小容量のメモリを作る (Distributed Memory Generator を使う) など、FPGA内には様々なメモリをウィザードで作ることができます。用途に応じて適切なメモリを利用できるようになれれば、きっと FPGA を効率良く使えるようになるはずです。

参考

より詳しくメモリの使い方を知りたい場合には、Xilinx のメモリジェネレータのドキュメントを参照してください。

おまけ: ビットストリーム内のメモリに初期値を設定

コンフィギュレーション直後の FPGA 内のメモリの値はデフォルトでは0に設定されます。また、ファイルなどに格納したデータを初期値として合成することもできます。

ところで、FPGA のビットストリームの作成には小さいデザインでも数分程度,大きなデザインになると数時間もの時間がかかります。そのため、たとえばたくさんの FPGA 向けに個別の初期値を設定したビットストリームを作るために毎度合成するのは大変です。

そんなときには、updatemem というツールを使ってビットストリームの中のメモリに初期値を埋め込むのが便利です。updatemem でデータを埋め込む作業は一瞬で完了しますので、一つのビットストリームから数十種類の別々の初期値をメモリに埋め込んだビットストリームを作る、といった作業も短時間で終えることができます。

今回の例を使って、あらかじめメモリに文字を格納しておく手順を紹介します。

メモリがどこにあるか確認する

ビットストリームのメモリに値をセットするためには、合成結果からどのメモリが割り当てられているかを確認する必要があります。これは,Implemented Design の結果からプロパティを参照して確認できます。今回の私の合成では、X0Y1 のメモリが割り当てられたようです。

メモリ設定ファイルとデータを用意する

どのメモリが使われているか確認できたら、次のように updatemem 用の設定ファイルを作成します。6行目の X0Y1 を各自の合成結果に応じて変更してください。

<?xml version="1.0" encoding="UTF-8"?>
<MemInfo Version="1" Minor="0">
  <Processor Endianness="Little" InstPath="dummy">
    <AddressSpace Name="user_configrom" Begin="0" End="4095">
      <BusBlock>
    <BitLane MemType="RAMB32" Placement="X0Y1">
          <DataWidth MSB="31" LSB="0"/>
          <AddressRange Begin="0" End="4095"/>
          <Parity ON="false" NumBits="0"/>
    </BitLane>
      </BusBlock>
    </AddressSpace>
  </Processor>
  <Config>
    <Option Name="Part" Val="xc7a35ticsg324-1l"/>
  </Config>
</MemInfo>

設定するデータは16進表記で値を並べて準備します。たとえば,次のデータはアルファベットの小文字・大文字と記号を並べたものです。

@00000000
61626364
65666768
696a6b6c
6d6e6f70
71727374
75767778
797a7b7c
7d7e6021
41424344
45464748
494a4b4c
4d4e4f50
51525354
55565758
595a2223
24252627

updatememでビットストリームを作成する

mmi ファイルと mem ファイルが用意できたら、次のように updatemem コマンドを使ってメモリに初期値を設定したビットストリームファイルを作成します。これでビットストリームに値を格納した 00.bit ができあがります。

updatemem -force \
          -meminfo ./confrom.mmi \
          -data ./confrom_00.mem \
      -bit bram_example_top.bit \
      -proc dummy -out 00.bit

できあがった 00.bit を FPGA にロードして動作している様子が次の通りです。メモリに書き込んだデータが UART から出力されている様子がみてとれます。

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

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