Verilog記述テクニック集

(1)学習の概要と目標

HDL にも知っていると便利な「コツ」があります。ここではトラブル例を通じて function と always 文の特徴を深く理解し、代入記号の使い分けといくつかのステートメントを学びます。

概要
  • トラブル例を参考に、function と always 文の特徴を理解する
  • 代入記号の使い分けと、いくつかのステートメントを学ぶ
目標
  • 組み合わせ回路記述での function と always 文のメリット/デメリットを理解し説明できる
  • 代入記号を状況に応じて使い分けできる
  • wait、repeat、forever の各ステートメントを理解し説明できる
修了判定

シミュレーションと論理合成により function と always 文の特徴を確認する

まず実験:case / casez / casex を同じパターンで比べる

3つはよく「ほぼ同じ」と説明されますが、どの値を don't care として扱うかが違います。同じパターンで書き分けた回路に、既知の入力と不明値を含む入力の両方を与えて確かめます。考え方はこのページの補足にもまとめてあるよ。

3通りの書き分けと自動判定テストベンチをダウンロード(ZIP)

回路の仕様

  • 4ビットの sel を受け、上位から優先して 3 / 2 / 1 を返します。該当しなければ 0 です。
  • パターンは3つとも同じ 4'b1??? / 4'b01?? / 4'b001? / default です。
  • 違いは case・casez・casex のどれで書いたかだけです。
回路本体 case_variants.v を見る
`timescale 1ns/1ps
`default_nettype none
// The same patterns written with case, casez and casex.
// The patterns use ? so the differences show up directly.
module case_variants (
    input  wire [3:0] sel,
    output reg  [1:0] y_case,
    output reg  [1:0] y_casez,
    output reg  [1:0] y_casex
);
    always @* begin
        case (sel)                 // exact match only; ? is z here
            4'b1???: y_case = 2'd3;
            4'b01??: y_case = 2'd2;
            4'b001?: y_case = 2'd1;
            default: y_case = 2'd0;
        endcase
    end

    always @* begin
        casez (sel)                // z is don't care on BOTH sides; ? denotes z
            4'b1???: y_casez = 2'd3;
            4'b01??: y_casez = 2'd2;
            4'b001?: y_casez = 2'd1;
            default: y_casez = 2'd0;
        endcase
    end

    always @* begin
        casex (sel)                // x and z are don't care on BOTH sides
            4'b1???: y_casex = 2'd3;
            4'b01??: y_casex = 2'd2;
            4'b001?: y_casex = 2'd1;
            default: y_casex = 2'd0;
        endcase
    end
endmodule
`default_nettype wire
テストベンチ tb_case_variants.v を見る
`timescale 1ns/1ps
`default_nettype none
// 16 known inputs, then inputs that contain x.
module tb_case_variants;
    reg [3:0] sel;
    wire [1:0] y_case, y_casez, y_casex;
    reg [1:0] want_priority;
    integer checks = 0;
    integer i;

    case_variants dut(.sel(sel), .y_case(y_case), .y_casez(y_casez), .y_casex(y_casex));

    task check;
        input [1:0] actual, expected;
        input [8*10-1:0] label;
        begin
            checks = checks + 1;
            if (actual !== expected)
                $fatal(1, "FAIL: check=%0d %0s sel=%b actual=%b expected=%b",
                       checks, label, sel, actual, expected);
        end
    endtask

    initial begin
        $dumpfile("case_variants.vcd");
        $dumpvars(0, tb_case_variants);

        // Known inputs: casez and casex agree, plain case never matches.
        for (i = 0; i < 16; i = i + 1) begin
            sel = i[3:0];
            #1;
            want_priority = sel[3] ? 2'd3 : sel[2] ? 2'd2 : sel[1] ? 2'd1 : 2'd0;
            check(y_case,  2'd0,          "case");
            check(y_casez, want_priority, "casez");
            check(y_casex, want_priority, "casex");
            #1;
        end

        // An input that contains x.
        sel = 4'bxx01; #1;
        $display("  sel=%b -> case=%0d casez=%0d casex=%0d", sel, y_case, y_casez, y_casex);
        check(y_case,  2'd0, "x case");
        check(y_casez, 2'd0, "x casez");
        check(y_casex, 2'd3, "x casex");   // matched 1??? although bit3 is unknown

        sel = 4'b0x10; #1;
        $display("  sel=%b -> case=%0d casez=%0d casex=%0d", sel, y_case, y_casez, y_casex);
        check(y_case,  2'd0, "x case");
        check(y_casez, 2'd0, "x casez");
        check(y_casex, 2'd2, "x casex");   // matched 01?? although bit2 is unknown

        // z in the expression: casez ignores it too, and plain case can match
        // because ? in a pattern is literally z.
        sel = 4'bz001; #1;
        $display("  sel=%b -> case=%0d casez=%0d casex=%0d", sel, y_case, y_casez, y_casex);
        check(y_case,  2'd0, "z case");
        check(y_casez, 2'd3, "z casez");   // the z in the expression is ignored
        check(y_casex, 2'd3, "z casex");

        sel = 4'b1zzz; #1;
        $display("  sel=%b -> case=%0d casez=%0d casex=%0d", sel, y_case, y_casez, y_casex);
        check(y_case,  2'd3, "z case");    // ? is z, so this is an exact match
        check(y_casez, 2'd3, "z casez");
        check(y_casex, 2'd3, "z casex");

        $display("PASS: case variants (%0d checks)", checks);
        $finish;
    end

    initial begin
        #2000;
        $fatal(1, "FAIL: testbench timeout");
    end
endmodule
`default_nettype wire

実行方法

iverilog -g2012 -Wall -s tb_case_variants -o sim.vvp case_variants.v tb_case_variants.v
vvp sim.vvp

既知の入力0〜15では、casezとcasexは同じ

0〜15の16通りを与えたときの結果(2026年10月27日 Icarus Verilog 13.0)
文結果
case常に default。パターンの ? は z であって、0や1とは一致しない
casez? が don't care として働き、優先順位どおりに選ばれる
casexcasez と同じ結果

case にパターンの ? を書いても効かない、というのが1つ目の落とし穴だよ。そして既知の入力だけを試しているうちは、casezとcasexの違いは出てきません。

xを含む入力を与えると、casexだけ分岐してしまう

  sel=xx01 -> case=0 casez=0 casex=3
  sel=0x10 -> case=0 casez=0 casex=2
PASS: case variants (60 checks)

casex は式の側の x も don't care として扱います。xx01 の最上位ビットは不明なのに、4'b1??? に一致して3を返しました。「1だったことにして」分岐を選んだわけです。casez は x を無視しないので、ここでは一致せず default になります。

z を含む入力だと、casezも一致する

  sel=z001 -> case=0 casez=3 casex=3
  sel=1zzz -> case=3 casez=3 casex=3

casez が無視するのは z(と ?)で、パターン側だけでなく式の側の z も無視します。だから z001 が 4'b1??? に一致しました。無視しないのは x のほうなんだ。1zzz では plain case まで一致していますが、これは ? が z そのものなので「ビットが厳密に一致した」結果で、don't care として働いたわけではありません。

無視される値(式の側・パターン側のどちらでも)
文無視する値
caseなし(4値の厳密比較。? は z という値)
casezz と ?
casexx と z と ?

これが casex の怖さだよ。不明値が来たときに、黙ってどれかの分岐に入ってしまう。しかもその不明値は出力に現れないから、波形を見ても「正常に動いている」ように見えるんだ。不明値の追い方は検証実験4の実習にあるよ。

既知の入力だけ試すと、見つからない

casez を casex に書き換えた回路に、既知の入力16通りだけを与えるテストベンチを実行すると、こうなります。

PASS: partial check (16 known inputs, no x)

合格表示です。xを入れないかぎり、この2つは見分けられないんだ。※この簡易テストベンチは、サイト側の検証スクリプトで実行したもので、配布ZIPには入っていません。

わざと入れ替えて、テストの働きも確かめる

2026年10月27日 Windows / Icarus Verilog 13.0での実行結果
入れ替えた場所結果
casez を casex にcheck=50 x casez sel=xx01 actual=11 expected=00
casex を casez にcheck=51 x casex sel=xx01 actual=00 expected=11

確認問題

don't care を使いたいとき、casez と casex のどちらを選ぶとよいでしょうか。

答え

x を無視させたくないなら casez です。casex は式の側の不明値まで don't care にしてしまうので、上で見たように「不明なまま分岐が選ばれる」状態を作ります。どうしても入力側の不明値を扱いたい場合は、その条件が起こりうるのかを先に仕様で確かめてね。また、? を書いても case では don't care にならない点にも注意です。合成時の扱いはツールにも依存するので、使うなら対象ツールの資料も確認しよう。

この後の節では、function と always の使い分け、代入記号、いろいろなステートメントを見ていきます。

(2)function と always 文

if 文などによる組み合わせ回路は function か always 文の中で記述します。どちらを使うのがよいでしょうか?双方のメリット・デメリットを比較します。

function による組み合わせ回路 always 文による組み合わせ回路
メリット 組み合わせ回路と順序回路の区別が明確 記述量が若干少ない(余分な識別子が不要)
出力信号に直接代入できる
デメリット ・引き数や戻り値のビット幅が呼び出し側と一致しないとき
・module 内の信号を引き数を通さず参照したとき
→ 動作の不具合を起こすが文法エラーにならない
・ラッチを生成する危険がある
・一つの always 文で複数の出力を記述すると動作結果が正しくない
使い分けの指針
  • 設計資産の有効活用のために、どちらかに統一する(職場・プロジェクトで採用している方に合わせる)
  • function を使う場合:引き数と戻り値のビット幅を入念にチェックする
  • always 文を使う場合:イベント式と代入方法を入念にチェックする

reg と always 文は順序回路 / function は組み合わせ回路

順序回路(reg + always)

// カウンタ
reg [7:0] CNT;

always @( posedge CK ) begin
    if ( DIN == 2'b00 )
        DOUT <= 4'b0001;
    else if ( DIN == 2'b01 )
        DOUT <= 4'b0010;
    // その他の条件
end

reg 宣言した信号はフリップフロップかラッチを含む順序回路になる

組み合わせ回路(function)

// デコーダ
function [3:0] DEC;
    input [1:0] DIN;
    case ( DIN )
        2'h0: DEC = 4'b0001;
        2'h1: DEC = 4'b0010;
        2'h2: DEC = 4'b0100;
        2'h3: DEC = 4'b1000;
        default: DEC = 4'bxxxx;
    endcase
endfunction

assign DOUT = DEC( DIN );

function で記述した回路は組み合わせ回路になる

always 文で記述した場合、出力信号に直接代入できるため、function に比べ記述量が少なくてすみます。

always 文(出力に直接代入)

// デコーダ
reg [3:0] DOUT;

always @( DIN ) begin
    case ( DIN )
        2'h0: DOUT <= 4'b0001;
        2'h1: DOUT <= 4'b0010;
        2'h2: DOUT <= 4'b0100;
        2'h3: DOUT <= 4'b1000;
        default: DOUT <= 4'bxxxx;
    endcase
end

function(識別子 DEC が必要)

// デコーダ
function [3:0] DEC;
    input [1:0] DIN;
    case ( DIN )
        2'h0: DEC = 4'b0001;
        2'h1: DEC = 4'b0010;
        2'h2: DEC = 4'b0100;
        2'h3: DEC = 4'b1000;
        default: DEC = 4'bxxxx;
    endcase
endfunction

assign DOUT = DEC( DIN );

(3)function のデメリット1 — 戻り値のビット幅を誤った記述

2to4 デコーダを function で記述した場合のトラブル例です。

⚠️ NG:戻り値のビット幅が間違っている

出力 DOUT は 4 ビットだが、function の戻り値 DEC を [2:0](3 ビット)で宣言してしまった例:

// function による 2to4 デコーダ
wire [1:0] DIN;
wire [3:0] DOUT;

function [2:0] DEC;   // ← 本来は [3:0] が正しい
    input [1:0] DIN;
begin
    case ( DIN )
        2'h0:    DEC = 4'b0001;
        2'h1:    DEC = 4'b0010;
        2'h2:    DEC = 4'b0100;
        2'h3:    DEC = 4'b1000;
        default: DEC = 4'bxxxx;
    endcase
end
endfunction

assign DOUT = DEC( DIN );
何が起きるか?
  • 最上位ビットが欠落し、DEC へは下位 3 ビットのみが出力される
  • 結果として DOUT の最上位ビットは 0 に固定される
  • 文法エラーにはならないため、問題の発見に手間取る
時刻DINDOUT(期待値)DOUT(実際)
0000010001 ✅
1000100100010 ✅
2000201000100 ✅
3000310000000 ❌(最上位ビット欠落)

(4)function のデメリット2 — 引き数を通さず module 内の信号を参照

1 ビットの 2to1 セレクタを function で記述した場合のトラブル例です。

⚠️ NG:引き数を通さず module 内の信号を直接参照している
wire  DIN0, DIN1, SEL, DOUT;

function SELECTOR;
    input S;
begin
    if ( S==1'b0 )
        SELECTOR = DIN0;   // ← 引き数ではなく module 内信号を直接参照
    else
        SELECTOR = DIN1;
end
endfunction

assign DOUT = SELECTOR( SEL );
何が起きるか?
  • 文法エラーにはならない(function は module 内の信号を参照できてしまう)
  • SEL が変化した場合は正しく動作するが、DIN0 や DIN1 が変化しても DOUT は変わらない
  • 理由:引き数が変化しないと function が再評価されないため

❌ NG:引き数なし(DIN0/DIN1 を直接参照)

function SELECTOR;
    input S;
begin
    if ( S==1'b0 )
        SELECTOR = DIN0;
    else
        SELECTOR = DIN1;
end
endfunction

assign DOUT = SELECTOR( SEL );
// SEL変化→OK、DIN0/DIN1変化→NG

✅ OK:すべての入力を引き数経由で渡す

function SELECTOR;
    input D0, D1, S;
begin
    if ( S==1'b0 )
        SELECTOR = D0;
    else
        SELECTOR = D1;
end
endfunction

assign DOUT = SELECTOR( DIN0, DIN1, SEL );
ルール:function へはすべて引き数を通してデータを与えること。引き数による不具合は発見しにくいので注意が必要。

(5)always 文のデメリット — 複数出力を同一 always 文で記述

8 ビットのフラグ付き加算回路(入力 A・B を加算した SUM と、SUM が 0 のとき 1 になる ZFLAG を出力)を例に解説します。

⚠️ NG:always 文内で出力(SUM)を参照して ZFLAG を生成している
wire [7:0] A, B;
reg  [7:0] SUM;
reg        ZFLAG;

always @( A or B ) begin
    SUM <= A + B;
    if ( SUM==8'h00 )
        ZFLAG <= 1'b1;
    else
        ZFLAG <= 1'b0;
end
何が起きるか?
  • ノンブロッキング代入を使用した場合、always 文内で出力(SUM)を参照すると ZFLAG が 1 周期遅れて出力される
時刻ABSUMZFLAG(期待)ZFLAG(実際)
000000010 ❌
10000fff0e01 ❌
200001ff0010 ❌
3000017f8001 ❌
✅ OK:出力ごとに always 文を分けて記述する
wire [7:0] A, B;
reg  [7:0] SUM;
reg        ZFLAG;

always @( A or B )
    SUM <= A + B;

always @( SUM )
    if ( SUM==8'h00 )
        ZFLAG <= 1'b1;
    else
        ZFLAG <= 1'b0;

出力 SUM は入力 A・B の回路、出力 ZFLAG は入力 SUM の回路のように always 文を分けることで正しく動作する。

(6)代入記号(= と <=)の使い分け

回路記述 テストベンチ
組み合わせ回路(always 文) 順序回路(always 文) initial 文・always 文・task で使用
reg への代入 用途 always 文 always 文 initial 文・always 文・task
記号 <= <= =
wire への代入 用途 assign 文・function の戻り値代入などに使用
記号 =(ここで <= を使うと文法エラー)

記述例

組み合わせ回路(reg + always + <=)

wire [7:0] A, B;
reg  [7:0] SUM;
// 加算回路
always @( A or B )
    SUM <= A + B;

順序回路(reg + always + <=)

wire        CK, RES;
reg  [2:0] Q;
// 3 ビットカウンタ
always @( posedge CK or posedge RES )
begin
    if ( RES==1'b1 )
        Q <= 3'h0;
    else
        Q <= Q + 3'h1;
end

テストベンチ(reg + = )

reg  CK, RES;
// クロックの記述
always begin
    CK = 0; #(STEP/2);
    CK = 1; #(STEP/2);
end

// テスト入力
initial begin
             RES = 0;
    #STEP    RES = 1;
    #STEP    RES = 0;
    #(STEP*20)
             $finish;
end

wire への代入(assign + =)

// 回路記述
assign DOUT = DEC( DIN );  // <= は文法エラー

// テストベンチ
wire CEB, OEB, WEB;
wire WRITE, READ;
// 読み出し/書き込みの内部信号
assign READ  = (OEB==1'b0) & (CEB==1'b0);
assign WRITE = (WEB==1'b0) & (CEB==1'b0);
以上の規則はこの教材が推奨する記述スタイルです。ここで示した通りに記述しなくてもシミュレーションや論理合成できる場合がありますが、トラブルの少ないこの方法を推奨します。

なお、業界全体では「組み合わせ回路は =(ブロッキング)、順序回路は <=(ノンブロッキング)」というスタイルも広く使われており、近年の実務ではむしろこちらが主流です(もふねこのnote教材はこちらの流儀で統一しています)。どちらの流儀でも論理合成の結果は同じになるので、職場やプロジェクトの規約に合わせてください🐾
もふねこ

もふねこ:
=(ブロッキング)と <=(ノンブロッキング)の使い分けは、初めのうちは絶対迷うよね🐾
シンプルに「フリップフロップ(順序回路)を作るときは <= を使う!」って覚えておけば、代入記号で迷う場面はぐっと減るよ!

(7)いろいろなステートメント

O6 図1:加算器 低コストの図。4bitの2入力が+に入り、4bitの出力となる。
O6 図1:加算器 低コストの図。横に長い図はスクロールして確認できます。
O6 図2:乗算器 高コスト 大規模の図。4bitの2入力が*に入り、8bitの出力となる。
O6 図2:乗算器 高コスト 大規模の図。横に長い図はスクロールして確認できます。

テストベンチの記述に役立ついくつかのステートメントを紹介します。これらは initial 文や task の中で実行できます。

ステートメント 構文 説明
repeat repeat ( <式> ) <ステートメント> 固定回数のループ。<式> は繰り返し回数
forever forever <ステートメント> 無限ループ
wait wait ( <式> ) <ステートメント> 式が真なら次の文を実行。偽ならステートメントを実行し、真になるのを待つ

repeat — CK の立ち上がりを 20 回待つ

initial begin
    // 初期化処理など
    // CK の立ち上がりを 20 回待つ
    repeat( 20 ) @( posedge CK );
    // その後の処理
end

forever — クロック生成(always 文と同等)

initial  // クロックの記述
    forever begin
        CK=1; #(STEP/2);
        CK=0; #(STEP/2);
    end

wait — BUSY 信号が 0 になるのを待つ

initial begin
    #STEP  BUSY = 1'b0;
    // その他の初期化処理など
    // BUSY が 0 になるまで待つ
    wait( BUSY==1'b0 );
end

(8)修了判定

設問
  • 本文の中で紹介した function 記述(2to4 デコーダ)をシミュレーションして動作確認する
  • また、論理合成して、最上位ビットがどのように合成されているか、回路図で確認する

ファイル名:デコード回路 → dec2to4.v、テストベンチ → dec2to4_test.v

2to4 デコーダの端子仕様
入力出力
DIN [1:0] DOUT [3:0](1 ビットのみ "1"、残りは "0")
シミュレーション結果(期待値)
Compiling source file "dec2to4.v"
Compiling source file "dec2to4_test.v"

   0 DIN=0 DOUT=0001
1000 DIN=1 DOUT=0010
2000 DIN=2 DOUT=0100
3000 DIN=3 DOUT=0000   ← 戻り値ビット幅誤りのため最上位が欠落
4000 DIN=x DOUT=0xxx
End of simulation

DIN=3 のとき DOUT の期待値は 1000 だが、function の戻り値ビット幅を [2:0] と誤って記述した場合 0000 になる。論理合成の回路図でも最上位ビットが接続されていないことを確認できる。

実習で確かめる、つまずきの補足

casexが未知値まで一致させる理由

casex はcase式と各項目の両方にあるX/Zを比較から除外するよ。未初期化の入力が正常な条件に一致して、問題が見えなくなることがあるんだ。casez もZと ? を無視するので、未接続のZが紛れ込む場合に気をつけよう。

通常の case はX/Zも含めて一致を調べる。0/1の項目だけなら未知入力はdefaultへ進むけれど、defaultの出力を0にしただけでは「未知入力だった」という問題まで通知できない。TBで入力の未知値を検査するのが役立つよ。

Don't Careは「この条件ではどの出力でも仕様を満たす」という設計上の約束。代入省略、未初期化のX、安全な無効入力処理と混同しないでね。

📌 まとめ

  • functionは計算をまとめる構文。reg + alwaysが組合せ回路か順序回路かは、イベント式と代入の網羅性で決まる
  • function の落とし穴:戻り値のビット幅ミス、引き数なしの直参照
  • always の落とし穴:ノンブロッキング代入で出力を参照すると 1 周期遅延 → 出力ごとに always 文を分ける
  • 代入記号:回路記述の reg には <=、テストベンチの reg には =、wire には必ず =
  • テストベンチ用ステートメント:repeat(固定ループ)、forever(無限ループ)、wait(条件待ち)