Posts Issued on October 3, 2026

メモリをBSVで書く (3)

posted by sakurai on October 3, 2026 #1134

2点目 VRAMをmkVramにする

図1134.1
図1134.1 VRAM書き換え前後

VRAMは2ポート2クロックのTrue Dual Portです。ポートAはゲームFSMの2MHzで読み書き、ポートBはグラフィクスFSMの49.5MHzで読み出しだけです。手書きのvram.vはこうでした。

書き換え前

module vram #(
   parameter DEPTH = 65536,
   parameter FILE  = "vram.mem"
) (
   input  wire        clka,
   input  wire        wea,
   input  wire [15:0] addra,
   input  wire [3:0]  dina,
   output reg  [3:0]  douta,

   input  wire        clkb,
   input  wire [15:0] addrb,
   output reg  [3:0]  doutb
);

   reg [3:0] mem [0:DEPTH-1];
   initial $readmemh(FILE, mem);

   always @(posedge clka) begin
      if (wea) mem[addra] <= dina;
      douta <= mem[addra];
   end

   always @(posedge clkb) doutb <= mem[addrb];

endmodule

書き換え後のVram.bsvは次のとおりで、これが全文です。

package Vram;

// ================= VRAM =================
// 256x256、4bit/画素の画面メモリ。True Dual Port で、ポート A はゲーム FSM の
// クロックで読み書き、ポート B はグラフィクス FSM のピクセルクロックで読み出し。
// BRAMCore ライブラリの mkSyncBRAMCore2Load がそのまま実体で、初期値は
// vram.mem を $readmemh で読む。両ポートとも毎周期アクセスする always_enabled で、
// 生成 Verilog の BRAM2Load は EN が常に 1 になる。

import BRAMCore::*;

typedef Bit#(16) VAddr_t;
typedef Bit#(4)  Pattern_t;

interface Vram_ifc;
   // ポート A: ゲーム FSM 側。既定クロック
   (* always_ready, always_enabled *)
   method Action    a_req(Bool we, VAddr_t addr, Pattern_t d);
   (* always_ready *)
   method Pattern_t a_data;
   // ポート B: グラフィクス側。pixClk
   (* always_ready, always_enabled *)
   method Action    b_req(VAddr_t addr);
   (* always_ready *)
   method Pattern_t b_data;
endinterface

(* synthesize *)
module mkVram#(Clock pixClk, Reset pixRst)(Vram_ifc);
   Clock clkA <- exposeCurrentClock;
   Reset rstA <- exposeCurrentReset;
   BRAM_DUAL_PORT#(VAddr_t, Pattern_t) ram <-
      mkSyncBRAMCore2Load(65536, False, clkA, rstA, pixClk, pixRst, "vram.mem", False);

   method Action    a_req(Bool we, VAddr_t addr, Pattern_t d) = ram.a.put(we, addr, d);
   method Pattern_t a_data = ram.a.read;
   method Action    b_req(VAddr_t addr) = ram.b.put(False, addr, ?);
   method Pattern_t b_data = ram.b.read;
endmodule

endpackage

2つ目のクロックはモジュールの引数で受けます。mkVram#(Clock pixClk, Reset pixRst)と書くと、生成VerilogにCLK_pixClkとRST_N_pixRstの入力が増えます。自分のクロックはexposeCurrentClockで取り出し、mkSyncBRAMCore2Loadの2組のクロックとリセットに渡します。ポートBのメソッドb_reqとb_dataは、BRAMのポートBがpixClkで動いていることからbscがクロック域を推論するので、clocked_byを書く必要がありません。両ポートにalways_enabledを付けたのは、vram.vと同じく毎周期読む形にするためで、生成VerilogのBRAM2LoadはENAとENBが定数1になります。

トップの接続は、vram.vのパラメータ付きインスタンスがbsc生成モジュールの接続に変わるだけです。

書き換え前

   vram #(.DEPTH(65536), .FILE("vram.mem")) u_vram (
      .clka  (clk_2m),
      .wea   (vwe_a),
      .addra (vaddr_a),
      .dina  (vdout_a),
      .douta (vdin_a),
      .clkb  (C49_5MHz),
      .addrb (vaddr_b),
      .doutb (vram_qb)
   );

書き換え後

   mkVram u_vram (
      .CLK          (clk_2m),
      .RST_N        (locked),
      .CLK_pixClk   (C49_5MHz),
      .RST_N_pixRst (locked),
      .a_req_we     (vwe_a),
      .a_req_addr   (vaddr_a),
      .a_req_d      (vdout_a),
      .a_data       (vdin_a),
      .b_req_addr   (vaddr_b),
      .b_data       (vram_qb)
   );

1つだけ振る舞いの違いがあります。vram.vはポートAで書き込みと同じ周期に読んだ値が書き込み前の内容になる読み出し優先で、bscのBRAM2Load.vは書き込んだ値が出る書き込み優先です。ブリッタは読む状態と書く状態を分けていて、書く周期の読み出し値を使う箇所がありません。それを確かめるために、vram.vとmkVramを同じ乱数の読み書きで並走させました。ポートAは2MHz、ポートBは49.5MHzで、ポートAは書き込みの無い周期の読み出し値を20万周期分比べて不一致0、書き込みの周期だけが46,666回違いました。ポートBは、ポートAが同じ番地に書いている瞬間を除いた4,799,764回の読み出しが全て一致しました。同じ番地への同時アクセスはどちらのモデルでも未定義で、設計上も起きません。

Yosysでは、mkVramがRAMB36E1を8個使い、LUTとFFは0です。vram.vのVivadoでの実装も8個でしたから、物量は同じです。


左矢前のブログ 次のブログ右矢