読み込み中...

Verilogにおける伝播遅延の基礎と活用14選

伝播遅延 徹底解説 Verilog
この記事は約50分で読めます。

【サイト内のコードはご自由に個人利用・商用利用いただけます】

この記事では、プログラム(回路記述)の基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

本記事のサンプルコードを活用して機能追加、目的を達成できるように作ってありますので、是非ご活用ください。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を満たす現役のプログラマチームによって監修されています。

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

●Verilogの伝播遅延とは?

Verilogは、デジタル回路設計において欠かせないハードウェア記述言語です。

この言語を使いこなすうえで、伝播遅延という概念が非常に重要な役割を果たします。

伝播遅延とは、信号が回路内を伝わる際に生じる時間的な遅れのことを指します。

デジタル回路の分野では、理想的には信号が瞬時に伝わることが望ましいですが、現実の回路ではそうはいきません。

電子の動きには物理的な制約があるため、必ず遅延が発生します。

この遅延を適切に管理することが、高性能で信頼性の高い回路設計につながるのです。

初めてVerilogに触れる方々にとって、伝播遅延の概念は少し難しく感じるかもしれません。

しかし、実際の回路動作を正確に表現し、シミュレーションを行うためには、避けて通れない重要なトピックなのです。

○伝播遅延が回路性能に与える影響

伝播遅延は、回路の性能に大きな影響を及ぼします。

例えば、高速な処理が求められるデジタル回路では、伝播遅延が大きいとクロック周波数を上げられず、結果として処理速度の向上が難しくなってしまいます。

また、複数の信号が合流する箇所では、伝播遅延の差によってタイミングのずれが生じる可能性があります。

このずれが原因で、予期せぬ動作や誤動作が起こることもあるのです。

例えば、データ信号とクロック信号の到達タイミングにずれが生じると、フリップフロップが正しくデータをラッチできない事態に陥る可能性があります。

このような問題は、設計段階で適切に伝播遅延を考慮することで回避できます。

伝播遅延を理解し、適切に制御することで、回路の動作速度を最適化し、信頼性を向上させることができるのです。

これは、高性能なFPGAやASICの設計において、非常に重要なスキルとなります。

○時間単位とタイムスケール

Verilogでは、時間の概念を扱うために「時間単位」と「タイムスケール」という2つの重要な要素があります。

これを正しく理解し、適切に設定することで、伝播遅延を正確にモデル化できるようになります。

時間単位は、シミュレーション時の最小時間単位を定義します。

例えば、1nsを時間単位として設定すると、その回路記述内では1ns単位で時間を表現することになります。

一方、タイムスケールは、時間値の精度を指定します。

Verilogでは、次のような形式で時間単位とタイムスケールを指定します。

`timescale 1ns / 100ps

この例では、時間単位が1ns、タイムスケールが100psと設定されています。

つまり、1nsを基本単位としつつ、100ps単位の精度で時間を表現できるということです。

時間単位とタイムスケールの設定は、回路の動作速度や要求される精度に応じて適切に選択する必要があります。

高速な回路であれば、より細かい単位(例:1ps)を選択し、低速な回路であれば、大きな単位(例:1μs)を選択することもあります。

適切な時間単位とタイムスケールの選択は、シミュレーション結果の正確性に直結します。

また、異なるモジュール間で時間単位が異なると、予期せぬ動作を引き起こす可能性があるため、プロジェクト全体で一貫性を保つことが重要です。

○デルタ遅延vs伝播遅延

Verilogのシミュレーションを理解する上で、デルタ遅延と伝播遅延の違いを把握することは非常に重要です。

この2つの概念は、しばしば混同されがちですが、実際には全く異なる役割を果たしています。

デルタ遅延は、Verilogシミュレーションの内部メカニズムで使用される概念です。

シミュレーション時間を進めることなく、論理的な因果関係を表現するために使用されます。

つまり、実際の時間経過はありませんが、信号の変化の順序を表現するための仮想的な遅延です。

一方、伝播遅延は実際の回路で発生する物理的な遅延を模倣します。

信号が回路内を伝播する際に要する時間を表現し、シミュレーション時間に影響を与えます。

ここでは、デルタ遅延と伝播遅延の違いを示す簡単な例を見てみましょう。

module delta_vs_propagation;
  reg a, b, c;

  // デルタ遅延の例
  always @(a) b = ~a;

  // 伝播遅延の例
  always @(b) c = #5 b;

  initial begin
    a = 0;
    #10 a = 1;
    #10 $finish;
  end

  initial begin
    $monitor("Time=%0t a=%b b=%b c=%b", $time, a, b, c);
  end
endmodule

このコードを実行すると、次のような出力が得られます。

Time=0 a=0 b=1 c=x
Time=5 a=0 b=1 c=1
Time=10 a=1 b=0 c=1
Time=15 a=1 b=0 c=0
Time=20 a=1 b=0 c=0

ご覧のように、bの値はaの値が変化するとすぐに(デルタ遅延で)更新されますが、cの値はbの値が変化してから5時間単位後に更新されます。

●Verilogで伝播遅延を記述する10の必須テクニック

Verilogで伝播遅延を扱う技術は、デジタル回路設計の要となります。

実際の回路動作を正確に表現し、高性能な設計を実現するためには、様々なテクニックを習得する必要があります。

ここでは、Verilogで伝播遅延を記述するための10の必須テクニックを紹介します。

○サンプルコード1:assign文による基本的な遅延設定

assign文は、Verilogで最も基本的な遅延設定方法です。

信号の伝播に要する時間を簡単に表現できます。

例えば、ANDゲートの出力に2ナノ秒の遅延を設定する場合、次のようなコードになります。

module delay_example(input a, b, output y);
    assign #2 y = a & b;
endmodule

このコードでは、aとbの入力信号がAND演算された後、2時間単位(通常はナノ秒)遅れてyに出力されます。

実行結果を確認するために、テストベンチを作成してみましょう。

module testbench;
    reg a, b;
    wire y;

    delay_example dut(.a(a), .b(b), .y(y));

    initial begin
        $monitor("Time=%0t a=%b b=%b y=%b", $time, a, b, y);
        a = 0; b = 0;
        #5 a = 1;
        #5 b = 1;
        #10 $finish;
    end
endmodule

実行結果は次のようになります。

Time=0 a=0 b=0 y=x
Time=5 a=1 b=0 y=0
Time=10 a=1 b=1 y=0
Time=12 a=1 b=1 y=1

yの値が変化するのは、a=1、b=1になってから2時間単位後であることがわかります。

○サンプルコード2:alwaysブロックでの遅延管理

alwaysブロックを使用すると、より複雑な遅延動作を表現できます。

例えば、立ち上がりと立ち下がりで異なる遅延を設定する場合、次のようなコードになります。

module edge_delay(input clk, input d, output reg q);
    always @(posedge clk) begin
        q <= #2 d;  // 立ち上がりエッジで2単位時間の遅延
    end
    always @(negedge clk) begin
        q <= #3 d;  // 立ち下がりエッジで3単位時間の遅延
    end
endmodule

このモジュールでは、クロックの立ち上がりエッジでは2時間単位、立ち下がりエッジでは3時間単位の遅延でdの値がqに反映されます。

テストベンチを作成して動作を確認しましょう。

module testbench;
    reg clk, d;
    wire q;

    edge_delay dut(.clk(clk), .d(d), .q(q));

    always #5 clk = ~clk;  // 10時間単位周期のクロック生成

    initial begin
        $monitor("Time=%0t clk=%b d=%b q=%b", $time, clk, d, q);
        clk = 0; d = 0;
        #7 d = 1;
        #20 d = 0;
        #20 $finish;
    end
endmodule

実行結果は次のようになります。

Time=0 clk=0 d=0 q=x
Time=5 clk=1 d=0 q=x
Time=7 clk=1 d=1 q=x
Time=7 d=1
Time=10 clk=0 d=1 q=0
Time=13 clk=0 d=1 q=1
Time=15 clk=1 d=1 q=1
Time=17 clk=1 d=1 q=1
Time=20 clk=0 d=1 q=1
Time=25 clk=1 d=1 q=1
Time=27 clk=1 d=0 q=1
Time=27 d=0
Time=30 clk=0 d=0 q=1
Time=33 clk=0 d=0 q=0
Time=35 clk=1 d=0 q=0
Time=37 clk=1 d=0 q=0
Time=40 clk=0 d=0 q=0
Time=45 clk=1 d=0 q=0

クロックの立ち上がり、立ち下がりそれぞれで異なる遅延が適用されていることがわかります。

○サンプルコード3:条件付き遅延の実装方法

条件に応じて異なる遅延を設定したい場合があります。

例えば、入力信号の値によって遅延を変更する場合、次のようなコードになります。

module conditional_delay(input a, b, output y);
    assign y = a ? #2 b : #5 ~b;
endmodule

このモジュールでは、aが1の場合は2時間単位の遅延でbの値がyに出力され、aが0の場合は5時間単位の遅延で~bの値がyに出力されます。

テストベンチで動作を確認しましょう。

module testbench;
    reg a, b;
    wire y;

    conditional_delay dut(.a(a), .b(b), .y(y));

    initial begin
        $monitor("Time=%0t a=%b b=%b y=%b", $time, a, b, y);
        a = 0; b = 0;
        #10 a = 1;
        #10 b = 1;
        #10 a = 0;
        #10 $finish;
    end
endmodule

実行結果は次のようになります。

Time=0 a=0 b=0 y=x
Time=5 a=0 b=0 y=1
Time=10 a=1 b=0 y=1
Time=12 a=1 b=0 y=0
Time=20 a=1 b=1 y=0
Time=22 a=1 b=1 y=1
Time=30 a=0 b=1 y=1
Time=35 a=0 b=1 y=0

aの値に応じて、yの値が変化するタイミングが異なることがわかります。

○サンプルコード4:パラメータ化された遅延モデル

遅延値をパラメータ化することで、再利用性の高いモジュールを作成できます。

例えば、遅延時間を外部から設定可能にする場合、次のようなコードになります。

module parameterized_delay
    #(parameter DELAY = 2)
    (input a, output y);

    assign #(DELAY) y = a;
endmodule

このモジュールでは、DELAYパラメータで遅延時間を指定できます。

テストベンチで異なる遅延値を持つインスタンスを作成して動作を確認しましょう。

module testbench;
    reg a;
    wire y1, y2;

    parameterized_delay #(2) dut1(.a(a), .y(y1));
    parameterized_delay #(5) dut2(.a(a), .y(y2));

    initial begin
        $monitor("Time=%0t a=%b y1=%b y2=%b", $time, a, y1, y2);
        a = 0;
        #10 a = 1;
        #10 a = 0;
        #10 $finish;
    end
endmodule

実行結果は次のようになります。

Time=0 a=0 y1=x y2=x
Time=2 a=0 y1=0 y2=x
Time=5 a=0 y1=0 y2=0
Time=10 a=1 y1=0 y2=0
Time=12 a=1 y1=1 y2=0
Time=15 a=1 y1=1 y2=1
Time=20 a=0 y1=1 y2=1
Time=22 a=0 y1=0 y2=1
Time=25 a=0 y1=0 y2=0

dut1とdut2で異なる遅延が適用されていることがわかります。

○サンプルコード5:配線遅延のモデリング手法

実際の回路では、配線にも遅延が発生します。

Verilogでは、ネット宣言時に遅延を指定することで配線遅延をモデル化できます。

例えば、次のようなコードになります。

module wire_delay(input a, b, output y);
    wire #3 w1;
    wire #2 w2;

    assign w1 = a & b;
    assign w2 = a | b;
    assign y = w1 ^ w2;
endmodule

このモジュールでは、w1に3時間単位、w2に2時間単位の配線遅延を設定しています。

テストベンチで動作を確認しましょう。

module testbench;
    reg a, b;
    wire y;

    wire_delay dut(.a(a), .b(b), .y(y));

    initial begin
        $monitor("Time=%0t a=%b b=%b y=%b", $time, a, b, y);
        a = 0; b = 0;
        #10 a = 1;
        #10 b = 1;
        #10 a = 0;
        #10 $finish;
    end
endmodule

実行結果は次のようになります。

Time=0 a=0 b=0 y=x
Time=2 a=0 b=0 y=0
Time=10 a=1 b=0 y=0
Time=12 a=1 b=0 y=1
Time=20 a=1 b=1 y=1
Time=22 a=1 b=1 y=0
Time=23 a=1 b=1 y=1
Time=30 a=0 b=1 y=1
Time=32 a=0 b=1 y=0
Time=33 a=0 b=1 y=1

配線遅延により、yの値が複雑に変化していることがわかります。

○サンプルコード6:FPGAに最適化された遅延設計

FPGAデバイスでは、特有の遅延特性を考慮した設計が求められます。

FPGAの内部構造に合わせて遅延を最適化することで、より効率的な回路を実現できます。

例えば、ルックアップテーブル(LUT)を使用した遅延設計を考えてみましょう。

module fpga_optimized_delay
    (input clk, input [3:0] data, output reg [3:0] out);

    (* ram_style = "distributed" *)
    reg [3:0] delay_line [0:7];

    always @(posedge clk) begin
        delay_line[0] <= data;
        delay_line[1] <= delay_line[0];
        delay_line[2] <= delay_line[1];
        delay_line[3] <= delay_line[2];
        delay_line[4] <= delay_line[3];
        delay_line[5] <= delay_line[4];
        delay_line[6] <= delay_line[5];
        delay_line[7] <= delay_line[6];
        out <= delay_line[7];
    end
endmodule

このモジュールでは、FPGAのLUTを利用して8クロックサイクルの遅延を実現しています。

ram_style = "distributed"属性を使用することで、LUTベースのメモリ実装を指示しています。

テストベンチで動作を確認しましょう。

module testbench;
    reg clk;
    reg [3:0] data;
    wire [3:0] out;

    fpga_optimized_delay dut(.clk(clk), .data(data), .out(out));

    always #5 clk = ~clk;

    initial begin
        $monitor("Time=%0t data=%h out=%h", $time, data, out);
        clk = 0; data = 4'h0;
        #10 data = 4'h1;
        #10 data = 4'h2;
        #10 data = 4'h3;
        #100 $finish;
    end
endmodule

実行結果は次のようになります。

Time=0 data=0 out=x
Time=10 data=1 out=x
Time=20 data=2 out=x
Time=30 data=3 out=x
Time=80 data=3 out=0
Time=90 data=3 out=1
Time=100 data=3 out=2
Time=110 data=3 out=3

8クロックサイクル後に入力データが出力に反映されていることがわかります。

FPGAの内部リソースを効率的に利用した遅延設計の一例です。

○サンプルコード7:複雑な回路での遅延計算テクニック

複雑な回路では、複数の遅延要素が組み合わさることがあります。

そうした場合、全体の遅延を正確に計算し、表現することが重要です。

例えば、複数のゲートを経由する信号の遅延を考えてみましょう。

module complex_delay_calculation
    (input a, b, c, d, output y);

    wire w1, w2, w3;

    assign #2 w1 = a & b;
    assign #3 w2 = c | d;
    assign #1 w3 = w1 ^ w2;
    assign #2 y = ~w3;
endmodule

このモジュールでは、複数のゲートを経由する信号の遅延を表現しています。

全体の遅延は各ゲートの遅延の合計となりますが、並列処理される部分がある点に注意が必要です。

テストベンチで動作を確認しましょう。

module testbench;
    reg a, b, c, d;
    wire y;

    complex_delay_calculation dut(.a(a), .b(b), .c(c), .d(d), .y(y));

    initial begin
        $monitor("Time=%0t a=%b b=%b c=%b d=%b y=%b", $time, a, b, c, d, y);
        a = 0; b = 0; c = 0; d = 0;
        #10 a = 1; b = 1;
        #10 c = 1; d = 1;
        #10 a = 0; c = 0;
        #20 $finish;
    end
endmodule

実行結果は次のようになります。

Time=0 a=0 b=0 c=0 d=0 y=x
Time=8 a=0 b=0 c=0 d=0 y=1
Time=10 a=1 b=1 c=0 d=0 y=1
Time=12 a=1 b=1 c=0 d=0 y=0
Time=20 a=1 b=1 c=1 d=1 y=0
Time=23 a=1 b=1 c=1 d=1 y=1
Time=30 a=0 b=1 c=0 d=1 y=1
Time=35 a=0 b=1 c=0 d=1 y=0

この結果から、入力の変化から出力の変化までの遅延が、経路によって異なることがわかります。

複雑な回路での遅延計算では、クリティカルパスを特定し、最大遅延を正確に見積もることが重要です。

○サンプルコード8:モジュール間通信の遅延管理

大規模な設計では、複数のモジュールが相互に通信を行います。

モジュール間の信号伝達にも遅延が発生するため、適切な管理が必要です。

例えば、パイプライン処理を行う2つのモジュール間の通信を考えてみましょう。

module sender(input clk, input [7:0] data, output reg [7:0] out);
    always @(posedge clk) begin
        out <= #2 data;  // 2単位時間の遅延を持つ出力
    end
endmodule

module receiver(input clk, input [7:0] in, output reg [7:0] result);
    always @(posedge clk) begin
        result <= #1 in + 8'd1;  // 1単位時間の遅延を持つ処理
    end
endmodule

module top_module(input clk, input [7:0] data, output [7:0] result);
    wire [7:0] intermediate;

    sender s(.clk(clk), .data(data), .out(intermediate));
    receiver r(.clk(clk), .in(intermediate), .result(result));
endmodule

このモジュール構成では、senderモジュールからreceiverモジュールへのデータ転送に2単位時間の遅延が発生し、receiverモジュールでの処理に1単位時間の遅延が発生します。

テストベンチで動作を確認しましょう。

module testbench;
    reg clk;
    reg [7:0] data;
    wire [7:0] result;

    top_module dut(.clk(clk), .data(data), .result(result));

    always #5 clk = ~clk;

    initial begin
        $monitor("Time=%0t data=%d result=%d", $time, data, result);
        clk = 0; data = 8'd0;
        #10 data = 8'd10;
        #10 data = 8'd20;
        #10 data = 8'd30;
        #30 $finish;
    end
endmodule

実行結果は次のようになります。

Time=0 data=0 result=x
Time=10 data=10 result=x
Time=20 data=20 result=x
Time=23 data=20 result=1
Time=30 data=30 result=1
Time=33 data=30 result=11
Time=43 data=30 result=21
Time=53 data=30 result=31

モジュール間の遅延により、入力データの変化が結果に反映されるまでに複数のクロックサイクルを要することがわかります。

モジュール間通信の遅延を適切に管理することで、大規模な設計でも正確なタイミング制御が可能になります。

○サンプルコード9:inertialとtransport遅延の使い分け

Verilogでは、inertial遅延とtransport遅延という2種類の遅延モデルが存在します。

inertial遅延はグリッチを除去する効果がある一方、transport遅延はすべての信号変化を保持します。

適切な遅延モデルを選択することで、より現実的な回路動作をシミュレーションできます。

module delay_models
    (input a, output y_inertial, output y_transport);

    assign #(2, 1) y_inertial = a;   // Rise 2, Fall 1 (inertial)
    assign #(2, 1) y_transport = a;  // Rise 2, Fall 1 (transport)

    specify
        (a => y_transport) = (2, 1);
        $setup(a, posedge y_transport, 1);
    endspecify
endmodule

このモジュールでは、同じ遅延値を持つinertial遅延とtransport遅延を定義しています。

テストベンチで両者の動作の違いを確認しましょう。

module testbench;
    reg a;
    wire y_inertial, y_transport;

    delay_models dut(.a(a), .y_inertial(y_inertial), .y_transport(y_transport));

    initial begin
        $monitor("Time=%0t a=%b y_inertial=%b y_transport=%b", 
                 $time, a, y_inertial, y_transport);
        a = 0;
        #5 a = 1;
        #1 a = 0;
        #1 a = 1;
        #5 a = 0;
        #10 $finish;
    end
endmodule

実行結果は次のようになります。

Time=0 a=0 y_inertial=0 y_transport=0
Time=5 a=1 y_inertial=0 y_transport=0
Time=6 a=0 y_inertial=0 y_transport=0
Time=7 a=1 y_inertial=0 y_transport=0
Time=7 a=1 y_inertial=1 y_transport=1
Time=8 a=1 y_inertial=1 y_transport=0
Time=9 a=1 y_inertial=1 y_transport=1
Time=12 a=0 y_inertial=1 y_transport=1
Time=13 a=0 y_inertial=0 y_transport=0

inertial遅延ではグリッチが除去されていますが、transport遅延ではすべての信号変化が保持されていることがわかります。

回路の特性や設計意図に応じて、適切な遅延モデルを選択することが重要です。

○サンプルコード10:SDF(Standard Delay Format)の活用法

SDF(Standard Delay Format)は、回路の詳細な遅延情報を記述するための業界標準フォーマットです。

SDFを活用することで、実際のデバイスの特性に基づいた精密なタイミングシミュレーションが可能になります。

SDFファイルは通常、合成ツールや配置配線ツールによって自動生成されますが、手動で作成することも可能です。

まず、簡単なVerilogモジュールを作成し、対応するSDFファイルを用意します。

module sdf_example(input a, b, output y);
    and #1 g1(w, a, b);
    not #1 g2(y, w);
endmodule

このモジュールに対応するSDFファイル(例:delays.sdf)を作成します。

(DELAYFILE
    (SDFVERSION "3.0")
    (DESIGN "sdf_example")
    (DATE "2023-04-01 12:00:00")
    (VENDOR "Example Vendor")
    (PROGRAM "Example Program")
    (VERSION "1.0")
    (DIVIDER /)
    (VOLTAGE 1.8:1.8:1.8)
    (PROCESS "typical")
    (TEMPERATURE 25.0:25.0:25.0)
    (TIMESCALE 1ns)
    (CELL
        (CELLTYPE "sdf_example")
        (INSTANCE *)
        (DELAY
            (ABSOLUTE
                (IOPATH a y (3:3:3) (2:2:2))
                (IOPATH b y (3:3:3) (2:2:2))
            )
        )
    )
)

このSDFファイルでは、入力aまたはbから出力yまでの遅延を、立ち上がり時3ns、立ち下がり時2nsと指定しています。

テストベンチでSDFファイルを使用するには、$sdf_annotateシステムタスクを使用します。

module testbench;
    reg a, b;
    wire y;

    sdf_example dut(.a(a), .b(b), .y(y));

    initial begin
        $sdf_annotate("delays.sdf", dut);
        $monitor("Time=%0t a=%b b=%b y=%b", $time, a, b, y);
        a = 0; b = 0;
        #10 a = 1;
        #10 b = 1;
        #10 a = 0;
        #10 b = 0;
        #10 $finish;
    end
endmodule

このテストベンチを実行すると、SDFファイルで指定した遅延が反映されたシミュレーション結果が得られます。

Time=0 a=0 b=0 y=1
Time=10 a=1 b=0 y=1
Time=20 a=1 b=1 y=1
Time=23 a=1 b=1 y=0
Time=30 a=0 b=1 y=0
Time=32 a=0 b=1 y=1
Time=40 a=0 b=0 y=1

SDFを活用することで、理想的な遅延ではなく、実際のデバイスの特性に基づいたより現実的なシミュレーションが可能になります。

FPGAやASIC設計において、SDFは非常に重要な役割を果たします。

製造プロセスの変動や動作条件の影響を考慮したタイミング解析が可能となり、より信頼性の高い設計を実現できます。

SDFの活用には注意点もあります。

例えば、SDFファイルの遅延値とVerilogコードの遅延値が異なる場合、一般的にSDFファイルの値が優先されます。

また、複雑な回路では膨大な量の遅延情報が生成されるため、シミュレーション時間が大幅に増加する可能性があります。

SDFを効果的に活用するためには、次のポイントに注意しましょう。

  1. SDFファイルの整合性を確認する -> 使用するSDFファイルが対象の回路設計と正確に対応していることを確認します。
  2. クリティカルパスに注目する -> 全ての遅延情報を詳細に解析するのではなく、クリティカルパスに焦点を当てることで、効率的なタイミング解析が可能になります。
  3. コーナーケースを考慮する -> 最悪ケース(Worst Case)や最良ケース(Best Case)など、異なる動作条件下でのSDFファイルを用意し、多角的な解析を行います。
  4. 階層的なアプローチを取る -> 大規模な設計では、トップレベルの解析と詳細なブロックレベルの解析を組み合わせることで、効率的なタイミング検証が可能になります。

SDFの活用は、Verilogによる回路設計において、理想的な動作と実際のハードウェアの動作のギャップを埋める重要な手段です。

適切に使用することで、信頼性の高い、高性能な回路設計が可能になります。

●伝播遅延を考慮した高度な回路設計の極意

Verilogで伝播遅延を扱う基本テクニックを習得したら、次は高度な回路設計に挑戦しましょう。

伝播遅延を考慮した設計は、高性能で信頼性の高い回路を生み出す鍵となります。

ここでは、プロの設計者が駆使する3つの極意を紹介します。

○クリティカルパス最適化

クリティカルパスとは、回路内で最も遅延の大きい経路のことです。

このパスの最適化が、回路全体の性能向上につながります。

クリティカルパス最適化の第一歩は、タイミング解析ツールを使用してパスを特定することです。

例えば、次のような回路があるとします。

module critical_path_example(
    input clk,
    input [7:0] a, b,
    output reg [7:0] result
);
    reg [7:0] temp1, temp2;

    always @(posedge clk) begin
        temp1 <= a + b;
        temp2 <= temp1 * 2;
        result <= temp2 - 1;
    end
endmodule

この回路では、加算、乗算、減算の3つの演算が連続して行われています。

タイミング解析の結果、temp1からresultまでの経路がクリティカルパスだと判明したとします。

最適化の一例として、パイプライン化を適用してみましょう。

module optimized_critical_path(
    input clk,
    input [7:0] a, b,
    output reg [7:0] result
);
    reg [7:0] temp1, temp2;

    always @(posedge clk) begin
        temp1 <= a + b;
        temp2 <= temp1;
        result <= (temp2 << 1) - 1;  // 乗算を左シフトに置き換え
    end
endmodule

この最適化では、乗算を左シフト演算に置き換え、計算を3段階に分けています。

結果、クリティカルパスの遅延が減少し、回路の動作周波数を上げることが可能になります。

○クロックドメイン間の遅延管理

複数のクロックドメインを持つ設計では、ドメイン間の信号伝達に特別な注意が必要です。

異なる周波数や位相のクロックを使用する部分間でデータをやり取りする際、メタステーブル状態や信号の取りこぼしが発生する可能性があります。

クロックドメイン間の遅延管理には、非同期FIFOの使用が効果的です。

ここでは簡単な非同期FIFOの実装例を紹介します。

module async_fifo #(
    parameter DATA_WIDTH = 8,
    parameter FIFO_DEPTH = 16
) (
    input wr_clk,
    input rd_clk,
    input reset,
    input [DATA_WIDTH-1:0] wr_data,
    input wr_en,
    input rd_en,
    output reg [DATA_WIDTH-1:0] rd_data,
    output full,
    output empty
);
    // FIFOメモリ
    reg [DATA_WIDTH-1:0] mem [0:FIFO_DEPTH-1];

    // 読み書きポインタ
    reg [$clog2(FIFO_DEPTH):0] wr_ptr, rd_ptr;
    reg [$clog2(FIFO_DEPTH):0] wr_ptr_gray, rd_ptr_gray;

    // ポインタの同期
    reg [$clog2(FIFO_DEPTH):0] wr_ptr_sync [1:0];
    reg [$clog2(FIFO_DEPTH):0] rd_ptr_sync [1:0];

    // グレイコード変換関数
    function [$clog2(FIFO_DEPTH):0] bin2gray(input [$clog2(FIFO_DEPTH):0] bin);
        bin2gray = (bin >> 1) ^ bin;
    endfunction

    // 書き込み制御
    always @(posedge wr_clk or posedge reset) begin
        if (reset) begin
            wr_ptr <= 0;
            wr_ptr_gray <= 0;
        end else if (wr_en && !full) begin
            mem[wr_ptr[$clog2(FIFO_DEPTH)-1:0]] <= wr_data;
            wr_ptr <= wr_ptr + 1;
            wr_ptr_gray <= bin2gray(wr_ptr + 1);
        end
    end

    // 読み出し制御
    always @(posedge rd_clk or posedge reset) begin
        if (reset) begin
            rd_ptr <= 0;
            rd_ptr_gray <= 0;
        end else if (rd_en && !empty) begin
            rd_data <= mem[rd_ptr[$clog2(FIFO_DEPTH)-1:0]];
            rd_ptr <= rd_ptr + 1;
            rd_ptr_gray <= bin2gray(rd_ptr + 1);
        end
    end

    // ポインタの同期
    always @(posedge wr_clk or posedge reset) begin
        if (reset)
            {wr_ptr_sync[1], wr_ptr_sync[0]} <= 0;
        else
            {wr_ptr_sync[1], wr_ptr_sync[0]} <= {wr_ptr_sync[0], rd_ptr_gray};
    end

    always @(posedge rd_clk or posedge reset) begin
        if (reset)
            {rd_ptr_sync[1], rd_ptr_sync[0]} <= 0;
        else
            {rd_ptr_sync[1], rd_ptr_sync[0]} <= {rd_ptr_sync[0], wr_ptr_gray};
    end

    // フル・エンプティフラグの生成
    assign full = (wr_ptr_gray == {~rd_ptr_sync[1][$clog2(FIFO_DEPTH):$clog2(FIFO_DEPTH)-1], rd_ptr_sync[1][$clog2(FIFO_DEPTH)-2:0]});
    assign empty = (rd_ptr_gray == wr_ptr_sync[1]);
endmodule

この非同期FIFOは、書き込みと読み出しで異なるクロックドメインを使用できます。

グレイコードを使用してポインタを同期させることで、メタステーブル状態のリスクを最小限に抑えています。

○温度・電圧変動に強い遅延マージン設計のコツ

実際の動作環境では、温度や電圧の変動が伝播遅延に影響を与えます。

信頼性の高い設計を行うには、これらの変動を考慮したマージン設計が欠かせません。

温度・電圧変動に強い設計のコツは、最悪ケース条件でのタイミング解析です。

例えば、高温・低電圧条件下での動作を想定し、十分なマージンを持たせた設計を行います。

module margin_design_example(
    input clk,
    input reset,
    input [7:0] data_in,
    output reg [7:0] data_out
);
    reg [7:0] stage1, stage2;

    always @(posedge clk or posedge reset) begin
        if (reset) begin
            stage1 <= 8'b0;
            stage2 <= 8'b0;
            data_out <= 8'b0;
        end else begin
            stage1 <= data_in;
            stage2 <= stage1;
            data_out <= stage2;
        end
    end
endmodule

この例では、データパスを3段のレジスタに分割しています。

各ステージ間に十分なマージンを設けることで、温度や電圧の変動による遅延増加を吸収できます。

実際の設計では、静的タイミング解析(STA)ツールを使用して、様々な動作条件下でのタイミングマージンを評価します。

●よくある伝播遅延関連のエラーと対処法

伝播遅延を考慮した設計を行う上で、いくつかの典型的なエラーパターンがあります。

ここでは、よく遭遇するエラーとその対処法を紹介します。

○タイミング違反/原因と解決策

タイミング違反は、信号が期待される時間内に目的地に到達しない状況を指します。

主な原因として、クリティカルパスが長すぎる、クロック周波数が高すぎる、などが挙げられます。

例えば、次のような回路でタイミング違反が発生したとします。

module timing_violation_example(
    input clk,
    input [31:0] a, b,
    output reg [63:0] result
);
    always @(posedge clk) begin
        result <= a * b + result;  // 複雑な演算によるタイミング違反
    end
endmodule

この回路では、乗算と加算が1クロックサイクル内に完了する必要があります。

高周波数で動作させようとすると、タイミング違反が発生する可能性が高くなります。

解決策として、演算をパイプライン化する方法があります。

module timing_violation_fixed(
    input clk,
    input [31:0] a, b,
    output reg [63:0] result
);
    reg [63:0] mul_result;

    always @(posedge clk) begin
        mul_result <= a * b;
        result <= mul_result + result;
    end
endmodule

この修正版では、乗算と加算を2段階に分けています。

各ステージの処理量が減少し、タイミング違反のリスクが軽減されます。

○グリッチの発生/検出と除去テクニック

グリッチとは、信号が安定するまでの間に発生する不要なパルスのことです。

組み合わせ論理回路で特に発生しやすく、誤動作の原因となることがあります。

グリッチの検出には、シミュレーションによる波形観察が効果的です。

例えば、次のような回路でグリッチが発生する可能性があります。

module glitch_example(
    input a, b, c,
    output y
);
    assign y = (a & b) | (b & c) | (a & c);
endmodule

この回路では、入力の変化のタイミングによっては、出力yに短いパルス(グリッチ)が発生する可能性があります。

グリッチの除去には、いくつかの方法があります。

一つは、フリップフロップを使用して信号を安定化させる方法です。

module glitch_fixed(
    input clk,
    input a, b, c,
    output reg y
);
    wire temp;
    assign temp = (a & b) | (b & c) | (a & c);

    always @(posedge clk) begin
        y <= temp;
    end
endmodule

この修正版では、組み合わせ論理の出力をフリップフロップでラッチすることで、グリッチを除去しています。

ただし、この方法ではレイテンシが増加するため、適用には注意が必要です。

○シミュレーションと実機の乖離/調整方法

シミュレーション結果と実機の動作に乖離が生じることがあります。

この問題は、シミュレーションモデルが実際のハードウェアの特性を完全には再現できていないことが主な原因です。

例えば、次のような回路があるとします。

module simulation_vs_hardware(
    input clk,
    input [7:0] data_in,
    output reg [7:0] data_out
);
    always @(posedge clk) begin
        data_out <= #2 data_in;  // 2単位時間の遅延
    end
endmodule

シミュレーションでは期待通りの動作を示すこの回路が、実機では正しく動作しないことがあります。

実際のハードウェアでは、配線遅延やセットアップ/ホールド時間など、様々な要因が影響するためです。

この問題に対処するためには、次のような方法が効果的です。

  1. バックアノテーションを活用する -> 配置配線後の実際の遅延情報をシミュレーションに反映させます。
  2. FPGAの場合、インシステム・アナライザを使用する -> 実際のハードウェア上で信号の動きを観察し、問題を特定します。
  3. マージンを持たせた設計を行う -> 最悪ケースを想定し、十分な余裕を持たせた設計を心がけます。
  4. プロトタイピングを積極的に行う -> 早い段階で実機での動作確認を行い、問題を早期に発見します。

例えば、上記の回路を次のように修正することで、実機での動作の信頼性を高めることができます。

module simulation_vs_hardware_improved(
    input clk,
    input [7:0] data_in,
    output reg [7:0] data_out
);
    (* keep = "true" *) reg [7:0] data_reg;

    always @(posedge clk) begin
        data_reg <= data_in;
        data_out <= data_reg;
    end
endmodule

この改良版では、中間レジスタを追加し、2段のレジスタを使用しています。

また、keep属性を使用することで、合成ツールによる最適化を制限し、意図した構造を保持しています。

このアプローチにより、タイミングマージンが増加し、実機での信頼性が向上します。

●伝播遅延の応用例

Verilogにおける伝播遅延の基本を理解したら、次は応用技術に挑戦しましょう。

高度な設計テクニックを駆使することで、より効率的で高性能な回路を実現できます。

ここでは、4つの実践的なサンプルコードを通じて、伝播遅延の創造的な活用方法を解説していきます。

○サンプルコード11:パイプライン処理による高速化

パイプライン処理は、複雑な演算を複数のステージに分割し、各ステージを並列に実行することで全体の処理速度を向上させる技術です。

伝播遅延を考慮しながらパイプラインを設計することで、高速かつ効率的な回路を実現できます。

module pipeline_example(
    input clk,
    input reset,
    input [7:0] data_in,
    output reg [7:0] data_out
);
    reg [7:0] stage1, stage2, stage3;

    always @(posedge clk or posedge reset) begin
        if (reset) begin
            stage1 <= 8'b0;
            stage2 <= 8'b0;
            stage3 <= 8'b0;
            data_out <= 8'b0;
        end else begin
            stage1 <= data_in + 8'd1;
            stage2 <= stage1 * 2;
            stage3 <= stage2 - 8'd3;
            data_out <= stage3;
        end
    end
endmodule

このコードでは、データ処理を4つのステージに分割しています。

各ステージで異なる演算を行い、結果を次のステージに渡していきます。

パイプライン処理により、1クロックサイクルごとに新しい入力データを受け取ることができ、スループットが向上します。

テストベンチを作成して動作を確認しましょう。

module testbench;
    reg clk, reset;
    reg [7:0] data_in;
    wire [7:0] data_out;

    pipeline_example dut(
        .clk(clk),
        .reset(reset),
        .data_in(data_in),
        .data_out(data_out)
    );

    always #5 clk = ~clk;

    initial begin
        clk = 0;
        reset = 1;
        data_in = 8'd0;
        #10 reset = 0;
        #10 data_in = 8'd10;
        #10 data_in = 8'd20;
        #10 data_in = 8'd30;
        #40 $finish;
    end

    initial begin
        $monitor("Time=%0t data_in=%d data_out=%d", $time, data_in, data_out);
    end
endmodule

実行結果は次のようになります。

Time=0 data_in=0 data_out=0
Time=20 data_in=10 data_out=0
Time=30 data_in=20 data_out=0
Time=40 data_in=30 data_out=0
Time=50 data_in=30 data_out=19
Time=60 data_in=30 data_out=39
Time=70 data_in=30 data_out=59

パイプライン処理により、入力から出力まで4クロックサイクルのレイテンシがありますが、毎サイクル新しい結果が出力されています。

○サンプルコード12:遅延を利用したタイミング調整回路

伝播遅延を積極的に利用して、信号のタイミングを微調整する技術も重要です。

特に、高速なインターフェースや精密なタイミング制御が必要な場面で役立ちます。

module timing_adjustment(
    input clk,
    input data_in,
    output reg [3:0] data_out
);
    wire delayed_data [3:0];

    // 異なる遅延を持つ配線を使用
    assign #1 delayed_data[0] = data_in;
    assign #2 delayed_data[1] = data_in;
    assign #3 delayed_data[2] = data_in;
    assign #4 delayed_data[3] = data_in;

    always @(posedge clk) begin
        data_out[0] <= delayed_data[0];
        data_out[1] <= delayed_data[1];
        data_out[2] <= delayed_data[2];
        data_out[3] <= delayed_data[3];
    end
endmodule

この回路では、入力信号を異なる遅延で4本の配線に分岐させています。

クロックの立ち上がりエッジでサンプリングすることで、微妙に異なるタイミングの信号を取得できます。

テストベンチで動作を確認しましょう。

module testbench;
    reg clk, data_in;
    wire [3:0] data_out;

    timing_adjustment dut(
        .clk(clk),
        .data_in(data_in),
        .data_out(data_out)
    );

    always #5 clk = ~clk;

    initial begin
        clk = 0;
        data_in = 0;
        #10 data_in = 1;
        #10 data_in = 0;
        #10 data_in = 1;
        #20 $finish;
    end

    initial begin
        $monitor("Time=%0t data_in=%b data_out=%b", $time, data_in, data_out);
    end
endmodule

実行結果は次のようになります。

Time=0 data_in=0 data_out=xxxx
Time=10 data_in=1 data_out=0000
Time=15 data_in=1 data_out=1000
Time=20 data_in=0 data_out=1100
Time=25 data_in=0 data_out=1110
Time=30 data_in=1 data_out=0111
Time=35 data_in=1 data_out=1000
Time=40 data_in=1 data_out=1100
Time=45 data_in=1 data_out=1110

data_outの各ビットが順次変化していく様子が観察できます。

この技術は、高速シリアル通信などでビットの位相を調整する際に活用できます。

○サンプルコード13:遅延ベースの乱数生成器

伝播遅延の不確定性を利用して、簡易的な乱数生成器を作成することができます。

この技術は、完全な乱数ではありませんが、シミュレーションやテスト用途には十分な場合があります。

module delay_based_rng(
    input clk,
    input reset,
    output reg [7:0] random_num
);
    wire feedback;

    assign #1.1 feedback = ~(random_num[7] ^ random_num[5] ^ random_num[4] ^ random_num[3]);

    always @(posedge clk or posedge reset) begin
        if (reset)
            random_num <= 8'hAA;  // 初期シード
        else
            random_num <= {random_num[6:0], feedback};
    end
endmodule

この回路では、線形フィードバックシフトレジスタ(LFSR)の原理を利用しています。

フィードバック信号に微小な遅延を加えることで、タイミングの不確定性を導入し、乱数性を向上させています。

テストベンチで動作を確認しましょう。

module testbench;
    reg clk, reset;
    wire [7:0] random_num;

    delay_based_rng dut(
        .clk(clk),
        .reset(reset),
        .random_num(random_num)
    );

    always #5 clk = ~clk;

    initial begin
        clk = 0;
        reset = 1;
        #10 reset = 0;
        #200 $finish;
    end

    initial begin
        $monitor("Time=%0t random_num=%h", $time, random_num);
    end
endmodule

実行結果は次のようになります。

Time=0 random_num=aa
Time=10 random_num=aa
Time=15 random_num=55
Time=25 random_num=aa
Time=35 random_num=55
Time=45 random_num=2a
Time=55 random_num=95
Time=65 random_num=ca
Time=75 random_num=e5
...

生成される数値が予測不可能な順序で変化していることがわかります。

この手法は、完全な乱数生成器ではありませんが、簡易的なランダム性が必要な場面で役立ちます。

○サンプルコード14:遅延を活用したデバウンス回路

機械式スイッチなどの入力信号には、チャタリングと呼ばれる不要な信号の揺れが発生することがあります。

遅延を活用したデバウンス回路を実装することで、安定した入力信号を得ることができます。

module debounce_circuit(
    input clk,
    input noisy_input,
    output reg clean_output
);
    reg [2:0] shift_reg;

    always @(posedge clk) begin
        shift_reg <= {shift_reg[1:0], noisy_input};
        if (shift_reg == 3'b111)
            clean_output <= 1'b1;
        else if (shift_reg == 3'b000)
            clean_output <= 1'b0;
    end
endmodule

この回路では、入力信号を3ビットのシフトレジスタに取り込み、3クロック連続で同じ値が続いた場合にのみ出力を変更します。

これで、短時間のノイズや揺れを除去できます。

テストベンチで動作を確認しましょう。

module testbench;
    reg clk, noisy_input;
    wire clean_output;

    debounce_circuit dut(
        .clk(clk),
        .noisy_input(noisy_input),
        .clean_output(clean_output)
    );

    always #5 clk = ~clk;

    initial begin
        clk = 0;
        noisy_input = 0;
        #10 noisy_input = 1;
        #2 noisy_input = 0;
        #3 noisy_input = 1;
        #20 noisy_input = 0;
        #2 noisy_input = 1;
        #3 noisy_input = 0;
        #50 $finish;
    end

    initial begin
        $monitor("Time=%0t noisy_input=%b clean_output=%b", $time, noisy_input, clean_output);
    end
endmodule

実行結果は次のようになります。

Time=0 noisy_input=0 clean_output=x
Time=10 noisy_input=1 clean_output=0
Time=12 noisy_input=0 clean_output=0
Time=15 noisy_input=1 clean_output=0
Time=25 noisy_input=1 clean_output=1
Time=35 noisy_input=0 clean_output=1
Time=37 noisy_input=1 clean_output=1
Time=40 noisy_input=0 clean_output=1
Time=55 noisy_input=0 clean_output=0

noisy_inputの短時間の変動がclean_outputに反映されていないことがわかります。

この技術は、機械式スイッチやセンサーからの入力を扱う際に非常に有用です。

まとめ

Verilogにおける伝播遅延は、単なる制約ではなく、創造的に活用できる強力なツールです。

この技術を適切に組み合わせることで、高性能で信頼性の高い回路設計が可能になります。

本記事で紹介した技術を基礎として、さらなる学習と実践を重ねることで、より複雑で高度な設計課題にも対応できる実力を身につけることができるはずです。

Verilogを使った回路設計の奥深さと面白さを、ぜひ体感してください。