東大のプロセッサ実験のwiki *1を見ると、読み出しアドレスをクロックに同期してreg
に書き込めば、(write-firstの)BlockRAMに推論されると書かれています。
そこで、1read/1write・読み書き両方ともクロック同期のRAMを、以下のように書いてみます。
module ram(raddr, rdata, we, waddr, wdata, clk); input wire[9:0] raddr; output wire[7:0] rdata; input wire we; input wire[9:0] waddr; input wire[7:0] wdata; input wire clk; reg[7:0] mem[0:1023]; reg[31:0] i; initial for(i=0; i<1024;i=i+1) mem[i] = 0; reg[9:0] raddr_reg; always @(posedge clk) begin if (we) mem[waddr] <= wdata; raddr_reg <= raddr; end assign rdata = mem[raddr_reg]; endmodule
これをVivadoで合成してみると、確かにBlockRAMに推論されます。
しかし、verilogシミュレーションとタイミングシミュレーションの結果(≒FPGAに焼いた時の結果)がまるで違います!いったいどういうことなのでしょうか。
観察
まず、このRAMを使うモジュールを定義します。書き込むアドレスは0番地だけですが、愚直にそう書いてしまうとうまくいきません。
最適化で他の番地が消えてしまうからです。よってあらゆる番地にアクセスできそうな雰囲気を出しておく必要があります(ptr+1
の部分)。
module top(sysclk, cpu_resetn, sw, led); input wire sysclk; input wire cpu_resetn; input wire[2:0] sw; output wire[7:0] led; reg[9:0] ptr; wire[9:0] next_ptr; assign next_ptr = sw[2] ? ptr + 1 : 0; ram ram0( .raddr(next_ptr), .rdata(led), .we(sw[0]), .waddr(ptr), .wdata(led + sw[1]), .clk(sysclk) ); always @(posedge sysclk or negedge cpu_resetn) begin if (!cpu_resetn) ptr <= 0; else ptr <= next_ptr; end endmodule
次に、テストベンチを書きます。20サイクルの間、RAMの0番地から読みだした値+1を書き込むということを繰り返します。
`timescale 1ns / 1ps module test(led); output [7:0] led; reg sysclk; reg cpu_resetn; reg[2:0] sw; top top0(sysclk, cpu_resetn, sw, led); initial begin sysclk <= 0; cpu_resetn <= 1; sw <= 0; #100 cpu_resetn <= 0; # 99 cpu_resetn <= 1; #100 sw <= 3; #200 $finish; end always #5 sysclk <= ~sysclk; endmodule
これを実行してみて、RAMの0番地に何が書かれているかを確認してみます。すると、verilogシミュレーションでは20、タイミングシミュレーションでは10になっています。
verilogシミュレーションの結果は、verilog HDLの記法上もそうであるように、ちゃんとwrite-firstになっていることを示唆しています。つまり、書き込んだ値が即読み出せるということです。
一方、タイミングシミュレーションでは、どうもread-firstになっているようです。書き込んだ値は、直後のサイクルではまだ見えないということになります。
解決法
Xilinx社のマニュアルでは、write-firstなBlockRAMに推論してほしい時の書き方として、以下の書き方が推奨されていました。
always @(posedge clk) begin if(we) begin do <= data; mem[address] <= data; end else do <= mem[address]; end
実際、この書き方をした場合は、おかしな問題は発生しません。
結論
Xilinx社のマニュアルに記載してある通りに書けば、おかしなことは発生しません。
しかし、そうでない書き方をした時に不完全な推論が行われ、verilog HDLの記述どおりに合成されないのは、不親切とかいう次元を超えて明らかにVivadoのバグだと思います。
とはいっても、RAMに書いた直後に読みだすというパターンは普通は存在しないため、問題につながることは少ないようです。
キャッシュメモリをBlockRAMで書いたりすると、書いた*2直後に読み出すということは十分あり得そうです(12/24 指摘を受け追記)。
そもそも、BlockRAMを書くつもりでなくても、クロック同期なメモリっぽいものを書いてしまうと勝手にBlockRAM化されてわけのわからないバグにつながるということも考えられます。
1read1writeのRAMの正しい作り方(12/24 追記)
「解決法」のところで書いたものはreadポートとwriteポートが共有されているものになっています。1read1writeのRAMを作るにはどうしたらいいか?という疑問があったので作ってみました。
まず、以下のBlockRAMのマニュアルの20ページ目を見てみると、たとえwrite-firstを指定したとしても、readポートで指定した番地とwriteポートで指定した番地が同じ場合、readポート側に何が読みだされるかは保証されないと記載されています。 https://www.xilinx.com/support/documentation/user_guides/ug473_7Series_Memory_Resources.pdf
よって、このBlockRAMプリミティブを使うだけでは絶対に1read1writeのRAMを作ることはできません。
そこで、readアドレスとwriteアドレスが一致している場合のフォワーディング回路を自前で用意する必要がありそうです(自動で推論してほしいですね)。
以下のように書くと、ちゃんとBlockRAMに推論され、最初の記述と論理的に同じ動作になり、合成してもこの問題は発生しなくなります。
module ram(raddr, rdata, we, waddr, wdata, clk); input wire[9:0] raddr; output wire[7:0] rdata; input wire we; input wire[9:0] waddr; input wire[7:0] wdata; input wire clk; reg[7:0] mem[0:1023]; reg[31:0] i; initial for(i=0; i<1024;i=i+1) mem[i] = 0; reg[9:0] last_raddr, last_waddr; reg[7:0] rdata1, rdata2; always @(posedge clk) begin if (we) begin mem[waddr] <= wdata; rdata1 <= wdata; end else begin rdata1 <= mem[waddr]; end rdata2 <= mem[raddr]; last_raddr <= raddr; last_waddr <= waddr; end assign rdata = last_raddr == last_waddr ? rdata1 : rdata2; endmodule
writeポートの方は、「解決法」のところで書いたような、Xilinx社マニュアル推奨のwrite-firstな読み出しになる書き方をしています。
よってそこから読み出した結果rdata1
は直前の書き込み値last_wdata
の代わりとして使えます。
これによる無駄なフリップフロップ削減効果に加え、そもそもwrite-firstにしたほうが高速と書かれているので、たぶんこれが一番いい書き方なのだと思います。
*1:12/26追記:見れなくなっていました。アーカイブ→RAMの書き方 - マイクロプロセッサの設計と実装
*2:キャッシュメモリへのwriteアクセス(直後に読み出すことは少なさそう)ではなく、キャッシュラインの置換時のRAM書き込み