1. はじめに
勉強がてらBSV(Bluespec SystemVerilog)による高位合成を用いてフルハードウェアでスペースインベーダーを設計しましたが、その中にはゲームシナリオを司るGameFSMと、GameFSMからサウンドコードを受けて様々なサウンドを鳴らすSoundFSMの2種のFSM (Finite State Machine)が存在します。その2つのFSMをバッファする1段キューOneStageが入っています。
図1 GameFSMとSoundFSM |
それぞれの設計手法は、たまたま以下のようにしました。
- SoundFSM ---- ステートベース設計
- GameFSM ---- シーケンスベース設計
ここで、ステートベース設計とは、ステートを一つ一つBSVで書いていくことを意味し、一方シーケンスベース設計とはステート分解をせずに、高位合成を用いて設計し、ステートマシンの実装は高位合成コンパイラに任せることを意味します。
設計手法がこのように異なっていた理由は、verilog版を設計したときは、全てステートベース設計としたため、SoundFSMはその比較的簡単なステート遷移を忠実にBSVで設計したためです。一方でGameFSMはステート数が非常に多く、ステート遷移はコンパイラに任せたいと思ったためです。
今回はSoundFSMもシーケンスベース設計に変更しました。その理由はもちろん、ステートベース設計ではステート分解を人力で行うため、高級言語のメリットがあまり出なく、また同じ機能をステートベース設計とシーケンスベース設計とで設計してみて、結果の違いを見たかったためです。
図2 作成したPMODインタフェースボード |
図2はベンダーであるJLCPCBから実装が完了して送付されたUltra96toPMODボードです。最新版のUltra96toPMODV10ボードデータの場所等はこの記事で示しています。
2. システム構成
SoundFSMのソフトブロック階層を図3に示します。ソフトブロックとは中身が見える階層で、これはSoundFSMのインスタンスであるmkSoundFSM及び8bit×32KwordのシングルポートROMで構成されます。上位のsound階層では従来通り、この階層を4チャネル使用し、ミキサーで合成した後パラシリ変換によりシリアルDAC用のデータとしています。
図3 SoundFSMと専用ROMを含む階層 |
2.1 SoundFSM
基本的には前記事のとおりですが、ステートマシンを構成していたrule分を取り去り、
import StmtFSM::*;
としてステートマシンを自動設計するライブラリを呼び出しています。後は
while(True) seq
という無限ループの中に処理を書き、それらの処理を高位合成コンパイラがステートマシンに変換します。
2.1.1 ハンドシェーク
図4に、図1と同一ですがGameFSMとSoundFSMの間のハンドシェークを示します。
図4 GameFSMとSoundFSMの間のハンドシェーク |
いわゆる2線式のハンドシェークであり、以下のようなアルゴリズムにより動作します。
- GameFSM側はemtpyを待ち、emptyの場合にはコードとwr_enを出力します。
- OneStage側はwr_enによりemptyフラグが!emptyとなります。
- GameFSM側はコードの出力後は次の処理に移ります。
- SoundFSM側は自コードかつ!emptyを待ち、!emptyの場合はrd_enを返します。
- OneStage側はrd_enによりemptyとなります。
- SoundFSM側はemptyを待ち、emptyの場合には!rd_enを出力します。
- SoundFSM側はUFOオフコマンドならUFOフラグをOFFして終了します。
- SoundFSM側はUFOオフコマンドでなければフォーマットをデコードします。
- SoundFSM側はサウンドをカウント分演奏します。
ただし、UFOの飛行音は一旦ONになると、OFFが来るまで鳴り続ける仕様となっているため、上記ハンドシェークに依らない演奏が必要です。そのため、UFO飛行音が来るとUFOフラグを内部的に立て、起動時に上記ハンドシェークによるコマンド入力またはUFOフラグのONにより、FSMを起動します。
- GameFSM側はなにもしません。
- SoundFSM側はUFOフラグ==Trueの場合に実行はしますが、emptyを見ず、かつrd_enは返しません。
- SoundFSM側は内部的にはUFO飛行音と扱います。
- SoundFSM側はフォーマットをデコードします。
- SoundFSM側はサウンドをカウント分演奏します。
2.2 ROM
基本的には前記事のとおりですが、音量バランスの再調整を実施したことと、自機破壊音が短かめだったので、図5のように約40%延長し、さらにフェードアウトをかけました。
図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との違いを吸収してソースの共通化を図っています。
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はあまり変わらないので、工数削減とのトレードオフに関して、十分自動ステートマシン設計(シーケンスベース設計)が優れていると評価できます。