みなさんこんにちは。この「MIG を使って DRAM メモリを動かそう」のシリーズでは、全5回を通じて Xilinx Memory Interface Generator (MIG) という IP コアをベースに Xilinx FPGA で DRAM メモリを動かす方法を紹介していきます。説明では教育向けに設計された Arty A7-35T FPGA ボードを用いますが、他の FPGA ボードにも同様に適用できます。
第1回の今回は、FPGA 設計で DRAM を利用する必要性について説明します。また、MIG で DRAM をアクセスする FPGA 設計の一般的な構成、MIG の入出力の概要を紹介します。
FPGA 設計での DRAM の利用の必要性
FPGA 内部には SRAM (Static Random-Access Memory) と呼ばれるアクセスの速いメモリがありますが、容量が非常に限られています。Xilinx 社の FPGA の場合、FPGA 内部の SRAM は Block RAM (BRAM) と呼ばれています。
このブログで扱う FPGA ボードが搭載する FPGA (Arty A7-35T) は50個の 36Kb BRAM (トータルで 1,800Kb < 0.25MB) を持ちます。大規模な FPGA である Virtex-7 VX485T (普及している VC707 ボードに搭載されている FPGA) でも1,030個の 36Kb BRAM (トータルで 37,080Kb < 5MB) しか持ちません。
しかし、現実のほとんどのアプリケーションはそれ以上のメモリ容量を求めています。そのため、FPGA 外部の大容量のメモリの利用が必要となります。
FPGA 設計では、DRAM (Dynamic Random-Access Memory) が外部メモリとして広く利用されています。FPGA から DRAM をアクセスするために、Xilinx 社は MIG という IP コアを提供しています。これにより、DRAM アクセスの制御のためのロジック設計が軽減されますが、FPGA 設計の初心者にとって、MIG の利用もまだ難しい状況です。そこで、このブログでは MIG を使って DRAM を動かす方法を説明します。
MIG で DRAM にアクセスをする FPGA 設計の一般的な構成
下図は、MIG で DRAM にアクセスをする FPGA 設計の一般的な構成の概要を示しています。
ここで、DRAM のアクセスを担当する回路を DRAM コントローラを呼ぶことにします。DRAM コントローラを MIG ベースで構築することになります。
アプリケーション回路 (User Design) は MIG を通じて DRAM のアクセスしますが、MIG とのやり取りが複雑になるため、DRAM コントローラでユーザインタフェース (User Interface) という MIG とのやり取りを抽象化するためのモジュールを設けます。
一般に、アプリケーション回路と DRAM コントローラは別々の周波数で動作します。異なる動作周波数のドメイン間のデータ転送のために、非同期 FIFO (Asynchronous FIFO)を利用します。以下は非同期 FIFO を Verilog HDL で実装した例です。
module AsyncFIFO #(
parameter DATA_WIDTH = 512,
parameter ADDR_WIDTH = 8) // FIFO_DEPTH = 2^ADDR_WIDTH
(
input wire wclk,
input wire rclk,
input wire i_wrst,
input wire i_rrst,
input wire i_wen,
input wire [DATA_WIDTH-1 : 0] i_data,
input wire i_ren,
output wire [DATA_WIDTH-1 : 0] o_data,
output wire o_empty,
output wire o_full);
reg [DATA_WIDTH-1 : 0] afifo[(2**ADDR_WIDTH)-1 : 0];
reg [ADDR_WIDTH : 0] waddr;
reg [ADDR_WIDTH : 0] raddr;
reg [ADDR_WIDTH : 0] raddr_gray1;
reg [ADDR_WIDTH : 0] raddr_gray2;
reg [ADDR_WIDTH : 0] waddr_gray1;
reg [ADDR_WIDTH : 0] waddr_gray2;
wire [DATA_WIDTH-1 : 0] data;
wire [ADDR_WIDTH : 0] raddr_gray;
wire [ADDR_WIDTH : 0] waddr_gray;
wire [ADDR_WIDTH : 0] raddr2;
wire [ADDR_WIDTH : 0] waddr2;
genvar genvar_i;
// output signals
assign o_data = data;
assign o_empty = (raddr == waddr2);
assign o_full = (waddr[ADDR_WIDTH] != raddr2[ADDR_WIDTH]) &&
(waddr[ADDR_WIDTH-1 : 0] == raddr2[ADDR_WIDTH-1 : 0]);
// binary code to gray code
assign raddr_gray = raddr[ADDR_WIDTH : 0] ^ {1'b0, raddr[ADDR_WIDTH : 1]};
assign waddr_gray = waddr[ADDR_WIDTH : 0] ^ {1'b0, waddr[ADDR_WIDTH : 1]};
// gray code to binary code
generate
for (genvar_i = 0; genvar_i <= ADDR_WIDTH; genvar_i = genvar_i + 1) begin
assign raddr2[genvar_i] = ^raddr_gray2[ADDR_WIDTH : genvar_i];
assign waddr2[genvar_i] = ^waddr_gray2[ADDR_WIDTH : genvar_i];
end
endgenerate
// double flopping read address before using it in write clock domain
always @(posedge wclk) begin
if (i_wrst) begin
raddr_gray1 <= 0;
raddr_gray2 <= 0;
end else begin
raddr_gray1 <= raddr_gray;
raddr_gray2 <= raddr_gray1;
end
end
// double flopping write address before using it in read clock domain
always @(posedge rclk) begin
if (i_rrst) begin
waddr_gray1 <= 0;
waddr_gray2 <= 0;
end else begin
waddr_gray1 <= waddr_gray;
waddr_gray2 <= waddr_gray1;
end
end
// read
assign data = afifo[raddr[ADDR_WIDTH-1 : 0]];
always @(posedge rclk) begin
if (i_rrst) begin
raddr <= 0;
end else if (i_ren) begin
raddr <= raddr + 1;
end
end
// write
always @(posedge wclk) begin
if (i_wrst) begin
waddr <= 0;
end else if (i_wen) begin
afifo[waddr[ADDR_WIDTH-1 : 0]] <= i_data;
waddr <= waddr + 1;
end
end
endmodule
この実装では、リングバッファを採用します。FIFO の読み出し (76-84行) と書き込み (86-94行) は別々の動作周波数 (クロック信号 rclk と wclk) で動作します。
読み出し側で FIFO の空 (empty) かどうかの状態をチェックする必要があります。これは、一般の同期 FIFO の場合、読み出しポインタ raddr と書き込みポインタ waddr を比較することで実現できます。しかし、非同期 FIFO の場合、raddr と waddr は異なるクロックドメインの信号であるため、直接に比較できません。
ここでは、waddr を、ある値から隣接した値に変化する際に常に1ビットしか変化しない、グレイコードに変換し (44行)、それを rclk ドメインの信号 waddr_gray2 にパスします (65-74行)。それから、 waddr_gray2 を通常のバイナリコード waddr2 に戻し (50行)、waddr2 と raddr を比較して FIFO が空であるかどうかを決定します (38行)。
書き込み側での FIFO が満杯 (full) かどうかチェックも同様です。
グレイコードを使う理由は論文 [1] で詳しく解説されていますが、簡単に説明すると、信号をあるクロックドメインから別のクロックドメインに正確に渡すためです。
通常のバイナリコードの場合、ある値から隣接した値に変化する際に複数のビットが変化する可能性があります (例えば、4ビットの信号が7 (0111) から8 (1000) に変化する際にすべてのビットが変化する)。このため、異なるクロックドメインで正確に渡すことが困難になります。
次に、図1の説明に戻ります。
アプリケーション回路からの DRAM アクセスの命令には次の3つの情報が必要です。
- addr (address): 読み出し・書き込みアドレス。
- re/we (read enable / write enable): 読み出しあるいは書き込みの実行を指示します。
- wdata (write data): 書き込みデータ。
DRAM コントローラは、ユーザインタフェースのこれらの情報に基づいて MIG に対する命令を生成します。読み出し命令の場合、DRAM から読み出されたデータ rdata はアプリケーション回路に返されます。次回の記事で、MIG に対する命令の詳細を解説します。
MIG の入出力の概要
下図は MIG の入出力の概要を示しています。
この図では、このブログで扱う FPGA ボード (Arty A7-35T) を想定しています。Arty A7-35T が搭載する DRAM は DDR3 SDRAM (Double Data Rate 3 Synchronous DRAM) MT41K128M16JT-125 です。
MIG の入出力の信号は3つのグループに分けることができます。
ddr で始まる信号は、DRAM デバイスに接続する信号です。読み出し・書き込みアドレスは ddr3_addr, ddr3_ba で指定します。読み出し・書き込みデータは ddr3_dq という双方向の信号で運ばれます。書き込みの場合、書き込みデータのどちらかのバイトをマスクする (書き込みを行わない) ことができます。これは、ddr3_dm 信号で指定します。これらを理解するために、DRAM の基本的な構造を理解する必要がありますが、これも次回の記事で説明します。
app で始まる信号はユーザインタフェースに接続する信号です。ユーザインタフェースでは app_addr で読み出し・書き込みアドレスを指定します。MIG は app_addr から ddr3_addr, ddr3_ba の物理アドレスに変換します。app_cmd は MIG に対する命令 (読み出しか書き込み) を指定する信号です。app_en は app_cmd が有効かどうかを示します。MIG が新しい命令を受け付けることができるとき、app_rdy がアサート (assert) されます (1に設定されます)。app_wdf_ で始まる信号は書き込み、app_rd_ で始まる信号は読み出し関連です。こちらも、詳しくは次回の記事で解説します。
その他の信号の sys_clk_i, sys_rst は MIG の入力クロック、リセット信号です。ui_clk, ui_clk_sync_rst はユーザインタフェース用のクロック、リセット信号です。app で始まる信号は ui_clk で同期されます。ui_clk からアプリケーション回路用のクロック信号を生成することができます。最後に、init_calib_complete は DRAM の初期化が完了したかどうかを示します。
まとめ
今回は、FPGA 設計における DRAM の必要性、MIG で DRAM のアクセスをする FPGA 設計の一般的な構成、MIG の入出力の概要を説明しました。説明では Arty A7-35T FPGA ボードを想定しました。今後の記事でもこの FPGA ボードを想定します。
次回は、DRAM の基本的な構成、MIG とユーザインタフェースの間のやり取りを解説していきます。
東工大 Thiem Van Chu
参考文献
- C. E. Cummings, “Simulation and Synthesis Techniques for Asynchronous FIFO Design,” SNUG 2002 (Synopsys Users Group Conference, San Jose, CA, 2002) User Papers. 2002.