Posts Issued on September 21, 2026

posted by sakurai on September 21, 2026 #1126

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全体の規則を数えました。


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