入門 · レッスン 5 / 10

初めてのHDLモジュール

VerilogモジュールとVHDLエンティティの構造、ポート、wire・reg・signal、連続代入とクロック付き代入を解説し、LEDを点滅させる完全なカウンタを両言語で1行ずつ説明します

入門35 分更新 2026年11月13日無料期間 2027年1月31日

このレッスンで学ぶこと

  • VerilogモジュールとVHDLのエンティティ・アーキテクチャの構造を説明し、各部分をもう一方の言語に対応付けられる
  • 連続代入とクロック付き代入の違いと、それぞれが生成するハードウェアを説明できる
  • パラメータ化されたLED点滅カウンタをVerilogとVHDLで書き、そのすべての行を説明できる
  • パラメータまたはジェネリックを使って、モジュールを異なるクロック周波数で再利用できるようにする

前提レッスン: レジスタ、クロック、状態という考え方

LUTが何をするか(レッスン03)とレジスタが何をするか(レッスン04)はもう分かりました。本レッスンでは、それらを記述するファイルを書きます。1秒に1回LEDを点滅させるカウンタという1つの小さな設計を取り上げ、VerilogとVHDLで同時に、1行ずつ組み立てます。終えるころには、このトラックの残りを追えるだけ、そして自分のモジュールを書けるだけ、どちらの言語も読めるようになります。

モジュールの形

HDLファイルは、境界を持つ1つのハードウェアブロックを記述します。Verilogではそのブロックはmoduleです。VHDLではentity(境界)とarchitecture(中身)の対です。どちらも3つのことを述べます。ブロックの名前、境界をまたぐポート、そして内部で何が起きるかです。

blinkCLK_HZ, BLINK_HZclkrstcountレジスタ、Nビット== HALF-1比較器ledレジスタwrap折り返しでcountを0に、それ以外はcount + 1led
箱として見たblinkモジュール。入力2本、出力1本、内部にはカウンタレジスタ、終端カウントを検出する比較器、カウンタが折り返すときにトグルするLEDレジスタがあります。

Verilog版の完全なコードを示します。まず一度読み、次に説明を読み、もう一度読んでください。

module blink #(
  parameter CLK_HZ   = 100_000_000,
  parameter BLINK_HZ = 1
) (
  input  wire clk,
  input  wire rst,
  output reg  led
);
  localparam HALF_PERIOD = CLK_HZ / (2 * BLINK_HZ);
  localparam CNT_WIDTH   = $clog2(HALF_PERIOD);

  reg [CNT_WIDTH-1:0] count;

  always @(posedge clk) begin
    if (rst) begin
      count <= 0;
      led   <= 1'b0;
    end else if (count == HALF_PERIOD - 1) begin
      count <= 0;
      led   <= ~led;
    end else begin
      count <= count + 1'b1;
    end
  end
endmodule

同じハードウェアをVHDLで書くと次のようになります。

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity blink is
  generic (
    CLK_HZ   : positive := 100_000_000;
    BLINK_HZ : positive := 1
  );
  port (
    clk : in  std_logic;
    rst : in  std_logic;
    led : out std_logic
  );
end entity;

architecture rtl of blink is
  constant HALF_PERIOD : natural := CLK_HZ / (2 * BLINK_HZ);
  signal count : natural range 0 to HALF_PERIOD - 1 := 0;
  signal led_r : std_logic := '0';
begin
  process (clk)
  begin
    if rising_edge(clk) then
      if rst = '1' then
        count <= 0;
        led_r <= '0';
      elsif count = HALF_PERIOD - 1 then
        count <= 0;
        led_r <= not led_r;
      else
        count <= count + 1;
      end if;
    end if;
  end process;

  led <= led_r;
end architecture;

1行ずつ

ヘッダとパラメータ

Verilogはmodule blinkで始まり、#( ... )内の省略可能なパラメータリスト、そして( ... )内のポートリストが続きます。パラメータはコンパイル時定数で、モジュールの利用者が上書きできます。ここでの2つは、クロックの速さとLEDの点滅速度を表します。デフォルトの100 MHzと1 HzはBasys 3をはじめ多くのボードに合います。数値中のアンダースコアは無視され、大きな値を読みやすくします。

VHDLは、標準ロジック型をインポートするlibraryとuseの行で始まります。ほぼすべてのVHDLファイルがこのように始まります。std_logic_1164がstd_logic型を提供し、numeric_stdがベクタの算術演算を提供します。次にentityがジェネリック(パラメータに相当するVHDLの用語)とポートを宣言します。VHDLのジェネリックには型があることに注意してください。positiveは1以上の整数で、誰かが0を渡すとコンパイルエラーになります。そうでなければゼロ除算を引き起こすところです。

ポート

各ポートには方向と型があります。Verilogでは、input wire clkは1ビットの入力、output reg ledはalwaysブロック内で代入される1ビットの出力です。VHDLではclk : in std_logicとled : out std_logicが同じことを述べます。std_logicは9つの値を取り得る1ビット型ですが、主に使うのは’0’と’1’で、ときどきハイインピーダンスの’Z’と、シミュレーションでの不定を表す’X’を使います。

複数ビットのポートはベクタとして宣言します。Verilogではinput wire [7:0] data、VHDLではdata : in std_logic_vector(7 downto 0)です。どちらもビット7を左に置きます。レッスン03のadd_or_passモジュールが使用例です。

wire、reg、signal

モジュール内部では内部接続を宣言します。ここがVerilogの命名がほぼすべての初心者を誤解させる箇所です。Verilogには2種類のネットがあります。wireとregです。wireは連続的なassign文か、別のモジュールの出力からしか駆動できません。regはalwaysまたはinitialブロックの中でしか代入できません。regという名前は「レジスタ」を意味しません。クロック付きalwaysブロックで代入されるregはフリップフロップになります。組み合わせalwaysブロックで代入されるregはLUTになります。ハードウェアを決めるのはブロックであって、キーワードではありません。SystemVerilogは両方の場所で使えるlogicを導入し、この混乱を取り除きました。Vivado、Quartus Prime、Yosysはいずれもこれを受け付けます。

VHDLの内部接続はsignalの1種類だけで、プロセスからも並行文からも駆動でき、ハードウェアはやはり代入がどこに書かれているかで決まります。唯一の注意点は、2008年より前のVHDLではoutポートをアーキテクチャ内で読めないことです。そのためコードではトグル用に内部信号led_rを使い、最後にポートへコピーしています。ツールがVHDL-2008を受け付ける場合でもこの習慣は保つ価値があります。レジスタが明示的になるからです。

Verilogの2つのlocalparam行とVHDLのconstant行は派生定数を計算します。HALF_PERIODは点滅周期の半分に含まれるクロックサイクル数で、100 MHzと1 Hzでは50,000,000です。Verilog版のCNT_WIDTHは、組み込み関数$clog2(2を底とする対数の切り上げ)でカウンタのサイズを決めます。50,000,000には26ビットが必要です。VHDL版では幅は不要です。countをnatural range 0 to HALF_PERIOD - 1として宣言すると合成がサイズを決めてくれるからで、これはカウンタに対するよくあるVHDLのイディオムです。

クロック付きブロック

この設計で起きることはすべて、clkの立ち上がりエッジで動く1つのブロックの中で起きます。Verilogではalways @(posedge clk) begin ... endです。VHDLではprocess (clk) begin if rising_edge(clk) then ... end if; end process;です。レッスン04では単純なレジスタに同じ形を示しました。このブロック内の各代入は、左辺の信号に対してフリップフロップを作るので、countは26個のフリップフロップに、ledは1個になります。

本体は3段階の優先順位です。第一に、rstがハイならすべてをクリアします。第二に、カウンタが終端値HALF_PERIOD引く1に達していれば、ゼロに折り返してLEDを反転します。それ以外は1を足します。比較count == HALF_PERIOD - 1は26ビットの等価比較で、合成は数個のLUTでこれを作り、count + 1はキャリーチェーン上の26ビットのインクリメンタです。図が示すハードウェアそのものです。

if分岐の順序が優先順位です。リセットは折り返しに勝ち、折り返しはインクリメントに勝ちます。このパターンは何百回も書くことになります。

代入演算子

Verilogにはブロック内で使う代入演算子が2つあります。ノンブロッキングの<=は「クロックエッジの処理が終わったときにこの値が現れるよう予約し、その間は古い値を使い続ける」という意味です。クロック付きブロック内のすべての代入はこれを使うべきです。ブロッキングの=は「今すぐ更新し、同じブロック内の後続の文は新しい値を見る」という意味で、組み合わせブロックとテストベンチに属します。クロック付きブロックで=を使うのは、シミュレーションとハードウェアの不一致の古典的な原因です。

VHDLはすべての信号代入に<=を使い、プロセス内では常にノンブロッキングとして振る舞います。値は即座にではなく、プロセスが終了したときに現れます。:=演算子は変数と定数用で、変数はまだ必要ありません。

プロセスの外にあるVHDLのled <= led_r;は並行代入で、ポートを内部信号に継続的に接続し、フリップフロップはありません。Verilogでの相当物は、レッスン03で使ったassignです。

なぜ折り返しでトグルするのか

この設計に対する初心者の最初の試みは、フリーランニングカウンタの最上位ビットをそのままLEDにすることがよくあります。それでも動きますが、得られる点滅レートはクロックを2のべき乗で割ったものに限られ、そのビットを別の何かのクロックとして使いたくなる誘惑にもつながります。レッスン04で警告したとおりです。正確な終端値まで数えて別のレジスタをトグルするのが一般的なパターンです。LEDは好きなレートで点滅し、カウンタの折り返しはイネーブルとして再利用できる1サイクルのイベントになり、clkが唯一のクロックのままです。

動作を見る

タイミング図は、点滅全体が数サイクルに収まるようHALF_PERIODを3に減らした設計を示しています。ボード上ではHALF_PERIODは5,000万で、LEDの変化の間に0.5秒待つことになりますが、形は同じです。

HALF_PERIOD = 3の点滅カウンタ。countは0、1、2と上がって折り返します。ledはcountが折り返すエッジでトグルするので、点滅1周期は6サイクルです。clkrstcount012012012led
HALF_PERIOD = 3の点滅カウンタ。countは0、1、2と上がって折り返します。ledはcountが折り返すエッジでトグルするので、点滅1周期は6サイクルです。

コードと照らし合わせて注意深く読んでください。リセット中はcountが0でledも0です。リセット解除後、countはエッジごとにインクリメントします。countがHALF_PERIOD引く1に等しい2であるエッジで折り返し分岐が発火します。countは0に戻り、ledが反転します。LEDが変化するのは、countが2と読めるサイクルの間ではなく、その次のエッジであることに注意してください。条件を見てからそれに応じて動くまでのこの1サイクルの遅延は、レッスン04のレジスタの振る舞いであり、すべての同期設計に存在します。

テストベンチとそれが教えてくれること

シミュレーションを短くするためにパラメータを上書きし、リセットを解除し、LEDのトグル回数を数える小さなVerilogテストベンチを示します。インスタンス化時に#(...)でパラメータを渡しており、これが1つのモジュールを異なる設定で再利用する方法です。

`timescale 1ns/1ps
module blink_tb;
  reg  clk = 1'b0;
  reg  rst = 1'b1;
  wire led;

  // Small numbers so the simulation finishes quickly: 1 kHz clock, 100 Hz blink
  blink #(.CLK_HZ(1000), .BLINK_HZ(100)) dut (.clk(clk), .rst(rst), .led(led));

  always #5 clk = ~clk;

  integer toggles = 0;
  always @(led) if (!rst) toggles = toggles + 1;

  initial begin
    repeat (2) @(posedge clk);
    rst <= 1'b0;
    repeat (52) @(posedge clk);   // 10 half-periods of 5 cycles, plus margin
    $display("led toggled %0d times (expect 10)", toggles);
    $finish;
  end
endmodule

CLK_HZが1000、BLINK_HZが100ならHALF_PERIODは5なので、LEDは5サイクルごとにトグルし、52サイクルには10回のトグルが含まれます。iverilog -g2012 -o blink.vvp blink.v blink_tb.vとvvp blink.vvpを実行すると、led toggled 10 times (expect 10)と表示されるはずです。

相当するVHDLテストベンチはインスタンス化時にgeneric mapを使います。VHDL-2008で書かれており、nvcとGHDLのどちらも受け付けます。

library ieee;
use ieee.std_logic_1164.all;

entity blink_tb is
end entity;

architecture sim of blink_tb is
  signal clk : std_logic := '0';
  signal rst : std_logic := '1';
  signal led : std_logic;
  signal done : boolean := false;
begin
  dut : entity work.blink
    generic map (CLK_HZ => 1000, BLINK_HZ => 100)
    port map (clk => clk, rst => rst, led => led);

  clk <= not clk after 5 ns when not done;

  stim : process
  begin
    wait for 20 ns;
    rst <= '0';
    wait for 500 ns;
    report "simulation finished";
    done <= true;
    wait;
  end process;
end architecture;

波形を開いて、自分でトグルを数えてください。レッスン06では、この手作業の確認を自己検査テストベンチに変えます。

実践でのパラメータとジェネリック

パラメータ化する理由は再利用です。同じblinkモジュールが、ファイルを編集することなく、12 MHzのiCE40ボード(CLK_HZ = 12_000_000)、50 MHzのAlteraボード、100 MHzのAMDボードに使えます。BLINK_HZを変えれば、ハートビート用に2 Hz、アラーム用に10 Hzで点滅させることもできます。大きな設計では、異なる設定で何度もインスタンス化することになります。

いくつかの経験則を挙げます。

  • パラメータは、ハードウェアが構築されるときに固定されなければならないもののためにあります。幅、深さ、クロックレート、アドレスなどです。実行時に変わるものはポートでなければなりません。
  • すべてのパラメータに妥当なデフォルトを与え、すべてを指定しなくてもモジュールを使えるようにしてください。
  • 本体で算術を繰り返すのではなく、パラメータから内部定数(localparam、constant)を導出してください。
  • VHDLでは、ジェネリックにpositiveや部分範囲のような制約付きの型を与え、不正な値がコンパイル時に失敗するようにしてください。SystemVerilogでは型付きパラメータで同じことができます。

演習

  1. 同じ点滅周波数を保ったまま、LEDが周期の4分の1の間点灯し、4分の3の間消灯するようにblinkを変更してください。カウンタを半周期ではなく全周期にわたって動かし、LEDにはトグルではなく比較を使う必要があります。両言語を揃えてください。
  2. カウンタが折り返すたびにちょうど1クロックサイクルだけハイになる2つ目の出力tickを追加してください。これが、レッスン04が派生クロックの代わりに推奨したイネーブルパルスです。
  3. 新しいトップレベルモジュールでblinkを2回、1 Hzと5 Hzでインスタンス化して2つのLEDを駆動し、上のテストベンチを新しいトップレベルに合わせて書き換えてシミュレーションしてください。

ボードがあれば、レッスン10でまさにこの設計をビルドして書き込む手順を追えます。先に進んでビルドし、戻ってきても構いません。

まとめ

  • Verilogのmoduleと、VHDLのentityにarchitectureを加えたものは、それぞれ名前、方向と型を持つポート、本体を宣言します。
  • パラメータ(Verilog)とジェネリック(VHDL)はモジュールを再利用可能にするコンパイル時定数です。派生定数はlocalparamとconstantから得ます。
  • Verilogではwireはassignで駆動され、regはalwaysの中で駆動されます。結果がLUTになるかフリップフロップになるかを決めるのはキーワードではなくブロックです。VHDLは両方にsignalを使います。
  • クロック付きのalways @(posedge clk)、またはrising_edgeを使うprocess (clk)がフリップフロップを作ります。その中ではノンブロッキングの<=を使い、まずリセット、次に優先順位どおりの条件を書きます。
  • 終端カウントまで数えてトグルするパターンは、1つのクロックから任意の点滅レートと、イネーブルとして再利用できる1サイクルの折り返しイベントを与えます。

さらに読む