Verilog記述テクニック集

(1)学習の概要と目標

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

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

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

(2)function と always 文

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

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);
以上の規則はこの教材が推奨する記述スタイルです。ここで示した通りに記述しなくてもシミュレーションや論理合成できる場合がありますが、トラブルの少ないこの方法を推奨します
もふねこ

もふねこ:
=(ブロッキング)と <=(ノンブロッキング)の使い分けは、初めのうちは絶対迷うよね🐾
シンプルに「フリップフロップ(順序回路)を作るときは <= を使う!」って覚えておけば、大半のバグは防げるから安心してね!

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

graph LR subgraph 加算器 \(低コスト) ADD[+] A1[4bit] --> ADD B1[4bit] --> ADD ADD --> O1[4bit] end style ADD fill:#e8f5e9,stroke:#388e3c;
graph LR subgraph 乗算器 \(高コスト・大規模) MUL[*] A2[4bit] --> MUL B2[4bit] --> MUL MUL --> O2[8bit] end style MUL fill:#ffebee,stroke:#c62828;

テストベンチの記述に役立ついくつかのステートメントを紹介します。これらは 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 になる。論理合成の回路図でも最上位ビットが接続されていないことを確認できる。

📌 まとめ

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