Posts Tagged with "BSV"

既に発行済みのブログであっても適宜修正・追加することがあります。
We may make changes and additions to blogs already published.

GameFSMの改良 (21)

posted by sakurai on December 4, 2025 #1046

夜空に瞬く星をシミュレーションする星の点滅プログラムです。

Advent Calenderで発表の都合上その後に公開します。 $$\img[-1.35em]{/images/withinseminar.png}$$

ファイルstars_xy.vhは、サポートプログラムにより元画像から白点の位置を抜き出したデータであり、以下のようなものです。コメントにもありますが、 $$(x, y, P, \phi)$$ の形式です。これをStringでChatGPTに改善してもらったように{ps1, ps2, ...., psn}と並べます。 packed star形式としてps関数を用いて以下のように初期化し、

// loaded 76 stars from stars_xy.csv
// ps(x, y, P, phi) 形式
  ps(  0,   0, 64, 19),
  ps(204,   1, 16,  7),
  ps( 63,   2, 16, 11),
:
  ps( 33, 251, 32,  8),
  ps(165, 254,  8,  0),
  ps(232, 254, 64, 33)

使う場合はあらかじめunpackしてreverseしたテーブルのほうを引いて使います。

ChatGPT 5.1に各種サポートプログラムをpythonで作成してもらった他、bsvについて 9割方はChatによるもので ユーザはデバッグ係となっていました。

ChatGPT 5.1は慣れないbsvこそ時々文法誤りコードを吐くことがあるものの、pythonは完璧な出来映えで一度もバグを入れることはありませんでした。pythonの方が得意な印象ですが、世の中に有るコードベースを考えたら無理もありません。

これ程pythonが得意なら。今後もう人間がpythonを書くことは無くなり、pythonは機械語のような位置付けになるかもしれません。昔は機械語のバイナリを覚えたものですが(C3,00,80,...)それと同じことになりそうです。

幸いbsvはまだそこまで得意ではないので、まだ人間が書く楽しみが残されています。


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

GameFSMの改良 (20)

posted by sakurai on December 3, 2025 #1045

次に夜空の星の点滅プログラムです。8bitレトロマシンとは思えないくらいの立体感のある映像になりました。もともと立体感のある画像だからなのですが。

例によって星の点滅をChatGPTと相談したら3案提示されたのですが、周期と位相を適当にずらす案を採用し、そのコードを書きました。

案3:座標から「周期と位相」を決めるだけの簡単版

擬似乱数すら使わず、各星に違う周期と位相を与えるだけでも、それなりに「ランダムっぽく」見えます。

  1. フレームカウンタ$t$を用意。

  2. 星の座標$(x,y)$から、その星固有の周期$P$と位相$\phi$を決める。 例: $$ P = 8 + ((x + 2y) \bmod 5) \quad (8 \sim 12; \text{フレーム周期}) $$ $$ \phi = (3x + y) \bmod P $$

  3. 星は次の条件で ON にする。 $$ \text{star_on} = 1 \iff ((t + \phi) \bmod P) < \frac{P}{2} $$

  • つまり各星は周期$P$で点滅し、そのうち半分だけ点灯。
  • 星ごとに$P$と$\phi$が違うため、全体としてはかなりバラバラに光って見える。

ハード実装としては

  • $P$を事前に ROM に入れておくか、座標から組み合わせ回路で計算、
  • Pを2の冪にすれば、$\bmod P$の演算も不要

Advent Calenderで発表の都合上その後に公開します。 $$\img[-1.35em]{/images/withinseminar.png}$$

図1045.1 オープニング画面 星空

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

GameFSMの改良 (19)

posted by sakurai on December 1, 2025 #1044

ブリッジのコードに怪しいところがあるので、修正中です。さて、オープニング画面を追加したいと思い、bsvコードを書きました。8bit(?)レトロマシンとは思えないくらいの立体感のある映像になりました。

まず画像のロード関数です。元画像はここにあった解像度の高いものを256x256に縮め、さらに色数を4色に落としました。単純にパターンROMに画像を置いてVRAMに転送するだけではあまり面白くないので、RLE (run length encoding)を行いました。

元画像はpythonでRLEしましたが、そのデコーダ部のbsvコードを示します。

// PROM アドレス(1 nibble 単位)
Reg#(PAddr_t)   rleAddr      <- mkReg(0);

// 展開先の相対座標 (0..255, 0..255)
Reg#(U8)        rleX         <- mkReg(0);
Reg#(U8)        rleY         <- mkReg(0);

// 残りピクセル数 256 * 256
Reg#(UInt#(16)) pixelsLeft   <- mkReg(0);

// 現在のランの残り長さ (1..256)
Reg#(UInt#(9))  runLen       <- mkReg(0);

// 色と長さ nibble
Reg#(Pattern_t) rleColor     <- mkReg(0);
Reg#(Pattern_t) rleLenHiNib  <- mkReg(0);
Reg#(Pattern_t) rleLenLoNib  <- mkReg(0);

// PROM から nibble(Pattern_t) を1つ読み取り、dst に入れ、rleAddr を 1 進める
function Stmt rleNextNibble(Reg#(Pattern_t) dst);
  return (seq
    action
      // PROM アドレスをセット
      p_addr <= rleAddr;
    endaction
    // read timing 調整が必要ならここに noAction を挟む
    noAction;
    action
      // nibble を取り込み
      dst     <= romdata;      // romdata : Pattern_t
      rleAddr <= rleAddr + 1;  // 次の nibble へ
    endaction
  endseq);
endfunction

// PROM 上で RLE が始まるニブルアドレス(行256の先頭なら 128*256)
`define RLE_START_ADDR 128*256
`define VRAM_WIDTH     256
`define VRAM_HEIGHT    256
   
function Stmt rleDecode_org();
  return (seq
    //------------------------------------------------
    // 初期化
    //------------------------------------------------
    action
      // RLE データは PROM の 256 行目から始まる前提
      rleAddr    <= fromInteger(`RLE_START_ADDR);
      // 出力先相対座標
      rleX       <= 0;
      rleY       <= 0;
      pixelsLeft <= fromInteger(`VRAM_WIDTH * `VRAM_HEIGHT -1);
      runLen     <= 0;
    endaction
    //------------------------------------------------
    // 全ピクセルを描き終えるまでループ
    //------------------------------------------------
    while (pixelsLeft != 0) seq
      // --- 新しいランを読み込む必要があるなら ---
      if (runLen == 0) seq
        // color
        rleNextNibble(rleColor);
        // len_hi
        rleNextNibble(rleLenHiNib);
        // len_lo
        rleNextNibble(rleLenLoNib);
        action
          UInt#(4) hi = unpack(rleLenHiNib);
          UInt#(4) lo = unpack(rleLenLoNib);
          // ラン長 L = 16*hi + lo + 1
          runLen <= extend(unpack({pack(hi), pack(lo)})) + 1;
        endaction
      endseq
      // --- このランから 1 ピクセル描画 ---
      setDot(rleX, rleY, rleColor);
      action
        pixelsLeft <= pixelsLeft - 1;
        runLen     <= runLen     - 1;
        // 256 幅で折り返し
        if (rleX == 8'd255) begin
          rleX <= 0;
          rleY <= rleY + 1;
        end
        else begin
          rleX <= rleX + 1;
        end
      endaction
    endseq   // while (pixelsLeft != 0)
  endseq);
endfunction
FSM rle_fsm <- mkFSM(rleDecode());

// rleDecodeを起動するラッパ
function Stmt rleDecode();
  return (seq
    `RUN_FSM(rle_fsm)
  endseq);
endfunction

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

GameFSMの改良 (18)

posted by sakurai on November 21, 2025 #1043

過去記事の続きです。GameFSM(ゲームシナリオ)とSoundFSM(サウンドプレーヤ)の間をOneStageというセマフォで接続していて、それを最適化(除去)しようとしたところ、ChatGPTにCDCを考慮していないと怒られてしまいました。「なら作って」と言って同期化ブリッジを作ってもらいました。実際にはやり直しが何度もありましたが。

マルチクロック設計で、2つの非同期クロックドメインにまたがる1段の同期化FIFOを用いています。

図%%.1
図1043.1 同期化FIFO

作ってもらったコードを示します。

GSBridge.bsv:

package GSBridge;

import Clocks::*;

typedef Bit#(4) SoundCode_t;

// Game 側 IF:busy を見て !busy のときだけ setReq する前提
interface GSBridgeGameIfc;
  method Action setCode(SoundCode_t code);   // このサイクルのコード値
  method Action setReq (Bool fire);          // このサイクルで発行するなら True
//  method Bool   busy;                        // バッファ占有中なら True
endinterface

// Sound 側 IF:valid が立ったサイクルで code を取り込む
interface GSBridgeSoundIfc;
  method SoundCode_t code;    // 取り込まれたコマンド値
  method Bool        valid;   // 新コマンド到着 1 サイクルパルス
endinterface

interface GSBridgeIfc;
  interface GSBridgeGameIfc  game;
  interface GSBridgeSoundIfc sound;
endinterface
(* synthesize, always_ready, always_enabled, no_default_clock, no_default_reset *)
module mkGSBridge#(Clock gameClk, Reset gameRst,
                   Clock sndClk)
                  (GSBridgeIfc);

  // gameRst を sndClk ドメインに同期させたリセット
  Reset sndRst <- mkSyncReset(2, gameRst, sndClk);

  // Game→Sound の 4bit コマンド用 Sync FIFO(深さ1)
  SyncFIFOIfc#(SoundCode_t) fifo
    <- mkSyncFIFO(1, gameClk, gameRst, sndClk);

  // Game ドメイン側入力(必ず gameClk/gameRst にぶら下げる)
  Wire#(SoundCode_t) w_code <- mkWire(clocked_by gameClk, reset_by gameRst);
  Wire#(Bool)        w_req  <- mkWire(clocked_by gameClk, reset_by gameRst);

  // Sound ドメイン側の出力レジスタ(sndClk/sndRst ドメイン)
  Reg#(SoundCode_t) r_code  <- mkRegU   (clocked_by sndClk, reset_by sndRst);
  Reg#(Bool)        r_valid <- mkReg(False,           clocked_by sndClk, reset_by sndRst);

  //-------------------------
  // Game ドメイン: fire かつ FIFO に空きがあるときだけ enq
  //-------------------------
  rule rl_enq (w_req && fifo.notFull);
    fifo.enq(w_code);
  endrule

  //-------------------------
  // Sound ドメイン: FIFO から 1 件取り出して r_code にラッチ
  //                 valid を 1 サイクルだけ立てる
  //-------------------------
  rule rl_deq (fifo.notEmpty && !r_valid);
    r_code  <= fifo.first;
    fifo.deq;
    r_valid <= True;
  endrule

  rule rl_clear (r_valid);
    r_valid <= False;
  endrule

  //-------------------------
  // Game 側サブインタフェース実装
  //-------------------------
  interface GSBridgeGameIfc game;

    // このサイクルのコード値を保持
    method Action setCode(SoundCode_t code);
      w_code <= code;
    endmethod

    // このサイクルで発行するなら fire=True
    method Action setReq(Bool fire);
      w_req <= fire;
    endmethod

    // ★ busy は「未消費のコマンドが FIFO にあるかどうか」
    //    = 元の 1bit セマフォと等価
//    method Bool busy;
//      return fifo.notEmpty;
//    endmethod

  endinterface

  //-------------------------
  // Sound 側サブインタフェース実装
  //-------------------------
  interface GSBridgeSoundIfc sound;

    // 直近に取り込んだコマンド値(valid が 1 のサイクルに有効)
    method SoundCode_t code;
      return r_code;
    endmethod

    // 新コマンドが届いたサイクルだけ 1 になるパルス
    method Bool valid;
      return r_valid;
    endmethod

  endinterface

endmodule

endpackage

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

GameFSMの改良 (17)

posted by sakurai on October 24, 2025 #1039

3番目にボディが厚い関数の最適化をトライします。対象はinitAll()(毎回のステージでの初期化関数)で、45行あります。

初期化関数(initAll())の最適化前後 比較
BSV合成 コンパイル時間 1'03'' 0'54'' ▲14.3%
Verilog合成 ファイルサイズ[KB] 4,283 3,554 ▲17.0%
合成時間 0'58'' 0'51'' ▲12.1%
Vivado LUT数 5,582 5,489 ▲1.7%
Vivado FF数 1,907 1,907 0.0%

結果としては、FSMオーバヘッドはほぼ0でした。一方、ボディがやや厚かったため、bscコンパイル時間とverilog量の削減となりました。物量も削減されているのは2度呼ばれているためかもしれません。bscコンパイル時間に効果があるため、この最適化を採用します。

かつて1時間以上かかっていたbscコンパイルがPCやbscの更新により16分になり、さらに今回のFSM切り出しにより、とうとう1分を切るようになりました。まさに隔世の感があります。

最初は論理の構築に集中しているため最適化までは手が出せず、コンパイル時間が長いため思うように検証を進められませんでした。

一方、現在は論理が固まったことで最適化も容易になり、コンパイル時間は劇的に短縮されました。皮肉なものですがこの効果を当初に得られていればと感じます。


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

GameFSMの改良 (16)

posted by sakurai on October 21, 2025 #1038

2番目にボディが厚い関数の最適化をトライします。対象はupdatePlayerBullet()(自弾処理関数)で、64行あります。

自弾処理関数(updatePlayerBullet())の最適化前後 比較
BSV合成 コンパイル時間 1'15'' 1'03'' ▲16.0%
Verilog合成 ファイルサイズ[KB] 5,253 4,283 ▲18.5%
合成時間 0'52'' 0'58'' 11.5%
Vivado LUT数 5,490 5,582 1.7%
Vivado FF数 1,894 1,907 0.7%

結果としては、おなじくFSMオーバヘッドが想定されそのとおりになりました。一方、ボディが厚かったため、bscコンパイル時間とverilog量の削減となりました。物量は微増していますが、bscコンパイル時間を1分を切らせたかったのでこの最適化を採用します。


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

GameFSMの改良 (15)

posted by sakurai on October 17, 2025 #1037

前回の1度しか呼ばれていない関数のdrawString関数はボディが薄い(関数の行数が少ない)ので効果が出なかったかもしれないと思い、ボディが厚い関数をトライします。対象はupdateAlienBullet()(敵弾処理関数)で、行数は83行あります。

敵弾処理関数(updateAlienBullet())の最適化前後 比較
BSV合成 コンパイル時間 1'27'' 1'15'' ▲13.8%
Verilog合成 ファイルサイズ[KB] 5,922 5,253 ▲11.3%
合成時間 0'59'' 0'52'' ▲11.9%
Vivado LUT数 5,685 5,490 ▲3.4%
Vivado FF数 1,789 1,894 5.9%

結果としてはまず1度しか呼ばれない関数のため、物量削減どころかFSMオーバヘッドが増加しました。一方、ボディが厚かったため、bscコンパイル時間とverilog量及びそれに比例するvivado合成時間が削減されました。

物量は増えたもののbscコンパイル時間が減ったため、この最適化を採用します。


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

GameFSMの改良 (14)

posted by sakurai on October 16, 2025 #1036

call順位が高い関数のFSM化が完了したので、次に1度しか呼ばれていない関数もFSM化してみます。これは物量にはほぼ影響はないか若干増加するものの、関数のシーケンスを巨大なシーケンスループからはずすことで、コンパイル時の競合条件計算量の減少を目的とするものです。まず、6個あるdrawTitle関数の1つをFSM化します。

まず、オリジナルのコードは、

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

コードの修正法は前回と同様なので省略します。

以下に結果の表を示します。思ったほどはコンパイル時間は減りませんでした。またverilog量は若干減ったものの、物量は若干増加しています。これは新たにFSMの起動、終了待ちが増えるためでしょう。

タイトル文字表示1(drawTitle1())の最適化前後 比較
BSV合成 コンパイル時間 1'26'' 1'25'' ▲1.2%
Verilog合成 ファイルサイズ[KB] 5,922 5,790 ▲2.2%
合成時間 0'59'' 0'53'' ▲10.2%
Vivado LUT数 5,583 5,638 1.0%
Vivado FF数 1,784 1,794 0.6%

全体としてあまり意味が無さそうなのでこの最適化は撤回します。


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

GameFSMの改良 (13)

posted by sakurai on October 15, 2025 #1035

他にもcall順位の上位では出てこなかった関数にdrawLives()がありました。これは自機の残り数を表示するもので、staticには6回呼ばれているため、これもFSM化して最適化します。

まず、オリジナルのコードは、

    // 残機表示
   function Stmt drawLives();
      return (seq
         // 残機数字の表示
         copyArea(gun_no*8, 161, 23, 241, 8, 8);
         if (gun_no == 1) seq
            eraseArea(42, 241, 16, 8);
         endseq else if (gun_no > 1) seq
            eraseArea(16*gun_no + 26, 241, 16, 8);
            copyArea(0, 16, 16*gun_no+10, 241, 16, 8);
         endseq // if
      endseq);
   endfunction

ここでgnu_noが自機の数を示します。これを例によってFSM化してメインでは起動し、終了待ちをするだけに変更します。以下が変更後のコードです。

   // 残機表示
   function Stmt drawLives_org();
      return (seq
         // 残機数字の表示
         copyArea(gun_no*8, 161, 23, 241, 8, 8);
         if (gun_no == 1) seq
            eraseArea(42, 241, 16, 8);
         endseq else if (gun_no > 1) seq
            eraseArea(16*gun_no + 26, 241, 16, 8);
            copyArea(0, 16, 16*gun_no+10, 241, 16, 8);
         endseq // if
      endseq);
   endfunction

   // 単一インスタンスのFSMを生成(モジュールスコープ)
   FSM drawLives_fsm <- mkFSM(drawLives_org());

   // “起動ラッパ”を元の名前に
   function Stmt drawLives();
      return (seq
         `RUN_FSM(drawLives_fsm)
      endseq);
   endfunction

本体の関数名に_orgを付けるだけでロジックは変えません。元の関数名は起動マクロで本体を起動し終了待ちする関数に付け替えます。

以前作成したマクロは以下のとおりです。呼ぶ側で必ずseq/endseqで挟む必要があります。

`define RUN_FSM(F) action F.start(); endaction await(F.done);

以下に結果の表を示します。bsvソース量はほとんど変わらないので表示していません。bscの見る場合の数が減るためコンパイル時間がかなり減少し、物量も若干減少しています。

自機表示(drawLives())の最適化前後 比較
BSV合成 コンパイル時間 1'54'' 1'25'' ▲25.4%
Verilog合成 ファイルサイズ[KB] 7,509 5,924 ▲21.1%
合成時間 1'00'' 0'51'' ▲15%
Vivado LUT数 5,700 5,551 ▲2.6%
Vivado FF数 1,794 1,790 ▲0.2%

結果が良かったためこの最適化を採用します。


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

GameFSMの改良 (12)

posted by sakurai on October 1, 2025 #1032

次にかなり大きな修正となりますが、OneStageの除去をトライします。もともと外部投稿記事にあるようにGameFSMとSoundFSMの両FSMのタイミング調停を図る目的で1stage FIFOを設けましたが、実はBSVの特徴として、自動ハンドシェーク、すなわちBack pressureを自動的にかける機能があります。

前記事のあたりで検討していたもので、この時はトップでconnectableで接続すれば配線だけになると誤解していました。実際にはconnectableは配線だけではなく、トップでANDを生成します。具体的には図1032.1に示すように、上流モジュールの送信RDYと下流モジュールの受信RDYのANDを最上位でとり、これが通信RDYを意味するわけですが、それを上流モジュールへ送信ENとして配り、かつ下流モジュールへも受信ENとして配るものです。

図%%.1
図1032.1 connectableハンドシェイクロジック

これはcallerであるトップが2つのcalleeを呼ぶ際に調停ロジックとしてANDゲートを配置するためです。

しかしながら、これだとVivadoのBlock DesignerによりANDゲートを起こさなければいけないためあまりきれいではないので、このANDゲートをwrapperで吸収してもらうようにChatGPTに依頼します。

ところが、ChatGPTとの会話の結果、問題はCDC(clock domain crossing)であり、GameFSMのクロックとSoundFSMのクロックが非同期であるため、cdc対策をしなければならないと怒られてしまいました。OneStageを抜くことはできず、むしろそこにcdcを組み込めばよいとのアドバイスでした。


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


ページ: