回路を作って確かめる実習集
基礎を読んだら、入力を変えて結果を確かめてみよう。noteで分かれていた入門キットと設計・検証教材を、教科書の各章から使える実習にまとめたよ。ソース・解説・解答はこのサイトで無料で読めるんだ。
- 初めて動かすなら AND回路
- 保持・ロード・リセットから始めるなら カウンタの10チェックと40チェック
- 部品を組み合わせるなら 基礎部品
- 仕様からFSMを作るなら 電子錠
- 理解を確認するなら 12問の演習
1. カウンタ・LED・パイプライン
展開後の practice-projects フォルダで次を実行してね。ツールの準備は Getting Started にあるよ。
iverilog -g2012 -Wall -s tb_learning_blocks -o blocks.vvp learning_blocks.v tb_learning_blocks.v
vvp blocks.vvp
| 部品 | 仕様 | 変えて確かめること |
|---|---|---|
| modulo_counter | 0〜MODULUS-1。enable=0は保持、リセットで0 | 終端の前後、保持、途中リセット |
| blink_led | 有効クロックHALF_PERIOD回ごとにLEDを反転 | 反転間隔と点滅1周期の違い |
| pipeline2 | データとvalidを2段のFFへ送る | validの隙間、毎回異なるデータ、途中リセット |
WIDTH≥1、MODULUS≥1、MODULUS≤2**WIDTH、HALF_PERIOD≥1がパラメータの条件だよ。LEDの反転間隔は、enableが常に1なら HALF_PERIOD / クロック周波数 秒。50MHzで1秒ごとに反転させるにはHALF_PERIOD=50,000,000、点滅1周期は2秒になる。実機でLEDが点く極性は基板に合わせてね。
パイプラインは、入力を取り込むエッジをkとすると、1段目にk、出力段にk+1で入る。段数と「入力を変えてから何回目のエッジか」を混同しないよう、まず次の期待値表を書こう。
| エッジ直後 | 今回取り込む入力 | 1段目 | 出力段 |
|---|---|---|---|
| リセット | — | valid=0 | valid=0 |
| k | 12 / valid=1 | 12 / 1 | 無効 |
| k+1 | a5 / valid=0 | a5 / 0 | 12 / 1 |
| k+2 | 3c / valid=1 | 3c / 1 | a5 / 0(利用しない) |
| k+3 | 次の入力 | 次の入力 | 3c / 1 |
基礎部品の完全なソースを見る
`timescale 1ns/1ps
`default_nettype none
// MODULUS >= 1, WIDTH >= 1, MODULUS <= 2**WIDTH.
module modulo_counter #(
parameter integer WIDTH=4,
parameter integer MODULUS=10
) (
input wire clk, rst_n, enable,
output reg [WIDTH-1:0] count,
output wire terminal
);
assign terminal = (count == MODULUS-1);
always @(posedge clk or negedge rst_n)
if (!rst_n) count <= {WIDTH{1'b0}};
else if (enable) begin
if (terminal) count <= {WIDTH{1'b0}};
else count <= count + 1'b1;
end
endmodule
// LED changes every HALF_PERIOD enabled clock cycles (not a new clock).
module blink_led #(parameter integer HALF_PERIOD=4) (
input wire clk, rst_n, enable,
output reg led
);
localparam integer WIDTH=(HALF_PERIOD<=1)?1:$clog2(HALF_PERIOD);
wire [WIDTH-1:0] count;
wire terminal;
modulo_counter #(.WIDTH(WIDTH), .MODULUS(HALF_PERIOD)) timer (
.clk(clk), .rst_n(rst_n), .enable(enable),
.count(count), .terminal(terminal)
);
always @(posedge clk or negedge rst_n)
if (!rst_n) led <= 1'b0;
else if (enable && terminal) led <= ~led;
endmodule
module pipeline2 (
input wire clk, rst_n, in_valid,
input wire [7:0] in_data,
output reg out_valid,
output reg [7:0] out_data
);
reg valid1;
reg [7:0] data1;
always @(posedge clk or negedge rst_n)
if (!rst_n) begin
valid1 <= 1'b0; out_valid <= 1'b0;
data1 <= 8'd0; out_data <= 8'd0;
end else begin
valid1 <= in_valid; out_valid <= valid1;
data1 <= in_data; out_data <= data1;
end
endmodule
`default_nettype wire
100サイクルを確認するテストベンチを見る
`timescale 1ns/1ps
`default_nettype none
module tb_learning_blocks;
reg clk=0, rst_n=1, enable=0, in_valid=0;
reg [7:0] in_data=0;
wire [3:0] count;
wire [0:0] count_one;
wire terminal, terminal_one, led, led_one, out_valid;
wire [7:0] out_data;
integer checks=0, cycle, model_count=0, enabled_cycles=0;
reg old_valid=0;
reg [7:0] old_data=0;
modulo_counter dut_count(.clk(clk),.rst_n(rst_n),.enable(enable),.count(count),.terminal(terminal));
modulo_counter #(.WIDTH(1),.MODULUS(1)) dut_one(.clk(clk),.rst_n(rst_n),.enable(enable),.count(count_one),.terminal(terminal_one));
blink_led dut_blink(.clk(clk),.rst_n(rst_n),.enable(enable),.led(led));
blink_led #(.HALF_PERIOD(1)) dut_blink_one(.clk(clk),.rst_n(rst_n),.enable(enable),.led(led_one));
pipeline2 dut_pipe(.clk(clk),.rst_n(rst_n),.in_valid(in_valid),.in_data(in_data),.out_valid(out_valid),.out_data(out_data));
always #5 clk=~clk;
initial begin #10000; $fatal(1,"TIMEOUT blocks"); end
task check;
input condition;
input [255:0] label;
begin
checks=checks+1;
if (condition !== 1'b1) $fatal(1,"%0s cycle=%0d t=%0t",label,cycle,$time);
end
endtask
initial begin
$dumpfile("blocks.vcd"); $dumpvars(0,tb_learning_blocks);
for (cycle=0;cycle<100;cycle=cycle+1) begin
@(negedge clk);
rst_n=(cycle!=0 && cycle!=47);
enable=(cycle%5!=0);
in_valid=(cycle%3!=0);
in_data=(cycle*37+18)%256;
if (!rst_n) begin
model_count=0;enabled_cycles=0;old_valid=0;old_data=0;
#1; check(count===0 && led===0 && out_valid===0,"async reset");
end else if (enable) begin
model_count=(model_count+1)%10;
enabled_cycles=enabled_cycles+1;
end
@(posedge clk); #1;
check(count===model_count[3:0],"modulo count");
check(terminal===(model_count==9),"terminal");
check(count_one===1'b0 && terminal_one===1'b1,"modulus one");
check(led===((enabled_cycles/4)%2==1),"LED period");
check(led_one===(enabled_cycles%2==1),"LED period one");
check(out_valid===old_valid,"valid latency");
if (old_valid) check(out_data===old_data,"data latency");
if (rst_n) begin old_valid=in_valid;old_data=in_data;end
end
$display("PASS blocks checks=%0d cycles=100",checks);
$finish;
end
endmodule
`default_nettype wire
2. 仕様から作る、2ボタン式の電子錠
ここではFSMの学習用モデルを作るよ。実際の錠や安全装置を制御する完成品ではない。入力は、クロックへ同期化し、チャタリングを除去した後の1サイクルパルスを前提にするんだ。
仕様を先に固定しよう。 リセットで施錠。A→Bで解錠し、解錠後はリセットまで保持する。最初のBは無視する。Aの次にA、またはAとBの同時入力が来たら最初へ戻る。待機中の同時押しも無視するよ。途中で何も押さない期間があっても、Aを受け付けた状態は保持する。
| 現在の状態 | 入力なし | Aだけ | Bだけ | AとB | unlock |
|---|---|---|---|---|---|
| IDLE | IDLE | WAIT_B | IDLE | IDLE | 0 |
| WAIT_B | WAIT_B | IDLE | OPEN | IDLE | 0 |
| OPEN | OPEN | OPEN | OPEN | OPEN | 1 |
| 未使用の符号11 | IDLE | IDLE | IDLE | IDLE | 0 |
リセットはこの表より優先だよ。状態FF、次状態を決める組合せ回路、現在状態から作る出力に分けて実装する。next_state = state は「入力なしでは保持」という既定値を表すんだ。
電子錠の完全なRTLを見る
`timescale 1ns/1ps
`default_nettype none
// Inputs are already synchronized, debounced, one-clock pulses.
// Both pressed: discard the partial sequence. Unlock holds until reset.
module electronic_lock (
input wire clk, rst_n, btn_a, btn_b,
output wire unlock
);
localparam [1:0] IDLE=2'd0, WAIT_B=2'd1, OPEN=2'd2;
reg [1:0] state, next_state;
always @(posedge clk or negedge rst_n)
if (!rst_n) state <= IDLE;
else state <= next_state;
always @* begin
next_state = state;
case (state)
IDLE: begin
if (btn_a && !btn_b) next_state = WAIT_B;
end
WAIT_B: begin
if (btn_a) next_state = IDLE;
else if (btn_b) next_state = OPEN;
end
OPEN: next_state = OPEN;
default: next_state = IDLE;
endcase
end
assign unlock = (state == OPEN);
endmodule
`default_nettype wire
iverilog -g2012 -Wall -s tb_electronic_lock -o lock.vvp electronic_lock.v tb_electronic_lock.v
vvp lock.vvp
テストベンチでは、入力なし・A・B・同時押しの4種類を6回並べた4096通りを試すよ。判定はRTLとは別に書いた状態遷移表を使う。各列の前にリセットし、同時押し、誤順序、保持、解錠後の入力を含めるんだ。最後に未使用状態を注入してRTL上での復帰も確認する。
電子錠のテストベンチを見る
`timescale 1ns/1ps
`default_nettype none
module tb_electronic_lock;
reg clk=0, rst_n=1, btn_a=0, btn_b=0;
wire unlock;
integer checks=0, sequence_id, step, stimulus, encoded;
integer model=0;
electronic_lock dut(.clk(clk),.rst_n(rst_n),.btn_a(btn_a),.btn_b(btn_b),.unlock(unlock));
always #5 clk=~clk;
initial begin #1000000; $fatal(1,"TIMEOUT lock"); end
task check;
input expected;
begin
checks=checks+1;
if (unlock !== expected)
$fatal(1,"lock check=%0d t=%0t got=%b expected=%b",checks,$time,unlock,expected);
end
endtask
task reset;
begin
@(negedge clk); rst_n=0; btn_a=0; btn_b=0;
#1; check(0); model=0;
@(negedge clk); rst_n=1;
end
endtask
initial begin
$dumpfile("lock.vcd"); $dumpvars(0,tb_electronic_lock);
// All 4^6 event sequences. Encoding: 0=none,1=A,2=B,3=both.
for (sequence_id=0; sequence_id<4096; sequence_id=sequence_id+1) begin
reset;
encoded=sequence_id;
for (step=0; step<6; step=step+1) begin
stimulus=encoded%4; encoded=encoded/4;
@(negedge clk); btn_a=stimulus[0]; btn_b=stimulus[1];
// Independent reference transition table.
case (model)
0: if (stimulus==1) model=1;
1: case (stimulus)
1,3: model=0;
2: model=2;
endcase
2: model=2;
endcase
@(posedge clk); #1; check(model==2);
end
end
// Inject the unused RTL encoding; this is not a physical fault test.
reset;
@(negedge clk); force dut.state=2'b11;
#1; check(0);
@(negedge clk); release dut.state;
@(posedge clk); #1; check(0);
if (dut.state !== 2'b00) $fatal(1,"illegal state did not recover");
$display("PASS lock checks=%0d sequences=4096",checks);
$finish;
end
endmodule
`default_nettype wire
2026-10-26の実行結果: Windows / Icarus Verilog 13.0、-g2012 -Wall で、基礎部品666チェック・電子錠28,675チェックがPASS。さらに入力順序、カウンタの折り返し値、validの遅延を意図的に壊した3種類では、TBが失敗を検出して非ゼロ終了したよ。
これはRTL機能の確認結果だよ。合成・配置配線・実機での動作確認は含まない。FPGAへ進むときは、デバイス、ピン、電圧、クロック、リセット解除、ボタンの同期化とデバウンスを基板の仕様で決めよう。例えば100MHzのclkなら下のクロック制約が出発点になるけれど、入出力制約なども別途必要だね。
create_clock -name sys_clk -period 10.0 [get_ports clk]
追加課題: 解錠からNクロックで自動施錠する機能を足してみよう。N-1、N、N+1の境界と、タイマー途中のリセットを先に期待値として書いてね。物理ボタンへつなぐ前の課題として、同期化・デバウンス・1パルス化を別モジュールに分ける方法も考えよう。
3. 設計・検証・合成をつなぐ12問
解答を開く前に、変数の値と更新時刻を書いてみよう。元の演習の断定的な説明を修正し、仕様によって答えが変わる点も明記したよ。
Q1:順序回路の代入
always @(posedge clk) count = count + 1; を、この教材のFFの記述方針に合わせて修正しよう。この1行だけで「必ず合成警告が出て実機が壊れる」と言えるかな?
解答と理由
count <= count + 1'b1; を使うよ。単独のカウンタだけなら同じ機能に合成される場合もあり、必ず警告や不具合が出るとは言えない。別プロセスが同時にcountを読む場合の競合や、複数段で更新済み値を読んでしまう問題を避けるために揃えるんだ。E2の更新時刻で確認しよう。
Q2:caseの条件漏れ
2ビットmodeに00、01、10の出力しか代入していない組合せ処理。11では0を出す仕様にするには?
解答と理由
default: out = 1'b0; を加えるか、caseの前にoutの既定値を代入するよ。大切なのは全経路への代入。空のdefaultだけでは保持を防げない。ラッチ比較実験では保持がシミュレーションでも見えるよ。
Q3:何サイクル後の結果?
組合せの y = a & b と、正エッジ内の tmp <= a + b; result <= tmp; を比べよう。resultに入るのはどのtmp?
解答と理由
同じエッジで更新される前のtmpだよ。NBAは次のクロックまで待つ仕組みではなく、今の時刻のイベント処理の中で更新する仕組み。この2段では加算結果がtmpを経て次のエッジでresultへ移る。上のパイプライン表で説明してみよう。
Q4:次状態を代入しない経路
IDLEでstart=1のときだけnext_stateへRUNを代入し、RUNでdone=1のときだけIDLEを代入した。start=0やdone=0では?
解答と理由
次状態変数への代入がなく、その前値を保持してしまうよ。組合せ処理の先頭でnext_state = state;を設定し、未使用状態にはdefaultで復帰先を決める。電子錠の実装もこの形だね。
Q5:終端フラグと折り返しパルス
8ビットカウンタが0〜255を巡回するとき、assign terminal = (cnt == 8'hff); はバグかな? FFで overflow <= (cnt == 8'hff); と受けた場合との違いは?
解答と理由
終端値を示す仕様ならterminalは正しいよ。登録したoverflowは古いcntを判定するので、cntが0へ戻ったエッジ後に1となる。用途とサイクルが違うんだ。組合せ出力のグリッチを非同期制御へ渡す危険と、同一クロックでタイミングを満たして受ける用途を区別しよう。254→255は1ビットだけの変化なので「8ビット同時切替が原因」と説明するのも誤りだよ。
Q6:FSMを2プロセスに分ける
en=1でIDLE→ACTIVE、en=0でACTIVE→IDLEとなる回路を、状態FFと次状態の組合せ処理へ分けよう。
解答と理由
FFはリセット時IDLE、それ以外はstate<=next_state。組合せ側はnext_state=stateを先に置き、IDLEならenでACTIVE、ACTIVEなら!enでIDLEへ。出力はassign out = (state == ACTIVE);とできるよ。ただし元の登録出力と同じサイクルになることを確認する。1プロセスFSMも有効な書き方で、分割自体が正しさを保証するわけではないんだ。
Q7:終わらないテスト
クロックは動くけれど、wait(done); が帰ってこない。何を追加する?
解答と理由
待機条件とdoneの駆動元を調べ、独立したinitialに上限時間と$fatal(1,"timeout")を用意するよ。単に$finishでは成功終了と区別できない場合がある。クロック停止でも時間上限で検出できる構成にしよう。
Q8:同じ正エッジで判定する
@(posedge clk); if (q !== expected) ... で、DUTの q <= d の新しい値を必ず読める?
解答と理由
NBA更新前の値を読む可能性があるよ。この教材では入力を負エッジで変え、正エッジの後に明示した小さな待ち時間を入れて判定する。timescaleと周期に合わせてね。大規模なSystemVerilog検証ではclocking blockなども選択肢になるよ。
Q9:XでもPASSしてしまう
if (q != expected) FAIL; else PASS; で未知値が紛れた。どう直す?
解答と理由
TBではq !== expectedを使い、X/Zも不一致として扱う。!=の結果自体がXだとifは真側を選ばない場合があるよ。期待値にもXが混入していないか確認し、最後のPASSは全チェックの成功後だけに出そう。
Q10:Latch inferredの判断
modeを条件付きで保持したい仕様でラッチ推論が出た。どう判断する?
解答と理由
まず保持のタイミングを確認する。クロックで更新する仕様なら、クロック付きFFとenableで書こう。透明期間を持つラッチが意図された設計なら、その対象資源とタイミング設計を確認する。「意図したラッチは存在しない」という断定は誤りだよ。無視するのではなく設計意図との対応を記録するんだ。
Q11:data_outとdata_ot
宣言名data_outをdata_otと書き間違えた。必ず1ビットwireが暗黙にできる?
解答と理由
使われた文脈によるよ。連続代入やモジュール接続では暗黙ネットの規則が関わる一方、未宣言名への手続き代入はエラーになる場合がある。名前を直し、default_nettype noneで許可される暗黙ネットも止めよう。ただし既に宣言された別名への書き間違いまでは防げない。リセットもないカウンタなら初期値の問題も別途修正する必要があるね。
Q12:50MHzなのにperiod=10
50MHz入力に create_clock -period 10.0 を設定した。何を直し、何を再確認する?
解答と理由
実際の要件が50MHzなら20.0nsへ直す。周期(ns)=1000/周波数(MHz)だよ。その後、制約が正しいポートへ適用されたか、未制約パス、setup/hold、入出力条件を確認する。修正しただけでSlackが必ず正になるとは言えないんだ。