カウンタ、タイマーとデバウンス
分周クロックの代わりにクロックイネーブルを使う理由、ミリ秒tick生成器、エッジ検出付きのボタンデバウンサ、多重化した7セグメント表示器を、テストベンチ付きで解説します。
このレッスンで学ぶこと
- 分周したクロックを、新しいクロックではなくクロックイネーブルにすべき理由を説明できる
- パラメータ化したtick生成器を作り、クロック周波数からその周期を計算できる
- プッシュボタンをVerilogとVHDLで同期化、デバウンス、エッジ検出できる
- 時分割多重で4桁の7セグメント表示器を駆動できる
入門用ボードのほぼすべてには高速なクロック、いくつかのボタン、そしてLEDか7セグメント表示器があり、最初のプロジェクトのほぼすべては、何かを人間のペースで起こす必要があります。このレッスンでは、100 MHzの設計が誤動作せずに点滅し、秒を数え、指に反応するための小さな部品を扱います。tick生成器、エッジ検出付きのデバウンサ、表示マルチプレクサです。どれも数行ですが、FPGAがどう使われたがっているかについての教訓がそれぞれ隠れています。レッスン04のレジスタとレッスン06のテストベンチに慣れている必要があります。
クロック分周器とクロックイネーブル
設計を遅くする一番わかりやすい方法は、クロックを分周することです。100 MHzの入力を26ビットのカウンタに通すと最上位ビットに1.5 Hzの方形波が得られ、そのビットを設計の残りのクロックとして使いたくなります。
// Do not do this on an FPGA.
always @(posedge count[25]) led <= ~led;
LEDが点滅するという意味では動きます。しかしこれは初心者の設計で最もよく見られる構造上の誤りでもあり、理由は次のとおりです。FPGAには少数の専用クロックネットワークがあり、クロックエッジがすべてのフリップフロップにほぼ同時に届くように配線されています。フリップフロップから出て一般配線に入る信号は、そのネットワーク上にはありません。ツールは、駆動先のフリップフロップ間に大きなスキューを持つ長く不均一な経路で配線するか、コストを払ってクロックネットワークに昇格させるかのどちらかで、いずれにせよタイミングツールには解析すべき第2のクロック領域が生じます。100 MHzのロジックと1.5 Hzのロジックの間を渡るすべての信号はクロックドメイン間の乗り換え(CDC)であり、それに伴う準安定状態(メタステーブル)の懸念がすべて付いてきます。Lチカでは問題になりません。それより大きなものでは、ハードウェア上でしか現れないバグの恒常的な発生源になります。
正しいパターンはクロックイネーブルです。すべてが1つの高速クロックで動き、遅い動作は、1クロック幅のパルスであるイネーブルがHighのサイクルで起こります。
ミリ秒tick生成器
このモジュールはクロックサイクルを数え、折り返すたびに1サイクルのパルスを出します。DIVIDEを100,000にして100 MHzのクロックを与えると、パルスは1ミリ秒に1回になります。$clog2関数がパラメータからカウンタのサイズを決めるので、ビット幅を手で計算する必要はありません。
// Produces a one-clock-wide pulse every DIVIDE clock cycles.
// With a 100 MHz clock and DIVIDE = 100_000 that is one pulse per millisecond.
module tick_gen #(
parameter integer DIVIDE = 100_000
) (
input wire clk,
input wire rst,
output reg tick
);
localparam integer W = $clog2(DIVIDE);
reg [W-1:0] count;
always @(posedge clk) begin
if (rst) begin
count <= 0;
tick <= 1'b0;
end else if (count == DIVIDE - 1) begin
count <= 0;
tick <= 1'b1;
end else begin
count <= count + 1'b1;
tick <= 1'b0;
end
end
endmodule
注目すべき点が2つあります。カウンタはDIVIDEではなくDIVIDE - 1と比較します。0からN-1まで走るカウンタはN個の状態を持つからです。分周器のオフバイワン誤りは起こしやすく、LEDでは見えません。これは、tick生成器をテストベンチに入れてエッジを数える良い理由です。そしてtickはレジスタ出力です。フリップフロップから直接出てくるのでクリーンで、どこにでも同時に届きます。
tick生成器は連鎖できます。1 msのtickで、1,000まで数える第2のカウンタをイネーブルすれば1 sのtickが得られ、第2のカウンタは10ビットで済みます。
ボタンのデバウンス
機械式ボタンは、押したときに1つのきれいなエッジを生みません。接点は数百マイクロ秒から数ミリ秒の間バウンスし、100 MHzの設計はそのバウンスを1つ1つ別の押下として喜んで数えます。もうひとつ、目立ちにくい問題があります。ボタンはクロックと無関係なタイミングで変化し、変化中の入力をサンプリングしたフリップフロップは準安定状態になって、1サイクル以上にわたって中間電圧を保つことがあります。どちらの問題も同じモジュールで解決します。
// Synchronise a button, debounce it with a millisecond tick, and
// produce one-clock pulses on each press and release.
module debounce (
input wire clk,
input wire rst,
input wire tick_1ms,
input wire btn_raw, // asynchronous, straight from the pin
output reg btn_level, // clean, debounced level
output wire btn_pressed, // one-clock pulse on 0 -> 1
output wire btn_released // one-clock pulse on 1 -> 0
);
// Two-flop synchroniser.
reg [1:0] sync;
always @(posedge clk) sync <= {sync[0], btn_raw};
wire btn_s = sync[1];
// The level is accepted once it has been stable for 16 ticks (16 ms).
reg [3:0] stable;
always @(posedge clk) begin
if (rst) begin
stable <= 4'd0;
btn_level <= 1'b0;
end else if (btn_s == btn_level) begin
stable <= 4'd0;
end else if (tick_1ms) begin
if (stable == 4'd15) begin
btn_level <= btn_s;
stable <= 4'd0;
end else
stable <= stable + 4'd1;
end
end
// Edge detector on the clean level.
reg btn_level_d;
always @(posedge clk) btn_level_d <= btn_level;
assign btn_pressed = btn_level & ~btn_level_d;
assign btn_released = ~btn_level & btn_level_d;
endmodule
このモジュールは3段からなり、それぞれが1つの仕事をします。
同期化回路(シンクロナイザ)は直列の2つのフリップフロップです。1段目は準安定状態になるかもしれませんが、2段目がサンプリングするまでに丸1クロック周期の整定時間があり、100 MHzでは2段目も準安定状態になる確率は、ボタン用途なら無視できるほど小さくなります。設計内のどこもbtn_rawやsync[0]を直接見てはいけません。
デバウンサはミリ秒tickで駆動されるカウンタです。同期化された入力が受理済みのレベルと一致している間、カウンタはクリアされます。異なるときは、tickごとにカウンタが1つ進みます。16 tick連続、つまり16 msの不一致の後にはじめて、受理されたレベルが変わります。それより短いバウンスはカウントをリセットするだけで、無視されます。生のクロックではなくtickを使うことで、カウンタは4ビットで済みます。
エッジ検出器は、クリーンなレベルの1サイクル遅れのコピーを保持し、両者のANDを取ります。btn_pressedは立ち上がりエッジでちょうど1クロックだけHighになり、これこそFSMの遷移が求めるものです。レッスン07では、レベル入力が機械を複数の状態に歩かせました。これがその修正です。
VHDLのデバウンサ
-- Synchroniser, debouncer and edge detector for one push button.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity debounce is
port (
clk : in std_logic;
rst : in std_logic;
tick_1ms : in std_logic;
btn_raw : in std_logic;
btn_level : out std_logic;
btn_pressed : out std_logic;
btn_released : out std_logic
);
end entity;
architecture rtl of debounce is
signal sync : std_logic_vector(1 downto 0) := "00";
signal stable : unsigned(3 downto 0) := (others => '0');
signal level : std_logic := '0';
signal level_d : std_logic := '0';
begin
process (clk)
begin
if rising_edge(clk) then
-- Two-flop synchroniser.
sync <= sync(0) & btn_raw;
if rst = '1' then
stable <= (others => '0');
level <= '0';
elsif sync(1) = level then
stable <= (others => '0');
elsif tick_1ms = '1' then
if stable = 15 then
level <= sync(1);
stable <= (others => '0');
else
stable <= stable + 1;
end if;
end if;
level_d <= level;
end if;
end process;
btn_level <= level;
btn_pressed <= level and not level_d;
btn_released <= (not level) and level_d;
end architecture;
VHDL版は内部のlevel信号を使い、それをポートにコピーしています。VHDL-2008ではoutポートを読めますが、古いツールは拒否するからです。同期化回路と遅延コピーは意図的にリセット分岐の外にあります。リセットが何をしていようと動くべきものだからです。
デバウンサをテストする
以下のテストベンチは両方のモジュールをインスタンス化し、シミュレーションを短く保つためにtickを10クロックに1回に縮め、それぞれ6回バウンスする押下と解放を駆動します。その後パルスを数え、それぞれちょうど1つであることを期待します。
`timescale 1ns / 1ps
// Testbench: a bouncy press and release, with a fast tick to keep it short.
module tb_debounce;
reg clk = 0, rst = 1, btn_raw = 0;
wire tick, level, pressed, released;
// DIVIDE = 10 makes a "millisecond" tick every 100 ns in simulation.
tick_gen #(.DIVIDE(10)) u_tick (.clk(clk), .rst(rst), .tick(tick));
debounce u_db (.clk(clk), .rst(rst), .tick_1ms(tick), .btn_raw(btn_raw),
.btn_level(level), .btn_pressed(pressed), .btn_released(released));
always #5 clk = ~clk;
integer presses = 0, releases = 0;
always @(posedge clk) begin
if (pressed) begin presses = presses + 1; $display("t=%0t pressed", $time); end
if (released) begin releases = releases + 1; $display("t=%0t released", $time); end
end
// Bounce: toggle quickly a few times, then settle.
task bounce_to(input final_level);
integer k;
begin
for (k = 0; k < 6; k = k + 1) begin
btn_raw = ~btn_raw; #37;
end
btn_raw = final_level;
end
endtask
initial begin
$timeformat(-9, 0, " ns", 8);
$dumpfile("debounce.vcd");
$dumpvars(0, tb_debounce);
#25 rst = 0;
#200 bounce_to(1); // press, with bounce
#3000 bounce_to(0); // release, with bounce
#3000;
if (presses == 1 && releases == 1) $display("PASS");
else $display("FAIL: %0d presses, %0d releases", presses, releases);
$finish;
end
endmodule
iverilog -g2012 -o debounce.vvp tick_gen.v debounce.v tb_debounce.v
vvp debounce.vvp
出力はタイムスタンプ2行とPASSです。押下はバウンスが止まってから約1.6 µs後、つまり100 nsの高速tick 16回分の後に報告されます。VCDを開くと、stableが15まで登り、バウンスが来るたびにリセットされるのを見られます。モジュールを編集するのではなくテストベンチでパラメータを上書きするのは、真似る価値のある習慣です。合成されるモジュールが、テストされたものと同じになるからです。
7セグメントマルチプレクサ
Basys 3などのボードには4桁の7セグメント表示器があり、セグメント線は全桁で共有され、桁ごとに1本のイネーブルがあります。一度に点灯できるのは1桁だけなので、設計は目で追えないほど速く桁を巡回します。1ミリ秒ごとに桁を切り替えると各桁は250 Hzでリフレッシュされ、ちらつきの閾値を十分に上回ります。ここでもtick生成器がペースを決めます。
// Four-digit multiplexed seven-segment driver.
// Anodes and segments are active low, as on the Basys 3.
module seven_seg_mux (
input wire clk,
input wire rst,
input wire tick_1ms,
input wire [15:0] value, // four hex digits
output reg [3:0] an, // digit enables, active low
output reg [6:0] seg // a..g, active low
);
reg [1:0] digit_sel;
reg [3:0] nibble;
// Advance one digit every millisecond: 250 Hz per digit, no visible flicker.
always @(posedge clk) begin
if (rst)
digit_sel <= 2'd0;
else if (tick_1ms)
digit_sel <= digit_sel + 2'd1;
end
always @(*) begin
case (digit_sel)
2'd0: begin an = 4'b1110; nibble = value[3:0]; end
2'd1: begin an = 4'b1101; nibble = value[7:4]; end
2'd2: begin an = 4'b1011; nibble = value[11:8]; end
default: begin an = 4'b0111; nibble = value[15:12]; end
endcase
end
// Segment pattern, bit 0 = a ... bit 6 = g, 0 lights the segment.
always @(*) begin
case (nibble)
4'h0: seg = 7'b1000000;
4'h1: seg = 7'b1111001;
4'h2: seg = 7'b0100100;
4'h3: seg = 7'b0110000;
4'h4: seg = 7'b0011001;
4'h5: seg = 7'b0010010;
4'h6: seg = 7'b0000010;
4'h7: seg = 7'b1111000;
4'h8: seg = 7'b0000000;
4'h9: seg = 7'b0010000;
4'hA: seg = 7'b0001000;
4'hB: seg = 7'b0000011;
4'hC: seg = 7'b1000110;
4'hD: seg = 7'b0100001;
4'hE: seg = 7'b0000110;
default: seg = 7'b0001110;
endcase
end
endmodule
2つの組み合わせブロックは純粋なルックアップテーブルで、少数のLUTになります。anとsegの出力はdigit_selと同じサイクルで変わるので、桁Nのセグメントデータは常に桁Nのイネーブルと一緒に提示されます。ある桁のパターンが隣の桁にうっすらゴーストとして見えることがあれば、たいていの原因は、2つのうち一方だけをレジスタで受けてもう一方を受けていない設計で、両者が1サイクルずれて変化していることです。
組み合わせる
入門用ボードの典型的なトップレベルは、1つのtick_genをインスタンス化し、その出力をボタンごとのdebounceとseven_seg_muxに供給し、btn_pressedで、表示器に出す値を持つカウンタを進めます。すべてが1つのボードクロックで動きます。派生クロックがないのでタイミングツールには単一の領域しか見えず、設計は何の努力もなしにタイミングを満たします。それがこのレッスンの要点です。クロックは1つに保ち、それより遅いものにはすべてイネーブルを使い、ブロック間のイベントはレジスタ出力の1サイクルパルスに運ばせてください。
演習
お使いのボード向けにtop_counter.vを書いてください。tick_gen、1つのdebounce、seven_seg_muxをインスタンス化します。btn_pressedでインクリメントする16ビットのレジスタを持ち、表示器に表示します。次に、1 msのtickから供給される第2のtick生成器を追加して1,000 tickを数え、代わりに1秒に1回レジスタをインクリメントするようにし、ボタンで2つのモードを切り替えてください。DIVIDEを減らしてシミュレーション上の1秒が数マイクロ秒になるテストベンチを書き、シミュレーション上の3秒後のレジスタ値を検査してください。ボードに7セグメント表示器がなければ、代わりに下位4ビットをLEDに表示してください。
まとめ
- 分周した信号をクロックとして使わないでください。クロックは1つに保ち、それより遅いものには1サイクルのイネーブルパルスを使います。
- tick生成器はDIVIDEより1小さい値まで数え、レジスタ出力のパルスを出します。長い周期には連鎖させます。
- ボタンには3段が必要です。2段フリップフロップの同期化回路、入力が安定するのを待つデバウンサ、レベルをパルスに変えるエッジ検出器です。
- 合成するものと同じモジュールをテストしながらシミュレーションを短く保つために、パラメータはテストベンチで上書きします。
- 7セグメント表示器は一度に1桁ずつ駆動し、イネーブルとセグメントパターンを同時に変えます。
さらに読む
- AMD UG949、UltraFast Design Methodology Guide。クロッキングと、派生クロックをイネーブルにすべき理由について
- AMD UG472、7 Series FPGAs Clocking Resources。専用クロックネットワークとは何かについて
- Digilent Basys 3リファレンスマニュアル。表示器とボタンの回路について
- Icarus Verilogユーザーガイド