第3章:Verilog RTL記述とシミュレーション(初学者検証版)
本章では、ハードウェア記述言語であるVerilog HDLの基礎を学び、Lチカ(LED点滅カウンタ)の回路を記述します。さらに、記述した回路が狙い通りに動くかを、PC上のシミュレータで検証するスキルを身につけます。
3.1 【思考転換】ソフトウェア脳からハードウェア脳へ
C言語やPythonはコードを上から下へ1行ずつ順番に実行する「逐次処理」ですが、Verilog HDLは記述されたすべてのブロックが電源が入った瞬間から「同時に、並列で、常に」物理回路として電気的に駆動し続けます。
教訓: Verilogのコードは、「電子部品(ゲートやレジスタ)の接続図をテキストで表現したもの」であると脳内を切り替えてください。
※「HDL」とは ― VerilogとVHDL、そしてSystemVerilog
ハードウェアを記述する言語(HDL:Hardware Description Language)には、代表的なものとして Verilog と VHDL があります。文法はそれぞれ異なりますが、どちらも「回路を文章で記述する」ための言語で、FPGAでもASICでも用いられています。初学者が「どちらを学べばよいのか」と最初に迷う点ですが、本講座では Verilog を使用します。
これは、本講座が用いるオープンソースの半導体設計フロー(Tiny Tapeout/LibreLane/Yosys)が、Verilog(.v ファイル)を主に想定しているためです。テンプレートに含まれるサンプルもVerilogで記述されているため、本講座ではVerilogで進めます。これはあくまで本講座における選択であり、VHDLが劣っているという意味ではありません。VHDLは航空宇宙や防衛をはじめ多くの分野で長く使われてきた言語であり、学ぶ価値のある選択肢の一つです。どちらを選ぶかは、目的や周囲の環境(職場・研究室・対象とするツール)に応じて判断するのがよいと考えられます。
また、Verilog をもとに機能を加えた SystemVerilog という言語もあります。検証のための機能などが加えられているとされ、設計や検証の現場で広く使われています。本章ではVerilogを用いて進めるため詳しくは扱いませんが、HDLにはこうした言語もある、という形で紹介しておきます。HDL言語そのものをより深く学びたい場合は、シリーズの別講座(FPGA入門編)で扱う予定です。本章ではまず、Verilogで実際に手を動かすことを優先します。
※本章で必要となるVerilogの知識は限られています
「新しいプログラミング言語を一から覚えるのか」と身構える必要はありません。本章のLチカ回路を読み書きするために必要なVerilogの知識は、ごく少数の要素に限られます。具体的には、次の節で登場する「module/入出力の宣言/reg と wire/always/<=(代入)」の5つ程度を理解すれば、Lチカ回路は読み解けます。
言語のすべての機能を最初から網羅する必要はありません。「まずはLチカを動かすために必要な範囲だけ」と区切って進め、不足する部分は必要に応じて補っていく形で問題ありません。
※そもそも、なぜ「波形」を見るのか ― 回路は"太鼓のリズム"で動いている
本章の後半では、回路の動きを波形という形で表示して確認します。その前に、「そもそも、なぜ波形を見るのか」という根本のイメージを持っておくと、後半の作業がより意味のあるものになります。
全員で体操をするとき、先生が太鼓でテンポを取り、みんながその音に合わせていっせいに動く場面を思い浮かべてください。デジタル回路も、これとよく似ています。クロックと呼ばれる一定のリズム(太鼓の音)に合わせて、回路全体がいっせいに動いていきます。人間の心臓が一定の間隔で脈を打ち、そのたびに血液を送り出すのにもたとえられます。クロックは、回路にとっての"鼓動"にあたります。
では、本章で作るカウンタとは何でしょうか。太鼓に合わせて体操する場面で、各人が目をつむっていても「いま何個目の音か」を頭の中で数え続けているとします。その「何個目か」という数を覚えておき、音が鳴るたびに1つ増やしていく――これがカウンタです。本章のカウンタも、クロック(太鼓)が1回鳴るたびに、自分が保持している数を1つ増やしていきます。
そして、何個目かを数え続けていくと、大きな節目(たとえば「何百個目ごと」といった上の位)は、なかなかやってきません。カウンタでいえば、上の桁(最上位ビット)は、ゆっくりとしか切り替わりません。このゆっくりとしか変わらない桁をLEDにつなぐと、点いたり消えたりがゆっくり繰り返されます。これが「Lチカ(LED点滅)」の正体です。
こうした回路の動きは、「時間にそって、各信号がどう変化していくか」として現れます。それを目に見える形にしたものが波形です。波形を見ることは、いわば回路の心電図を診るようなもので、「クロックという鼓動に合わせて、各信号が意図したとおりに動いているか」を時間軸で確かめる作業にあたります。「なぜ波形を見るのか」が腹に落ちていれば、本章後半の波形の読み取りも、ただの記号の並びではなく"回路の鼓動と動き"として見えてきます。
3.2 【準備】設計を記述するためのテンプレート(雛形)を用意する
コードの記述を始める前に、まず設計を格納するための「入れ物」を用意します。ここは第2章とつながる重要な切り替え点であるため、最初に整理しておきます。
※第2章で用意したものと、本章で用意するものは別のものです
第2章では factory-test を複製(クローン)しました。これは「すでに完成している動作確認用の見本」であり、環境が正しく動作するかを確かめる目的のものでした。その中身を自分で記述することはありませんでした。
第3章からは、自分自身で設計を記述していきます。そのために用いるのが ttsky-verilog-template という「自分の設計を書き込むための雛形」です。これは factory-test(完成した見本)とは別のものであり、本節で新たに複製(クローン)します。「第2章でテンプレートも用意したのではないか」と混同しやすい点ですが、第2章で複製したのは見本(factory-test)であり、雛形(template)を扱うのは本節が最初です。
第2章で用いたもの 第3章で用いるもの factory-test ttsky-verilog-template =完成した見本 =設計用の雛形 (環境の動作確認用) → (自分の設計を記述する用) 中身は記述しない 中身を自分で記述する
3.2.1 テンプレートを複製(クローン)する
第2章で構築した環境をそのまま使用します。ターミナルを新しく開き直した場合や、第2章から続けて作業していない場合は、先に第2章2.9の「再開時の確認手順(Docker Desktopの起動・Ubuntuの起動・プロジェクトへの移動)」を済ませておいてください。
※以下のコマンドを実行するディレクトリについて: 次のコマンドは、まずホームディレクトリ(~)で実行します。自分が現在どのディレクトリにいるか分からなくなった場合は、pwd コマンドで確認できます(第2章でも触れたとおりです)。
# 1. いま自分がどのディレクトリにいるかを確認する pwd # 2. ホームディレクトリ(~)へ移動する cd ~ # 3. 移動できたかを確認する(/home/ユーザー名 と表示されれば成功) pwd # 4. テンプレートを my_chip_project という名前で複製(クローン)する git clone https://github.com/TinyTapeout/ttsky-verilog-template ~/my_chip_project
※ git clone とは: git clone は、インターネット上で公開されているリポジトリ(ファイル一式)を、自分のPCに丸ごと複製(クローン)するコマンドです。上記コマンド4を実行すると、Receiving objects: 100% ... のような進捗が表示され、ホームディレクトリの下に my_chip_project というフォルダが作成されます。このフォルダが、これから設計を進めるための作業場所となります。
※ git clone はフォルダの作成も兼ねています: コマンド4の末尾にある ~/my_chip_project は、複製したものを格納するフォルダ名の指定です。このフォルダは git clone が自動的に作成するため、あらかじめ mkdir で作成しておく必要はありません(第2章と同じ考え方です)。なお、第2章で複製した ~/factory-test はそのまま残るため、消えたり混在したりすることはありません。
※入力を誤った場合・やり直したい場合は「削除してから複製し直す」
フォルダ名の入力を誤った場合や、中身を編集して状態が分からなくなった場合でも、対処できます。フォルダごと削除し、再度 git clone を実行すれば、未編集の雛形を改めて取得できます。
# やり直す場合:フォルダごと削除してから、改めて複製(クローン)する rm -rf ~/my_chip_project git clone https://github.com/TinyTapeout/ttsky-verilog-template ~/my_chip_project
⚠️ rm -rf は確認を求めずに削除を実行するコマンドです。削除の対象(~/my_chip_project)に誤りがないかを必ず確認してから実行してください。このフォルダを削除しても、第2章の ~/factory-test には影響しません(別のフォルダであるためです)。
3.2.2 テンプレートの構成を確認する ― どこに何があるか
複製が完了したら、フォルダの中へ移動し、構成を確認します。どのような要素で構成されているかを最初に把握しておくと、以降の作業で迷いにくくなります。
# 1. プロジェクトのフォルダへ移動する cd ~/my_chip_project # 2. 現在のディレクトリを確認する(.../my_chip_project と表示されれば成功) pwd # 3. フォルダの中身を一覧表示する ls -la
主なフォルダ・ファイルは次のとおりです(当面は、太字で示した3つを把握しておけば十分です)。
src/: 設計本体(Verilog)を格納する場所です。本章の中心となります。内部にproject.vというサンプルが最初から含まれています。test/: テスト(動作確認)に用いる場所です。tb.vというテストベンチが最初から含まれています。info.yaml: プロジェクトの設定ファイルです。「トップモジュール名」「使用するVerilogファイル」などを記述します(第4章・第5章で詳しく扱います)。README.md:テンプレート自身の説明書です。不明な点があれば、最初に参照すると参考になります。docs/・.github/など:ドキュメントや、クラウドでの提出(第6章)に用いる設定です。現時点では特に意識する必要はありません。
※このテンプレートには、最初からサンプルが含まれています
「雛形」と述べましたが、正確には動作するサンプルがあらかじめ含まれた雛形です。src/project.v には、簡単なサンプル回路(入力どうしを加算するだけのもの)が記述されています。本章では、このサンプル project.v の中身を、自分のLチカ回路に書き換えるという方法で進めます。新しいファイルをゼロから作成するのではなく、用意された見本を書き換えていくことで、ファイル名や設定の整合に悩まずに進められます。
構成を把握できたら、次節で src/project.v の中身を、自分のLチカ回路へ書き換えていきます。
3.3 【Step 1】設計本体(project.v)をLチカ回路へ書き換える
ここから、テンプレートに含まれるサンプル src/project.v の中身を、自分のLチカ(LED点滅)回路へ書き換えます。前節で確認したとおり、新しいファイルを作成するのではなく、既存の project.v を書き換える方法で進めます。
3.3.1 まず元のサンプルを退避(バックアップ)しておく
書き換えを始める前に、元のサンプルを別名でコピーして残しておきます。こうしておくと、書き換えに失敗しても元に戻せます。
※以下のコマンドを実行するディレクトリについて: 次のコマンドは、プロジェクトのルート(~/my_chip_project)で実行します。現在地が不明な場合は pwd で確認してください。
# プロジェクトのルートにいることを確認(.../my_chip_project と表示されれば成功) pwd # 元のサンプルを project.v.bak という名前でコピーして退避する cp src/project.v src/project.v.bak
コマンドの実行後に何も表示されなければ、コピーは成功しています。project.v.bak が、書き換え前の状態を保存した控えになります。
3.3.2 nanoエディタでproject.vを開き、書き換える
テキストエディタ nano を使って project.v を開き、中身を入れ替えます。
# nanoエディタで project.v を開く nano src/project.v
nano が起動したら、次の手順で中身を入れ替えます。
- 既存の内容をすべて削除する:
Ctrl + Kを押し続けると、カーソル行が次々と削除されます。画面が空になるまで削除してください。 - 下記のLチカ回路コードを貼り付ける: 下のコードをコピーし、
nanoの画面へ貼り付けます(貼り付けはCtrl + Shift + V、または右クリック)。 - 保存する:
Ctrl + Oを押します。画面下部に保存先のファイル名(src/project.v)が表示されるので、変更せずにEnterを押すと上書き保存されます。 - エディタを終了する:
Ctrl + Xを押すとnanoが終了します。
/*
* Lチカ(LED点滅カウンタ) - 学習用
* SPDX-License-Identifier: Apache-2.0
*/
`default_nettype none
module tt_um_example (
input wire [7:0] ui_in, // 汎用入力(未使用)
output wire [7:0] uo_out, // 汎用出力(uo_out[0] をLEDに使用)
input wire [7:0] uio_in, // 双方向ピン入力(未使用)
output wire [7:0] uio_out, // 双方向ピン出力(未使用)
output wire [7:0] uio_oe, // 双方向ピン出力有効化(0で入力固定)
input wire ena, // 電源が入っている間は常に1(無視してよい)
input wire clk, // クロック
input wire rst_n // リセット(0でリセット=負論理)
);
// 24ビットのカウンタ。clkの立ち上がりごとに+1される。
reg [23:0] counter;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
counter <= 24'b0; // 非同期リセット
end else begin
counter <= counter + 1'b1; // カウントアップ
end
end
// カウンタの最上位ビットをLED(uo_out[0])へ接続。残りのビットは0で固定。
assign uo_out = {7'b0, counter[23]};
assign uio_out = 8'b0;
assign uio_oe = 8'b0;
// 未使用の入力を明示的にまとめて、警告を抑制する
wire _unused = &{ena, ui_in, uio_in, 1'b0};
endmodule
※モジュール名を tt_um_example のままにしている理由
上のコードでは、モジュール名をテンプレート既定の tt_um_example のままにしています。これは、テストベンチ(test/tb.v)やプロジェクト設定(info.yaml)が、いずれもこの名前を前提に書かれているためです。名前を別のものへ変更すると、それらの複数のファイルも合わせて修正する必要が生じます。本章では学習を円滑に進めるため、名前は変えずに、中身だけを書き換える方針を採ります。
3.3.3 書き換えた内容を確認する
保存できたら、ファイルの中身を表示して、意図したとおりに書き換わっているかを確認します。
# project.v の中身を表示して確認する cat src/project.v
表示された内容が、先ほど貼り付けたLチカ回路コードと一致していれば、書き換えは成功です。
3.3.4 【最小Verilog解説】このコードを読むために必要な5つの要素
3.1で触れたとおり、このLチカコードはVerilogのごくわずかなルールで読み解けます。ここでは、上のコードに登場する5つの要素に絞って解説します。これだけ把握すれば、コードが何をしているかを追えます。
① module(モジュール)
module ... endmodule で囲まれた範囲が、1つの回路のまとまりです。module tt_um_example ( ... ); の丸括弧の中に、外部とやり取りする信号(入出力)を並べます。回路を1つの部品として定義する枠組み、と捉えてください。
② 入出力の宣言(input / output)
input は外から入ってくる信号、output は外へ出していく信号です。[7:0] は「8本まとめた信号(8ビット幅)」を表します。たとえば output wire [7:0] uo_out は、8ビットの出力信号 uo_out を意味します。
③ reg と wire
reg は値を記憶できる信号(カウンタの現在値など、状態を保持するもの)に用います。wire は値を保持せず、つなぐだけの信号(配線)に用います。このコードでは counter が reg、入出力ピンが wire です。
④ always(常時動作するブロック)
always @(posedge clk ...) は、「clk が立ち上がるたびに、この中の処理を実行する」という意味です。クロックに合わせて繰り返し動作する回路(順序回路)を記述するための構文です。
⑤ <=(ノンブロッキング代入)
always の中で値を更新するときに使う代入記号です。counter <= counter + 1'b1; は「クロックの立ち上がりで counter を1増やす」という意味になります。順序回路では、通常の = ではなくこの <= を用います(理由は後述の発展で触れます)。
この5つを手がかりに、もう一度コードを眺めてみてください。「module で回路を定義し、reg のカウンタを用意し、always でクロックごとに <= で1ずつ増やし、その値を output のLEDへつないでいる」という流れが読み取れれば、Lチカ回路の理解としては十分です。
🔍 詳しく知りたい方向け:Verilog記述の「お作法」(書き方の慣習)
Verilogには、誤動作や思わぬバグを避けるために、現場で広く共有されている記述上の慣習があります。上のLチカコードも、これらの慣習に沿って書かれています。絶対の規則ではありませんが、知っておくと、なぜこの書き方なのかが理解しやすくなります。
`default_nettype noneを先頭に置く: これを書いておくと、信号名のタイプミスなどで未定義の信号が生じた際に、エラーとして検出されやすくなります。意図しない配線が黙って作られる事態を防ぐ目的の記述です。- 順序回路には
<=、組み合わせ回路には=を使う: クロックに同期して動く回路(always @(posedge clk)の中)ではノンブロッキング代入<=を、組み合わせ回路ではブロッキング代入=を用いるのが一般的です。両者を混在させると、シミュレーションと実際の回路で挙動が食い違う原因になることがあるとされています。 - 出力には必ず値を割り当てる: 使用しない出力にも
assign uio_out = 8'b0;のように値を与えておきます。割り当てが無いと、警告や合成上の問題につながることがあります。 - 未使用の入力をまとめて扱う:
wire _unused = &{ena, ui_in, uio_in, 1'b0};は、使用しない入力をまとめて1つの信号に束ね、未使用に伴う警告を抑制するための慣用的な書き方です。 - 数値にはビット幅を明示する: 単に
0と書く代わりに24'b0や8'b0のように幅を示すと、意図しないビット幅による不具合を避けやすくなります。 - リセットの極性をそろえる: Tiny Tapeoutでは
rst_n(0でリセットする負論理)に統一されています。リセット処理はif (!rst_n)と記述します。
記述の順番にも、ゆるやかな型があります。上のコードも次の流れに沿っています。
1. `default_nettype none ← 冒頭の宣言 2. module 名 ( 入出力の宣言 ); ← 外部とつなぐ信号の定義 3. 内部信号の宣言(reg / wire)← counter など 4. always ブロック ← クロックに同期する処理(カウンタ本体) 5. assign による出力への接続 ← 出力ピンへのつなぎこみ 6. 未使用信号の処理(_unused) 7. endmodule
3.4 【Step 2】動作を確認するための「テストベンチ」を用意する
設計本体(project.v)はLチカ回路になりましたが、回路は単体では動きを確認できません。クロックやリセットといった入力信号を外から与える仕組みが必要です。その役割を担うのがテストベンチです。テストベンチは test/ フォルダ内の tb.v に記述します。
※テンプレート既定の tb.v と、本章で用いるテストベンチの違い
テンプレートには test/tb.v が最初から含まれていますが、これはcocotb(Pythonでテストを記述する仕組み)と組み合わせて使うことを前提としています。そのため、クロックを発生させる記述などが含まれておらず、このまま単体でシミュレーションを実行しても、信号が変化せず波形が得られません。
本章では、仕組みを理解しやすくするため、クロックやリセットを自分で記述した、単体で動作するテストベンチに置き換えて進めます。cocotbを用いる本格的な方法は、より進んだ検証として別途扱います(本節末の発展を参照)。
3.4.1 元のtb.vを退避し、書き換える
設計本体のときと同様に、まず元の tb.v を退避してから書き換えます。
※以下のコマンドを実行するディレクトリについて: 次のコマンドは、プロジェクトのルート(~/my_chip_project)で実行します。
# 元の tb.v を tb.v.bak として退避する cp test/tb.v test/tb.v.bak # nanoエディタで tb.v を開く nano test/tb.v
nano が開いたら、3.3と同じ手順(Ctrl + K で全削除 → 下記を貼り付け → Ctrl + O → Enter → Ctrl + X)で、次のテストベンチに置き換えます。
`timescale 1ns / 1ps
`default_nettype none
module tb;
reg [7:0] ui_in;
reg [7:0] uio_in;
reg ena;
reg clk;
reg rst_n;
wire [7:0] uo_out;
wire [7:0] uio_out;
wire [7:0] uio_oe;
// Lチカ回路(tt_um_example)をインスタンス化(回路を1個分、組み込む)
tt_um_example uut (
.ui_in (ui_in),
.uo_out (uo_out),
.uio_in (uio_in),
.uio_out (uio_out),
.uio_oe (uio_oe),
.ena (ena),
.clk (clk),
.rst_n (rst_n)
);
// クロックを宣言時に0で初期化しておく(不定値から始まるのを避けるため)
initial clk = 1'b0;
// 50MHz相当のクロックを発生させる(10nsごとに反転=20ns周期)
always #10 clk = ~clk;
initial begin
$dumpfile("tb.vcd"); // 波形をtb.vcdに記録する
$dumpvars(0, tb);
// 初期値を設定する
rst_n = 0; // リセット状態にする
ena = 1;
ui_in = 8'b0;
uio_in = 8'b0;
#40; // 40ns待機(リセットを十分な期間維持する)
rst_n = 1; // リセットを解除する
#1000000; // 1ミリ秒分のシミュレーションを実行する
$display("Simulation finished.");
$finish;
end
endmodule
※波形ファイルに .vcd 形式を用いる理由
上のテストベンチでは、波形を tb.vcd(VCD形式)として記録するようにしています。波形には他に .fst 形式もありますが、波形ビューワとの組み合わせによっては .fst がうまく開けない場合があります。.vcd は古くから用いられている形式で、波形ビューワで安定して開きやすいため、本章では .vcd を用います。
🔍 詳しく知りたい方向け:cocotb を用いた、より本格的な検証方法
テンプレートの test/ フォルダには、Makefile や test.py といったファイルも含まれています。これらは cocotb と呼ばれる仕組みのためのもので、テストの内容をPythonで記述し、make -B といったコマンドで実行します。入力信号の与え方や期待値の確認をPythonの柔軟な記述で行えるため、より複雑な検証に向いています。
本章では、まず仕組みを把握しやすい「自分で記述するテストベンチ」を用いていますが、本節で tb.v を書き換えたことにより、テンプレート既定のcocotb用テスト(make -B)とはそのままでは整合しなくなる点に注意してください。cocotbの方法を試す場合は、退避しておいた tb.v.bak から元の tb.v を戻す(cp test/tb.v.bak test/tb.v)とよいでしょう。
3.5 【Step 3】シミュレーションを実行する
設計本体とテストベンチがそろったので、シミュレーションを実行します。ここでは、設計の論理が意図どおりに動くかを、PC上で確認します。
3.5.1 シミュレーション用ツールを確認・導入する
シミュレーションには iverilog(Icarus Verilog)と、波形を表示する gtkwave を用います。まず、これらが導入済みかどうかを確認します。
# iverilog / vvp / gtkwave が導入済みかを確認する which iverilog vvp gtkwave
3つのパス(たとえば /usr/bin/iverilog など)が表示されれば導入済みです。何も表示されない場合は、次のコマンドで導入します。
# シミュレーション用ツールを導入する sudo apt update && sudo apt install -y iverilog gtkwave
導入後、もう一度 which iverilog vvp gtkwave を実行し、3つのパスが表示されることを確認してください。
3.5.2 コンパイルしてシミュレーションを実行する
テストベンチと設計本体を組み合わせてコンパイルし、シミュレーションを実行します。
※以下のコマンドを実行するディレクトリについて: 次のコマンドは、プロジェクトのルート(~/my_chip_project)で実行します。
# 1. テストベンチ(tb.v)と設計本体(project.v)をまとめてコンパイルし、sim.vvp を作る iverilog -o sim.vvp test/tb.v src/project.v # 2. コンパイル結果を実行し、波形ファイル tb.vcd を生成する vvp sim.vvp
コマンド2を実行して、画面に Simulation finished. と表示されれば、シミュレーションは正常に完了しています。
※生成された波形ファイルを確認する
シミュレーションが正しく動くと、波形ファイル tb.vcd が生成されます。次のコマンドでファイルのサイズを確認できます。
# 波形ファイルが生成されたかを確認する ls -la tb.vcd
カウンタが一定時間動作した記録が書き込まれるため、ファイルにはある程度のサイズ(数MB程度)が生じます。極端に小さい場合は、テストベンチでクロックが正しく発生していない可能性があります。
3.5.3 波形ビューワ(GTKWave)を起動する
生成した波形ファイルを、波形ビューワ gtkwave で開きます。
# 波形ビューワGTKWaveで tb.vcd を開く(末尾の & で、ターミナルを引き続き使えるようにする) gtkwave tb.vcd &
しばらくすると、Windows側にGTKWaveのウィンドウが表示されます。初回の起動には時間がかかる場合があります。表示されるまで、操作せずに待ってください。
※起動時に表示される dconf の警告について
GTKWaveの起動時に、unable to create directory '/run/user/.../dconf' といった dconf に関する警告が表示される場合があります。これはWSL環境で表示されることのある警告で、波形の表示そのものには影響しないため、無視して問題ありません。
GTKWaveの画面が表示されたら、次節で波形の読み方を確認します。
3.6 【波形の読み方】コードと波形を対応づけて理解する
本節は本章の山場です。GTKWaveに表示された波形を読み解き、「3.3で記述したコードのどの部分が、波形のどの動きに対応しているか」を確認します。コードと波形を結びつけて見ることで、3.1で述べた「Verilogは回路の接続図を文章にしたもの」という考え方が、実感としてつかめるようになります。
3.6.1 GTKWaveの画面構成
GTKWaveの画面は、大きく3つの領域に分かれています。
- SST(左上): 回路の階層をツリー表示する領域です。テストベンチ
tbや、その中に組み込んだ回路uutが並びます。 - Signals(左下): 選んだ階層に含まれる信号(
clkやcounterなど)の一覧が表示される領域です。 - Waves(右側): 選んだ信号の波形が、時間を横軸として表示される領域です。
3.6.2 信号を選んで波形に表示する
次の手順で、見たい信号を波形領域へ追加します。
- 左上のSST領域で、
tbの左にある三角マーク(▶)をクリックして展開します。すると、その中にuut(組み込んだLチカ回路)が現れます。 tbをクリックすると、左下のSignals領域にclk・rst_n・uo_outなどの信号が一覧表示されます。- 見たい信号を選び、左下の 「Append」ボタンを押すと、右側の波形領域に追加されます。複数の信号は、
Ctrlを押しながらクリックすることでまとめて選べます。 - まずは
clk・rst_n・uo_outを追加してみてください。
3.6.3 表示範囲を調整する(ズーム操作)
信号を追加した直後は、表示範囲がごく狭い時間に絞られているため、波形が平らな線に見えることがあります。画面上部のメニューやツールバーのズーム機能で、表示範囲を全体に広げます。
- 全体を表示する: メニューの「Time」→「Zoom」→「Zoom Best Fit」を選ぶと、シミュレーション全体が画面に収まります。
- 拡大・縮小する: ツールバーの虫眼鏡アイコン(「+」が拡大、「-」が縮小)で、見たい範囲を調整します。縮小すると表示の時間単位が
psからns、さらにusへと変化していきます。
clk は非常に細かく振動しているため、全体表示では帯のようにまとまって見えます。
clk が一定間隔で 0 と 1 を繰り返す矩形波(メトロノームのような信号)として見えます。3.6.4 カウンタの動きを表示する
Lチカ回路の心臓部である counter の動きも確認します。counter は回路内部の信号なので、uut の階層から追加します。
- SST領域で
tbを展開し、現れたuutをクリックします。 - 左下のSignals領域に
counter[23:0]が表示されるので、これを選んで「Append」で追加します。 - 適度なズームに調整すると、
counterの値が16進数で次々と変化していく様子が見えます。
counter を追加した例。clk の立ち上がりに合わせて、値が1ずつ増えていきます(例:00002C → 00002D → 00002E …)。3.6.5 【本章の核心】コードと波形の対応
ここで、3.3・3.4で記述したコードと、いま見ている波形を対応づけてみます。「コードにこう書いたから、波形がこう動く」という因果のつながりを確認することが、本章の理解の中心です。
コードの記述 波形に現れる動き
───────────────────────────────────────────────────────────────
always #10 clk = ~clk; → clk が 20ns周期で 0↔1 を繰り返す
(10nsごとにclkを反転) (規則的な矩形波として見える)
rst_n = 0; ... #40; rst_n = 1; → rst_n が最初の40nsだけ0、その後1
(40ns後にリセット解除) (最初に1段低い区間として見える)
always @(posedge clk) → clk の立ち上がりのたびに
counter <= counter + 1; counter の値が 1 ずつ増える
(クロックごとに+1) (値が切り替わる位置がclkと一致)
assign uo_out = → counter[23] がまだ 0 のため、
{7'b0, counter[23]}; uo_out は 00 のまま
(最上位ビットをLEDへ接続) (後述のとおり、点滅には時間が必要)
とりわけ counter の値が変化する位置と、clk が立ち上がる位置が一致していることに注目してください。これは、always @(posedge clk) という記述が「クロックの立ち上がりごとに処理する」という意味であることが、波形の上で確認できている状態です。コードに書いたとおりに、回路が時間にそって動いていることを、目で確かめられます。
3.6.6 【重要】LED(uo_out)が点滅しない理由と、合否の判断
波形を見ると、uo_out がずっと 00 のままで、LEDに対応する uo_out[0] が点滅していないことに気づきます。これは回路の誤りではなく、シミュレーション時間の長さによるものです。
uo_out[0] は counter[23] に接続されています。counter[23] が初めて 0 から 1 へ変化するのは、カウンタが 223(約840万)回クロックを数えたときです。クロックが20ns周期であれば、これはおよそ 0.17秒後に相当します。一方、本章のテストベンチで実行しているのは 約1ミリ秒(0.001秒)分であり、点滅が起きる時間にはまだ到達していません。そのため、uo_out は 00 のままとなります。
したがって、本章のシミュレーションでの合否の判断は、LEDの点滅ではなく counter が正しくカウントアップしているかで行います。rst_n が0の間は counter が0で止まり、rst_n が1になった後はクロックの立ち上がりごとに counter が1ずつ増えていれば、Lチカ回路の論理は正しく記述できています。
🔍 詳しく知りたい方向け:点滅そのものを波形で見たい場合
シミュレーション上で実際の点滅(uo_out[0] の反転)まで確認したい場合は、いくつかの方法があります。たとえば、カウンタのビット幅を一時的に小さくする(counter[23] ではなく、より下位のビットをLEDに接続する)と、短いシミュレーション時間でも反転を観測できます。学習段階では、まずは counter のカウントアップを確認できれば十分であり、点滅の観測は任意の発展として位置づけられます。なお、実際のLEDの点滅は、第6章で扱う実チップ上での動作確認で確かめられます。
本章の確認ポイント(まとめ)
rst_n が0の区間で counter が0のまま止まっていること、そして rst_n が1になった後に clk の立ち上がりへ同期して counter が1ずつ増えていくことを、波形上で確認できれば、論理設計は正しく行えています。uo_out が 00 のままであっても、それはシミュレーション時間が点滅の周期に達していないためであり、問題ではありません。
3.7 この章のまとめ
本章では、テンプレートを用意するところから始め、設計本体(project.v)をLチカ回路へ書き換え、テストベンチを用いてシミュレーションを実行し、波形からその動作を読み解くまでを行いました。到達したことを整理します。
- 設計の入れ物を用意した:
ttsky-verilog-templateを複製(クローン)し、その構成(src/・test/・info.yaml)を把握しました。 - 自分の回路を記述した: サンプルの
project.vを、24ビットカウンタによるLチカ回路へ書き換えました。あわせて、コードを読むために必要な最小限のVerilog(module/入出力/reg・wire/always/<=)を確認しました。 - 動作を検証した: テストベンチでクロックとリセットを与え、シミュレーションを実行し、波形で
counterのカウントアップを確認しました。これにより、回路の論理が意図どおりであることを、製造前にPC上で確かめられました。
ここで確認したのは、あくまで「論理が正しいか」という段階です。次章(第4章)では、この論理をシリコン上の物理的な形(レイアウト)へ変換する物理設計へ進み、回路が実際のチップとしてどのような姿になるのかを見ていきます。