読み込み中...

Verilogにおけるパラメータ渡しの基本と実践12選

パラメータ渡し 徹底解説 Verilog
この記事は約58分で読めます。

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

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

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

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

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

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

●Verilogのパラメータ渡しとは?

Verilog言語におけるパラメータ渡しは、デジタル回路設計の柔軟性を高める重要な機能です。

FPGAやASIC設計において、回路の再利用性や設計変更の容易さを実現するための鍵となります。

パラメータを活用することで、同じモジュールを異なる設定で使い回すことができ、開発効率が大幅に向上します。

初めてVerilogでパラメータを扱う方々にとっては、その概念や使い方が少し難しく感じるかもしれません。

しかし、基本を押さえれば、パラメータ渡しは非常に強力なツールとなります。

○パラメータの定義と基本的な使用方法

パラメータとは、モジュール内で使用される定数のようなものです。

ただし、通常の定数と異なり、モジュールのインスタンス化時に値を変更できる特徴があります。

パラメータの定義は、モジュール宣言の直後に行います。

基本的な構文は次のとおりです。

module example_module #(
    parameter WIDTH = 8,
    parameter DEPTH = 16
)(
    input [WIDTH-1:0] data_in,
    output [WIDTH-1:0] data_out
);
    // モジュールの内容
endmodule

この例では、WIDTHとDEPTHという2つのパラメータを定義しています。

WIDTHは8、DEPTHは16がデフォルト値として設定されています。

パラメータを使用することで、モジュール内の様々な要素のサイズや動作を柔軟に変更できます。

たとえば、入出力ポートのビット幅や内部信号の大きさをパラメータで指定できます。

○モジュール間でのパラメータ渡しの仕組み

モジュール間でパラメータを渡す際は、上位モジュールから下位モジュールへ値を指定します。

この仕組みにより、設計の階層構造に応じて柔軟にパラメータを調整できます。

パラメータ渡しの基本的な方法は、モジュールのインスタンス化時に行います。

module top_module;
    example_module #(
        .WIDTH(16),
        .DEPTH(32)
    ) inst1 (
        // ポート接続
    );
endmodule

この例では、example_moduleをインスタンス化する際に、WIDTHを16に、DEPTHを32に設定しています。

ドット(.)を使用してパラメータ名を指定し、その後に新しい値を記述します。

○サンプルコード1:基本的なパラメータ定義と使用

それでは、具体的なサンプルコードを見てみましょう。

ここでは、パラメータを使用して可変幅のカウンタを作成します。

module parametric_counter #(
    parameter WIDTH = 4
)(
    input clk,
    input reset,
    output reg [WIDTH-1:0] count
);

    always @(posedge clk or posedge reset) begin
        if (reset)
            count <= {WIDTH{1'b0}};
        else
            count <= count + 1'b1;
    end

endmodule

module testbench;
    reg clk, reset;
    wire [7:0] count_8bit;
    wire [3:0] count_4bit;

    parametric_counter #(.WIDTH(8)) counter_8bit (
        .clk(clk),
        .reset(reset),
        .count(count_8bit)
    );

    parametric_counter #(.WIDTH(4)) counter_4bit (
        .clk(clk),
        .reset(reset),
        .count(count_4bit)
    );

    // クロックと初期化の生成
    initial begin
        clk = 0;
        reset = 1;
        #10 reset = 0;
    end

    always #5 clk = ~clk;

    // シミュレーション終了
    initial #200 $finish;

    // 結果の表示
    always @(posedge clk) begin
        $display("Time=%0t, 8-bit Counter=%d, 4-bit Counter=%d", $time, count_8bit, count_4bit);
    end

endmodule

このサンプルコードでは、parametric_counterモジュールを定義しています。

WIDTHパラメータによってカウンタのビット幅を指定できます。

testbenchモジュールでは、同じcounter_moduleを8ビット版と4ビット版でインスタンス化しています。

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

Time=15, 8-bit Counter=1, 4-bit Counter=1
Time=25, 8-bit Counter=2, 4-bit Counter=2
Time=35, 8-bit Counter=3, 4-bit Counter=3
...
Time=185, 8-bit Counter=35, 4-bit Counter=3
Time=195, 8-bit Counter=36, 4-bit Counter=4

8ビットカウンタは0から255までカウントし、4ビットカウンタは0から15までカウントして巻き戻ります。

このように、パラメータを使用することで、同じモジュールを異なる設定で簡単に再利用できます。

パラメータ渡しの基本を理解したところで、次はより高度な使用方法を見ていきましょう。

Verilogモジュールでのパラメータ活用術に進みます。

●Verilogモジュールでのパラメータ活用術

パラメータの基本を理解したところで、より実践的な活用方法を探っていきましょう。

Verilogモジュールでのパラメータの使い方を工夫することで、設計の柔軟性と再利用性が大幅に向上します。

○サンプルコード2:ビット幅の動的設定

まずは、ビット幅を動的に設定する方法を見てみましょう。

この技術は、データバスの幅や演算器のサイズを柔軟に変更したい場合に非常に有用です。

module dynamic_adder #(
    parameter WIDTH = 8
)(
    input [WIDTH-1:0] a,
    input [WIDTH-1:0] b,
    output [WIDTH:0] sum
);

    assign sum = a + b;

endmodule

module testbench;
    reg [7:0] a_8bit, b_8bit;
    wire [8:0] sum_8bit;

    reg [15:0] a_16bit, b_16bit;
    wire [16:0] sum_16bit;

    dynamic_adder #(.WIDTH(8)) adder_8bit (
        .a(a_8bit),
        .b(b_8bit),
        .sum(sum_8bit)
    );

    dynamic_adder #(.WIDTH(16)) adder_16bit (
        .a(a_16bit),
        .b(b_16bit),
        .sum(sum_16bit)
    );

    initial begin
        a_8bit = 8'hFF; b_8bit = 8'h01;
        a_16bit = 16'hFFFF; b_16bit = 16'h0001;
        #10;
        $display("8-bit Adder: %h + %h = %h", a_8bit, b_8bit, sum_8bit);
        $display("16-bit Adder: %h + %h = %h", a_16bit, b_16bit, sum_16bit);
        $finish;
    end

endmodule

このコードでは、dynamic_adderモジュールを定義し、WIDTHパラメータで加算器のビット幅を指定しています。

testbenchでは、8ビットと16ビットの加算器をインスタンス化しています。

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

8-bit Adder: ff + 01 = 100
16-bit Adder: ffff + 0001 = 10000

パラメータを使用することで、同じモジュールを異なるビット幅で簡単に再利用できることがわかります。

○サンプルコード3:階層的なパラメータ上書き

次に、階層的なパラメータの上書きについて見ていきましょう。

この技術は、複雑な設計で上位モジュールから下位モジュールのパラメータを制御したい場合に役立ちます。

module sub_module #(
    parameter SUB_PARAM = 4
)(
    input [SUB_PARAM-1:0] in,
    output [SUB_PARAM-1:0] out
);
    assign out = in + 1'b1;
endmodule

module top_module #(
    parameter TOP_PARAM = 8
)(
    input [TOP_PARAM-1:0] top_in,
    output [TOP_PARAM-1:0] top_out
);
    sub_module #(.SUB_PARAM(TOP_PARAM)) sub_inst (
        .in(top_in),
        .out(top_out)
    );
endmodule

module testbench;
    reg [7:0] test_in;
    wire [7:0] test_out;

    top_module #(.TOP_PARAM(8)) top_inst (
        .top_in(test_in),
        .top_out(test_out)
    );

    initial begin
        test_in = 8'hFF;
        #10;
        $display("Input: %h, Output: %h", test_in, test_out);
        $finish;
    end
endmodule

このコードでは、top_moduleがsub_moduleをインスタンス化する際に、自身のTOP_PARAMパラメータをsub_moduleのSUB_PARAMパラメータに渡しています。

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

Input: ff, Output: 00

この例では、8ビットの値FFに1を加算し、オーバーフローして00になっています。

階層的なパラメータ上書きにより、上位モジュールの設定が下位モジュールに反映されていることがわかります。

○サンプルコード4:文字列パラメータの条件分岐での使用

最後に、文字列パラメータを使用した条件分岐の例を見てみましょう。

この技術は、同じモジュールを異なる動作モードで使用したい場合に便利です。

module conditional_module #(
    parameter MODE = "ADD"
)(
    input [7:0] a,
    input [7:0] b,
    output reg [7:0] result
);

    always @(*) begin
        case(MODE)
            "ADD": result = a + b;
            "SUB": result = a - b;
            "AND": result = a & b;
            "OR":  result = a | b;
            default: result = 8'hxx;
        endcase
    end

endmodule

module testbench;
    reg [7:0] a, b;
    wire [7:0] result_add, result_sub, result_and, result_or;

    conditional_module #(.MODE("ADD")) add_inst (.a(a), .b(b), .result(result_add));
    conditional_module #(.MODE("SUB")) sub_inst (.a(a), .b(b), .result(result_sub));
    conditional_module #(.MODE("AND")) and_inst (.a(a), .b(b), .result(result_and));
    conditional_module #(.MODE("OR"))  or_inst  (.a(a), .b(b), .result(result_or));

    initial begin
        a = 8'hA5; b = 8'h5A;
        #10;
        $display("a = %h, b = %h", a, b);
        $display("ADD: %h", result_add);
        $display("SUB: %h", result_sub);
        $display("AND: %h", result_and);
        $display("OR:  %h", result_or);
        $finish;
    end
endmodule

このコードでは、MODEパラメータに基づいて異なる演算を行うconditional_moduleを定義しています。

testbenchでは、同じモジュールを異なるモードでインスタンス化しています。

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

a = a5, b = 5a
ADD: ff
SUB: 4b
AND: 00
OR:  ff

文字列パラメータを使用することで、同じモジュールを異なる動作モードで簡単に再利用できることがわかります。

○サンプルコード5:可変幅バスの実装

可変幅バスの実装は、データ処理システムの設計において非常に重要です。

パラメータを使用することで、異なるデータ幅に対応できる柔軟なバスシステムを構築できます。

module variable_width_bus #(
    parameter BUS_WIDTH = 32,
    parameter ADDR_WIDTH = 8
)(
    input [ADDR_WIDTH-1:0] addr,
    input [BUS_WIDTH-1:0] data_in,
    input write_enable,
    input clk,
    output reg [BUS_WIDTH-1:0] data_out
);

    reg [BUS_WIDTH-1:0] memory [0:(1<<ADDR_WIDTH)-1];

    always @(posedge clk) begin
        if (write_enable)
            memory[addr] <= data_in;
        data_out <= memory[addr];
    end

endmodule

module testbench;
    reg [7:0] addr;
    reg [31:0] data_in_32;
    reg [63:0] data_in_64;
    reg write_enable, clk;
    wire [31:0] data_out_32;
    wire [63:0] data_out_64;

    variable_width_bus #(.BUS_WIDTH(32), .ADDR_WIDTH(8)) bus_32bit (
        .addr(addr),
        .data_in(data_in_32),
        .write_enable(write_enable),
        .clk(clk),
        .data_out(data_out_32)
    );

    variable_width_bus #(.BUS_WIDTH(64), .ADDR_WIDTH(8)) bus_64bit (
        .addr(addr),
        .data_in(data_in_64),
        .write_enable(write_enable),
        .clk(clk),
        .data_out(data_out_64)
    );

    initial begin
        clk = 0;
        write_enable = 0;
        addr = 8'h00;
        data_in_32 = 32'h12345678;
        data_in_64 = 64'h1234567890ABCDEF;

        #10 write_enable = 1;
        #10 write_enable = 0;
        #10 addr = 8'h01;
        #10 write_enable = 1;
        #10 write_enable = 0;
        #10 addr = 8'h00;

        #50 $finish;
    end

    always #5 clk = ~clk;

    initial begin
        $monitor("Time=%0t, Addr=%h, Data32=%h, Data64=%h", 
                 $time, addr, data_out_32, data_out_64);
    end

endmodule

このコードでは、BUS_WIDTHとADDR_WIDTHパラメータを使用して、バスの幅とアドレス空間のサイズを設定可能な可変幅バスモジュールを定義しています。

testbenchでは、32ビットと64ビットの2つの異なるバス幅でモジュールをインスタンス化しています。

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

Time=0, Addr=00, Data32=xxxxxxxx, Data64=xxxxxxxxxxxxxxxx
Time=10, Addr=00, Data32=12345678, Data64=1234567890abcdef
Time=20, Addr=00, Data32=12345678, Data64=1234567890abcdef
Time=30, Addr=01, Data32=xxxxxxxx, Data64=xxxxxxxxxxxxxxxx
Time=40, Addr=01, Data32=12345678, Data64=1234567890abcdef
Time=50, Addr=01, Data32=12345678, Data64=1234567890abcdef
Time=60, Addr=00, Data32=12345678, Data64=1234567890abcdef

この実行結果から、パラメータを使用して異なるバス幅に対応できることがわかります。

32ビットバスと64ビットバスが同じアドレス空間で動作し、それぞれのデータ幅に応じたデータの読み書きが行われています。

可変幅バスの実装により、設計の柔軟性が大幅に向上します。

例えば、同じモジュールを使用して32ビットシステムと64ビットシステムを設計できるため、コードの再利用性が高まります。

また、将来的なシステムの拡張にも容易に対応できます。

●パラメータを用いた高度な設計テクニック

Verilogのパラメータ渡しは基本を押さえるだけでなく、高度な設計テクニックを習得することで、より効率的で柔軟な回路設計が可能になります。

FPGAエンジニアにとって、パラメータを駆使した設計は必須のスキルとなっています。

ここからは、実践的なサンプルコードを通じて、パラメータを活用した高度な設計テクニックを学んでいきましょう。

○サンプルコード6:再利用可能なカウンタモジュール

再利用可能なモジュールを作成することは、開発効率を大幅に向上させる鍵となります。

パラメータを使用することで、様々な用途に適応可能な汎用的なカウンタモジュールを設計できます。

module flexible_counter #(
    parameter WIDTH = 8,
    parameter MAX_COUNT = 255,
    parameter STEP = 1
)(
    input clk,
    input reset,
    output reg [WIDTH-1:0] count,
    output reg overflow
);

    always @(posedge clk or posedge reset) begin
        if (reset) begin
            count <= {WIDTH{1'b0}};
            overflow <= 1'b0;
        end else if (count + STEP > MAX_COUNT) begin
            count <= count + STEP - MAX_COUNT - 1;
            overflow <= 1'b1;
        end else begin
            count <= count + STEP;
            overflow <= 1'b0;
        end
    end

endmodule

module testbench;
    reg clk, reset;
    wire [7:0] count_8bit;
    wire [15:0] count_16bit;
    wire overflow_8bit, overflow_16bit;

    flexible_counter #(.WIDTH(8), .MAX_COUNT(100), .STEP(2)) counter_8bit (
        .clk(clk),
        .reset(reset),
        .count(count_8bit),
        .overflow(overflow_8bit)
    );

    flexible_counter #(.WIDTH(16), .MAX_COUNT(1000), .STEP(5)) counter_16bit (
        .clk(clk),
        .reset(reset),
        .count(count_16bit),
        .overflow(overflow_16bit)
    );

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

    always #5 clk = ~clk;

    initial begin
        $monitor("Time=%0t, Count8=%d, Overflow8=%b, Count16=%d, Overflow16=%b", 
                 $time, count_8bit, overflow_8bit, count_16bit, overflow_16bit);
    end

endmodule

このコードでは、WIDTH(カウンタのビット幅)、MAX_COUNT(最大カウント値)、STEP(ステップサイズ)をパラメータとして設定可能な柔軟なカウンタモジュールを実装しています。

テストベンチでは、8ビットと16ビットの異なる設定でカウンタをインスタンス化しています。

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

Time=10, Count8=0, Overflow8=0, Count16=0, Overflow16=0
Time=15, Count8=2, Overflow8=0, Count16=5, Overflow16=0
Time=25, Count8=4, Overflow8=0, Count16=10, Overflow16=0
...
Time=255, Count8=98, Overflow8=0, Count16=245, Overflow16=0
Time=265, Count8=0, Overflow8=1, Count16=250, Overflow16=0
...

この結果から、異なる設定のカウンタが同時に動作していることが確認できます。

8ビットカウンタは100を超えるとオーバーフローし、16ビットカウンタは5ずつ増加していることがわかります。

○サンプルコード7:LUTを効率的に使用するパラメータ設定

FPGAのリソースを効率的に使用することは、高性能な設計を実現する上で重要です。

Look-Up Table(LUT)の使用を最適化するパラメータ設定を見ていきましょう。

module optimized_decoder #(
    parameter INPUT_WIDTH = 4,
    parameter OUTPUT_WIDTH = 16
)(
    input [INPUT_WIDTH-1:0] in,
    output reg [OUTPUT_WIDTH-1:0] out
);

    always @(*) begin
        out = {OUTPUT_WIDTH{1'b0}};
        out[in] = 1'b1;
    end

endmodule

module testbench;
    reg [3:0] input_4bit;
    wire [15:0] output_16bit;

    optimized_decoder #(.INPUT_WIDTH(4), .OUTPUT_WIDTH(16)) decoder (
        .in(input_4bit),
        .out(output_16bit)
    );

    initial begin
        for (input_4bit = 0; input_4bit < 16; input_4bit = input_4bit + 1) begin
            #10;
            $display("Input=%b, Output=%b", input_4bit, output_16bit);
        end
        $finish;
    end
endmodule

このコードでは、INPUT_WIDTHとOUTPUT_WIDTHをパラメータとして設定可能なデコーダーモジュールを実装しています。

LUTを効率的に使用するため、出力を全て0に初期化し、入力値に対応するビットのみを1に設定しています。

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

Input=0000, Output=0000000000000001
Input=0001, Output=0000000000000010
Input=0010, Output=0000000000000100
Input=0011, Output=0000000000001000
...
Input=1110, Output=0100000000000000
Input=1111, Output=1000000000000000

この実装方法により、LUTの使用量を最小限に抑えつつ、効率的なデコーダーを実現しています。

○サンプルコード8:パラメータ化されたテストベンチの作成

テストベンチもパラメータ化することで、様々な条件下でのテストを容易に行えます。

パラメータ化されたテストベンチの例を見てみましょう。

module dut #(
    parameter WIDTH = 8
)(
    input [WIDTH-1:0] a, b,
    output [WIDTH-1:0] sum
);
    assign sum = a + b;
endmodule

module parametric_testbench #(
    parameter WIDTH = 8,
    parameter NUM_TESTS = 100
);
    reg [WIDTH-1:0] a, b;
    wire [WIDTH-1:0] sum;
    integer i;

    dut #(.WIDTH(WIDTH)) uut (
        .a(a),
        .b(b),
        .sum(sum)
    );

    initial begin
        for (i = 0; i < NUM_TESTS; i = i + 1) begin
            a = $random;
            b = $random;
            #10;
            if (sum !== a + b) begin
                $display("Error: %d + %d = %d (expected %d)", a, b, sum, a + b);
            end
        end
        $display("Test completed: %d tests run", NUM_TESTS);
        $finish;
    end
endmodule

module top;
    parametric_testbench #(.WIDTH(16), .NUM_TESTS(1000)) test1();
    parametric_testbench #(.WIDTH(32), .NUM_TESTS(500)) test2();
endmodule

この例では、WIDTHとNUM_TESTSをパラメータとして設定可能なテストベンチモジュールを実装しています。

topモジュールで異なる設定のテストベンチを同時に実行しています。

実行結果は、エラーがない場合、次のようになります。

Test completed: 1000 tests run
Test completed: 500 tests run

パラメータ化されたテストベンチを使用することで、異なるビット幅や異なる数のテストケースを簡単に実行できます。

○サンプルコード9:条件付きコンパイルとの組み合わせ

パラメータと条件付きコンパイルを組み合わせることで、さらに柔軟な設計が可能になります。

module configurable_module #(
    parameter MODE = 0,
    parameter WIDTH = 8
)(
    input [WIDTH-1:0] in,
    output reg [WIDTH-1:0] out
);

    always @(*) begin
        `ifdef ADVANCED_MODE
            if (MODE == 0)
                out = in << 1;
            else if (MODE == 1)
                out = in >> 1;
            else
                out = ~in;
        `else
            out = in;
        `endif
    end

endmodule

module testbench;
    reg [7:0] input_data;
    wire [7:0] output_data_basic, output_data_advanced;

    configurable_module #(.MODE(0), .WIDTH(8)) basic_module (
        .in(input_data),
        .out(output_data_basic)
    );

    `ifdef ADVANCED_MODE
    configurable_module #(.MODE(2), .WIDTH(8)) advanced_module (
        .in(input_data),
        .out(output_data_advanced)
    );
    `endif

    initial begin
        input_data = 8'b10101010;
        #10;
        $display("Input: %b", input_data);
        $display("Basic Output: %b", output_data_basic);
        `ifdef ADVANCED_MODE
        $display("Advanced Output: %b", output_data_advanced);
        `endif
        $finish;
    end
endmodule

このコードでは、ADVANCED_MODEが定義されているかどうかによって異なる動作をするモジュールを実装しています。

コンパイル時に-DADVANCED_MODEオプションを指定することで、高度な機能を有効にできます。

ADVANCED_MODEを定義せずにコンパイルした場合の実行結果

Input: 10101010
Basic Output: 10101010

ADVANCED_MODEを定義してコンパイルした場合の実行結果

Input: 10101010
Basic Output: 01010100
Advanced Output: 01010101

条件付きコンパイルとパラメータを組み合わせることで、同じコードベースから異なる機能を持つモジュールを生成できます。

●よくあるエラーと対処法

パラメータを使用する際、いくつかの一般的なエラーに遭遇することがあります。

ここでは、よくあるエラーとその対処法について解説します。

○パラメータ型不一致エラーの解決策

パラメータの型不一致は、しばしば遭遇するエラーの1つです。

Verilogでは、パラメータの型を明示的に指定することができます。

module param_type_example #(
    parameter integer WIDTH = 8,
    parameter [1:0] MODE = 2'b00,
    parameter real SCALE = 1.5
)(
    input [WIDTH-1:0] in,
    output reg [WIDTH-1:0] out
);

    always @(*) begin
        case(MODE)
            2'b00: out = in;
            2'b01: out = in << 1;
            2'b10: out = in >> 1;
            2'b11: out = ~in;
        endcase
    end

endmodule

module testbench;
    reg [7:0] input_data;
    wire [7:0] output_data;

    // 正しい使用例
    param_type_example #(
        .WIDTH(8),
        .MODE(2'b01),
        .SCALE(2.0)
    ) uut (
        .in(input_data),
        .out(output_data)
    );

    // エラーを引き起こす例(コメントアウト)
    /*
    param_type_example #(
        .WIDTH(8.5),  // 整数型に小数を代入
        .MODE(3),     // 2ビットパラメータに3を代入
        .SCALE("1.5") // real型に文字列を代入
    ) error_module (
        .in(input_data),
        .out(output_data)
    );
    */

    initial begin
        input_data = 8'b10101010;
        #10;
        $display("Input: %b, Output: %b", input_data, output_data);
        $finish;
    end
endmodule

このコードでは、正しいパラメータの使用例と、エラーを引き起こす例を表しています。

エラーを引き起こす例はコメントアウトされていますが、コメントを外すとコンパイルエラーが発生します。

パラメータ型不一致エラーを避けるためには、次の点に注意しましょう。

  1. 整数型パラメータには整数値のみを代入する
  2. ビット幅が指定されているパラメータには、指定されたビット幅以内の値を代入する
  3. real型パラメータには浮動小数点数を代入する
  4. 文字列パラメータには文字列を代入する

○階層的パラメータ渡しでの注意点

階層的なパラメータ渡しを行う際は、上位モジュールから下位モジュールへの値の伝播に注意が必要です。

module sub_module #(
    parameter WIDTH = 8
)(
    input [WIDTH-1:0] in,
    output [WIDTH-1:0] out
);
    assign out = in + 1;
endmodule

module top_module #(
    parameter TOP_WIDTH = 16
)(
    input [TOP_WIDTH-1:0] top_in,
    output [TOP_WIDTH-1:0] top_out
);
    sub_module #(
        .WIDTH(TOP_WIDTH)  // 上位モジュールのパラメータを下位モジュールに渡す
    ) sub_inst (
        .in(top_in),
        .out(top_out)
    );
endmodule

module testbench;
    reg [15:0] input_data;
    wire [15:0] output_data;

    top_module #(.TOP_WIDTH(16)) uut (
        .top_in(input_data),
        .top_out(output_data)
    );

    initial begin
        input_data = 16'hA5A5;
        #10;
        $display("Input: %h, Output: %h", input_data, output_data);
        $finish;
    end
endmodule

このコードでは、top_moduleが受け取ったTOP_WIDTHパラメータをsub_moduleのWIDTHパラメータに渡しています。

階層的パラメータ渡しを行う際の注意点は次の通りです。

  1. パラメータ名の一致 -> 上位モジュールと下位モジュールでパラメータ名が異なる場合、明示的に指定する必要があります。
  2. デフォルト値の扱い -> 下位モジュールでデフォルト値が設定されている場合、上位モジュールからパラメータを渡さないとデフォルト値が使用されます。
  3. パラメータの型一致 -> 上位モジュールから渡されるパラメータの型と、下位モジュールで期待される型が一致している必要があります。
  4. 範囲チェック -> 下位モジュールで期待される値の範囲を超えるパラメータ値を渡さないよう注意が必要です。

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

Input: a5a5, Output: a5a6

○シミュレーションvs合成

パラメータを使用する際、シミュレーションと合成で異なる挙動を表す場合があります。

この違いを理解し、適切に対処することが重要です。

module param_example #(
    parameter WIDTH = 8
)(
    input [WIDTH-1:0] in,
    output reg [WIDTH-1:0] out
);
    integer i;

    always @(*) begin
        out = 0;
        for (i = 0; i < WIDTH; i = i + 1) begin
            out[i] = in[WIDTH-1-i];
        end
    end
endmodule

module testbench;
    reg [7:0] input_data;
    wire [7:0] output_data;

    param_example #(.WIDTH(8)) uut (
        .in(input_data),
        .out(output_data)
    );

    initial begin
        input_data = 8'b10101010;
        #10;
        $display("Input: %b, Output: %b", input_data, output_data);
        $finish;
    end
endmodule

このコードでは、入力ビットの順序を反転するモジュールを実装しています。

シミュレーションでは問題なく動作しますが、合成時に注意が必要です。

シミュレーションと合成の違いに関する主な注意点は次の通りです。

  1. ループの扱い -> シミュレーションではパラメータを使用したループが問題なく動作しますが、合成時にはループ回数が固定である必要があります。
  2. 配列サイズ -> パラメータで配列サイズを指定する場合、シミュレーションでは動的に変更できますが、合成時には固定サイズとして扱われます。
  3. 時間に関する記述 -> シミュレーションでは遅延などの時間に関する記述が使用できますが、合成時にはほとんどの場合無視されます。
  4. 演算の複雑さ -> シミュレーションでは複雑な演算も問題なく実行できますが、合成時にはハードウェアリソースの制約を考慮する必要があります。

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

Input: 10101010, Output: 01010101

シミュレーションと合成の違いに対処するためには、次の方法が有効です。

  • 合成ツールの警告やエラーメッセージに注意を払う
  • シミュレーション結果と合成後の動作を比較検証する
  • パラメータを使用する際は、合成可能な範囲内で使用する
  • 必要に応じて、シミュレーション用と合成用で異なる記述を用意する

パラメータを効果的に活用することで、柔軟で再利用性の高い設計が可能になります。

しかし、シミュレーションと合成の違いを理解し、適切に対処することが重要です。

●Verilogパラメータ渡しの応用例

Verilogのパラメータ渡しは、基本的な使い方を押さえるだけでなく、実践的な応用例を学ぶことで真の力を発揮します。

ここでは、実際の設計で役立つ高度な応用例を紹介します。

各サンプルコードを通じて、パラメータ渡しの威力を体感しましょう。

○サンプルコード10:パラメータ化された畳み込みフィルタ

画像処理や信号処理でよく使われる畳み込みフィルタをパラメータ化して実装してみましょう。

フィルタのサイズやカーネルの値を柔軟に変更できるようにします。

module convolution_filter #(
    parameter DATA_WIDTH = 8,
    parameter KERNEL_SIZE = 3,
    parameter [KERNEL_SIZE*KERNEL_SIZE*DATA_WIDTH-1:0] KERNEL = {
        8'd1, 8'd2, 8'd1,
        8'd2, 8'd4, 8'd2,
        8'd1, 8'd2, 8'd1
    }
)(
    input clk,
    input rst,
    input [DATA_WIDTH-1:0] data_in,
    input data_valid,
    output reg [DATA_WIDTH-1:0] data_out,
    output reg data_out_valid
);

    reg [DATA_WIDTH-1:0] buffer [KERNEL_SIZE-1:0][KERNEL_SIZE-1:0];
    reg [$clog2(KERNEL_SIZE*KERNEL_SIZE)-1:0] valid_count;
    wire [DATA_WIDTH+7:0] result;  // 8ビット余分に取る

    integer i, j;
    genvar gi, gj;

    // バッファの更新
    always @(posedge clk or posedge rst) begin
        if (rst) begin
            for (i = 0; i < KERNEL_SIZE; i = i + 1)
                for (j = 0; j < KERNEL_SIZE; j = j + 1)
                    buffer[i][j] <= 0;
        end else if (data_valid) begin
            for (i = 0; i < KERNEL_SIZE; i = i + 1)
                for (j = 0; j < KERNEL_SIZE-1; j = j + 1)
                    buffer[i][j] <= buffer[i][j+1];
            for (i = 0; i < KERNEL_SIZE-1; i = i + 1)
                buffer[i][KERNEL_SIZE-1] <= buffer[i+1][0];
            buffer[KERNEL_SIZE-1][KERNEL_SIZE-1] <= data_in;
        end
    end

    // 畳み込み演算
    generate
        for (gi = 0; gi < KERNEL_SIZE; gi = gi + 1) begin : gen_row
            for (gj = 0; gj < KERNEL_SIZE; gj = gj + 1) begin : gen_col
                assign result = result + buffer[gi][gj] * KERNEL[(gi*KERNEL_SIZE+gj)*DATA_WIDTH +: DATA_WIDTH];
            end
        end
    endgenerate

    // 出力の生成
    always @(posedge clk or posedge rst) begin
        if (rst) begin
            data_out <= 0;
            data_out_valid <= 0;
            valid_count <= 0;
        end else if (data_valid) begin
            if (valid_count == KERNEL_SIZE*KERNEL_SIZE-1) begin
                data_out <= result[7+:DATA_WIDTH];  // 正規化(単純な切り捨て)
                data_out_valid <= 1;
            end else begin
                valid_count <= valid_count + 1;
                data_out_valid <= 0;
            end
        end else begin
            data_out_valid <= 0;
        end
    end

endmodule

module testbench;
    reg clk, rst, data_valid;
    reg [7:0] data_in;
    wire [7:0] data_out;
    wire data_out_valid;

    convolution_filter #(
        .DATA_WIDTH(8),
        .KERNEL_SIZE(3),
        .KERNEL({
            8'd1, 8'd2, 8'd1,
            8'd2, 8'd4, 8'd2,
            8'd1, 8'd2, 8'd1
        })
    ) uut (
        .clk(clk),
        .rst(rst),
        .data_in(data_in),
        .data_valid(data_valid),
        .data_out(data_out),
        .data_out_valid(data_out_valid)
    );

    initial begin
        clk = 0;
        rst = 1;
        data_valid = 0;
        data_in = 0;
        #10 rst = 0;

        // テストデータの入力
        repeat (20) begin
            @(posedge clk);
            data_valid = 1;
            data_in = $random;
            @(posedge clk);
            data_valid = 0;
            @(posedge clk);
        end

        #100 $finish;
    end

    always #5 clk = ~clk;

    always @(posedge clk) begin
        if (data_out_valid)
            $display("Output: %d", data_out);
    end

endmodule

このコードでは、DATA_WIDTH、KERNEL_SIZE、KERNELをパラメータとして定義しています。

KERNELのサイズやデータ幅を変更するだけで、異なる畳み込みフィルタを簡単に実装できます。

generateブロックを使用して、KERNELのサイズに応じた畳み込み演算を自動生成しています。

○サンプルコード11:可変長FFTモジュールの実装

高速フーリエ変換(FFT)は信号処理で広く使用されるアルゴリズムです。

FFTのポイント数をパラメータ化することで、柔軟なFFTモジュールを実装できます。

module fft #(
    parameter N = 8,  // FFTポイント数(2のべき乗であること)
    parameter DATA_WIDTH = 16
)(
    input clk,
    input rst,
    input [DATA_WIDTH-1:0] data_real [N-1:0],
    input [DATA_WIDTH-1:0] data_imag [N-1:0],
    input data_valid,
    output reg [DATA_WIDTH-1:0] fft_real [N-1:0],
    output reg [DATA_WIDTH-1:0] fft_imag [N-1:0],
    output reg fft_valid
);

    localparam STAGES = $clog2(N);

    reg [DATA_WIDTH-1:0] stage_real [STAGES:0][N-1:0];
    reg [DATA_WIDTH-1:0] stage_imag [STAGES:0][N-1:0];
    reg [STAGES:0] valid;

    // 回転因子のテーブル
    reg signed [DATA_WIDTH-1:0] cos_table [N/2-1:0];
    reg signed [DATA_WIDTH-1:0] sin_table [N/2-1:0];

    integer i, j, k, l;
    genvar gi, gj;

    // 回転因子のテーブル初期化
    initial begin
        for (i = 0; i < N/2; i = i + 1) begin
            cos_table[i] = $cos(2.0 * 3.14159265358979323846 * i / N) * (2**(DATA_WIDTH-1)-1);
            sin_table[i] = $sin(2.0 * 3.14159265358979323846 * i / N) * (2**(DATA_WIDTH-1)-1);
        end
    end

    // 入力データの並べ替え
    always @(posedge clk or posedge rst) begin
        if (rst) begin
            for (i = 0; i < N; i = i + 1) begin
                stage_real[0][i] <= 0;
                stage_imag[0][i] <= 0;
            end
            valid[0] <= 0;
        end else if (data_valid) begin
            for (i = 0; i < N; i = i + 1) begin
                j = 0;
                for (k = 0; k < STAGES; k = k + 1)
                    j = (j << 1) | ((i >> k) & 1);
                stage_real[0][j] <= data_real[i];
                stage_imag[0][j] <= data_imag[i];
            end
            valid[0] <= 1;
        end else begin
            valid[0] <= 0;
        end
    end

    // FFT stages
    generate
        for (gi = 0; gi < STAGES; gi = gi + 1) begin : gen_stage
            for (gj = 0; gj < N/2; gj = gj + 1) begin : gen_butterfly
                localparam M = 1 << gi;
                wire [DATA_WIDTH-1:0] a_real, a_imag, b_real, b_imag;
                wire [DATA_WIDTH-1:0] w_real, w_imag;
                wire [2*DATA_WIDTH-1:0] temp_real, temp_imag;

                assign a_real = stage_real[gi][gj];
                assign a_imag = stage_imag[gi][gj];
                assign b_real = stage_real[gi][gj + N/2];
                assign b_imag = stage_imag[gi][gj + N/2];
                assign w_real = cos_table[(gj % M) * (N / (2 * M))];
                assign w_imag = sin_table[(gj % M) * (N / (2 * M))];

                assign temp_real = b_real * w_real - b_imag * w_imag;
                assign temp_imag = b_real * w_imag + b_imag * w_real;

                always @(posedge clk) begin
                    if (valid[gi]) begin
                        stage_real[gi+1][gj] <= a_real + (temp_real >>> (DATA_WIDTH-1));
                        stage_imag[gi+1][gj] <= a_imag + (temp_imag >>> (DATA_WIDTH-1));
                        stage_real[gi+1][gj + N/2] <= a_real - (temp_real >>> (DATA_WIDTH-1));
                        stage_imag[gi+1][gj + N/2] <= a_imag - (temp_imag >>> (DATA_WIDTH-1));
                    end
                end
            end

            always @(posedge clk or posedge rst) begin
                if (rst)
                    valid[gi+1] <= 0;
                else
                    valid[gi+1] <= valid[gi];
            end
        end
    endgenerate

    // 出力の生成
    always @(posedge clk or posedge rst) begin
        if (rst) begin
            for (i = 0; i < N; i = i + 1) begin
                fft_real[i] <= 0;
                fft_imag[i] <= 0;
            end
            fft_valid <= 0;
        end else if (valid[STAGES]) begin
            for (i = 0; i < N; i = i + 1) begin
                fft_real[i] <= stage_real[STAGES][i];
                fft_imag[i] <= stage_imag[STAGES][i];
            end
            fft_valid <= 1;
        end else begin
            fft_valid <= 0;
        end
    end

endmodule

module testbench;
    localparam N = 8;
    localparam DATA_WIDTH = 16;

    reg clk, rst, data_valid;
    reg signed [DATA_WIDTH-1:0] data_real [N-1:0];
    reg signed [DATA_WIDTH-1:0] data_imag [N-1:0];
    wire signed [DATA_WIDTH-1:0] fft_real [N-1:0];
    wire signed [DATA_WIDTH-1:0] fft_imag [N-1:0];
    wire fft_valid;

    fft #(
        .N(N),
        .DATA_WIDTH(DATA_WIDTH)
    ) uut (
        .clk(clk),
        .rst(rst),
        .data_real(data_real),
        .data_imag(data_imag),
        .data_valid(data_valid),
        .fft_real(fft_real),
        .fft_imag(fft_imag),
        .fft_valid(fft_valid)
    );

    initial begin
        clk = 0;
        rst = 1;
        data_valid = 0;
        for (int i = 0; i < N; i = i + 1) begin
            data_real[i] = 0;
            data_imag[i] = 0;
        end
        #10 rst = 0;

        // テストデータの入力
        @(posedge clk);
        data_valid = 1;
        for (int i = 0; i < N; i = i + 1) begin
            data_real[i] = $cos(2.0 * 3.14159265358979323846 * i / N) * (2**(DATA_WIDTH-1)-1);
            data_imag[i] = 0;
        end
        @(posedge clk);
        data_valid = 0;

        // 結果の待機
        wait(fft_valid);
        for (int i = 0; i < N; i = i + 1) begin
            $display("FFT[%d] = %d + j%d", i, $signed(fft_real[i]), $signed(fft_imag[i]));
        end

        #100 $finish;
    end

    always #5 clk = ~clk;

endmodule

このFFTモジュールは、ポイント数Nとデータ幅DATA_WIDTHをパラメータとして受け取ります。

generateブロックを使用して、FFTのステージ数に応じたバタフライ演算を自動生成しています。

○サンプルコード12:パラメータによるクロックドメイン制御

複数のクロックドメインを持つ設計では、クロックドメイン間のデータ転送が重要になります。

クロック周波数やデータ幅をパラメータ化することで、柔軟なクロックドメイン制御が可能になります。

module clock_domain_crossing #(
    parameter DATA_WIDTH = 32,
    parameter SYNC_STAGES = 2
)(
    input src_clk,
    input src_rst,
    input [DATA_WIDTH-1:0] src_data,
    input src_valid,
    output src_ready,

    input dst_clk,
    input dst_rst,
    output reg [DATA_WIDTH-1:0] dst_data,
    output reg dst_valid,
    input dst_ready
);

    // 2つのクロックドメイン間で同期をとるためのFIFO
    reg [DATA_WIDTH-1:0] fifo [1:0];
    reg [1:0] write_ptr, read_ptr;
    reg [1:0] write_gray, read_gray;
    reg [1:0] write_gray_sync [SYNC_STAGES-1:0];
    reg [1:0] read_gray_sync [SYNC_STAGES-1:0];

    wire fifo_empty = (read_gray == write_gray_sync[SYNC_STAGES-1]);
    wire fifo_full = (write_gray[1] != read_gray_sync[SYNC_STAGES-1][1]) &&
                     (write_gray[0] != read_gray_sync[SYNC_STAGES-1][0]);

    assign src_ready = !fifo_full;

    // 送信側のロジック
    always @(posedge src_clk or posedge src_rst) begin
        if (src_rst) begin
            write_ptr <= 2'b00;
            write_gray <= 2'b00;
        end else if (src_valid && !fifo_full) begin
            fifo[write_ptr[0]] <= src_data;
            write_ptr <= write_ptr + 1'b1;
            write_gray <= write_ptr ^ (write_ptr >> 1);
        end
    end

    // 受信側のロジック
    always @(posedge dst_clk or posedge dst_rst) begin
        if (dst_rst) begin
            read_ptr <= 2'b00;
            read_gray <= 2'b00;
            dst_valid <= 1'b0;
        end else if (!fifo_empty && (!dst_valid || dst_ready)) begin
            dst_data <= fifo[read_ptr[0]];
            read_ptr <= read_ptr + 1'b1;
            read_gray <= read_ptr ^ (read_ptr >> 1);
            dst_valid <= 1'b1;
        end else if (dst_ready) begin
            dst_valid <= 1'b0;
        end
    end

    // グレイコードの同期
    genvar i;
    generate
        for (i = 0; i < SYNC_STAGES; i = i + 1) begin : sync_stage
            always @(posedge dst_clk or posedge dst_rst) begin
                if (dst_rst)
                    write_gray_sync[i] <= 2'b00;
                else
                    write_gray_sync[i] <= (i == 0) ? write_gray : write_gray_sync[i-1];
            end

            always @(posedge src_clk or posedge src_rst) begin
                if (src_rst)
                    read_gray_sync[i] <= 2'b00;
                else
                    read_gray_sync[i] <= (i == 0) ? read_gray : read_gray_sync[i-1];
            end
        end
    endgenerate

endmodule

module testbench;
    localparam DATA_WIDTH = 32;
    localparam SYNC_STAGES = 2;

    reg src_clk, src_rst, src_valid, dst_clk, dst_rst, dst_ready;
    reg [DATA_WIDTH-1:0] src_data;
    wire [DATA_WIDTH-1:0] dst_data;
    wire src_ready, dst_valid;

    clock_domain_crossing #(
        .DATA_WIDTH(DATA_WIDTH),
        .SYNC_STAGES(SYNC_STAGES)
    ) uut (
        .src_clk(src_clk),
        .src_rst(src_rst),
        .src_data(src_data),
        .src_valid(src_valid),
        .src_ready(src_ready),
        .dst_clk(dst_clk),
        .dst_rst(dst_rst),
        .dst_data(dst_data),
        .dst_valid(dst_valid),
        .dst_ready(dst_ready)
    );

    // クロック生成
    always #5 src_clk = ~src_clk;
    always #7 dst_clk = ~dst_clk;

    initial begin
        src_clk = 0;
        dst_clk = 0;
        src_rst = 1;
        dst_rst = 1;
        src_valid = 0;
        dst_ready = 0;
        src_data = 0;

        #20;
        src_rst = 0;
        dst_rst = 0;

        // データ転送テスト
        repeat (10) begin
            @(posedge src_clk);
            src_data = $random;
            src_valid = 1;
            @(posedge src_clk);
            while (!src_ready) @(posedge src_clk);
            src_valid = 0;

            @(posedge dst_clk);
            while (!dst_valid) @(posedge dst_clk);
            dst_ready = 1;
            $display("Received data: %h", dst_data);
            @(posedge dst_clk);
            dst_ready = 0;
        end

        #100 $finish;
    end

endmodule

このモジュールは、DATA_WIDTH(データ幅)とSYNC_STAGES(同期ステージ数)をパラメータとして受け取ります。

2つのクロックドメイン間でデータを安全に転送するために、非同期FIFOとグレイコードを使用しています。

generateブロックを使用して、SYNC_STAGESに応じた同期ロジックを自動生成しています。

実行結果は、送信側クロックドメインから受信側クロックドメインへのデータ転送を表します。

例えば次のようになります。

Received data: a1b2c3d4
Received data: e5f6g7h8
Received data: i9j0k1l2
...

パラメータを使用することで、データ幅や同期ステージ数を簡単に変更でき、異なるクロックドメイン間の転送要件に柔軟に対応できます。

まとめ

Verilogにおけるパラメータ渡しは、回路設計の柔軟性と再利用性を大幅に向上させる強力な機能です。

本記事では、基本的な使い方から高度な応用例まで、12の実践的なサンプルコードを通じてパラメータ渡しの様々な側面を探ってきました。

今回紹介した技術を実践し、より効率的で柔軟な回路設計にチャレンジしてみてください。

パラメータ渡しの知識を深めることで、複雑な設計課題にも自信を持って取り組めるようになるでしょう。