🤖

BSV (Bluespec SystemVerilog)を用いたサウンドFSMのシーケンスベースによる再設計

2022/10/19に公開

1. はじめに

勉強がてらBSV(Bluespec SystemVerilog)による高位合成を用いてフルハードウェアでスペースインベーダーを設計しましたが、その中にはゲームシナリオを司るGameFSMと、GameFSMからサウンドコードを受けて様々なサウンドを鳴らすSoundFSMの2種のFSM (Finite State Machine)が存在します。その2つのFSMをバッファする1段キューOneStageが入っています。

image.png
図1 GameFSMとSoundFSM

それぞれの設計手法は、たまたま以下のようにしました。

  • SoundFSM ---- ステートベース設計
  • GameFSM ---- シーケンスベース設計

ここで、ステートベース設計とは、ステートを一つ一つBSVで書いていくことを意味し、一方シーケンスベース設計とはステート分解をせずに、高位合成を用いて設計し、ステートマシンの実装は高位合成コンパイラに任せることを意味します。

設計手法がこのように異なっていた理由は、verilog版を設計したときは、全てステートベース設計としたため、SoundFSMはその比較的簡単なステート遷移を忠実にBSVで設計したためです。一方でGameFSMはステート数が非常に多く、ステート遷移はコンパイラに任せたいと思ったためです。

今回はSoundFSMもシーケンスベース設計に変更しました。その理由はもちろん、ステートベース設計ではステート分解を人力で行うため、高級言語のメリットがあまり出なく、また同じ機能をステートベース設計とシーケンスベース設計とで設計してみて、結果の違いを見たかったためです。

image.png
図2 作成したPMODインタフェースボード

図2はベンダーであるJLCPCBから実装が完了して送付されたUltra96toPMODボードです。最新版のUltra96toPMODV10ボードデータの場所等はこの記事で示しています。

2. システム構成

SoundFSMのソフトブロック階層を図3に示します。ソフトブロックとは中身が見える階層で、これはSoundFSMのインスタンスであるmkSoundFSM及び8bit×32KwordのシングルポートROMで構成されます。上位のsound階層では従来通り、この階層を4チャネル使用し、ミキサーで合成した後パラシリ変換によりシリアルDAC用のデータとしています。

image.png
図3 SoundFSMと専用ROMを含む階層

2.1 SoundFSM

基本的には前記事のとおりですが、ステートマシンを構成していたrule分を取り去り、

import StmtFSM::*;

としてステートマシンを自動設計するライブラリを呼び出しています。後は

	     while(True) seq

という無限ループの中に処理を書き、それらの処理を高位合成コンパイラがステートマシンに変換します。

2.1.1 ハンドシェーク

図4に、図1と同一ですがGameFSMとSoundFSMの間のハンドシェークを示します。

image.png
図4 GameFSMとSoundFSMの間のハンドシェーク

いわゆる2線式のハンドシェークであり、以下のようなアルゴリズムにより動作します。

  1. GameFSM側はコードを出力し、wr_enによりOneStageキューのemptyフラグがFalseとなります。
  2. SoundFSM側は正当コードかつ!emptyを待ち、入力されたらrd_en == Trueを返します。
  3. OneStage側はrd_enによりempty == Trueとします。
  4. GameFSM側はempty == Trueを待ち、それが来たら終了です。
  5. SoundFSM側はempty == Trueを待ち、それが来たらrd_en <= Falseとします。
  6. SoundFSM側はUFOオフコマンドならUFOフラグをOFFして終了します。
  7. SoundFSM側はUFOオフコマンドでなければフォーマットをデコードします。
  8. SoundFSM側はサウンドをカウント分演奏します。

ただし、UFOの飛行音は一旦ONになると、OFFが来るまで鳴り続ける仕様となっているため、上記ハンドシェークに依らない演奏が必要です。そのため、UFO飛行音が来るとUFOフラグを内部的に立て、起動時に上記ハンドシェークによるコマンド入力またはUFOフラグのONにより、FSMを起動します。

  1. GameFSM側はなにもしません。
  2. SoundFSM側はUFOフラグ==Trueの場合に実行はしますが、emptyを見ず、かつrd_enは返しません。
  3. SoundFSM側は内部的にはUFO飛行音と扱います。
  4. SoundFSM側はフォーマットをデコードします。
  5. SoundFSM側はサウンドをカウント分演奏します。

2.2 ROM

基本的には前記事のとおりですが、音量バランスの再調整を実施したことと、自機破壊音が短かめだったので、図5のように約40%延長し、さらにフェードアウトをかけました。

image.png
図5 自機破壊音の延長

ROM構成は旧版と同一で、8bit×32KwordのROMをFSMのチャネル数分だけ4個使用します。

表1. ROM構成表
Markdownにcolumnspan, rowspanが無いため画像とします。

3. テストケース

コードを開発する際にはテストケースが重要です。バグを発見してアルゴリズムを修正した場合、バグを発見したケースだけを確認すれば良いのではなく、全てのケースにおいてdegradeしていないかの確認が必要です。そのため、一か所修正したら、基本的には全てのテストケースを流す必要があります。人力ではとてもできないため、テストケースを流すための自動化をせざるをえません。

表2. テストケース進捗表
同様に画像とします。

表中のV1はステートベース設計のコード、V2はシーケンスベース設計のコードを意味します。

このテストケースによりバグを発見し、修正した後に再度全て流すループを回し、ソースコードを開発しました。実際にはテストケースには漏れがあり、テストケース上では動作してもFPGAに焼いて実行させてバグを発見した場合もあります。その際はそのバグを再現するようなテストケースをまず作成し、バグを修正し、テストケース及びFPGAで確認しました。

4. ソースコード

完成したBSVのソースコードを添付します。define疑似命令によりFSM0, 1, 2, 3との違いを吸収してソースの共通化を図っています。

SoundFSM.bsv
    import StmtFSM::*;

    `define SOUND1_ON     1        // 自弾発射音_ON
    `define SOUND2_ON     2        // 自機爆発音_ON
    `define SOUND3_ON     3        // インベーダ爆発音_ON
    `define SOUND4_ON     4        // インベーダ歩行音1_ON
    `define SOUND5_ON     5        // インベーダ歩行音2_ON
    `define SOUND6_ON     6        // インベーダ歩行音3_ON
    `define SOUND7_ON     7        // インベーダ歩行音4_ON
    `define SOUND8_ON     8        // UFO爆発音_ON
    `define SOUND9_ON     9        // 自機増加音_ON
    `define SOUND10_ON   10        // UFO飛行音_ON
    `define SOUND10_OFF  11        // UFO飛行音_OFF
    `define NULL         'h80
    `define COND_FSM0 !emptyf && (code == `SOUND1_ON || code == `SOUND2_ON || code == `SOUND9_ON)
    `define COND_FSM1 !emptyf && (code == `SOUND3_ON)
    `define COND_FSM2 !emptyf && (code == `SOUND4_ON || code == `SOUND5_ON || code == `SOUND6_ON || code == `SOUND7_ON)
    `define COND_FSM3 !emptyf && (code == `SOUND8_ON || code == `SOUND10_ON || code == `SOUND10_OFF)
    
    typedef UInt#(15) Addr_t;
    typedef UInt#(8) Data_t;
    typedef Bit#(4) Code_t;
    
    interface FSM_ifc;
       method Action sound(Code_t code);
       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
    
     (* synthesize,always_ready,always_enabled *)
    `ifdef FSM0
    module mkSoundFSM0(FSM_ifc);
    `elsif FSM1
    module mkSoundFSM1(FSM_ifc);
    `elsif FSM2
    module mkSoundFSM2(FSM_ifc);
    `elsif FSM3
    module mkSoundFSM3(FSM_ifc);
    `endif
    
       Wire#(Code_t) code <- mkWire,
                       current <- mkRegU;
       Wire#(Bool) lrclk <- mkWire;
       Reg#(Data_t) romdata <- mkRegU,
                       data <- mkRegU,
                       dout <- mkReg(`NULL);
       Reg#(UInt#(32)) workd <- mkRegU;
       Reg#(UInt#(15)) dcount <- mkRegU;
       Reg#(Addr_t) worka <- mkRegU,
                       romaddr <- mkRegU,
                       addr <- mkRegU;
       Reg#(UInt#(8)) ii <- mkReg(0);
       Reg#(Bool) son <- mkReg(False),
                       sonEarly <- mkReg(False),
                       ren <- mkReg(False),
                       emptyf <- mkReg(True);
    `ifdef FSM3
        Reg#(Bool) fUFO <- mkReg(False);
    `endif

       // subfunctions
       //   READ MEM
       //     input:  worka
       //     output: romdata;
       function Stmt readmem;
          return (seq
             addr <= worka;
             noAction;
             data <= romdata;
          endseq);
       endfunction

       //   READ COUNT
       //     input:  romaddr
       //     output: (romaddr,...,romaddr+3) => dcount;
       //             romaddr + 4 => romaddr;
       function Stmt readcount;
          return (seq
             workd <= 0;
             for (ii <= 0; ii <= 3; ii <= ii + 1) seq
                worka <= romaddr + extend(3-ii);
                readmem;
                if (ii == 3) dcount <= truncate(workd<<8) | extend(romdata);
                else workd <= workd<<8 | extend(romdata);
             endseq
             romaddr <= romaddr + 4;
          endseq);
       endfunction
          
       Stmt main = seq
          while(True) seq
             action
                dout <= `NULL;
                sonEarly <= False;
                son <= False;
                ren <= False;
             endaction
    `ifdef FSM0
             await(`COND_FSM0);
             action
                ren <= True;
                current <= code;
             endaction
    `elsif FSM1
             await(`COND_FSM1);
             action
                ren <= True;
                current <= code;
             endaction
    `elsif FSM2
             await(`COND_FSM2);
             action
                ren <= True;
                current <= code;
             endaction
    `elsif FSM3
             await(`COND_FSM3 || fUFO);
             if (`COND_FSM3) action
                fUFO <= (code == `SOUND10_ON);
                ren <= True;
                current <= code;
             endaction else if (fUFO) action
                current <= `SOUND10_ON;
            endaction
    `endif
             await(emptyf);
             ren <= False;
    `ifdef FSM3
             if (code == `SOUND10_OFF) continue;
    `endif
             await(lrclk);
             await(!lrclk);
             delay(4);

             action    
                case (current)
    `ifdef FSM0
                   `SOUND1_ON:  romaddr <=     0 + 16;
                   `SOUND2_ON:  romaddr <=  3422 + 16;
                   `SOUND9_ON:  romaddr <= 16150 + 16;
    `elsif FSM1
                   `SOUND3_ON:  romaddr <=     0 + 16;
    `elsif FSM2
                   `SOUND4_ON:  romaddr <=     0 + 16;
                   `SOUND5_ON:  romaddr <=  1266 + 16;
                   `SOUND6_ON:  romaddr <=  2836 + 16;
                   `SOUND7_ON:  romaddr <=  4406 + 16;
    `elsif FSM3
                   `SOUND8_ON:  romaddr <=     0 + 16;
                   `SOUND10_ON: romaddr <= 25968 + 16;
    `endif
                endcase
             endaction
             readcount;
             romaddr <= romaddr + extend(dcount) + 4;
          
             readcount;
             romaddr <= romaddr - 1;

             while (!((dcount == 0) || 
    `ifdef FSM0
                (`COND_FSM0 && current !=`SOUND9_ON))) seq
    `elsif FSM1
                (`COND_FSM1)))seq
    `elsif FSM2
                (`COND_FSM2))) seq
    `elsif FSM3
                (`COND_FSM3))) seq
    `endif
                if (sonEarly == False) seq
                   readmem;
                   action
                      sonEarly <= True;
                      son <= False;
                      dout <= `NULL;
                   endaction
                endseq else seq
                   readmem;
                   action
                      son <= True;
                      dout <= romdata;
                   endaction
                endseq

                delay(11);
                action
                   romaddr <= romaddr + 1;
                   worka <= romaddr + 1;
                   dcount <= dcount - 1;
                endaction
             endseq
    `ifdef FSM3
             if ((code == `SOUND8_ON || code == `SOUND10_OFF) && !emptyf) fUFO <= False;
    `endif
          endseq
       endseq;

       mkAutoFSM(main);
       
       method Action sound(Code_t incode);
          code <= incode;
       endmethod
       method Action rom_data(Data_t indata);
          romdata <= indata;
       endmethod
       method Addr_t rom_address();
          return addr;
       endmethod
       method Data_t sdout();
          return dout;
       endmethod
       method Bool soundon();
          return son;
       endmethod
       method Action sync(Bool inlrclk);
          lrclk <= inlrclk;
       endmethod
       method Bool fifo_ren();
          return ren;
       endmethod
       method Action empty(Bool flag);
          emptyf <= flag;
       endmethod
    
    `ifdef FSM0
    endmodule: mkSoundFSM0
    `elsif FSM1
    endmodule: mkSoundFSM1
    `elsif FSM2
    endmodule: mkSoundFSM2
    `elsif FSM3
    endmodule: mkSoundFSM3
    `endif

5. 結果比較

表3にステートベース設計及びシーケンスベース設計という、2種類の設計手法で設計した同じ仕様のSoundFSMの1チャネル分の結果を示します。行数やFPGA LUT数に幅があるのはチャネル0~3までで仕様が若干異なるためです。

表3 旧BSV版(ステートベース)SoundFSM結果

BSV行数 生成Verilog行数 FPGA LUT数
437 890~936 184~197

表4 新BSV版(シーケンスベース)SoundFSM結果

BSV行数 生成Verilog行数 FPGA LUT数
288 1,807~1,977 220~231
  • BSV行数は437行から288行へ66%と減少しました。
  • 生成されたVerilog行数は平均913行から1,892行へとほぼ200%と大幅に増加しました。ただし、中を見ると、論理合成に関係の無いエラー表示が約1,000行と多く、論理合成対象は約800行であり、人手設計とあまり変わりませんでした。
  • FPGAのLUTで比較すれば、平均191個から226個へと118%と若干増加しました。Verilog行数が増えたほどは物量は増えていません。その理由は前述のとおりです。
  • 開発工数は行数が増えると線形では増えず、組み合わせのため指数的に増加すると思われます。そのため、BSV行数が減ったことは開発工数が1/3程度になったと考えられます。

今回は同じサウンドFSMとして、前回がステート(ルール)ベースによる設計、今回がシーケンスベースの設計という、異なった2つの設計手法によりFSMを設計しました。表2と表3に関してBSVとVerilogの行数の比を見ると、ルールベース(表3)では2.09倍であるのに対して、シーケンスベース(表4)では6.57倍と3.14倍程度増加しています。ただし、増加する原因は、ステートマシンの自動生成のエラー表示のためのようです。最終的なFPGAのLUTはあまり変わらないので、工数削減とのトレードオフに関して、十分自動ステートマシン設計(シーケンスベース設計)が優れていると評価できます。

Discussion