Posts Issued in September, 2025

GameFSMの改良 (11)

posted by sakurai on September 29, 2025 #1031

コール順位4位のsound関数は元々サウンドFSMを構成しており、それを起動するだけなので、このままでOKです。

最後にコール順位6位のketa関数をリファクタします。これは元は10進4桁の数値を表示する関数であり、一桁ずつ10^nを引いて商を求めるものでした。従ってStmtを用いてループを構成しており、汎用FSM起動マクロでもうまく行きましたが、一方で内部変数への競合の回避が困難でした。

ChatGPTに相談したところ、組み合わせ回路で可能とのことで全面リファクタしたものです。

  // 14bit → BCD4(加算+比較のみ、組合せ)
   function Tuple4#(UInt#(4), UInt#(4), UInt#(4), UInt#(4)) dec4_dd (UInt#(14) x);
     Bit#(16) b16 = 0;  // [15:12]=千, [11:8]=百, [7:4]=十, [3:0]=一
     Bit#(14) bx  = pack(x);           // ★ UInt を Bit に変換してからビット参照
     for (Integer i = 13; i >= 0; i = i - 1) begin
       // 各桁 >=5 なら +3
       for (Integer n = 0; n < 4; n = n + 1) begin
         Bit#(4) nib = b16[n*4 + 3 : n*4];
         if (nib >= 5) nib = nib + 3;
         b16[n*4 + 3 : n*4] = nib;
       end
       // 左シフトして次ビットを流し込む
       b16 = { b16[14:0], bx[i] };
     end
     UInt#(4) d0 = unpack(b16[3:0]);
     UInt#(4) d1 = unpack(b16[7:4]);
     UInt#(4) d2 = unpack(b16[11:8]);
     UInt#(4) d3 = unpack(b16[15:12]);
     return tuple4(d3, d2, d1, d0);
   endfunction

よく見ると、dec4_dd関数には=は使われていますが、<=(レジスタ代入)は使われていません。これはdec4_ddには見かけはforループがあるものの、全てstaticに展開され、全体として組み合わせ回路になっていることがわかります。

最後にこれを用いてscoreとhigh_scoreを画面の特定の場所に表示する関数です。

   // Draw Score and High-score
   function Stmt scores;
      match { .s3, .s2, .s1, .s0 } = dec4_dd(score);
      match { .h3, .h2, .h1, .h0 } = dec4_dd(high_score);
      return (seq
         // display score
         if (score > high_score) high_score <= score;
         copyArea(zeroExtend(s3) << 3, 169, 38      , 24, 8, 8);
         copyArea(zeroExtend(s2) << 3, 169, 38 +  8 , 24, 8, 8);
         copyArea(zeroExtend(s1) << 3, 169, 38 + 16 , 24, 8, 8);
         copyArea(zeroExtend(s0) << 3, 169, 38 + 24 , 24, 8, 8);
         copyArea(zeroExtend(h3) << 3, 177, 110      , 24, 8, 8);
         copyArea(zeroExtend(h2) << 3, 177, 110 +  8 , 24, 8, 8);
         copyArea(zeroExtend(h1) << 3, 177, 110 + 16 , 24, 8, 8);
         copyArea(zeroExtend(h0) << 3, 177, 110 + 24 , 24, 8, 8);
      endseq);
   endfunction

このようにバイナリを4桁BCD化する関数を組合わせ回路にしたところ、ワンサイクルなので回路は増加するかと思いきや、以下のように若干減少する結果になりました。組合せ回路は多少増えても10^nを引くループが8回削減された効果が大きいようです。

BCD4桁表示を最適化前後 比較
BSV合成 コンパイル時間 2:08 2:04 ▲3.1%
Verilog合成 ファイルサイズ[KB] 8,396 8,101 ▲3.5%
合成時間 1:01 0:54 ▲11.5%
Vivado LUT数 5,901 5,565 ▲5.7%
Vivado FF数 1,812 1,766 ▲2.5%


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

GameFSMの改良 (10)

posted by sakurai on September 26, 2025 #1030

前記事でコール順位3位のwait_timerを最適化します。

元のwait_timerは単純に60Hzの外部信号ticに同期して動作する関数です。これをまずwait_timer_orgにリネームします。

   // 時間待ち
   function Stmt wait_timer_org(
      UInt#(12) count
   );
      return (seq
         repeat(pack(extend(count))) seq
         await(tic == 0 || (foa && fbutton));
         await((tic == 1 && sreq == 0) || (foa && fbutton));
         endseq
      endseq);
   endfunction

これに対して、FSM wt_fsmを作成し、さらにそれを起動する汎用マクロを用いたタイマFSM起動関数を元の名前で作成します。

  // ラッパ用:引数をラッチするだけ(Reg は1本)
  Reg#(UInt#(12)) wt_count <- mkReg(0);
  // 本体は org を mkFSM 化(count は Reg 値を読ませる)
  FSM wt_fsm <- mkFSM( wait_timer_org(wt_count) );

  // 同名の薄ラッパ:値ラッチ → 1サイクル待つ → start → done 待ち
  function Stmt wait_timer (UInt#(12) count);
    return (seq
      action wt_count <= count; endaction
      `RUN_FSM(wt_fsm)
    endseq);
  endfunction

このようにすれば、wait_timerを読んでもStmtがほとんど増加せずにFSMに起動をかけ、終了を待つだけになります。本体のStmtとしてはやや小さいものの、前ページのように16回呼ばれているため多少の効果はあるようです。

この関数だけのFSM化効果を調べたら以下のようにverilog量(byte数)で10.6%の削減となったので、そこそこの効果はありました。

wait_timerを最適化前後 比較
BSV合成 コンパイル時間 2:36 2:10 ▲16.7%
Verilog合成 ファイルサイズ[KB] 9,394 8,395 ▲10.6%
合成時間 0:51 0:51 0.0%
Vivado LUT数 5,992 5,901 ▲1.5%
Vivado FF数 1,871 1,812 ▲3.2%


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

GameFSMの改良 (9)

posted by sakurai on September 25, 2025 #1029

GameFSM中で2回以上staticに呼ばれているsystem functionではない関数とそのstaticな回数です。

順位関数名回数備考
1copyArea31基本関数でありblitに集約
2eraseArea17基本関数でありblitに集約
3wait_timer16
4sound11
5copyGlyph6copyAreaを呼んでいるだけ
6keta6
7eraseBullet4eraseAreaを呼んでいるだけ
8eraseInvBullet3eraseAreaを呼んでいるだけ
9eraseAreaSP 2基本関数でありblitに集約
10explodeBullet2orAreaを呼んでいるだけ
11explodeInvBullet2eraseAreaを呼んでいるだけ
12orArea2基本関数でありblitに集約

前回まででVRAMアクセス関数をFSM化したため、色付けされている、69回も展開されているこれらの関数が全てFSMの起動-終了待ちとなりました。

次に比較的多数回呼ばれている時間待ち関数wait_timer()をFSM化します。


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

GameFSMの改良 (8)

posted by sakurai on September 24, 2025 #1028

従来のcopyAreaをStmtで記述して呼び出す方式(変更前)と今回engine方式によりメインではFSMを起動する方式の2つのbsvとverilogの結果比較です。

画面アクセス改良前後 比較
BSV合成 ソースサイズ[byte] 52,138 60,756 17%増
コンパイル時間 16:27 2:10 ▲87%
Verilog合成 ファイルサイズ[KB] 29,156 8,398 ▲71%
合成時間 1:47 1:00 ▲44%
Vivado LUT数 7,212 5,901 ▲18%
Vivado FF数 1,821 1,812 ▲0.5%

この比較で見るように、従来Stmtが多数展開され、スケジューリング爆発を起こしていたシーケンスが軽くなり、コンパイル時間と生成verilog量が激減しました。FPGAの合成時間と物量も多少減少しました。

BSVソースはスイッチによりオリジナルと小FSM方式を出力し分けるように両方のコードが入っているため却って増大しています。

なお、前記事のfunction _BLIT()においてnoActionをコメントにしている場合の数値であり、その代わりに変数競合の警告がでます。実際には問題ないので警告offにしていますが、警告を消す場合はこのnoActionを入れる必要があります。ただしその分コンパイル時間、物量共増大します。


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

GameFSMの改良 (7)

posted by sakurai on September 23, 2025 #1027

最後にcopyArea()等の従来関数を引数等の順番や個数を変えずに薄いラッパーで定義します。

   // 同名ラッパは _BLIT を呼ぶだけに
  function Stmt copyArea(U8 sx,U8 sy,U8 dx,U8 dy,U8 w,U8 h);
    return _BLIT(Blit{op:BLIT_COPY, sx:sx, sy:sy, dx:dx, dy:dy, w:w, h:h});
  endfunction
  function Stmt orArea(U8 sx,U8 sy,U8 dx,U8 dy,U8 w,U8 h);
    return _BLIT(Blit{op:BLIT_OR, sx:sx, sy:sy, dx:dx, dy:dy, w:w, h:h});
  endfunction
  function Stmt eraseArea(U8 dx,U8 dy,U8 w,U8 h);
    return _BLIT(Blit{op:BLIT_ERASE, sx:0, sy:0, dx:dx, dy:dy, w:w, h:h});
  endfunction
  function Stmt eraseAreaSP(U8 sx,U8 sy,U8 dx,U8 dy,U8 w,U8 h);
    return _BLIT(Blit{op:BLIT_ANDN, sx:sx, sy:sy, dx:dx, dy:dy, w:w, h:h});
  endfunction
  function Stmt getVdots(U8 x, U8 y);
      return _BLIT(Blit{ op:BLIT_VDOTS, sx:0, sy:0, dx:x, dy:y, w:0, h:0});
  endfunction
  function Stmt getHdots(U8 x, U8 y);
      return _BLIT(Blit{ op:BLIT_HDOTS, sx:0, sy:0, dx:x, dy:y, w:0, h:0});
  endfunction

以上のように、copyArea()等の6種のVRAMアクセス関数は全てFSMを起動し、終了を待つだけの関数に書き換えます。


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

GameFSMの改良 (6)

posted by sakurai on September 22, 2025 #1026

元はcopyArea()等の4種の関数でしたが、それを統合して内部で振り分けます。これはVRAMへのsingle writer化が目的です。single writer化すると警告が消えるだけでなく、競合条件の数が減るため競合チェック時間も短くなります。

またこれら4種の関数とは別にVRAMアクセスのためのコードも含めています。これもsingle writer化のためです。

  // 元の Stmt を呼び分ける統合 blit(本文は従来の関数をそのまま呼ぶだけ)
  function Stmt blit(Blit b);
    return (seq
      if (b.op == BLIT_COPY)
        copyArea_org(b.sx, b.sy, b.dx, b.dy, b.w, b.h);
      else if (b.op == BLIT_OR)
        orArea_org(b.sx, b.sy, b.dx, b.dy, b.w, b.h);
      else if (b.op == BLIT_ERASE)
        eraseArea_org(b.dx, b.dy, b.w, b.h);
      else if (b.op == BLIT_ANDN)
        eraseAreaSP_org(b.sx, b.sy, b.dx, b.dy, b.w, b.h);
      else if (b.op == BLIT_VDOTS)
        getVdots_org(b.dx, b.dy);
      else // BLIT_HDOTS
        getHdots_org(b.dx, b.dy);
    endseq);
  endfunction

次にコマンド変数とともにFSMを作成します。

  // mkFSM
  Reg#(Blit) blitArg <- mkRegU;
  FSM gfx <- mkFSM( blit(blitArg) ); // FSMの作成

このFSMを起動するRUN_FSMコマンドは汎用に作成されています。fsmを渡すと起動、終了待ちを実行します。

  // FSM起動コマンドの定義
  `define RUN_FSM(F)  seq \
    action F.start(); endaction \
    await(F.done); \
  endseq

  // 起動・完了待ちの薄ラッパ
  function Stmt _BLIT(Blit b);
    return (seq
//      noAction; // read/write競合警告を止めるため
      action blitArg <= b; endaction
      `RUN_FSM(gfx_fsm) // gfx_fsmの起動
    endseq);
  endfunction

_BLIT()はFSMを起動し、終了を待つだけの関数です。


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

GameFSMの改良 (5)

posted by sakurai on September 19, 2025 #1025

調査の結果、copyAreaがstaticに31回呼ばれ(関連関数だと69回)、それが状態爆発を起こしていることが分かってきました。数年前に開発した時は同じソースで、bsvコンパイルが1時間もかかっていましたが、PCの更新と合わせてbscもアップデートされたことから、現在は16分程度になっています。かなり時間がかかるのはこの巨大なFSMの構築のためです。

これもChatGPT5に相談したところ、メインのStmtからcopyAreaのStmtを31回呼ぶと巨大なFSMのスケジューリングが必要になり時間がかかるとのことでした。対処法は画面アクセスする関数を小FSM化しておき、メインからは起動コマンドを発行するだけにします。

まず、blitEngine()が動作するコマンド種別を定義します。

typedef UInt#(8) U8;

typedef enum { BLIT_COPY, BLIT_OR, BLIT_ERASE, BLIT_ANDN }
  BlitOp deriving (Bits, Eq);

typedef struct {
  BlitOp op;
  U8 sx; U8 sy;   // ソース(prom)のx, yアドレス
  U8 dx; U8 dy;   // デスト(vram)のx, yアドレス
  U8 w; U8 h;      // 図形の幅と高さ
} Blit deriving (Bits, Eq);

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

GameFSMの改良 (4)

posted by sakurai on September 18, 2025 #1024

2つのbsvとverilogの結果比較です。

ストリング改良前後
BSV合成 ソース行数[byte数] 50,564 52,456
コンパイル時間$\dagger$ 2:29 2:29
Verilog合成 ファイルサイズ[KB] 9,707 9,716
合成時間 0:56 0:59
Vivado LUT数 6,001 6,001
Vivado FF数 1,900 1,900

$\dagger$ソースはゲーム部分をカットして文字列表示のみとしている。

この比較で見るように、ストリングパラメータを文字毎に持つ方式にしても、BSV量、Verilog量、FPGA物量共にほとんど変化はありませんでした。


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

GameFSMの改良 (3)

posted by sakurai on September 17, 2025 #1023

これはChatGPT5に作成して貰ったものですが、s1_bitsをunpackすると逆順になるためreverseしているとのことです。いずれにしろ、Cのような2次元の初期値定義は結局できないようなので、関数を使ったトリッキーな方法で行いました。

初期化部分:

typedef UInt#(8) U8;
typedef struct {
  U8 sx; U8 sy;
  U8 dx; U8 dy;
  U8 w; U8 h;
} Glyph deriving (Bits, Eq, FShow);

   // 位置指定で埋められるコンストラクタ関数
   function Glyph mkGlyph(U8 sx, U8 sy, U8 dx, U8 dy, U8 w, U8 h);
     return Glyph { sx:sx, sy:sy, dx:dx, dy:dy, w:w, h:h };
   endfunction

   // pack ∘ mkGlyph のラッパ(短い別名)
   function Bit#(SizeOf#(Glyph)) pg(U8 sx, U8 sy, U8 dx, U8 dy, U8 w, U8 h);
     return pack(mkGlyph(sx, sy, dx, dy, w, h));
   endfunction

   // s1: "PLA^SPACE__INVADERS"
   Bit#(TMul#(19, SizeOf#(Glyph))) s1_bits = {
     pg( 42,137,112, 68, 5, 7), // P
     pg( 50,137,120, 68, 5, 7), // L
     pg( 58,137,128, 68, 5, 7), // A
     pg( 77,126,136, 68, 5, 7), // ^
     pg( 66,137, 72, 93, 5, 7), // S
     pg( 42,137, 80, 93, 5, 7), // P
     pg( 58,137, 88, 93, 5, 7), // A
     pg( 74,137, 96, 93, 5, 7), // C
     pg( 82,137,104, 93, 5, 7), // E
     pg( 42,129,112, 93, 5, 7), // _
     pg( 42,129,120, 93, 5, 7), // _
     pg( 90,137,128, 93, 5, 7), // I
     pg( 98,137,136, 93, 5, 7), // N
     pg(106,137,144, 93, 5, 7), // V
     pg( 58,137,152, 93, 5, 7), // A
     pg(114,137,160, 93, 5, 7), // D
     pg( 82,137,168, 93, 5, 7), // E
     pg(122,137,176, 93, 5, 7), // R
     pg( 66,137,184, 93, 5, 7)  // S
   };
   Vector#(19, Glyph) s1 = reverse(unpack(s1_bits));

描画部分:

  function Stmt stringS1; // PLAY SPACE INVADERS
      return (seq
         for (str_idx <= 0; str_idx < 19; str_idx <=  str_idx + 1) seq
            copyGlyph(s1[str_idx]);
            wait_timer(`TICK_WAIT8);
            if (fbutton) break;
         endseq // for
      endseq);
   endfunction

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

GameFSMの改良 (2)

posted by sakurai on September 15, 2025 #1022

まずString配列があまり美しくなかったので改良します。

以下にオリジナルのソースの部分を示します。本来はCと同様にstructの配列を構成したかったのですが、初期値の設定がうまくいかなかったので、しかたなく要素を分解し、以下のようにsx, sy, dx, dy, w, hの6要素のそれぞれの配列としました。sxはソースのx座標、syはソースのu座標、dxはデストのx座標、dyはデストのu座標、wは幅、hは高さを示します。

初期化部分:

 //                     P   L   A   ^   S   P   A   C   E  --  --   I   N   V   A   D   E   R   S
   UInt#(8) s1sx[19] = { 42, 50, 58, 77, 66, 42, 58, 74, 82, 42, 42, 90, 98,106, 58,114, 82,122, 66},
            s1sy[19] = {137,137,137,126,137,137,137,137,137,129,129,137,137,137,137,137,137,137,137},
            s1dx[19] = {112,120,128,136, 72, 80, 88, 96,104,112,120,128,136,144,152,160,168,176,184},
        s1dy[19] = { 68, 68, 68, 68, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93},
        s1w[19]  = {  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5,  5},
        s1h[19]  = {  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7};

描画部分:

  function Stmt stringS1; // PLAY SPACE INVADERS
      return (seq
         for (str_idx <= 0; str_idx < 19; str_idx <=  str_idx + 1) seq
            copyArea(s1sx[str_idx], s1sy[str_idx], s1dx[str_idx], s1dy[str_idx], s1w[str_idx], s1h[str_idx]);
            wait_timer(`TICK_WAIT8);
            if (fbutton) break;
         endseq // for
      endseq);
   endfunction

これをChatGPTの助けを借りながら初期値付きの2次元配列にしようと思います。


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


ページ: