Verilogプリミティブ入門!10の実用的なコード例で学ぶ

Verilogのプリミティブ理解が10倍早くなる! 丁寧な使い方とサンプルコード15選の記事イメージVerilog
この記事は約20分で読めます。

 

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

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

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

基本的な知識があればカスタムコードを使って機能追加、目的を達成できるように作ってあります。

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

サイト内のコードを共有する場合は、参照元として引用して下さいますと幸いです

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

はじめに

デジタル回路の設計は、エレクトロニクス技術者にとって重要なスキルであり、この世界には多くの方法とツールが存在します。

その中でも、ハードウェア記述言語 (HDL) の一種であるVerilogは、特に一般的に使用されています。

Verilogを効果的に使いこなすための基本的な要素として、今日は「プリミティブ」に焦点を当て、その使用方法と実用的なコード例を10個紹介します。

○Verilogとは

Verilogは、デジタルシステムの設計と検証のために使用されるハードウェア記述言語 (HDL) です。

一般的に、集積回路やFPGAの設計に使用されます。

Verilogの特徴は、ソフトウェア言語に似た構文を持ちながら、ハードウェアの性質を正確に記述することができる点です。

○Verilogのプリミティブとは

Verilogのプリミティブは、基本的な論理ゲート(AND、OR、NOTなど)やその他のハードウェア要素を記述するための組み込みオブジェクトです。

これらのプリミティブを組み合わせて、より複雑なデジタル回路を設計することができます。

●Verilogプリミティブの基本

プリミティブはVerilogの基本的な構成要素であり、デジタルロジック設計の基礎となるゲートレベルの要素を直接記述することができます。

○プリミティブの構造

Verilogのプリミティブは、入力と出力を持つゲートを表現します。

一般的に、これらのゲートは1つの出力と複数の入力を持ちます。

プリミティブの基本的な形式は次のようになります。

// ゲート型 インスタンス名 (出力, 入力1, 入力2, ...);

たとえば、2入力ANDゲートのプリミティブは次のようになります。

// ANDゲートの例
and and1 (out, in1, in2);

このコードでは、andプリミティブを使用して2入力ANDゲートを作成しています。

この例では、outは出力、in1in2は入力を表しています。

○プリミティブの種類

Verilogでは、基本的な論理ゲートからタイムディレイやワイヤなどの要素まで、さまざまなプリミティブが提供されています。

最も一般的なプリミティブには、andnandornorxorxnornotなどの論理ゲートがあります。

●実用的なコード例

○サンプルコード1:ANDゲート

まずは最も基本的なゲートの一つであるANDゲートから始めます。

ANDゲートは、すべての入力が1のときにのみ出力が1になる論理ゲートです。

下記のコードは、2入力ANDゲートを作成するVerilogプリミティブの例です。

module and_gate(input wire a, input wire b, output wire y);
    and(y, a, b);
endmodule

このコードでは、andプリミティブを使用して、入力abのANDゲートを作成しています。

出力yは、abが両方とも1のときのみ1になります。

このコードをシミュレーションすると、abの両方が1の場合に限り、出力yが1になることが確認できます。

○サンプルコード2:ORゲート

ここでは、Verilogプリミティブを用いてORゲートを作成する方法を解説します。

ORゲートは基本的な論理ゲートの一つであり、入力されたすべての信号のうち、どれか一つでも”1″があれば出力が”1″となる特性を持ちます。

その動作を模倣するVerilogコードの例を紹介します。

// ORゲートを表現するVerilogプリミティブ
module orgate (output Y, input A, B);
  or(Y, A, B); // ORゲートのプリミティブ関数
endmodule

このコードは、名前が’orgate’のモジュールを宣言しています。

このモジュールはORゲートの動作を行います。

モジュールの入力として’A’と’B’が定義され、出力として’Y’が定義されています。

その後、ORゲートのプリミティブ関数であるor()関数が使用されています。

この関数は、第一引数に出力の名前を、以降の引数には入力の名前を指定します。

このコードを実行すると、入力’A’と’B’の論理ORをとった結果が出力’Y’として得られます。

つまり、’A’または’B’が”1″であれば、出力’Y’は”1″となります。’A’と’B’がともに”0″の場合のみ、出力’Y’は”0″となります。

○サンプルコード3:NOTゲート

次に、Verilogプリミティブを使用してNOTゲートを作成する方法を解説します。NOTゲートは入力の論理値を反転するゲートです。

つまり、入力が”0″なら出力は”1″となり、入力が”1″なら出力は”0″となります。

その動作を模倣するVerilogコードを紹介します。

// NOTゲートを表現するVerilogプリミティブ
module notgate (output Y, input A);
  not(Y, A); // NOTゲートのプリミティブ関数
endmodule

このコードでは、名前が’notgate’のモジュールを宣言しています。

このモジュールはNOTゲートの動作を行います。モジュールの入力として’A’が定義され、出力として’Y’が定義されています。

その後、NOTゲートのプリミティブ関数であるnot()関数が使用されています。

この関数も、第一引数に出力の名前を、第二引数に入力の名前を指定します。

このコードを実行すると、入力’A’の論理反転が出力’Y’として得られます。

つまり、’A’が”0″であれば、出力’Y’は”1″となります。逆に、’A’が”1″であれば、出力’Y’は”0″となります。

○サンプルコード4:NANDゲート

次に紹介するのは、NANDゲートを表現するVerilogのプリミティブの実用例です。

このコードでは、nandゲートプリミティブを使用して、単純なNANDゲートを表現するVerilogコードを書きます。

// NANDゲートの実装
module nand_gate(input wire a, b, output wire y);
    nand(y, a, b); // NANDゲートのプリミティブ
endmodule

このコードでは、入力としてワイヤaとbを、出力としてワイヤyを指定しています。

その後、NANDゲートのプリミティブ(nand)を使用して、出力yと入力a, bの間にNANDゲートを作成します。

つまり、この例では、入力aとbが両方とも論理1(真)のときのみ出力yが論理0(偽)となり、それ以外の場合では出力yは論理1(真)となります。

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

// NANDゲートのテストベンチ
module test;
    reg a, b;
    wire y;

    // NANDゲートのインスタンス化
    nand_gate u1(.a(a), .b(b), .y(y));

    initial begin
        $monitor("a = %b, b = %b, y = %b", a, b, y);

        // 全ての可能な入力パターンをテスト
        a = 0; b = 0; #10;
        a = 0; b = 1; #10;
        a = 1; b = 0; #10;
        a = 1; b = 1; #10;
    end
endmodule

このテストベンチでは、NANDゲートの振る舞いを確認するため、全ての可能な入力パターンについてその結果を表示します。

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

a = 0, b = 0, y = 1
a = 0, b = 1, y = 1
a = 1, b = 0, y = 1
a = 1, b = 1, y = 0

これらの結果は、NANDゲートの真理表と一致します。

それぞれの入力の組み合わせに対して、出力yが期待通りの結果を示していることがわかります。

つまり、このコードは正しくNANDゲートをシミュレートしていると言えます。

○サンプルコード5:NORゲート

我々が次に触れるのは、NORゲートです。

NORゲートはORゲートの否定を行う論理ゲートで、すべての入力がローである場合にのみ出力がハイになります。それ以外の場合、出力はローとなります。

この特性を用いて、次のサンプルコードを作成しました。

// NORゲートの例
module nor_gate(input wire A, B, output wire Y);
  assign Y = ~(A | B);
endmodule

このコードはVerilogのプリミティブを使ってNORゲートを表現しています。

assign Y = ~(A | B);という行では、入力AとBの論理ORの結果を否定し、それを出力Yに割り当てています。

その結果、AとBが両方ともローの時にのみYがハイとなる、NORゲートの振る舞いを模倣しています。

実行後の結果を確認するために、次のテストベンチを使ってみましょう。

// NORゲートのテストベンチ
module tb_nor_gate();
  reg A, B;
  wire Y;

  // NORゲートのインスタンス化
  nor_gate u1 (A, B, Y);

  initial begin
    // 入力パターンと結果を表示
    $display("A B | Y");
    $display("--+--");
    A = 0; B = 0; #10; $display("%b %b | %b", A, B, Y);
    A = 0; B = 1; #10; $display("%b %b | %b", A, B, Y);
    A = 1; B = 0; #10; $display("%b %b | %b", A, B, Y);
    A = 1; B = 1; #10; $display("%b %b | %b", A, B, Y);
    $display("--+--");
  end
endmodule

テストベンチでは、2つの入力AとBに対して可能なすべての組み合わせを試しています。

各組み合わせに対するNORゲートの出力Yが表示されます。

このテストベンチを使用して、実装したNORゲートが正しく機能することを確認できます。

なお、このテストベンチのコードは、Verilogのシミュレーションのための基本的な構造を示しています。

initial beginブロック内で、各テストケースを順に設定し、結果を$display関数を用いて表示しています。

また、#10;という表記は、10単位時間待機することを表しています。

○サンプルコード6:XORゲート

次に進む前に、XORゲートについて確認しましょう。

XORは排他的論理和を意味し、入力のどちらか一方だけが1であるときにのみ出力が1となるゲートです。

VerilogでのXORゲートのプリミティブは次のように表現されます。

// XORゲートの例
module xor_gate(input a, input b, output y);
  xor(x, a, b);
endmodule

このコードでは、Verilogのプリミティブとして用意されているXORを使用してXORゲートを作成しています。

この例では、二つの入力信号aとbに対してXOR演算を行い、その結果を出力信号yに出力しています。

XORゲートの特性から、aとbの値が異なるときだけyは1になります。

このゲートは、同等な論理式により構築することも可能です。

たとえば、次のようなコードもXORゲートと同じ動作をします:

// XORゲートの論理式による実装
module xor_gate_logic(input a, input b, output y);
  assign y = (a & ~b) | (~a & b);
endmodule

このコードでは、ANDゲートとNOTゲートを組み合わせてXORゲートの論理式を構築しています。

具体的には、aとbのNOT、bとaのNOT、それぞれをANDした後にORしています。

これは、XORの動作を表現しています。

○サンプルコード7:XNORゲート

私たちが調査してきた最後の基本的な論理ゲートがXNORゲートです。

XORゲートの振る舞いを反転したものと考えると理解しやすいでしょう。

具体的には、すべての入力が同じである場合にのみ、出力が高(1)になります。

それ以外のすべての場合では、出力は低(0)です。

これはNOTゲートを用いてXORゲートを反転させることで実現できますが、Verilogでは一つのプリミティブとして提供されています。

下記のサンプルコードでは、2入力XNORゲートを作成し、その動作をテストします。

// モジュール宣言
module xnor_gate(input wire a, input wire b, output wire y);
  // XNORゲートの宣言
  assign y = ~(a ^ b);  // コメント:aとbのXORの結果を反転
endmodule

// テストベンチ
module tb_xnor_gate;
  reg a, b;
  wire y;

  // テスト対象のXNORゲートのインスタンス化
  xnor_gate u1 (a, b, y);

  initial begin
    // 入力値を変更してゲートの動作をテスト
    $monitor("a=%b b=%b y=%b", a, b, y);
    #10 a=0; b=0;
    #10 a=1; b=0;
    #10 a=0; b=1;
    #10 a=1; b=1;
    #10 $finish;
  end
endmodule

このコードでは、モジュールxnor_gateは2つの入力aとbを受け取り、それらのXNORを計算して出力yに割り当てます。

テストベンチでは、異なる入力値を使用してゲートの動作をテストします。入力値が同じ場合にのみ出力が1になることを確認できます。

このサンプルコードを実行すると、次のような結果が得られます。

a=0 b=0 y=1
a=1 b=0 y=0
a=0 b=1 y=0
a=1 b=1 y=1

これにより、XNORゲートが正常に動作していることが確認できます。

それぞれの入力の組み合わせに対して出力yが予期した値になっています。

○サンプルコード8:ビット幅を指定したゲート

ビット幅を指定することにより、一度に多数のデータを扱うことが可能になります。

これは、大規模なデジタル回路で特に有効で、Verilogではこのようなシチュエーションでもシンプルに記述することが可能です。

このケースでは、2ビットANDゲートを例にします。

// 2ビットANDゲートの例
module two_bit_AND_gate(input [1:0] a, input [1:0] b, output [1:0] y);
assign y = a & b;
endmodule

ここでは、2つの2ビット入力 ‘a’ と ‘b’ と1つの2ビット出力 ‘y’ を定義しています。

そして ‘y’ の値は ‘a’ と ‘b’ のAND演算結果になります。

この例では、一度に2ビットずつのデータを扱っています。

このコードの実行結果は、入力が ‘a = 11’, ‘b = 10’ の場合、出力 ‘y’ は ’10’ となります。

これは、入力ビットごとにAND演算が行われ、’1 AND 1′ は ‘1’ に、’1 AND 0′ は ‘0’ になるためです。

次に、このコードの一部を変更して4ビットANDゲートを作成しましょう。

// 4ビットANDゲートの例
module four_bit_AND_gate(input [3:0] a, input [3:0] b, output [3:0] y);
assign y = a & b;
endmodule

このコードは先ほどのコードと似ていますが、入力と出力のビット幅が4ビットになっています。

この例では、一度に4ビットずつのデータを扱っています。

このコードの実行結果は、入力が ‘a = 1101’, ‘b = 1010’ の場合、出力 ‘y’ は ‘1000’ となります。

これは、入力ビットごとにAND演算が行われ、’1 AND 1′ は ‘1’ に、’0 AND 1′ や ‘1 AND 0’ は ‘0’ になるためです。

このように、Verilogではビット幅を指定することで、複数のデータを一度に扱うことが可能になり、大規模なデジタル回路の設計やシミュレーションを簡単に行うことができます。

○サンプルコード9:管理ゲート

Verilogで信号の流れを制御する方法を学ぶため、サンプルコード9では管理ゲートを使ったプログラムを作成します。

管理ゲートは、1つの入力信号によって他の信号の流れを制御する特別な種類のゲートで、バッファとトライステートバッファの2種類があります。

この例ではトライステートバッファを使ってみましょう。

// Verilog
module tristate_buffer(input wire control, input wire data_in, output wire data_out);
    assign data_out = control ? data_in : 1'bz; 
endmodule

このコードでは、controlが真であればdata_indata_outに出力され、そうでなければdata_outはハイ・インピーダンス状態(z)になります。

このハイ・インピーダンス状態は、その出力が他の信号源によって上書き可能な状態を紹介します。

つまり、controlによってdata_inの信号がdata_outに伝えられるかどうかが制御されます。

では、次にこのコードがどのように動作するか確認してみましょう。

// Verilog
module tb;
    reg control;
    reg data_in;
    wire data_out;

    tristate_buffer u1 (.control(control), .data_in(data_in), .data_out(data_out));

    initial begin
        $monitor("At time %d, control=%b, data_in=%b, data_out=%b", $time, control, data_in, data_out);
        control = 0; data_in = 0; #10;
        control = 1; data_in = 0; #10;
        control = 0; data_in = 1; #10;
        control = 1; data_in = 1; #10;
        $finish;
    end
endmodule

テストベンチの結果は次の通りです。

At time 0, control=0, data_in=0, data_out=z
At time 10, control=1, data_in=0, data_out=0
At time 20, control=0, data_in=1, data_out=z
At time 30, control=1, data_in=1, data_out=1

この結果を見ると、controlが1の時にdata_inの値がdata_outに正確に出力されていることが分かります。

逆に、controlが0の時には、data_outはハイ・インピーダンス状態(z)となり、信号は出力されていません。

こうした管理ゲートは、一つのハードウェア資源(たとえばデータバス)を複数のデバイスで共有したいときに有用です。

それぞれのデバイスは、自分がデータを送信するべき時だけデータバスに接続し、それ以外の時間はバスから切り離します。

しかし、このような構造は適切に制御しなければなりません。

もし複数のデバイスが同時にバスにデータを送信しようとすれば、データの衝突が起きて不具合が生じます。

そのような状況を避けるために、バスに接続するための制御ロジックが必要になります。

この制御ロジックは、たとえばトライステートバッファのcontrol入力を制御することで実現できます。

○サンプルコード10:ユーザ定義プリミティブ(UDP)

Verilogでは、プリミティブの機能を自分で定義することも可能です。

これをユーザ定義プリミティブ(UDP)と言います。

ユーザ定義プリミティブを使用して3入力のXORゲートを定義したサンプルコードを紹介します。

// 3入力XORゲートの定義
primitive tri_input_xor (output y, input a, b, c);
  table
    // a b c :  y
      0 0 0 :  0;
      0 0 1 :  1;
      0 1 0 :  1;
      0 1 1 :  0;
      1 0 0 :  1;
      1 0 1 :  0;
      1 1 0 :  0;
      1 1 1 :  1;
  endtable
endprimitive

このコードでは、primitiveキーワードで新しいプリミティブを定義し、その後に入力と出力を指定しています。

その次にあるtableendtableの間には、全ての可能な入力とそれに対応する出力を記述しています。

この例では3つの入力a, b, cがあり、それぞれに0または1を取ることができるため、全8パターンの組み合わせが考えられます。

それぞれの組み合わせに対するXORゲートの出力を表に記述しています。

なお、ユーザ定義プリミティブは、基本的な論理ゲートだけでなく、より複雑な動作をするモジュールを定義することも可能です。

ただし、ユーザ定義プリミティブはシンプルな動作しか記述できないという制限があります。

したがって、より複雑な動作をするモジュールを作成する場合は、通常のモジュールを用いることが推奨されます。

なお、UDPのコードは基本的にシミュレーションのみで使用され、一部のFPGAやASICでは実装できない場合があります。

これはハードウェアのリソースや実装方法によりますので、具体的な製品のマニュアル等を参照してください。

●注意点と対策

Verilogでデジタル回路を設計する際には、次のような問題が発生する可能性があります。

これらの問題を理解し、適切な対策を講じることが重要です。

○タイミング問題

デジタル回路設計においては、回路の動作タイミングが重要な役割を果たします。

特に、クロック同期のロジック設計においては、各回路の遅延時間を正確に把握し、それに基づいてクロック周期を設定することが必要です。

例えば、下記のコードはクロックエッジに同期するフリップフロップを表しています。

module D_flip_flop(input wire clk, input wire D, output reg Q);
  always @(posedge clk) begin
    Q <= D;
  end
endmodule

このコードの動作は、クロックの立ち上がりエッジ(posedge clk)でDの値がQに格納されるというものです。

ただし、DからQへの値の伝播には一定の遅延時間があり、この遅延時間がクロック周期よりも長いと、期待した動作をしない可能性があります。

したがって、適切なクロック周期を設定するためには、このような遅延時間を正確に把握することが必要です。

○シミュレーションと実装の違い

Verilogで書かれたコードは、基本的にシミュレーションを行うためのものです。

したがって、シミュレーション上では期待通りの動作をするコードでも、実際のハードウェア上で動作させたときには異なる動作をする可能性があります。

これは、シミュレーションでは考慮されないハードウェアの特性や、物理的な制約等によるものです。

したがって、実際のハードウェアに実装する前には、複数の角度からシミュレーションを行い、可能な限り多くの状況を検証することが必要です。

また、FPGAやASICへの実装を行う際には、そのハードウェアの特性や制約を理解し、適切な設計や最適化を行うことが重要です。

まとめ

この記事では、Verilogのプリミティブの基本的な使い方から、ユーザ定義プリミティブ(UDP)の作成方法、さらには実用的なコード例までを詳細に解説しました。

それぞれのコード例では、具体的なコードの書き方とその動作を理解することができるように、詳細な説明とサンプルコードを交えて解説しました。

また、Verilogでのデジタル回路設計における注意点と対策も見てきました。

具体的には、回路の動作タイミングを考慮した設計や、シミュレーションと実装の違いについて理解し、適切な設計や最適化を行うことが重要であることを学びました。

これらの知識をもとに、あなたもVerilogのプロとして、効率的で高品質なデジタル回路設計を行うことができるようになることを期待しています。