Verilogの記述スタイルを学ぶ
1. 4つの基本スタイル
文法を覚えたら、次はこの「4つの書き方の型(スタイル)」をマスターしよう🐾
どんなに複雑な回路も、結局はこの4つの組み合わせでできているんだよ!
VerilogHDLによる回路記述には以下の4つの基本スタイルがあります。数百ゲートでも数十万ゲートでもこれらをベースにしています。
| # | スタイル | 用途 | 特徴 |
|---|---|---|---|
| 1 | assign文による組合せ回路 | シンプルな論理式・算術式 | 1行で記述できる単純な回路 |
| 2 | functionによる組合せ回路 | 条件分岐を含む複雑な回路 | 複数行・always文でも同様に記述可 |
| 3 | always文による順序回路 | ラッチ・フリップフロップを含む回路 | posedge/negedgeでクロックエッジを指定 |
| 4 | 下位モジュール接続 | 階層設計・モジュールの組み合わせ | インスタンス名で個別識別 |
2. スタイル1: assign文による組合せ回路
1行の論理式や算術式で記述できる回路は assign 文を用います。assign ではじまり ;(セミコロン)で終わります。代入記号 = の左辺に代入先、右辺に式を記述します。フリーフォーマットなので見かけ上2行以上になっても構いません。
記述例
// 2入力NAND(ビット演算子で論理式表現)
assign nand_out = ~(a & b);
// セレクタ2to1(sel=0ならd0、sel=1ならd1を選択)
assign dout = sel ? d1 : d0;
// 桁上がり信号(cntoutが9のときcarryに1を出力)
assign carry = (cntout == 4'd9) ? 1'b1 : 1'b0;
// 加算回路(aとbを足してqに出力)
assign q = a + b;
3. スタイル2: functionによる組合せ回路
条件分岐などを含む複雑な回路は function を使って記述します。functionには定義部と呼び出し部があります。
例: 2入力4出力デコーダ(2→4デコード回路)
入力 din で指定したビットだけが1になり dout に出力されます(ワンホット出力)。
// ─── 定義部 ───────────────────────────────────────────
function [3:0] dec; // 戻り値4ビット、function名はdec
input [1:0] din; // 入力引数の宣言
begin
case (din)
2'b00: dec = 4'b0001;
2'b01: dec = 4'b0010;
2'b10: dec = 4'b0100;
2'b11: dec = 4'b1000;
default: dec = 4'bxxxx;
endcase
end
endfunction
// ─── 呼び出し部(代入文の右辺として使用)─────────────
assign dout = dec(din); // 引数dinを渡してfunction decを呼び出す
| dinの値 | doutの値 |
|---|---|
| 2'b00 (0) | 4'b0001 |
| 2'b01 (1) | 4'b0010 |
| 2'b10 (2) | 4'b0100 |
| 2'b11 (3) | 4'b1000 |
4. スタイル3a: always文による組合せ回路
組合せ回路は always 文でも記述できます。functionとほぼ同一の記述が可能です。
例: 同じ2→4デコーダをalways文で記述
reg [3:0] dout; // 出力をレジスタ宣言
always @(din) begin // dinに変化があれば実行(センシティビティリスト)
case (din)
2'b00: dout <= 4'b0001;
2'b01: dout <= 4'b0010;
2'b10: dout <= 4'b0100;
2'b11: dout <= 4'b1000;
default: dout <= 4'bxxxx;
endcase
end
5. スタイル3b: always文による順序回路
順序回路はレジスタ信号と always 文を使って記述します。
| 要素 | 内容 |
|---|---|
| 出力信号の宣言 | 必ず reg(レジスタ型)で宣言する |
| クロックエッジ指定 | posedge=立ち上がり、negedge=立ち下がり |
| always内の代入 | ネット型(wire)には代入不可。regのみ |
| 代入記号 | ノンブロッキング代入 <= を使用する |
| センシティビティリスト | クロックと非同期リセット/セット信号のみ記述 |
例1: Dフリップフロップ(最もシンプルな順序回路)
module DFF (CK, D, Q);
input CK, D;
output Q;
reg Q;
always @(posedge CK) begin // CKの立ち上がりごとに実行(無限ループ)
Q <= D; // DフリップフロップDの値をQに記録
end
endmodule
always @(posedge CK) はCKの立ち上がりを待ち続け、立ち上がるたびにブロック内を実行します。これを無限に繰り返すことでフリップフロップの動作を表現します。
例2: 非同期リセット付きカウンタ
reg [3:0] cnt;
// センシティビティリスト: クロックckと非同期リセットresのエッジを監視
always @(posedge ck or posedge res) begin
if (res) begin
cnt <= 4'b0000; // res=1なら非同期リセット(0クリア)
end else begin
cnt <= cnt + 1; // res=0ならカウントアップ
end
end
6. スタイル4: 下位モジュール接続
下位モジュールの接続は以下の書式で記述します。
モジュール名 インスタンス名 (ポートリスト);
// 例:DFFを4つ接続して4ビットシフトレジスタを作成
DFF DFF0 (.CK(ck), .D(d), .Q(q0));
DFF DFF1 (.CK(ck), .D(q0), .Q(q1));
DFF DFF2 (.CK(ck), .D(q1), .Q(q2));
DFF DFF3 (.CK(ck), .D(q2), .Q(q3));
4ビットシフトレジスタの構造
ポート接続の2つの方法
// ポートリストの順番に接続
DFF DFF0 (ck, d, q0);
DFF DFF1 (ck, q0, q1);
信号を追加・変更した際にポート定義順と一致しないと誤接続が起きる。
// .ポート名(接続信号名) で明示
DFF DFF0 (.CK(ck), .D(d), .Q(q0));
DFF DFF1 (.CK(ck), .D(q0), .Q(q1));
信号を追加・順番を変更しても誤接続が起きない。デバッグ時の変更に強い。
これでEシリーズ(文法ノート)は全部クリアだね、すごい!🐾
次はいよいよ、これらの文法を使って『実際の論理回路』を組み立てる「Cシリーズ(回路ノート)」に挑戦しよう!
7. 記述上の注意事項
| 注意点 | 内容 |
|---|---|
| 記述量 ≠ 回路規模 | 1行のassign文でも +,-,* などを含むと回路規模が大きくなる場合がある。逆に記述が複雑でも回路規模が小さい場合もある。 |
| case文のdefault | defaultがなくても文法エラーにはならないが、列挙していない入力のときの動作を明示でき、全経路で必要な出力が決まっているか確認しやすくなるので記述する。ただしdefaultを書いても、別の分岐で代入していない出力があればラッチは残る。分岐の前に既定値を代入する書き方でも避けられる。 |
| モジュール接続は名前接続を使う | デバッグ中に信号を追加することはよくある。順番接続だと誤接続のリスクがあるため、名前接続を使う習慣をつける。 |
| 順序回路の代入は <= を使う | always文内の順序回路でははノンブロッキング代入(<=)を使う。ブロッキング代入(=)との違いはO2ユニットで解説。 |
assign q = a * b; のような乗算は、論理合成すると大規模な乗算回路が生成されます。1行でも回路規模が大きくなることを意識しましょう。
🐾 もふねこの現場メモ:
assign文で三項演算子を何重にもネストして「1行で書いた!」と満足する学生がいるんだけど、レビューで「読めない」と指摘されることが多いんだ。
書いた本人も翌日には読めなくなるし、バグの温床になる。迷ったらalways文のcase/ifで書く方が、条件と出力の対応を追いやすいよ。合成結果が小さくなるかどうかは別の話で、同じデバイス・同じ制約で合成してレポートを比べないと分からないんだ。4つの書き方が機能として同じであることはC1の実習で確かめられるよ。
実習で確かめる、つまずきの補足
短いセレクタにも入力範囲の仕様を
assign dout = din[sel]; は有効な可変ビット選択だよ。1行だから悪い、caseなら常に良い、という区別ではない。dinが4ビットなのにselが3ビットなら、4〜7をどう扱うかまで仕様を決める必要があるんだ。
assign dout = (sel < 3'd4) ? din[sel] : 1'b0;
これは範囲外の既知値を0にする仕様の例だよ。sel自体にX/Zがある場合まで0になる保証ではないため、TBで未知値も別に検査しよう。入力幅の変更時には、選択範囲と境界のテストも更新する。警告が出ないことだけを根拠に正しいと判断しないでね。
📝 E6 まとめ:4つの基本スタイル チェックリスト
| スタイル | キーポイント |
|---|---|
| assign文(組合せ) | 1行の論理式・算術式。assign〜; の形式 |
| function(組合せ) | 定義部 + 呼び出し部。case文・if文が使える |
| always(組合せ) | センシティビティリストに全入力を列挙。出力はreg宣言 |
| always(順序) | posedge/negedgeでエッジ指定。代入は<=(ノンブロッキング) |
| 下位モジュール接続 | モジュール名 + インスタンス名 + ポートリスト。名前接続を推奨 |
| case文のdefault | 省略可。全経路で必要な出力が決まるようにする(defaultを書くか、分岐の前に既定値を代入する) |
| 接続方法の混在 | 順番接続と名前接続は同一モジュール内で混在不可 |
