0
0

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

Last updated at Posted at 2022-08-27

ezgif.com-effects.gif

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側はemtpyを待ち、emptyの場合にはコードとwr_enを出力します。
  2. OneStage側はwr_enによりemptyフラグが!emptyとなります。
  3. GameFSM側はコードの出力後は次の処理に移ります。
  4. SoundFSM側は自コードかつ!emptyを待ち、!emptyの場合はrd_enを返します。
  5. OneStage側はrd_enによりemptyとなります。
  6. SoundFSM側はemptyを待ち、emptyの場合には!rd_enを出力します。
  7. SoundFSM側はUFOオフコマンドならUFOフラグをOFFして終了します。
  8. SoundFSM側はUFOオフコマンドでなければフォーマットをデコードします。
  9. 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構成表

Channel Code Sound Start Size [bytes] Entry
自機音
チャネル(#0)
1 自機弾発射音 0 3,422 0+16
2 自機爆発音 3,422 16,150 3,422+16
9 自機増加音 16,150 5,500 16,150+16
合計 [bytes] (32KB ROM使用率) 21,650 (66%)
インベーダ音
チャネル(#1)
3 インベーダ爆発音 0 4,622 0+16
合計 [bytes] (32KB ROM使用率) 4,622 (14%)
インベーダ音
チャネル(#2)
4 インベーダ歩行音 1 0 1,266 0+16
5 インベーダ歩行音 2 1,266 1,570 1,266+16
6 インベーダ歩行音 3 2,836 1,570 2,836+16
7 インベーダ歩行音 4 4,406 2,180 4,406+16
合計 [bytes] (32KB ROM使用率) 6,586 (20%)
UFO音
チャネル(#3)
8 UFO爆発音 0 25,968 0+16
10 UFO飛行音 25,968 1,846 25,968+16
合計 [bytes] (32KB ROM使用率) 27,814 (85%)

3. テストケース

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

表2. テストケース進捗表

FSM No. テストケース Pass/Fail
No. 内容 V1 (State) V2 (Seq.)
自機音
チャネル(#0)
1 CODE1演奏中にCODE2がプリエンプト可能なこと Pass Pass
2 CODE1演奏中にCODE9がプリエンプト可能なこと Pass Pass
3 CODE9演奏中にCODE1がプリエンプト不可能なこと
(自機増加音が妨げられないこと)
Pass Pass
4 CODE3を無視すること Pass Pass
インベーダ音
チャネル(#1)
5 CODE3演奏中にCODE3がプリエンプト可能なこと
(実際には起こらないため不要)
Pass Pass
6 CODE1を無視すること Pass Pass
インベーダ音
チャネル(#2)
7 CODE4演奏中にCODE5がプリエンプト可能なこと Pass Pass
8 CODE5演奏中にCODE6がプリエンプト可能なこと Pass Pass
9 CODE1を無視すること Pass Pass
UFO音
チャネル(#3)
10 CODE10演奏中にCODE8がプリエンプト可能なこと Pass Pass
11 CODE10演奏後にCODE10OFFが来るまで演奏を継続すること Pass Pass
(12) (11で)CODE10演奏中にプチプチ音が鳴らないこと Pass Pass
13 CODE10演奏中にCODE8がプリエンプトし最後にOFFになること Pass Pass
14 CODE10演奏中にCODE8がプリエンプトしOFFがプリエンプト
した後OFFになること
Pass Pass
15 CODE1を無視すること Pass Pass

表中の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はあまり変わらないので、工数削減とのトレードオフに関して、十分自動ステートマシン設計(シーケンスベース設計)が優れていると評価できます。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0