![]() |
24 |
BSVによるUARTの再設計 (7) |
Posts Tagged with "BSV"
既に発行済みのブログであっても適宜修正・追加することがあります。We may make changes and additions to blogs already published.
![]() |
23 |
BSVによるUARTの再設計 (6) |
![]() |
Tb.bsvを示します。
import StmtFSM::*;
import Uart::*;
import Connectable::*;
typedef Bit#(8) Byte;
(* synthesize *)
module mkTb();
// UARTインターフェースのインスタンスを生成
Uart_ifc uart <- mkUart;
// テストバイトレジスタの定義
Reg#(Byte) tbrReg <- mkRegU;
// UARTの送信と受信を接続
mkConnection(uart.sout, uart.sin);
// データを取得する関数
function Action getData;
return
action
// UARTからデータを読み取り、tbrRegに格納
let data <- uart.read;
tbrReg <= data;
endaction;
endfunction
// データを送信する関数
function Stmt putData(Byte c);
return (seq
// UARTにデータを書き込む
uart.write(c);
endseq);
endfunction
// 応答を確認する関数
function Stmt checkResp(Byte correctData);
return (seq
// データを取得
getData;
// 正しいデータと比較
if (correctData != tbrReg) seq
// データが異なる場合、表示して終了
$display("%d->%d", correctData, tbrReg);
$finish;
endseq
endseq);
endfunction
// テストシーケンスの定義
Stmt test = seq
putData(8'haa); // データ1を送信
checkResp(8'haa); // 応答を確認
putData(8'h55); // データ2を送信
checkResp(8'h55); // 応答を確認
putData(8'hc3); // データ3を送信
checkResp(8'hc3); // 応答を確認
putData(8'h3c); // データ4を送信
checkResp(8'h3c); // 応答を確認
$display("test OK"); // テスト成功を表示
await(uart.pdone);
$finish; // テスト終了
endseq;
mkAutoFSM(test); // テストシーケンスを自動化
endmodule
![]() |
22 |
BSVによるUARTの再設計 (5) |
![]() |
次にUartを送受信化します。送信部のFSMと並行で動作する受信部のFSMを設計します。
import StmtFSM::*;
interface Uart_ifc;
method Bit#(1) sout; // シリアル出力
(* prefix="" *)
method Action write((* port="write" *) Bit#(8) nodata); // パラレル入力
(* prefix="" *)
method Action sin((* port="sin" *) Bit#(1) nidata); // シリアル入力
method ActionValue#(Bit#(8)) read; // パラレル出力
method Bool pdone; // 送信完了フラグ
endinterface
(* synthesize, always_ready="sin, sout, pdone" *)
module mkUart(Uart_ifc);
// レジスタ定義
Reg#(Bit#(8)) odata <- mkRegU; // 送信データレジスタ
Reg#(Bit#(1)) osdata <- mkReg(1'h1); // シリアル出力データレジスタ
Reg#(Bit#(8)) idata <- mkRegU; // 受信データレジスタ
Reg#(Bit#(1)) isdata <- mkReg(1'h1); // シリアル入力データレジスタ
Reg#(Bool) getfsmDone <- mkReg(False); // 受信完了フラグ
// 送信ステートマシンの定義
Stmt putseq = seq
osdata <= 1'h0; // スタートビット
repeat (8) action
osdata <= odata[0]; // データを1ビットずつシリアル出力
odata <= (odata >> 1); // 右シフト
endaction
osdata <= 1'h1; // ストップビット
endseq;
FSM putfsm <- mkFSM(putseq);
// 受信ステートマシンの定義
Stmt getseq = seq
await(isdata == 1'h0); // スタートビットを待つ
repeat (8) action
idata <= {isdata, (idata >> 1)[6:0]}; // データを1ビットずつ受信
endaction
getfsmDone <= True; // 受信完了フラグをセット
endseq;
FSM getfsm <- mkFSM(getseq);
// 受信ステートマシンの再起動
rule restartGetFSM if (getfsm.done);
getfsm.start;
endrule
// シリアル出力
method Bit#(1) sout;
return osdata;
endmethod
// パラレル入力(データを書き込む)
method Action write(Bit#(8) nodata) if (putfsm.done);
odata <= nodata;
putfsm.start;
endmethod
// シリアル入力
method Action sin(Bit#(1) nidata);
isdata <= nidata;
endmethod
// パラレル出力(データを読み込む)
method ActionValue#(Bit#(8)) read if (getfsmDone);
getfsmDone <= False; // 受信完了フラグをリセット
return idata;
endmethod
// 送信完了フラグ
method Bool pdone;
return putfsmDone;
endmethod
endmodule
送受信のFSMにおいて、起動のタイミングが非対称的です。
- 送信FSMの起動は上位からパラレル入力をロードされてパラシリが開始する
- 受信FSMはシリアルデータがいつくるかわからないので、常に起動されていなくてはならない
従って、送信側は
// パラレル入力(データを書き込む)
method Action write(Bit#(8) nodata) if (putfsm.done);
odata <= nodata;
putfsm.start;
endmethod
このように、データが書き込まれた契機でfsmをスタートさせるのに対し、受信側は
// 受信ステートマシンの再起動
rule restartGetFSM if (getfsm.done);
getfsm.start;
endrule
FSMの終了を待ってその起動をかけるルールを追加しています。これはAutoFSMとして自動起動にすることも可能です。
![]() |
21 |
BSVによるUARTの再設計 (4) |
![]() |
![]() |
20 |
BSVによるUARTの再設計 (3) |
![]() |
次にMakefileを示します。
# BSV simulation Makefile (done版 / drain版 併存)
# done版 : Tb.bsv + Uart.bsv → run / wave / vrun / vwave
# drain版 : TbDrain.bsv + UartDrain.bsv → drain-run / drain-wave / drain-vrun / drain-vwave
# make clean : 生成物の削除
.PHONY: all run wave vrun vwave drain-run drain-wave drain-vrun drain-vwave clean
all: run
# ==== done版 ====
# ---- Bluesim ----
Tb.exec: Tb.bsv Uart.bsv
bsc -u -sim Tb.bsv
bsc -sim -e mkTb -o Tb.exec
bsim.vcd: Tb.exec
./Tb.exec -V bsim.vcd
run: bsim.vcd
wave: bsim.vcd
gtkwave -A bsim.vcd &
# ---- Verilog sim ----
mkTb.exev: Tb.bsv Uart.bsv
bsc -u -verilog Tb.bsv
bsc -verilog -e mkTb -o mkTb.exev
verilog.vcd: mkTb.exev
./mkTb.exev +bscvcd=verilog.vcd
vrun: verilog.vcd
vwave: verilog.vcd
gtkwave -A verilog.vcd &
# ==== drain版 ====
# ---- Bluesim ----
TbDrain.exec: TbDrain.bsv UartDrain.bsv
bsc -u -sim TbDrain.bsv
bsc -sim -e mkTbDrain -o TbDrain.exec
bsim_drain.vcd: TbDrain.exec
./TbDrain.exec -V bsim_drain.vcd
drain-run: bsim_drain.vcd
drain-wave: bsim_drain.vcd
gtkwave -A bsim_drain.vcd &
# ---- Verilog sim ----
mkTbDrain.exev: TbDrain.bsv UartDrain.bsv
bsc -u -verilog TbDrain.bsv
bsc -verilog -e mkTbDrain -o mkTbDrain.exev
verilog_drain.vcd: mkTbDrain.exev
./mkTbDrain.exev +bscvcd=verilog_drain.vcd
drain-vrun: verilog_drain.vcd
drain-vwave: verilog_drain.vcd
gtkwave -A verilog_drain.vcd &
clean:
rm -f *.bo *.ba *.cxx *.h *.o *.so *.exec *.exev mk*.v *.vcd *~
![]() |
19 |
BSVによるUARTの再設計 (2) |
![]() |
インタフェースを変えたため、呼び出し元のコードで書けば、元が
Tb.bsv
Stmt test = seq
uart.load(8'h55);
uart.load(8'haa);
uart.load(8'hc3);
uart.load(8'h3c);
await (uart.done());
$finish;
endseq;
これが
Stmt test = seq
uart.load(8'h55);
uart.load(8'haa);
uart.load(8'hc3);
uart.load(8'h3c);
uart.drain();
$finish;
endseq;
このようになります。大きなメリットは無いものの、AI曰く、
- 第一に情報隠蔽の向きです。done 版は「完了したか」という述語をモジュールの外に公開し、それをどう使うか(待つのか、覗くだけか)は呼び側の責任で、await を書き忘れる余地があります。drain 版は待ち条件をモジュール側の呼出規約に畳み込むので、呼び側は呼ぶことしかできず、誤用の形が存在しません。
- 第二に語彙の統一で、done 版の Tb は「呼出4回+述語待ち1回」の2語彙、drain 版は「呼出5回」の1語彙になります。
- 第三に使い分けの指針も書けて、複数箇所から自由に覗きたい状態監視なら値メソッド(done)、シーケンスの合流点なら空アクション(drain)、という整理です。RUN_FSM マクロが await(F.done) を使っているのは前者の側で、これを後者に寄せる余地がある、という接続もできます。
呼び出し側の合成後の回路も同等で、前者が
// rule RL_action_l17c18
assign WILL_FIRE_RL_action_l17c18 =
uart$done && state_mkFSMstate == 4'd4 ;
となるのに対し、後者は
// rule RL_action_l17c17
assign WILL_FIRE_RL_action_l17c17 =
uart$RDY_drain && state_mkFSMstate == 4'd4 ;
となり、呼び出し側でも同一論理であることがわかります。前者はuartのdone、つまりfsmのdoneをawaitするという論理を発火論理に組み込むのに対して(awaitなどは消えている)、後者はdrainをコールするための自動バックプレッシャのRDYを発火論理に組み込むという違いとなります。
![]() |
18 |
BSVによるUARTの再設計 |
![]() |
Uart.bsvを若干修正しました。理由は、過去記事では呼び出しにdone=fsm.doneを返すようにしていたものを、ダミー関数drainの実行待ちでwaitさせるものです。
UartDrain.bsv
// UartDrain.bsv : drain版UART
// Uart.bsv (done版) との差分は done() → drain() のみ
package UartDrain;
import StmtFSM::*;
interface Uart_ifc;
method Bit#(1) read();
method Action load(Bit#(8) newdata);
method Action drain();
endinterface
(* synthesize *)
module mkUartDrain(Uart_ifc);
Reg#(Bit#(8)) data <- mkReg(8'haa);
Reg#(Bit#(1)) odata <- mkReg(1);
Stmt s = seq
odata <= 1'h0; // start bit
repeat (8) action
odata <= data[0];
data <= (data >> 1);
endaction
odata <= 1'h1; // stop bit
endseq;
FSM fsm <- mkFSM(s);
method Bit#(1) read();
return odata;
endmethod
method Action load(Bit#(8) newdata) if (fsm.done);
data <= newdata;
fsm.start;
endmethod
// ダミー関数: 完了を暗黙条件とする空アクション
// RDY_drain = fsm.done、EN_drain は内部で未使用
method Action drain() if (fsm.done);
noAction;
endmethod
endmodule
endpackage
UARTモジュールの合成後の回路は同等であり、前者が
// value method done
assign done =
fsm_abort_whas_AND_fsm_abort_wget_OR_fsm_state_ETC___d71 &&
!fsm_start_reg ;
となり、呼び出し側に明示的にdoneを見せるのに対して、後者は、
// action method drain
assign RDY_drain =
fsm_abort_whas_AND_fsm_abort_wget_OR_fsm_state_ETC___d71 &&
!fsm_start_reg ;
となり、呼び出し側をBSVの自動バックプレッシャのRDYで待たせるため、全く同じ回路となります。
![]() |
6 |
GameFSMの改良 (25) |
![]() |
次に原作にもある「化石」という症状をインプリしてもらいました。
仕様: 化石の再現 隊列がシールド帯まで降下した状態で、左端での下降パス中に自弾が右端シールドに命中すると、これを個体命中と誤認する。シールド内の弾の位置に敵爆発音と敵爆発マークを発生させ、マーク消去時にシールドへ 16×8 の穴を開ける一方、実際に死ぬのは弾の真上とは別の列の最前列個体である。その個体の画像は消去されないまま画面に残り、以後は敵として撃てず、隊列だけが先へ進む。
発症条件の幾何定数 — 隊列の降下判定しきい値と、右端シールドの x 範囲。
`ifdef FOSSIL_BUG
`define FOSSIL_BUG_Y 176 // 原作バグ発症条件: 隊列がシールド上端(192)の1ピッチ以内
`define FOSSIL_BUG_X 182 // これ以右のシールド命中で発症(=右端シールド全体、x=182〜204)
`endif
爆発マークの描画・消去位置を保持する — 犠牲個体の座標から切り離すことで「マークと犠牲が別の場所」という原作バグの本質を表現可能にする。
`ifdef FOSSIL_BUG
Reg#(UInt#(8)) expl_x <- mkRegU, // インベーダ爆発マークの描画・消去位置
expl_y <- mkRegU; // (バグ再現時は個体位置とマーク位置が食い違う)
`endif
常キル時にマーク位置=個体位置を expl_x/expl_y へラッチする(挙動は従来と同一の中立改造)。
`ifdef FOSSIL_BUG
eraseArea(
expl_x,
expl_y,
16, 8);
`else
eraseArea(
inv_x[gx][gy],
inv_y[gx][gy],
16, 8);
`endif
マーク消去を個体座標ではなく expl_x/expl_y に対して行う(バグ時はシールド内を消して穴を開ける)。
`ifdef FOSSIL_BUG
return (seq
action
alien_timer <= 1;
expl_x <= inv_x[gx][gy]; // マーク位置=個体位置(通常キル)
expl_y <= inv_y[gx][gy];
endaction
copyArea(96, colorAtY(inv_y[gx][gy])*16+16,
inv_x[gx][gy], inv_y[gx][gy],
16, 8);
endseq);
`else
return (seq
alien_timer <= 1;
copyArea(96, colorAtY(inv_y[gx][gy])*16+16,
inv_x[gx][gy], inv_y[gx][gy],
16, 8);
endseq);
`endif
発症4条件成立時のシールド命中を誤認キル化する本体 — マークと音は弾の位置、死ぬのは左端側の生存列の最前列、絵は残って化石になる。
`ifdef FOSSIL_BUG
endseq else if (fbase && ymax >= `FOSSIL_BUG_Y
&& inv_move == MoveLeftDown
&& bullet_x >= `FOSSIL_BUG_X) seq
// ---- 原作バグの再現(希少条件版) ----
// 右→左で来て左端で下降中(=MoveLeftDown、隊列スキュー中)、
// かつ隊列がシールド帯まで降りた状態で右端シールドを撃つと、
// 原作の座標→個体逆算の破綻(左下原点からの列計算オーバー
// フローが隣行へ回り込む)を模して、左端の生存個体が誤って
// 殺される。マークと消去は弾の位置、犠牲の絵は左端に無傷で
// 残り、隊列に取り残される(化石)。
fhit_alien <= False;
for (gx <= 0; gx < `InvCols; gx <= gx + 1) seq
if (inv_s[gx][inv_lead_y[gx]]) seq // 左端の生存列を探す
fhit_alien <= True;
break;
endseq
endseq
if (fhit_alien) seq
gy <= inv_lead_y[gx];
erasePlayerBullet(bullet_x, bullet_y);
inv_s[gx][gy] <= False;
inv_no <= inv_no - 1;
updateColumnFrontY();
playSound(SND_ALIEN_EXPLODE);
action // マークは弾の位置に描く
alien_timer <= 1;
expl_x <= bullet_x - 8;
expl_y <= bullet_y;
endaction
copyArea(96, colorAtY(bullet_y)*16+16,
bullet_x - 8, bullet_y, 16, 8);
action
score <= score + inv_score[gy];
bscore <= bscore + inv_score[gy];
endaction
endseq else seq
erasePlayerBullet(bullet_x, bullet_y); // 生存列なし: 通常のシールド命中
explodePlayerBullet(35, bullet_x, bullet_y);
bullet_timer <= 1;
endseq
`endif
![]() |
3 |
GameFSMの改良 (24) |
![]() |
最近は推論能力が高いため、もっぱらClaude Fable 5を使用しています。さてCaludeに以下の仕様を実装してもらいました。
「自弾がインベーダに命中すると、爆発マークの表示期間(ALIEN_EXPL_TMAX tick)のあいだ隊列全体の移動を停止する。停止中も自機・自弾・敵弾・UFO・サウンドの処理は継続し、爆発マークの消去とともに、停止した個体から隊列の移動を再開する。」
これはオリジナル動作を観察して発見した仕様です。理由は不明ながら、爆発マーク表示中も隊列を動かすと、インベーダの動きにより爆発マークが欠ける場合があり、その干渉を防止するためと考えられます。
for (noy <= 0; noy < `InvRows; noy <= noy + 1) seq
// 爆発凍結中はカーソルを進めない(増分が no-op になる)
for (nox <= 0; nox < `InvCols;
nox <= (alien_timer == 0) ? nox + 1 : nox) seq
if (inv_s[nox][noy] || alien_timer != 0) seq
if (alien_timer == 0)
updateAliens();
else
eraseAlienExplosion();
updatePlayer();
...(以下、元のまま)...
endseq // if inv
endseq // for nox
endseq // for noy
![]() |
26 |
Qiita Advent Calendar 2025 HDLへの投稿 (4) |
![]() |
Hardware Description Language Advent Calendar 2025の第4弾の記事です。タイトルは「キラキラ星アゲイン」です。 本ブログで説明した記事その他をまとめたものとなります。詳細は本ブログにて。

ページ:







