初めてのHDLモジュール
VerilogモジュールとVHDLエンティティの構造、ポート、wire・reg・signal、連続代入とクロック付き代入を解説し、LEDを点滅させる完全なカウンタを両言語で1行ずつ説明します
このレッスンで学ぶこと
- VerilogモジュールとVHDLのエンティティ・アーキテクチャの構造を説明し、各部分をもう一方の言語に対応付けられる
- 連続代入とクロック付き代入の違いと、それぞれが生成するハードウェアを説明できる
- パラメータ化されたLED点滅カウンタをVerilogとVHDLで書き、そのすべての行を説明できる
- パラメータまたはジェネリックを使って、モジュールを異なるクロック周波数で再利用できるようにする
LUTが何をするか(レッスン03)とレジスタが何をするか(レッスン04)はもう分かりました。本レッスンでは、それらを記述するファイルを書きます。1秒に1回LEDを点滅させるカウンタという1つの小さな設計を取り上げ、VerilogとVHDLで同時に、1行ずつ組み立てます。終えるころには、このトラックの残りを追えるだけ、そして自分のモジュールを書けるだけ、どちらの言語も読めるようになります。
モジュールの形
HDLファイルは、境界を持つ1つのハードウェアブロックを記述します。Verilogではそのブロックはmoduleです。VHDLではentity(境界)とarchitecture(中身)の対です。どちらも3つのことを述べます。ブロックの名前、境界をまたぐポート、そして内部で何が起きるかです。
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秒待つことになりますが、形は同じです。
コードと照らし合わせて注意深く読んでください。リセット中は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では型付きパラメータで同じことができます。
演習
- 同じ点滅周波数を保ったまま、LEDが周期の4分の1の間点灯し、4分の3の間消灯するように
blinkを変更してください。カウンタを半周期ではなく全周期にわたって動かし、LEDにはトグルではなく比較を使う必要があります。両言語を揃えてください。 - カウンタが折り返すたびにちょうど1クロックサイクルだけハイになる2つ目の出力
tickを追加してください。これが、レッスン04が派生クロックの代わりに推奨したイネーブルパルスです。 - 新しいトップレベルモジュールで
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サイクルの折り返しイベントを与えます。
さらに読む
- IEEE 1364-2005, Verilog Hardware Description Language: すべてのツールが実装するVerilog規格。
- IEEE 1800-2023, SystemVerilog:
logicをはじめ多くを追加した上位集合で、現在ほとんどのVerilogツールのデフォルトです。 - IEEE 1076-2019, VHDL Language Reference Manual: 現行のVHDL規格。
- AMD UG901, Vivado Design Suite User Guide: Synthesis: 主要ツールの1つが認識するHDLコーディングパターン。両言語の例付き。
- Yosys manual: オープンソースの合成ツールが何を受け付け、
alwaysブロックをどう扱うか。