![]() |
19 |
俺の勉強会#8 LT予告 |
Posts Tagged with "Space invaders"
既に発行済みのブログであっても適宜修正・追加することがあります。We may make changes and additions to blogs already published.
![]() |
5 |
メモリをBSVで書く (5) |
![]() |
4点目 運用
bsc付属のライブラリVerilogが3つ増えました。mkFIFOF1のFIFO1.v、mkBRAMCore1LoadのBRAM1Load.v、mkSyncBRAMCore2LoadのBRAM2Load.vで、bscはこれらを生成せず、インストール先のlib/Verilogにあるものをインスタンスします。教科書版のMakefileはこれをsrcに写して、生成Verilogと同じ扱いにします。
# bsc 付属の Verilog ライブラリ。mkBlitter が使う FIFO1 と BRAM1Load、
# mkVram が使う BRAM2Load はここから src に写す
BSC_VLIB ?= $(abspath $(dir $(shell which $(BSC)))../lib/Verilog)
BSC_LIB_V := FIFO1.v BRAM1Load.v BRAM2Load.v
$(addprefix $(RTL_DIR)/,$(BSC_LIB_V)): $(RTL_DIR)/%.v: $(BSC_VLIB)/%.v
@mkdir -p $(RTL_DIR)
cp $< $@
1つのソースから2つのモジュールを出す規則は、グループターゲットで3つの生成物を1回のレシピに結びます。Blitter.bsvからはmkBlitter.vとmkProm.vとBlitter.bo、SoundFSM.bsvからは変種ごとにmkSoundFSMn.vとmkSoundROMn.vです。
$(RTL_DIR)/mkBlitter.v $(RTL_DIR)/mkProm.v $(VOBJ_DIR)/Blitter.bo &: $(SRC_DIR)/Blitter.bsv
@mkdir -p $(RTL_DIR) $(VOBJ_DIR)
$(TIME) $(BSC) $(BSC_RT_OPTS) $(VLOG_FLAGS) $(PFLAGS) $(STEPFLAGS) \
-bdir $(VOBJ_DIR) -vdir $(RTL_DIR) -g mkBlitter -g mkProm $(SRC_DIR)/Blitter.bsv
$(RTL_DIR)/mkSoundFSM0.v $(RTL_DIR)/mkSoundROM0.v &: $(SRC_DIR)/SoundFSM.bsv $(SRC_DIR)/SoundCodes.vh
$(call BSC_GEN_D,mkSoundFSM0 mkSoundROM0,$(SRC_DIR)/SoundFSM.bsv,FSM0)
make checkはライブラリ3つの所在も確認します。ライブラリが別の場所にあるときはmake BSC_VLIB=場所で指定します。Vivadoには、srcの他の.vと同様にこの3つと、新しく増えたmkProm.v、mkVram.v、mkSoundROM0.vから3を登録します。memの.memはライブラリのVerilogの中から同じ名前で読まれるので、登録は今までのままです。
手書きVerilogからはvram.v、rom.v、sndch.vが消え、master.vからROMのインスタンスが消えました。残るのはtop.v、clock.v、master.v、sound.vの4つで、clock.vのMMCMと分周以外は配線です。
Yosysで試すときは1つ注意があります。read_verilogに-deferを付けないと、BRAM1Load.vがパラメータの既定値である空のファイル名で$readmemhを評価して止まります。-deferを付けると、パラメータが確定してから本体を解釈します。
まとめ
比べる前は、ブリッタの起動をFIFOにした版で、ROMとVRAMは手書きVerilogです。後はこの記事の3点を入れた版です。LUTとFFはYosysのsynth_xilinxでフラットに合成した概算で、mkGameFSMの行はmkBlitterとmkPromを含みます。動作はVerilatorでmkGameFSMを1,200万サイクル回し、VRAM書き込みとサウンドコードの系列と時刻を比べました。
| メモリのBSV化前後 | 前 | 後 | 備考 | |
|---|---|---|---|---|
| ソース | Blitter.bsvとGameFSM.bsv[行] | 1,870 | 1,883 | mkPromを足し、ポート2つとレジスタ1つが消えた |
| Vram.bsv[行] | 0 | 41 | vram.vの34行が置き換わった | |
| SoundFSM.bsv[行] | 261 | 306 | rom.vの26行とsndch.vの65行が置き換わった | |
| 生成Verilog[行] | 32,863 | 32,911 | mkBlitter.v、mkProm.v、mkGameFSM.vの合計 | |
| mkBlitter | LUT | 183 | 198 | ROMを3個のBRAMに分ける選択論理 |
| FF | 160 | 149 | p_addrの15ビットが消えた | |
| RAMB36 | 0 | 3 | mkProm 25728 x 4 | |
| mkGameFSM | LUT | 5,695 | 5,790 | 後はROMを含む |
| FF | 2,168 | 2,157 | ||
| mkVram | RAMB36 | 8 | 8 | 前はvram.v |
| mkSoundFSMn | ポート | 2本 | 0本 | rom_addressとrom_dataが消えた |
| 動作 | mkGameFSMの系列と時刻 | 一致 | 1,200万サイクル | |
| サウンド4チャネルの出力 | 一致 | 300万サイクル、852コマンド | ||
mkGameFSMのLUTが95増えていますが、前の値にはmaster.vにあったROMが入っていません。ROMの3個のBRAMを選ぶアドレスの比較と出力の選択がmkGameFSMの側に移った分で、設計全体では増えていません。FFの減りはp_addrの15ビットそのもので、読み出しデータのレジスタはBRAMの出力レジスタに置き換わりました。
メモリをBSVに移して得たものは、ブリッタのインタフェースからROMが消えたことです。ROMを読むのはブリッタだけなのに、アドレスとデータの2ポートがブリッタ、mkGameFSM、master.vの3階層を貫いていました。使う側の中に置けば、その経路は丸ごと要らなくなります。一方でFSMとROMは機能が別なので、ブリッタの中でもmkProm、サウンドFSMの中でもmkSoundROMnという別のモジュールにし、FSMはそれをメソッドで呼びます。パターンROMとサウンドROMが同じ形になりました。VRAMは2つのクロック域にまたがるのでトップに残りますが、クロックを引数で受けるBSVのモジュールになり、手書きVerilogは配線とクロック生成だけになりました。
![]() |
4 |
メモリをBSVで書く (4) |
![]() |
3点目 サウンドROMをmkSoundROMにする

図1135.1 Sound ROM書き換え前後
サウンドは4チャネルあり、チャネルごとに手書きのsndch.vがFSMのmkSoundFSMnとROMのrom.vを並べ、rom_addressとrom_dataの2本で結んでいました。パターンROMと同じ形にして、ROMをmkSoundROMnという別モジュールにし、FSMがその中に置いてメソッドで呼ぶようにしました。FSMはWAVを読んでPCMを出す機能、ROMは内容を持つ機能で、機能が別なので別モジュールのままです。
書き換え前のsndch.vはCHに加えてDEPTHとFILEをパラメータに取り、sound.vがチャネルごとに語数とファイル名を渡していました。
module sndch #(
parameter CH = 0,
parameter DEPTH = 21650,
parameter FILE = "ch00.mem"
) (
rom #(.WIDTH(8), .AWIDTH(15), .DEPTH(DEPTH), .FILE(FILE)) u_rom (
.clk (clk),
.addr (rom_addr),
.q (rom_q)
);
sndch #(.CH(0), .DEPTH(21650), .FILE("ch00.mem")) u_ch0 (
FSMの側は、アドレスをレジスタaddrに置いてrom_addressポートから出し、rom_dataポートから毎周期romdataレジスタに取り込んでいました。
書き換え前
interface FSM_ifc;
method Action sound(SoundCode_t incode);
method Action rom_data(Data_t indata);
method Action sync(Bool lrclk);
method Action empty(Bool flag);
method Addr_t rom_address();
method Data_t sdout();
method Bool soundon();
method Bool fifo_ren();
endinterface
Reg#(Data_t) romdata <- mkRegU, // ROM 読み出しデータ(毎サイクル入力)
dout <- mkReg(`NULL); // PCM 出力(無音=`NULL)
Reg#(UInt#(32)) workd <- mkRegU;
Reg#(UInt#(15)) dcount <- mkRegU; // 残りサンプル数(WAV data チャンクサイズ)
Reg#(Addr_t) worka <- mkRegU, // readMem への引数(読みたい番地)
romaddr <- mkRegU, // WAV 読み出しポインタ
addr <- mkReg(0); // rom_address ポートへの出力値
function Stmt readMem;
return (seq
addr <= worka;
noAction;
noAction;
endseq);
endfunction
if (ii == 3) dcount <= truncate(workd<<8) | extend(romdata);
else workd <= workd<<8 | extend(romdata);
method Action rom_data(Data_t indata);
romdata <= indata;
endmethod
method Addr_t rom_address();
return addr;
endmethod
書き換え後は、SoundFSM.bsvの末尾にROMのモジュールを足しました。FSMと同じ-D FSM0から3で作り分け、語数とファイル名は変種ごとにここで決まります。
// =================================================================
// SoundROM: 効果音 ROM(1ソース4変種)
// FSM と同じ -D FSM0〜FSM3 で mkSoundROM0〜3 を作り分け、語数と
// ファイル名を変種毎に持つ。実体は BRAMCore ライブラリの
// mkBRAMCore1Load で、生成 Verilog では bsc 付属の BRAM1Load.v が
// $readmemh でファイルを読む。req したアドレスの内容が次の周期から
// data に出て次の req まで保持される同期 ROM。
// FSM とは機能が別なので、同じソースに置くが別モジュールにしてある。
// =================================================================
`ifdef FSM0
`define ROM_DEPTH 21650
`define ROM_FILE "ch00.mem"
`elsif FSM1
`define ROM_DEPTH 4622
`define ROM_FILE "ch01.mem"
`elsif FSM2
`define ROM_DEPTH 6586
`define ROM_FILE "ch02.mem"
`elsif FSM3
`define ROM_DEPTH 27814
`define ROM_FILE "ch03.mem"
`endif
interface ROM_ifc;
(* always_ready *) method Action req(Addr_t addr);
(* always_ready *) method Data_t data();
endinterface
(* synthesize *)
`ifdef FSM0
module mkSoundROM0(ROM_ifc);
`elsif FSM1
module mkSoundROM1(ROM_ifc);
`elsif FSM2
module mkSoundROM2(ROM_ifc);
`elsif FSM3
module mkSoundROM3(ROM_ifc);
`endif
BRAM_PORT#(Addr_t, Data_t) rom <- mkBRAMCore1Load(`ROM_DEPTH, False, `ROM_FILE, False);
method Action req(Addr_t addr) = rom.put(False, addr, ?);
method Data_t data = rom.read;
endmodule
FSMはそれを中に置き、readMemでreqを呼び、データはrom.dataで読みます。インタフェースからrom_dataとrom_addressが消え、addrとromdataの2つのレジスタも消えました。
interface FSM_ifc;
method Action sound(SoundCode_t incode);
method Action sync(Bool lrclk);
method Action empty(Bool flag);
method Data_t sdout();
method Bool soundon();
method Bool fifo_ren();
endinterface
// 効果音 ROM。機能が別なので末尾の別モジュール mkSoundROMn にしてあり、
// 本FSMはそれを置いて readMem で req と data を呼ぶ。
`ifdef FSM0
ROM_ifc rom <- mkSoundROM0;
`elsif FSM1
ROM_ifc rom <- mkSoundROM1;
`elsif FSM2
ROM_ifc rom <- mkSoundROM2;
`elsif FSM3
ROM_ifc rom <- mkSoundROM3;
`endif
function Stmt readMem;
return (seq
rom.req(worka);
noAction;
noAction;
endseq);
endfunction
if (ii == 3) dcount <= truncate(workd<<8) | extend(rom.data);
else workd <= workd<<8 | extend(rom.data);
action
son <= True;
dout <= rom.data;
endaction
reqの中身は次の周期からdataに出て、次のreqまで保持されます。前の形ではaddrレジスタ、rom.vの出力レジスタ、romdataレジスタの3段を経てデータが届いていたので、readMemの2つのnoActionはそのための待ちでした。ROMの読み出しが1段になった後も、noActionはそのまま残してサイクル数を変えていません。ROMのreqはFSMの状態からだけ呼ばれるので、always_enabledは付けず、呼ばれた周期だけBRAMのENが立ちます。
sound.vはmkSoundFSM0から3を直接4つ置く形になり、sndch.vとrom.vは消えました。
mkSoundFSM0 u_ch0 (
.CLK(snd_fsmclk), .RST_N(snd_xrst), .sync_lrclk(LRCLK), .sound_incode(scode),
.empty_flag(fifo_empty), .fifo_ren(ren0), .sdout(ch_out0), .soundon(ch_on0)
);
動作は、旧sndch.vと新mkSoundFSMnを4チャネルとも同じ乱数のROM内容、同じコマンド列、同じlrclkで並走させて確かめました。SND_FSMCLKで300万サイクル、コマンド852件、再生中の周期がチャネル合計で488万あり、fifo_ren、soundon、sdoutの3出力が全周期一致しました。
![]() |
3 |
メモリをBSVで書く (3) |
![]() |
2点目 VRAMをmkVramにする

図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個でしたから、物量は同じです。
![]() |
2 |
メモリをBSVで書く (2) |
![]() |
書き換え後は、Blitter.bsvにmkPromというROMのモジュールを立て、ブリッタはそれを置いてreqとdataを呼ぶだけにします。mkPromの実体はBRAMCoreのmkBRAMCore1Loadで、引数は語数、出力レジスタの有無、ファイル名、ファイルが2進かどうかです。
import StmtFSM::*;
import FIFOF::*;
import BRAMCore::*;
// MemPort: 画面メモリ側の面。PROM は内蔵なので VRAM のポート A だけが出る
interface MemPort;
(* always_ready *) method VAddr_t vaddr;
(* always_ready *) method Bool vwe;
(* always_ready *) method Pattern_t vdout;
(* always_enabled *) method Action vdin(Pattern_t d);
endinterface
// ================= パターン ROM =================
// 25728 x 4 の同期 ROM。prom.mem を $readmemh で読む。req したアドレスの内容が
// 次の周期から data に出る。実体は BRAMCore ライブラリの mkBRAMCore1Load で、
// 生成 Verilog では bsc 付属の BRAM1Load.v がインスタンスされる。
// FSM とは機能が別なので、ブリッタの中に置く別モジュールにしてある。
// 語数は PROM_DEPTH で作り分けられる(テストベンチ用)。
`ifdef PROM_DEPTH
Integer promDepth = `PROM_DEPTH;
`else
Integer promDepth = 25728;
`endif
interface Prom_ifc;
(* always_ready *) method Action req(PAddr_t addr);
(* always_ready *) method Pattern_t data;
endinterface
(* synthesize *)
module mkProm(Prom_ifc);
BRAM_PORT#(PAddr_t, Pattern_t) rom <- mkBRAMCore1Load(promDepth, False, "prom.mem", False);
method Action req(PAddr_t addr) = rom.put(False, addr, ?);
method Pattern_t data = rom.read;
endmodule
Reg#(VAddr_t) v_addr <- mkRegU;
Reg#(Bool) fvwe <- mkReg(False);
Reg#(Bool) fobj <- mkReg(False),
fbase <- mkReg(False);
Reg#(Pattern_t) vd <- mkRegU;
Wire#(Pattern_t) vdata <- mkWire;
// パターン ROM。機能は mkProm という別のモジュールで、ブリッタはそれを置いて
// req と data を呼ぶだけ。階層図では mkBlitter の中に mkProm の箱として見える。
Prom_ifc prom <- mkProm;
action
prom.req({pack(ys)[7:0], pack(xs)[6:0]});
UInt#(8) y_addr = truncate(yd);
UInt#(8) x_addr = truncate(xd);
v_addr <= {pack(y_addr), pack(x_addr)};
endaction
noAction; // リード待ち(1サイクル)
action
case (b.op)
BLIT_COPY: vd <= prom.data;
BLIT_OR: vd <= vdata | prom.data;
BLIT_ANDN: vd <= vdata & ~prom.data;
BLIT_ERASE: vd <= 0;
endcase
xs <= xs + 1;
xd <= xd + 1;
endaction
interface MemPort mem;
method VAddr_t vaddr = v_addr;
method Bool vwe = fvwe;
method Pattern_t vdout = vd;
method Action vdin(Pattern_t d); vdata <= d; endmethod
endinterface
アドレスを置くactionはレジスタへの代入からprom.reqに、ラッチのactionはワイヤからprom.dataに変わりました。reqしたアドレスの内容は次の周期からdataに出るので、間のnoActionを含めて1画素あたりのサイクル数は変わりません。消えたのはp_addrレジスタ、romdataワイヤ、MemPortの2ポート、mkGameFSMの同名の2ポート、master.vのROMインスタンスと2本の配線です。mkPromは(* synthesize *)付きなので生成物にmkProm.vが1つ増え、Vivadoの階層図ではmkBlitterの中にmkPromの箱が出ます。ブリッタのFSMとROMが別のモジュールであることが、字面でも階層図でも見えます。
生成されたmkProm.vの中では、bsc付属のBRAM1Load.vがFILENAMEにprom.mem、MEMSIZEに25728を与えてインスタンスされます。BRAM1Load.vの中が$readmemhなので、memの運用は今までと同じです。語数はpromDepthにあり、テストベンチのように別の語数で作りたいときは-D PROM_DEPTH=32768のように与えます。
![]() |
1 |
メモリをBSVで書く |
![]() |
教科書版の手書きVerilogは、トップの配線、メモリ、クロック生成の3つでした。このうちメモリは、BSVの標準ライブラリBRAMCoreがそのまま当てはまる部分で、手書きのVerilogを残す理由がありません。パターンROM、VRAM、サウンドROMの3つをBSVのモジュールにしました。どれも初期値は今までどおりmemの$readmemhで読み、Vivadoの運用は変わりません。合成はYosysのsynth_xilinx、動作確認はVerilatorとiverilogで、前回までと同じ条件です。
置き場所を決める物差しは、機能が別なら別モジュール、というものです。FSMはROMを読む機能、ROMは内容を持つ機能で、2つは別のモジュールにします。そのうえで、ROMを読むのがそのFSMだけなら、ROMのモジュールはそのFSMの中に置き、FSMがメソッドで呼びます。パターンROMはブリッタの中に、サウンドROMはサウンドFSMの中に置きました。VRAMは2つのクロック域から共有されるので、独立のモジュールとしてトップに置いたままです。
1点目 パターンROMをブリッタの中に置く

図1132.1 パターンROM書き換え前後
パターンROMを読むのはブリッタだけです。それなのにROMはmaster.vにあり、ブリッタはアドレスをレジスタp_addrに置いて外に出し、読み出しデータをワイヤromdataで受けていました。ブリッタのMemPortにはprom_addrとpdinの2ポートがあり、mkGameFSMにも同じ名前のポートが素通しで並んでいました。
書き換え前
// MemPort: 画面メモリ側の面
interface MemPort;
(* always_ready *) method PAddr_t prom_addr;
(* always_ready *) method VAddr_t vaddr;
(* always_ready *) method Bool vwe;
(* always_ready *) method Pattern_t vdout;
(* always_enabled *) method Action pdin(Pattern_t d);
(* always_enabled *) method Action vdin(Pattern_t d);
endinterface
Reg#(PAddr_t) p_addr <- mkRegU;
Reg#(VAddr_t) v_addr <- mkRegU;
Reg#(Bool) fvwe <- mkReg(False);
Reg#(Bool) fobj <- mkReg(False),
fbase <- mkReg(False);
Reg#(Pattern_t) vd <- mkRegU;
Wire#(Pattern_t) vdata <- mkWire,
romdata <- mkWire;
action
p_addr <= {pack(ys)[7:0], pack(xs)[6:0]};
UInt#(8) y_addr = truncate(yd);
UInt#(8) x_addr = truncate(xd);
v_addr <= {pack(y_addr), pack(x_addr)};
endaction
noAction; // リード待ち(1サイクル)
action
case (b.op)
BLIT_COPY: vd <= romdata;
BLIT_OR: vd <= vdata | romdata;
BLIT_ANDN: vd <= vdata & ~romdata;
BLIT_ERASE: vd <= 0;
endcase
xs <= xs + 1;
xd <= xd + 1;
endaction
interface MemPort mem;
method PAddr_t prom_addr = p_addr;
method VAddr_t vaddr = v_addr;
method Bool vwe = fvwe;
method Pattern_t vdout = vd;
method Action pdin(Pattern_t d); romdata <= d; endmethod
method Action vdin(Pattern_t d); vdata <= d; endmethod
endinterface
master.vにはROMのインスタンスと、そこへ渡す2本の配線がありました。
wire [14:0] prom_addr;
wire [3:0] prom_dout;
mkGameFSM u_game (
.CLK (clk),
.RST_N (rst_n),
.COUNT (COUNT),
.START_BUTTON (start_db),
.LEFT_BUTTON (left_db),
.RIGHT_BUTTON (right_db),
.FIRE_BUTTON (fire_db),
.tick (tick),
.empty (fifo_empty),
.fifo_wen (fifo_wen),
.SCODE (scode),
.prom_addr (prom_addr),
.pdin (prom_dout),
.vaddr (vaddr),
.vdout (vdout),
.vwe (vwe),
.vdin (vdin),
.Gun_Exp (gun_exp)
);
rom #(.WIDTH(4), .AWIDTH(15), .DEPTH(25728), .FILE("prom.mem")) u_prom (
.clk (clk),
.addr (prom_addr),
.q (prom_dout)
);
![]() |
26 |
GameFSMの結果 |
![]() |
Vivado 2026.1で合成したゲーム全体の各モジュールの物量を表1131.1に示します。ロジックはすべてBSVで記述し、トップ配線とメモリ、クロック生成のみVerilogで書いています。論理合成はトップからのフラット合成で、物量は階層別レポートから読みました。小さなモジュールはフラット合成で親に吸収され計上されるため、その分は親の残差として示し、注記します。
| モジュール | 説明 | BSV行数 | 生成Verilog行数※1 | 物量 | ||||
|---|---|---|---|---|---|---|---|---|
| LUT数 | LUT割合[%] | FF数 | FF割合[%] | BRAM数 | ||||
| mkGameFSM | 自機、インベーダ、UFO、自弾、敵弾、スコア等を処理するFSM | 1,554 | 31,439 | 2,904 | 48.7 | 1,807 | 73.0 | - |
| mkBlitter | VRAMへの矩形転送、消去、衝突判定を実行するFSM(mkGameFSMから分離) | 335 | 1,406 | 2,073 | 34.8 | 128 | 5.2 | - |
| mkSoundFSM3 | UFO音を演奏するFSM | 261※2 | 1,968 | 215 | 3.6 | 116 | 4.7 | - |
| mkSoundFSM0 | 自弾発射音、自機爆発音、自機増加音を演奏するFSM | 1,821 | 201 | 3.4 | 116 | 4.7 | - | |
| mkSoundFSM2 | インベーダ歩行音を演奏するFSM | 1,823 | 198 | 3.3 | 109 | 4.4 | - | |
| mkSoundFSM1 | インベーダ爆発音を演奏するFSM | 1,796 | 193 | 3.2 | 109 | 4.4 | - | |
| mkGraphicFSM | VGA映像信号生成回路 | 196 | 410 | 54 | 0.9 | 33 | 1.3 | - |
| mkParaSeri | サウンドパラシリ変換、タイミング生成回路 | 77 | 251 | 23※3 | 0.4 | 39※3 | 1.6 | - |
| mkOneStage | ゲームFSMとサウンドFSMの間のバッファ | 62 | 201 | |||||
| mkMixer | 4チャネルのサウンドミキサー | 59 | 133 | |||||
| mkButtons | スイッチとボタンをORする回路 | 36 | 102 | 85※4 | 1.4 | 0 | 0 | - |
| clock | クロック制御回路 | - | 84 | 11※5 | 0.2 | 19※5 | 0.8 | - |
| vram | ビデオRAM | - | 34 | - | - | - | - | 8 |
| sound_rom0 | 自弾発射音、自機爆発音、自機増加音を格納するROM | - | 26※6 | - | - | - | - | 8 |
| sound_rom3 | UFO音を格納するROM | - | - | - | - | - | 8 | |
| sound_rom1 | インベーダ爆発音を格納するROM | - | - | - | - | - | 2 | |
| sound_rom2 | インベーダ歩行音を格納するROM | - | - | - | - | - | 2 | |
| pattern_rom | 自機、インベーダ、UFO、自弾、敵弾、スコア等の図形を格納するROM | - | - | - | - | - | 4 | |
| 合計 | 5,958 | 100 | 2,476 | 100 | 32 | |||
- ※1 bscが生成するVerilogには、
$displayによるエラー・デバッグ表示やルール衝突検知など、合成時に除去されシミュレーションでのみ働く記述が含まれる。合成対象となるのは概ね半分程度で、この行数がそのまま記述の圧縮率を表すわけではない。 - ※2 BSVのソースは1つで、defineにより0~3のバリエーションを生成する。
- ※3 mkParaSeri、mkOneStage、mkMixerはフラット合成で親のsoundモジュールに吸収され計上されるため、階層別レポートに個別の行が立たない。3つの合計を、soundの物量から4つのサウンドFSMを引いた残差として示す。
- ※4 mkButtonsは親のmasterモジュールに吸収されるため、masterの残差として示す。トップ配線のグルーロジックを含む。
- ※5 clockはトップ直下に吸収されるため、トップの残差として示す。
- ※6 ROM(
rom.v)は共通のパラメタ化モジュールで、4つのサウンドROMとパターンROMが同一ソースを共有する。
![]() |
24 |
ClaudeによるGameFSMの改善 (5) |
![]() |
まとめ
同じPCで、bsc 2025.07のコンパイル時間と、Vivado 2026.1で論理合成した結果を、描画のmodule化前後で比べます。前は8/3版、後は教科書版です。論理合成はArty A7のトップからのフラット合成で、階層別レポートからmkGameFSMの値を読みました。ソース行数はGameFSM.bsvとBlitter.bsvの合計、VerilogはmkGameFSM.vとmkBlitter.vの合計です。抑止した警告数は、前がG0036とG0010、後がそれにG0009を加えた数で、8/3版にG0009は出ません。合成対象は前後ともsvnのリビジョンで固定し、生成Verilogのmd5でWindowsへ渡した実物を照合しました。
| 描画モジュール化前後 | 前 | 後 | 比較 | |
|---|---|---|---|---|
| BSV合成 | ソース行数[行] | 1,878 | 1,889 | 0.6%増 |
| コンパイル時間 | 0:51 | 0:29 | ▲44% | |
| 抑止した警告数 | 1,734 | 451 | ▲74% | |
| 生成Verilog行数[行] | 47,117 | 32,845 | ▲30% | |
| Verilog合成 | 合成時間(トップ全体) | 1:10 | 1:11 | ほぼ同じ |
| Vivado LUT数 | 5,352 | 4,977 | ▲7.0% | |
| Vivado FF数 | 1,919 | 1,935 | 0.8%増 | |
表の各行はmkGameFSMとmkBlitterの範囲の値です。ただし合成時間だけはトップ全体の値で、フラット合成ではmkGameFSMの合成時間を単独に取り出せないためです。母数が他の行と異なる点に注意が要ります。
コンパイル時間を決める構造で見ると、前後の差はBlitterのmodule化だけです。前の版は既にRUN_FSMマクロによる切り出しを済ませており、描画の実体をmkFSMで1本に集約した状態です。後の版はその集約したBlitterを、ヒット判定まで含めて独立したmoduleに出しています。
効果はbscのコンパイル時間に集中しています。ソース行数はほぼ同じで、同じ量を書いてコンパイルが半分になりました。生成Verilogが30%減り、抑止した警告が4分の1になったのは、GameFSM側から描画とヒット判定の展開が消えた分です。
物量はLUTが7.0%減、FFが0.8%増です。モジュール化そのものは規則とレジスタの置き場所を変えるだけで論理を変えないので、物量を減らす理由を持ちません。むしろmkBlitterの境界に置いたkickとlaunchの規則、RWire、goレジスタの分だけ増える方向で、FFの0.8%増がそれです。LUTが減ったのは、前後の間に入った他の改変の効果です。2点目の呼び出し形の変更と4点目のFSM切り出しで各FSMの状態が合計418から338に減り、80状態ぶんの状態デコードと遷移条件の論理が消えました。またブリッタの矩形4種のループを1組に統合し、gfx_fsmの47状態が22になって、4種それぞれが持っていたループ制御とカウンタ更新の論理が1組に共有されました。減り方が小さいのは、減ったのが状態機械の制御部分だけで、各状態の中で行う描画や判定やゲームロジックの計算は8/3版と同じだからです。論理合成時間は動きません。
コンパイル時間が縮む機序は、ステート干渉チェックの境界での打ち切りです。同一ソースにあるとき、bscはBlitterの状態とGameFSMの状態を一緒くたに解析し、すべての状態間・ルール間の干渉を調べます。module化すると、Blitter内の解析はモジュール境界で閉じ、GameFSM側から見えなくなります。GameFSMが解析する状態空間が小さくなり、その分の解析時間が消えます。抑止した警告の数が4分の1になったことが、解析対象の縮小を裏付けます。
FFがわずかに増えるのは、module境界のインタフェースレジスタの分です。起動コマンドをRWireに載せて1サイクル後にFSMを起動する形にしたため、引数のラッチと完了・判定の戻りがレジスタとして加わりました。この1サイクルの遅延はmodule化の実行時の代償で、描画1回あたり1サイクルです。
記事#1028の改良は、呼び出しごとに展開されていた描画ループを1個のサブFSMの再利用に変え、コンパイル時間を87%減らしました。記事#1035から記事#1039は、同じRUN_FSMでdrawLivesと、本体の厚いupdateAlienBullet、updatePlayerBullet、initAllを順に切り出し、1分54秒を54秒まで縮めました。
今回の改良は、ブリッタをモジュールの境界で包んだもので、bscのコンパイル時間を同じPCで51秒から29秒に、44%減らしました。3段階とも主眼はbscのコンパイル時間で、物量は付随して動きます。LUTは各段階で減り、記事#1028で18%、記事#1037から記事#1039で3%、今回で7.0%です。FFは記事#1028で0.5%減ったあと、記事#1037から記事#1039で7%、今回で0.8%増えました。seqをFSMに切り出すたびに状態レジスタと引数のラッチが増えるためで、FSM化はFFを増やす方向に働きます。
![]() |
22 |
ClaudeによるGameFSMの改善 (4) |
![]() |
4. 1箇所からしか呼ばれないseqのFSM化
seqをFSMに切り出す第一の理由は再利用で、記事#1035のdrawLivesがそれです。6箇所から呼ばれるseqを1個のFSMにまとめ、コンパイル時間を25%減らしました。記事#1036からは別の狙いで、1箇所からしか呼ばれないseqでも巨大なメインのseqから外せば競合条件の計算量が減るはず、という試みが始まります。記事#1036のdrawTitle1は1度しか呼ばれず本体も薄く、コンパイル時間が1%しか動かなかったので撤回されました。記事#1037のupdateAlienBulletと記事#1038のupdatePlayerBulletは本体が厚く、コンパイル時間がそれぞれ14%と16%、Verilogが11%と19%減って採用され、記事#1039のinitAllも同様に採用されました。つまり効くかどうかは呼び出し回数ではなく本体の厚さで決まる、というところまでが8/3版の時点で分かっていました。
ここではその続きとして、同じ基準で残っていたupdateAliens、updatePlayer、updateSaucerの3つをFSMにしました。本体の厚いseqはメインから切り出す方がbscの負担が軽い、という記事#1037と記事#1038で見えていた傾向に従ったものです。動作はVRAM書き込みの系列がサイクル番号を除いて一致しました。
教科書版の最終形
以上から、教科書版は次の2つの基準で決めました。複数箇所から呼ばれるseqは、共有するためにFSM化します。1個のインスタンスを作り、RUN_FSMマクロで各所から呼びます。メインループから呼ぶ大きなseqは、1箇所からしか呼ばれなくても、メインを小さくするためにFSM化します。仕組みは同じで、動機が違います。前者は規則の複製を消し、後者はメインFSMの状態数を減らします。モジュール境界は、42箇所から呼ばれ自前のレジスタを持つブリッタだけに使います。
| seq | 呼び出し箇所 | 扱い | 置き場所 |
|---|---|---|---|
| copyAreaなど描画プリミティブ6種 | 42 | 共有するためのFSM化 | mkBlitter |
| drawLives | 6 | 共有するためのFSM化 | メイン |
| waitTicks | 4 | 共有するためのFSM化 | メイン |
| initAll | 2 | 共有するためのFSM化 | メイン |
| drawScores | 2 | 共有するためのFSM化 | メイン |
| updateAliens、updatePlayer、updatePlayerBullet、updateAlienBullet、updateSaucer | 各1 | メインを小さくするためのFSM化 | メイン |
| updateBonus、checkClear、erasePlayerBulletなど小さなseq | 1から5 | 展開のまま | メイン |
FSMはmkBlitterの1個とメインの9個で、メインループは各サブシステムのFSMをstartしてdoneを待つだけの短い列になります。教科書版の各FSMの状態数は次のとおりです。
| FSM | 状態数 |
|---|---|
| メイン | 90 |
| updatePlayerBullet_fsm | 54 |
| updateSaucer_fsm | 36 |
| updateAlienBullet_fsm | 33 |
| updatePlayer_fsm | 30 |
| initAll_fsm | 22 |
| blit_fsm、mkBlitterの中 | 22 |
| drawScores_fsm | 18 |
| updateAliens_fsm | 18 |
| drawLives_fsm | 9 |
| wt_fsm | 6 |
| 合計 | 338 |
メインFSMの状態数は8/3版の193から90になりました。FSM全体の合計では、8/3版の418から338です。コンパイル時間と物量の前後は記事#1129の表にまとめます。
![]() |
21 |
ClaudeによるGameFSMの改善 (3) |
![]() |
3. モジュール化
8/3版では、ブリッタが使う作業レジスタと画面メモリ側のレジスタが全部メインモジュールの中で宣言されていました。メインのどの規則からも読み書きできるので、実質はグローバル変数です。
書き換え前、mkGameFSMの中
Reg#(PAddr_t) p_addr <- mkRegU;
Reg#(VAddr_t) v_addr <- mkRegU;
Reg#(Bool) fvwe <- mkReg(False),
fwen <- mkReg(False),
fgunexp <- mkReg(False),
fbullet <- mkReg(False),
fobj <- mkReg(False),
fbase <- mkReg(False),
fempty <- mkReg(False),
fhit_alien <- mkReg(False);
Reg#(SoundCode_t) inscode <- mkRegU;
Reg#(Pattern_t) vd <- mkRegU;
Wire#(Pattern_t) vdata <- mkWire,
romdata <- mkWire;
Wire#(UInt#(3)) dipsw <- mkWire;
Reg#(UInt#(9)) xs <- mkRegU,
ys <- mkRegU,
xd <- mkRegU,
yd <- mkRegU;
method Action pdin(Pattern_t in_romdata);
romdata <= in_romdata;
endmethod
method Action vdin(Pattern_t in_vdata);
vdata <= in_vdata;
endmethod
method PAddr_t prom_addr();
return p_addr;
endmethod
method VAddr_t vaddr();
return v_addr;
endmethod
method Bool vwe();
return fvwe;
endmethod
method Pattern_t vdout();
return vd;
endmethod
書き換え後、Blitter.bsvのmkBlitterの中
// MemPort: 画面メモリ側の面
interface MemPort;
(* always_ready *) method PAddr_t prom_addr;
(* always_ready *) method VAddr_t vaddr;
(* always_ready *) method Bool vwe;
(* always_ready *) method Pattern_t vdout;
(* always_enabled *) method Action pdin(Pattern_t d);
(* always_enabled *) method Action vdin(Pattern_t d);
endinterface
interface Blitter_ifc;
interface BlitPort port;
interface MemPort mem;
endinterface
(* synthesize *)
module mkBlitter(Blitter_ifc);
Reg#(PAddr_t) p_addr <- mkRegU;
Reg#(VAddr_t) v_addr <- mkRegU;
Reg#(Bool) fvwe <- mkReg(False),
fobj <- mkReg(False),
fbase <- mkReg(False);
Reg#(Pattern_t) vd <- mkRegU;
Wire#(Pattern_t) vdata <- mkWire,
romdata <- mkWire;
Reg#(UInt#(9)) xs <- mkRegU,
ys <- mkRegU,
xd <- mkRegU,
yd <- mkRegU;
interface MemPort mem;
method PAddr_t prom_addr = p_addr;
method VAddr_t vaddr = v_addr;
method Bool vwe = fvwe;
method Pattern_t vdout = vd;
method Action pdin(Pattern_t d); romdata <= d; endmethod
method Action vdin(Pattern_t d); vdata <= d; endmethod
endinterface
書き換え後、mkGameFSMの中
Blitter_ifc blt <- mkBlitter;
method Action pdin(Pattern_t in_romdata);
blt.mem.pdin(in_romdata);
endmethod
method Action vdin(Pattern_t in_vdata);
blt.mem.vdin(in_vdata);
endmethod
method PAddr_t prom_addr();
return blt.mem.prom_addr;
endmethod
method VAddr_t vaddr();
return blt.mem.vaddr;
endmethod
method Bool vwe();
return blt.mem.vwe;
endmethod
method Pattern_t vdout();
return blt.mem.vdout;
endmethod
レジスタの宣言は一字も変わらず、置き場所だけがmkBlitterに移りました。名前はインタフェースに現れないので、メインからは触れません。これがBSVにおけるローカル変数の実体で、関数の中で実行時の状態を持つ手段が無いため、状態の局所化はモジュールの単位で行います。mkBlitterに入れたのは、VRAMとPROMのアドレスを出してデータを読み書きする部分です。矩形転送の本体と、自弾や敵弾の進路のVRAMを読むヒット判定がそれで、MemPortに出ているprom_addr、vaddr、vwe、vdout、pdin、vdinを駆動する機構がここにあります。drawLivesやdrawScoresは、copyAreaやeraseAreaを呼ぶだけで自分ではアドレスを出さないので、ゲームロジックとしてメインに残しています。
ここで、2点目のRUN_FSMマクロによる切り出しと、3点目のモジュール化の違いを押さえておきます。マクロで包むと、ブリッタの中身は人間から見えなくなります。人間はFSMが1個ずつしか動かないことを知っているので、呼び出し側の完了待ちだけを見れば中身は気にしなくてよい、と判断できます。ところがbscはその仮定を知りません。マクロは展開されてメインFSMの文に戻り、gfx_fsmも作業レジスタもmkGameFSMの中にあるままなので、bscは全部の規則の間で読み書きの順序を総当たりで調べます。マクロで切り出すとインライン展開が消えて規則の総数は減りますが、残った規則どうしの総当たりは続きます。8/3版で警告が1,734件出ていたのはそのためです。
モジュール化は、この総当たりの範囲を境界で切ります。mkBlitterに移した規則とレジスタは、mkGameFSMをコンパイルするときの解析対象から外れ、bscが見るのはblt.portのstartとdoneだけになります。総当たりの計算量は規則の数の2乗のオーダーで増えるので、規則の総数が同じでも、範囲を2つに分ければ組の数は大きく減ります。マクロ化は規則の数を減らし、モジュール化は総当たりの範囲を切る、と対比できます。人間から見えなくするのがマクロ化、コンパイラから見えなくするのがモジュール化です。
物量への効果は、この段階だけを取り出すと2点目に書いた状態数の削減と同じもので、モジュールの境界そのものはLUTを増やしも減らしもしません。モジュール化が効いたのは別の2点です。
一つはコンパイル時間で、bscのスケジューラはメインの規則とブリッタの規則の組を調べなくなり、mkBlitterは独立に数秒でコンパイルされます。抑止した警告の数がそれを裏付けます。8/3版はG0036とG0010で1,734件でした。教科書版ではG0036が127件、G0010が318件に減ります。加えて教科書版ではG0009が出ます。別のFSMの状態どうしが同じレジスタを読み書きして順序の循環ができ、bscがその組をconflictにして循環を切った、という警告で、FSMは1個ずつしか動かないので実害はありません。G0036、G0010と同じ根拠で抑止します。教科書版ではこの循環を順に解消し、G0009は6件です。合計は1,734件から451件になりました。コンパイル時間の前後は記事#1129の表にまとめます。
もう一つは、ブリッタが独立したことで内部の整理がしやすくなったことで、矩形4種のループを1組に統合しました。画素ごとの書き込み値だけが違うので、その部分だけをcaseに閉じ込めました。
action
case (b.op)
BLIT_COPY: vd <= romdata;
BLIT_OR: vd <= vdata | romdata;
BLIT_ANDN: vd <= vdata & ~romdata;
BLIT_ERASE: vd <= 0;
endcase
xs <= xs + 1;
xd <= xd + 1;
endaction
ブリッタ部分の規則数は8/3版の37から教科書版の14に減りました。規則数は、生成Verilogの中でWILL_FIRE_RLの信号として現れる規則の数です。8/3版はmkGameFSMの中のgfx_fsmに属する規則、教科書版はmkBlitter全体の規則を数えました。
ページ:



