ディジタル回路とVerilog入門
ディジタル回路とVerilog入門では、CPUを作る前に必要な基礎知識、そして作るために必要な道具の使い方を学んでいきます。
基礎知識
ここではCPUを作るのに必要な知識を説明します。覚える必要はありません。
CPU
CPU、我々が作る対象です。CPUはとは一体なんでしょうか?概要すら知らないのに作ろうとするのは流石に無謀と言えます。ちょっとだけ先に知っておきましょう。
プログラミングという単語は皆さん人生のどこかで聞いたことがあるでしょう。最近の中高生はプログラミングの授業があるんですかね、気の毒ですね。プログラミング、プログラムを書いてゲームを作ったりモーターを動かしたりするアレですね。あなたがこの記事を読んでいるSafariやChromeもプログラムですし、YoutubeもTwitterもInstagramもプログラムです。ああ素晴らしきかなプログラム。プログラムが無ければお前は生きてはいけません。
#include <stdio.h>
int main(){
printf("Hello World!\n");
return 0;
}
↑ 素晴らしいプログラム
ですが考えてみてください、そのプログラム、一体どこで動いているのでしょうか? その答えがCPUです。
CPUとはCentral Processing Unit、中央演算装置の略で、石みたいなガラスみたいな物質で出来ています。見た目は大体こんな感じです。
この世に存在するプログラムは全てCPUの上で動いています。このCPUが無ければプログラムは動かせませんし、いくらプログラムを書いても意味がありません。我々の生活基盤はこの石に支えられているという訳ですね、ありがとうCPU。愛してるCPU。本記事ではそんなCPUを作ります。
二進数と16進数
続いて知って頂きたいのが二進数です。二進数は数値の表記方法の1つであり、CPUを作るにはどうしても必要な知識ですのでここで説明します。
大前提として我々は普段、数値を0, 1, 2, 3, 4, 5, 6, 7, 8, 9
の10個の数字で表記していますね。そして9よりも大きい数値を表記したい場合、9より大きい数字を我々は持っていませんので、繰り上がりという操作を行い、10, 11, 12, ...
と表記します。これを十進数と呼びます。
二進数
二進数とは、我々が0, 1
しか数字を持っていない場合の数値の表記方法です。二進数において数値を0から順に数えていくと、まずは0, 1,
と進み、我々は1より大きい数字を持っていませんので繰り上がりを行い、10, 11, 100, 101, ...
と表記します。数字が0と1しか無い場合の数値の表記方法が二進数です。また二進数の桁数をビットと呼びます。例えば10101110
は8ビットです。
16進数
追加で知って頂きたいのが16進数です。これは逆に我々が0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F
の16個の数字を持っている場合の数値の表記方法です。これは二進数との相性が良いためよく使われます。16進数において数値を0から順に数えていくと、0, 1, 2, 3, 4, 5, 6, 7, 8, 9
と進み、我々は9より大きい数字としてAを持っていますので、A, B, C, D, E, F
と進み、Fより大きい数字は持っていませんので繰り上がりを行い、10, 11, 12, ...
と表記していきます。16進数は二進数と相性が良いと言いましたが、これは16進数と二進数で4ビットごとの繰り上がりのタイミングが一致しているためです。また16進数の数値を表記する場合は、十進数の値と区別するため0x
を数値の先頭に付ける場合があります。
まあ難しかったらググってください。
以下に16進数と十進数と二進数の対応表を載せておきます。
十進数 | 16進数 | 二進数 |
---|---|---|
0 |
0x0 |
0 |
1 |
0x1 |
1 |
2 |
0x2 |
10 |
3 |
0x3 |
11 |
4 |
0x4 |
100 |
5 |
0x5 |
101 |
6 |
0x6 |
110 |
7 |
0x7 |
111 |
8 |
0x8 |
1000 |
9 |
0x9 |
1001 |
10 |
0xA |
1010 |
11 |
0xB |
1011 |
12 |
0xC |
1100 |
13 |
0xD |
1101 |
14 |
0xE |
1110 |
15 |
0xF |
1111 |
16 |
0x10 |
10000 |
17 |
0x11 |
10001 |
18 |
0x12 |
10010 |
19 |
0x13 |
10011 |
20 |
0x14 |
10100 |
二進数の負の数
ここでは二進数の負の数について学びます。10進数で負の数を表現したい場合は-10
のように数の前にマイナスの記号を付けるだけで簡単です。二進数で負の数を表現するためには2の補数という表現方法を使うのが一般的です。
詳しい定義は省きますが、二進数の数値を負の数にしたい場合は、ビット反転して+1
を行うだけです。
またこの負の数の表現方法の場合、先頭のビットを見て0なら正の数であり、1なら負の数であると確認することが可能です。二進数の値は上記の符号付きと、符号なしに分類できます。例えば8bitで表現できる値の範囲は、符号なし8bitでは0~255ですが、符号付き8bitだと-128~127になります。二進数の値で計算する時は符号付きの値を扱ってるのか符号なしの値を扱っているのか注意しましょう。
この2の補数によって、二進数における引き算が可能となります。
ディジタル回路
CPUはディジタル回路というもので構成されています。ディジタル回路、難しそうな単語ですね。安心してください、ディジタル回路は単純なルールの集まりに過ぎません。少しずつ学んでいきましょう。
ディジタル回路が扱う値
ディジタル回路と呼ぶからには、電気的な何かを扱う物体だと思われるかもしれません。正解です。ディジタル回路は電圧の高低を扱う回路です。具体的には0Vと5Vや、0Vと1.2Vなどを入力に取りますが、実際に何Vなのかは回路によってまちまちなので入力をHigh, Lowとするか、それすら見づらいし面倒なので電圧の高い入力を1、電圧の低い入力を0とする表記が一般的です。
では実際にどのようなディジタル回路が存在するのか見ていきましょう。
NOT
一番シンプルなディジタル回路、NOTゲートです。
これは入力を反転します。例えば0が入力されたら1を出力し、1が入力されたら0を出力します。 以下にNOTの真理値表(そういうのがある)を置いておきます。
A |
NOT A |
---|---|
0 |
1 |
1 |
0 |
記号としては~x
とか!x
とかが使われがちです。
OR
次はORゲートです。これは入力の論理和、入力の少なくともどちらか一方が1なら1を出力します。
真理値表は以下の通りです。
A |
B |
A OR B |
---|---|---|
0 |
0 |
0 |
0 |
1 |
1 |
1 |
0 |
1 |
1 |
1 |
1 |
記号としてはa | b
が使われがちです。
AND
次はANDゲートです。これは入力の論理積、入力の両方が1なら1を出力します。
真理値表は以下の通りです。
A |
B |
A AND B |
---|---|---|
0 |
0 |
0 |
0 |
1 |
0 |
1 |
0 |
0 |
1 |
1 |
1 |
記号としてはa & b
が使われがちです。そのまんまですね。
NAND
次はNANDゲート、これはANDゲートの出力にNOTしたものです、入力の少なくともどちらか一方が0なら1を出力します。それ以外なら1を出力します。
真理値表は以下の通りです。
A |
B |
A NAND B |
---|---|---|
0 |
0 |
1 |
0 |
1 |
1 |
1 |
0 |
1 |
1 |
1 |
0 |
記号としては~(a & b)
で表せます。少し難易度が上がりましたかね?
NANDゲートの面白い特徴として、NANDゲートから他の全ての論理回路を構成できるという性質があります(Functional completeness)。実際にNANDゲートからORゲートを作るとこんな感じになります。
本当にORゲートになってるかわからない?真理値表を書くとこの通り、ORゲートになっている事が分かります。
A |
B |
A NAND A |
B NAND B |
(A NAND A) NAND (B NAND B) |
---|---|---|---|---|
0 |
0 |
1 |
1 |
0 |
0 |
1 |
1 |
0 |
1 |
1 |
0 |
0 |
1 |
1 |
1 |
1 |
0 |
0 |
1 |
このNANDゲートから他のディジタル回路を作り、それらを組み合わせCPUを作り、OSをその上で動かして、そのOSの上でテトリスを動かそうぜ!という熱い本が存在します。買うといいです。
XOR
論理回路ラスト、XORゲートです。これは排他的論理和と呼び、入力が異なる場合に1を出力し、同じ場合は0を出力します。
真理値表は以下の通りです。
A |
B |
A XOR B |
---|---|---|
0 |
0 |
0 |
0 |
1 |
1 |
1 |
0 |
1 |
1 |
1 |
0 |
記号としてはa ^ b
が使われがちです。
MUX
少し発展したディジタル回路、MUX(マルチプレクサ)です。これは入力を選択する回路です。選択用信号Sが0の場合はAからの入力を出力し、Sが1の場合はBからの入力を出力します。
真理値表は以下の通りです。
A |
B |
S |
Y |
---|---|---|---|
0 |
0 |
0 |
0 |
0 |
0 |
1 |
0 |
0 |
1 |
0 |
0 |
0 |
1 |
1 |
1 |
1 |
0 |
0 |
1 |
1 |
0 |
1 |
0 |
1 |
1 |
0 |
1 |
1 |
1 |
1 |
1 |
このマルチプレクサ、次のようにNOTとANDとORで作ることが出来ます。Y = (A & S) | (B & ~S)
。
HalfAdder
次はディジタル回路で少し面白い回路を紹介します。HalfAdder、半加算器です。以下のANDとXORで構成された回路を見てください。
この回路の真理値表を書いてみましょう。以下の通りです。
A |
B |
C |
S |
---|---|---|---|
0 |
0 |
0 |
0 |
0 |
1 |
0 |
1 |
1 |
0 |
0 |
1 |
1 |
1 |
1 |
0 |
CがAND回路の出力でSがXOR回路の出力になっています。当然っちゃ当然ですね。ですがよく見てください、このCとS、AとBの二進数の加算になっていますね。 ちなみにSは和を意味するSumの略で、Cは繰り上がりを意味するCarryの略です。
上記の表と下の数式を見比べればよく分かります。
\[\begin{align} 0_{(2)} + 0_{(2)} &= 00_{(2)} \\ 0_{(2)} + 1_{(2)} &= 01_{(2)} \\ 1_{(2)} + 0_{(2)} &= 01_{(2)} \\ 1_{(2)} + 1_{(2)} &= 10_{(2)} \end{align}\]この様に加算と同じ動作をする回路のことを加算器と呼びます。覚えておきましょう。
FullAdder
さて、半加算器は二進数の1桁同士の加算を行う回路でした。更に2桁、10桁の加算を行いたい時、この半加算器を並べるだけでは複数桁の加算を行う事は出来ません。 加算をやった事のある皆さんはご存知でしょうが、加算は繰り上がりが発生する演算です。よって、この半加算器を繰り上がり入力を受け取るように拡張する必要があります。 その回路を全加算器、FullAdderと呼びます。実際に見てみましょう。
半加算器2つとORで構成されていますね。新しく追加された全加算器の入力のXは繰り上がり入力を意味します。真理値表を見てみましょう。
A |
B |
X |
C |
S |
---|---|---|---|---|
0 |
0 |
0 |
0 |
0 |
0 |
0 |
1 |
0 |
1 |
0 |
1 |
0 |
0 |
1 |
0 |
1 |
1 |
1 |
0 |
1 |
0 |
0 |
0 |
1 |
1 |
0 |
1 |
1 |
0 |
1 |
1 |
0 |
1 |
0 |
1 |
1 |
1 |
1 |
1 |
入力のA, B, Xと出力のC, Sを下の二進数の加算の式を見比べてみましょう。一致していますね。
\[\begin{align} 0_{(2)} + 0_{(2)} + 0_{(2)} &= 00_{(2)} \\ 0_{(2)} + 0_{(2)} + 1_{(2)} &= 01_{(2)} \\ 0_{(2)} + 1_{(2)} + 0_{(2)} &= 01_{(2)} \\ 0_{(2)} + 1_{(2)} + 1_{(2)} &= 10_{(2)} \\ 1_{(2)} + 0_{(2)} + 0_{(2)} &= 01_{(2)} \\ 1_{(2)} + 0_{(2)} + 1_{(2)} &= 10_{(2)} \\ 1_{(2)} + 1_{(2)} + 0_{(2)} &= 10_{(2)} \\ 1_{(2)} + 1_{(2)} + 1_{(2)} &= 11_{(2)} \end{align}\]この全加算器を用いる事で、以下のように複数桁の加算を作ることが可能となります。
二進数の4桁の加算の例
\[\begin{align} 0011_{(2)} + 1001_{(2)} &= 1100_{(2)} \\ 0010_{(2)} + 0011_{(2)} &= 0101_{(2)} \end{align}\]この例は4桁の加算器ですが、8桁でも32桁でも作ることが出来ます。この世の足し算はこの回路で実行されているんですね。
D-FF
さてD-FF、謎の物体が出てきました。高校数学でANDやORをなんか見たことあるなあ、という人もD-FFは高校数学で学びません。これはD型フリップフロップというものです。
突然ですが、回路が情報を記憶する為には何が必須だと思いますか?それは時間です。時間の概念が無ければ記憶は意味を持ちえません。という訳で回路に時間の概念を導入します。 ディジタル回路における時間の概念、それはクロックです。
クロックとは一定の周波数で0と1を行ったり来たりする信号を指します。クロックに関係する用語として、0から1になる立ち上がりエッジ(posedge)と、1から0になる立ち下がりエッジ(negedge)がありますので覚えておきましょう。
クロックを理解した所でこのD-FF、これは信号を記憶する論理素子です。具体的な動作としては、クロックの立ち上がりエッジで入力を出力し、その他のタイミングでは出力を維持します。図にすると分かりやすいです。
このフリップフロップ、別名レジスタ(register)とも呼ばれる事があります、覚えておいて損はないです。
MUXによるD-FFの改良
最後にこのD-FFをMUXで少し改良して、データを記憶するタイミングを制御できるようにしましょう。
以下ではD-FFとMUXを組み合わせ、w_en
という信号でD-FFの保持するデータを書き換えるタイミングを決められるようにしてます。
波形は以下の通りです。w_en
が1になるとD-FFの保持するデータがw_data
のものになりました。D-FFに対する書き込みタイミングをw_en
で制御出来ていますね。
以上で我々はディジタル回路の基礎を完全に理解することが出来ました。やったね。 次はこのディジタル回路を作り出せる、FPGAの紹介です。
FPGA
ディジタル回路を現実世界で実装したい時、選択肢は主に3つ存在しています。
- ロジックIC
- LSI
- FPGA
ちなみに上記の3つ以外でCPUを作っている人を見かけたらそいつは異常な変態ですので近づきましょう。
ロジックICは秋葉で以下の黒いムカデみたいな奴を沢山買ってきて無限に半田付けするやつですね。
楽しいですが骨が折れるので今回はパス。
次のLSIですが、これは工場に大金を払って生産してもらう奴ですね。そんな金は無いので今回はパス。
↑ Googleの金を使った例(詳細)
そして最後の選択肢、FPGAです。FPGAとはField Programmable Gate Arrayの略で、一言で言ってしまうとディジタル回路を内部で自由に生成できるチップです。
詳しい仕組みの説明はここでは省きますが、どんな論理ゲートにもなれる特殊な論理ゲートが大量に詰まっていると思って頂ければ十分です。このFPGAでディジタル回路を作る場合、HDLというディジタル回路を設計する為の言語を使い、まるでプログラミングの様に回路を作る事が出来ます。
以後ではHDLの一種である、Verilog HDLを学んでいきましょう。
Verilog HDL入門
この章ではVerilog HDLについて解説していきます。Verilog HDLとはハードウェア記述言語(Hardware Description Language)の一種で、ディジタル回路の設計・検証に用いられます。早い話ハードウェア用のプログラミング言語みたいなものです。Verilog HDLで記述されたディジタル回路は、論理合成という処理を通して回路に変換されます。その他のHDLとしてSystemVerilog, VHDL等が存在しています。
本記事では、開発環境の快適さと対応ツールの多さからVerilog HDLを採用しています。
開発の流れ
ここではVerilog HDLを用いた開発の流れを簡潔に説明します。
開発はシミュレーションと実機検証に分かれています。基本的に書いたコードの動作確認は上段のシミュレーションで行い、現実世界での動作の確認は下段の実機検証の流れで行います。
シミュレーションにおいて、回路を記述したVerilog HDLファイルとテストベンチを書き、それをシミュレータ上で動かし動作を検証します。実機検証では、利用するソフトウェアはEDA(Electronic Design Automation)ツールのみです。シミュレーションで検証したVerilog HDLファイルをEDAに渡し、EDAでビルドを行いFPGA等の実機で動作を確認します。
本記事の大半の作業はシミュレーションで行いますので基本はPCを触ってればいいです。実機検証に関しては環境構築も作業も非常に煩雑であるため、本記事の後半で解説します。
開発環境構築
初めに開発環境を構築していきましょう。大半の初心者は環境構築で躓く事が経験的に知られています。心を強く持ちましょう。
以下ではWindows環境向けに解説を進めていきますが、LinuxやMacをお使いの方でも同名のソフトウェアをインストールして頂ければ本記事の内容を進めることが出来ます。
テキストエディタのインストール
テキストエディタとは文字を書く為のソフトウェアです。メモ帳でも自作CPUを出来なくはないですが、メモ帳は使い辛いのでVisual Studio Codeをインストールしましょう。もしVSCode以外にお気に入りのテキストエディタがありましたら(vimとか)、そのままお使いいただいて構いません。
このページ(https://code.visualstudio.com)からダウンロードを行ってください。
Verilog HDLシミュレータのインストール
本記事ではシミュレータとしてIcarus Verilogを利用します。
このページ(http://bleyer.org/icarus/)のDownloadの一番上のリンクからダウンロードしてください。v12がv13とかv14とかになってるかもしれませんが、とりあえずDownloadの一番上のリンクです。
またインストール時の注意点として1つだけ絶対に押すボタンが存在します。 以下の画像のSelect Additional TasksのAdd executable folder(s) to the user PATHです。絶対にチェックを入れてください。 それ以外は適当にNextなりFinishなりポチポチ押してけばいいです。
インストールが終わりましたら、正しくインストール出来ているか確かめるためにソフトウェアの検索欄にcmd
と入力してコマンドプロンプトを起動してください。起動方法をご存知ならPowerShellでもターミナルでも構いません。
そしてiverlog
コマンドとvvp
コマンドを実行してください。以下のような表示が出たら成功です。
またgtkwave
コマンドを実行してください。何かウィンドウが出たら成功です。
開発環境に慣れる
環境構築が完了しましたら、次は実際に開発環境を触って開発の流れを体感しましょう。
まずはverilog_test
という名前で新たにフォルダを作成してください。
Verilogファイルとテストベンチ
そしてVSCodeを開き、New File..
から先程作成したフォルダにadder8.v
という名前のファイルを作成します。
そうしましたら以下のコードをadder8.v
に書き写してください。何を書いているのか分からなくて不安でしょうが、今は写経していただくだけで結構です。
module adder8(
input wire clk,
input wire [7:0] in0,
input wire [7:0] in1,
output reg [7:0] out
);
always @(posedge clk) begin
out <= in0 + in1;
end
endmodule
書き写しましたら上書き保存してください。
次にadder8_tb.v
という名前で新たにファイルを作成し、以下のコードを書き写してください。
module adder8_tb;
reg clk = 1'b0;
reg [7:0] in0 = 8'h00;
reg [7:0] in1 = 8'h00;
wire [7:0] out;
initial begin
$dumpfile("wave.vcd");
$dumpvars(0, DUT);
end
always #1 begin
clk <= ~clk;
end
adder8 DUT(
.clk (clk ),
.in0 (in0 ),
.in1 (in1 ),
.out (out )
);
initial begin
#2
in0 = 8'h05;
in1 = 8'h04;
#2
in0 = 8'hff;
in1 = 8'h01;
#2
$finish;
end
endmodule
書き写しましたら上書き保存してください。
さて、今しがたadder8.v
とadder8_tb.v
というファイルを作りました。adder8.v
はVerilog HDLで書いた8bit同士の値の加算を行うディジタル回路です。またadder8_tb.v
というファイルも書きましたね、これはテストベンチというものです。Verilogをシミュレータ上で動かす為には、ディジタル回路を記述したVerilogファイルの他に、テストベンチと呼ばれるVerilogファイルが必要です。
テストベンチとは文字通り回路のテストを行うためのVerilogファイルです。例えば、あなたが何か回路を作成したとして、その回路が意図した通りに動作するか確かめたい場合、回路に入力を与え、その出力を調べる必要がありますね?テストベンチはそういった回路へ入力を与えたり、出力を確認したり、内部信号を検査したり、回路内の波形ファイルを出力したりするのに使います。
具体的なテストベンチの書き方は後々解説しますので、今はとりあえず手元でシミュレーションを動かしてみましょう。
シミュレーションを実行
回路とテストベンチが書き終わりましたら、作成したファイルがあるフォルダを開き、白い部分をShiftを押しながら右クリックをして「ターミナルで開く」をクリックしてください。これでverilog_test
フォルダ内でターミナルを開くことができます。
以下のコマンドを実行してください。
iverilog adder8_tb.v adder8.v
vvp a.out
実行が完了したらフォルダ内にwave.vcd
というファイルが生成されている筈です。これは波形ファイルです。波形ファイルは以下のコマンドで開きます。
gtkwave wave.vcd
このコマンドでGTKwaveが起動します。
GTKWaveでは信号名をダブルクリックか、信号名を選択してAppendボタンを押すと波形を見ることができます。実際にin0
とin1
の加算結果がout
に入力されているのが確認できると思います。適当に触ってみて気合で慣れてください。
以上のVerilog HDLでディジタル回路とテストベンチを記述し、シミュレーションを実行。波形を見てデバッグが開発の流れになります。この一連の流れは今後何度も繰り返します。今全てを覚える必要はありませんので大丈夫です、そのうち手癖で開発を回せるようになります。
- Verilog HDLで回路を書く
- Verilog HDLでテストベンチを書く
- シミュレーションを実行
- 波形を見る
Verilogの文法を学ぼう(基礎1)
では本格的にVerilog HDLを学んでいきましょう。最初は必須の構文です。
module
Verilog HDLではシステム全体をモジュールという単位で分割して構成します。
モジュールのイメージ
Verilogでモジュールを作成するにはmodule
というキーワードを用います。例えばtest_module
という名前のモジュールを作成したい場合は、module test_module();
と記述し、module
の後ろにモジュール名を記述します。そして回路の内容をmodule モジュール名();
~ endmodule
の間に記述します。
module test_module();
// ここに回路を記述
endmodule
入出力ポート
回路には入出力が必要です、回路の入出力ポートはinput
, output
というキーワードで定義します。入力信号名はinput
の後ろに宣言し、出力信号名はoutput
の後ろに宣言します。IOの個数や順番に決まりはありませんが、最後の宣言にはカンマが必要ないことに注意しましょう。
module test_module (
input test_input1, // 入力信号線の定義
input test_input2,
output test_output1 // 出力信号線の定義
);
// 回路
endmodule
その他に入出力両方に使えるinout
が存在しますが必須ではありませんので必要になった時に各自で調べてください。
wire型
回路の上には配線がありましたね、あの配線は信号を伝達する信号線です。Verilogにおいて、wire
というキーワードを用いることで信号線を定義する事が出来ます。
wire
の使い方は以下のように、wire 信号幅 信号線名;
の順で記述します。
wire [31:0] 信号線名;
wire
の後ろにある[31:0]
は信号線の幅です。この場合は32本の幅を持った信号線になります。[15:0]
にすれば16本の幅を持った信号線になりますし、[3:0]
にすると4本の幅を持った信号線になります。
また、以下のように信号線の幅の記述を省略すると1本の幅を持った信号線になります。
wire 信号線名;
演算子
Verilogでは信号線同士で演算を行うことが可能です。Verilogには様々な演算子が存在し、これらの演算子は複数ビットを纏めて演算が可能です。以下によく使う演算子を載せておきますので適宜参照してください。
演算名 | 演算子 | 例 |
---|---|---|
NOT | ~ |
~A |
OR | | |
A | B |
AND | & |
A & B |
XOR | ^ |
A ^ B |
加算 | + |
A + B |
減算 | - |
A - B |
乗算 | * |
A * B |
除算 | / |
A / B |
数値リテラル
Verilog HDLで数値を記述する際は、ビット幅'進数 数値
という形式で記述します。進数
はb
で2進数、d
で10進数、h
で16進数です。ビット幅
を指定しない場合のビット幅は処理系によって変わります。(Quartusでは32ビット)。
5'b10101; //2進5ビット
8'd43; //10進8ビット
16'hdead; //16進16ビット
進数 | 文字 | 例 |
---|---|---|
2 | b |
8'b0011 1101 |
10 | d |
8'd61 |
16 | h |
8'h3d |
assign文
wire
で作られた信号線にはキーワードassign
を用いて入力できます。assign
を用いて書かれた回路は、入力が変化すると即座に出力が変化する組み合わせ回路となります。
このassign
を用いた信号の入力時に、演算子を用いて演算を行うことが可能です。以下の例ではtest_wire0
とtest_wire1
の信号を加算し、test_output
に入力しています。
module test_module (
input [15:0] test_input,
output [7:0] test_output
);
wire [7:0] test_wire0;
wire [7:0] test_wire1;
assign test_wire0 = 16'h0505;
assign test_wire1 = test_input[7:0];
assign test_output = test_wire0 + test_wire1;
endmodule
またtest_wire1
に入力しているtest_input
の直後に書いてある[7:0]
はスライスという操作であり、test_input
の7ビット目から0ビット目の計8ビットを抜き出して、test_wire1
に入力しています。スライスはよく使う操作ですので覚えておきましょう。
Verilogの文法を学ぼう(練習1)
覚えてばかりもつまらないでしょうし、ここで少し手を動かしてみましょう。
練習問題1
以下の回路と同等のモジュールproblem1
をproblem1.v
という名前のファイルに作成してください。入出力は画像の通りです。
またテストベンチとテスト用のコマンドは以下のものを使ってください。
テストベンチ、これをproblem1_tb.v
というファイル名で保存する。
module problem1_tb;
reg [7:0] i_p0;
reg [7:0] i_p1;
reg [7:0] i_p2;
wire [7:0] o_p;
initial begin
$dumpfile("wave.vcd");
$dumpvars(0, DUT);
end
problem1 DUT(
.i_p0 (i_p0 ),
.i_p1 (i_p1 ),
.i_p2 (i_p2 ),
.o_p (o_p )
);
initial begin
i_p0 = 8'h00;
i_p1 = 8'h00;
i_p2 = 8'h00;
#2
$display("o_p = %02x", o_p);
i_p0 = 8'h0f;
i_p1 = 8'h55;
i_p2 = 8'h88;
#2
$display("o_p = %02x", o_p);
i_p0 = 8'h74;
i_p1 = 8'h81;
i_p2 = 8'h11;
#2
$display("o_p = %02x", o_p);
$finish;
end
endmodule
テスト用コマンド
iverilog problem1_tb.v problem1.v
vvp a.out
以下の出力が得られたら正解です。
VCD info: dumpfile wave.vcd opened for output.
o_p = 00
o_p = 8d
o_p = 11
problem1_tb.v:35: $finish called at 6 (1s)
練習問題2
以下の回路と同等のモジュールproblem2
を作成してください。入出力は画像の通りです。名前の書かれていない信号線は各自で定義してください。
またテストベンチとテスト用のコマンドは以下のものを使ってください。
テストベンチ、これをproblem2_tb.v
というファイル名で保存する。
module problem2_tb;
reg [7:0] i_p0;
reg [7:0] i_p1;
reg [7:0] i_p2;
wire [7:0] o_p;
initial begin
$dumpfile("wave.vcd");
$dumpvars(0, DUT);
end
problem2 DUT(
.i_p0 (i_p0 ),
.i_p1 (i_p1 ),
.i_p2 (i_p2 ),
.o_p (o_p )
);
initial begin
i_p0 = 8'h55;
i_p1 = 8'h77;
i_p2 = 8'h01;
#2
$display("o_p = %02x", o_p);
i_p0 = 8'hf0;
i_p1 = 8'h55;
i_p2 = 8'h5a;
#2
$display("o_p = %02x", o_p);
i_p0 = 8'hff;
i_p1 = 8'h88;
i_p2 = 8'h11;
#2
$display("o_p = %02x", o_p);
$finish;
end
endmodule
テスト用コマンド
iverilog problem2_tb.v problem2.v
vvp a.out
以下の出力が得られたら正解です。
VCD info: dumpfile wave.vcd opened for output.
o_p = 56
o_p = 40
o_p = ff
problem2_tb.v:35: $finish called at 6 (1s)
練習問題3
以下の回路と同等のモジュールproblem3
を作成してください。入出力は画像の通りです。名前の書かれていない信号線は各自で定義してください。
またテストベンチとテスト用のコマンドは以下のものを使ってください。
テストベンチ、これをproblem3_tb.v
というファイル名で保存する。
module problem3_tb;
reg [15:0] i_p0;
reg [15:0] i_p1;
wire [15:0] o_p;
initial begin
$dumpfile("wave.vcd");
$dumpvars(0, DUT);
end
problem3 DUT(
.i_p0 (i_p0 ),
.i_p1 (i_p1 ),
.o_p (o_p )
);
initial begin
i_p0 = 16'h0f0f;
i_p1 = 16'h0f0f;
#2
$display("o_p = %04x", o_p);
i_p0 = 16'h3366;
i_p1 = 16'h6633;
#2
$display("o_p = %04x", o_p);
i_p0 = 16'h1234;
i_p1 = 16'h5678;
#2
$display("o_p = %04x", o_p);
$finish;
end
endmodule
テスト用コマンド
iverilog problem3_tb.v problem3.v
vvp a.out
以下の出力が得られたら正解です。
VCD info: dumpfile wave.vcd opened for output.
o_p = ffff
o_p = eebb
o_p = fffb
problem3_tb.v:30: $finish called at 6 (1s)
模範解答1
module problem1(
input [7:0] i_p0,
input [7:0] i_p1,
input [7:0] i_p2,
output wire [7:0] o_p
);
wire [7:0] w_p;
assign w_p = i_p0 & i_p1;
assign o_p = w_p | i_p2;
endmodule
模範解答2
module problem2(
input [7:0] i_p0,
input [7:0] i_p1,
input [7:0] i_p2,
output wire [7:0] o_p
);
wire [7:0] w_p;
assign w_p = i_p1 & i_p2;
assign o_p = i_p0 + w_p;
endmodule
模範解答3
module problem3(
input [15:0] i_p0,
input [15:0] i_p1,
output wire [15:0] o_p
);
wire [15:0] w_p;
assign w_p = ~i_p0;
assign o_p = w_p | i_p1;
endmodule
お疲れ様です。
Verilogのシミュレーションを学ぶ
練習問題では特に説明なしにテストベンチを出しました。このタイミングでテストベンチの書き方を説明します。
以下のテストベンチのコードを例にテストベンチ用のVerilogの構文を学んでいきましょう。
module test_sim;
reg clk = 0;
reg [7:0] data_i = 8'h00;
wire [7:0] data_o;
initial begin
$dumpfile("wave.vcd"); // 波形ファイル
$dumpvars(0, DUT); // DUTインスタンスの全信号を出力
end
test_module DUT(
.clk (clk ),
.input_data (data_i ),
.output_data(data_o )
);
always #1 begin // クロックの生成 1秒毎に反転
clk <= ~clk;
end
initial begin
data_i = 8'h11;
#2 // 2秒待機
data_i = 8'h20;
#2
data_i = 8'h30;
#10 // 10秒待機
$finish; // 終了
end
endmodule
入出力用信号の定義
作成した回路の動作を検証するためには、回路に様々な信号を入力し、出力を検査する必要があります。テストベンチにおいて回路に入力する信号はreg ビット幅 信号名;
で定義します。また回路からの出力を受ける信号はwire ビット幅 信号名;
で定義します。両方ともビット幅を省略した場合は1bitの幅になります。
実際に入出力用信号を定義した例が以下になります。この場合、clk
、data_i
が入力用の信号で、data_o
が出力を受ける信号になります。
reg clk = 0;
reg [7:0] data_i = 8'h00;
wire [7:0] data_o;
テスト用回路の生成
テストベンチで回路をテストするためにはテストベンチ内で回路を生成する必要があります。そこで行うのがテスト用回路の生成です。
Verilogにおいて、回路はモジュール名 インスタンス名
で生成する事が可能です。以下の例ではtest_module
という名前のモジュールからDUT
という名前の回路(インスタンス)を生成しています。
また入出力信号は.インターフェイス名(信号線名)
で接続します。以下の例ではtest_module
がclk
, input_data
, output_data
というインターフェイスを持っているとして、先程定義した入出力信号をそれぞれのインターフェイスに接続しています。
カンマの有無に注意してください。
test_module DUT(
.clk (clk ),
.input_data (data_i ),
.output_data(data_o )
);
ちなみにDUTはDevice Under Testの略です。
システムタスク
上記のテストベンチには、$
で始まる名前の関数がいくつか使われていますね。これらの関数をシステムタスクと呼びます。システムタスクとは回路の検証に使うための関数であり、Verilog HDLには様々なシステムタスクが存在してます。
シミュレーション時に最低限使うべきシステムタスクは以下の4つです。
$dumpfile
: 波形ファイルの名前設定$dumpvars
: 内部信号を波形に出力するモジュールを設定$display
: ターミナルに文字列を表示$finish
: シミュレーションを終了する(必須)
必要な時にそれぞれ説明していきます。
波形ファイルの生成
作成した回路が正しく動作しているか確かめる際に重要となるのが波形ファイルです。$dumpfile("wave.vcd")
でwave.vcd
という名前の波形ファイルを生成出来ます。また$dumpvars()
でどの回路の波形を出力するか指定できます。例では$dumpvars(0, DUT)
としており、DUT
という名前の回路の波形を得ています。
実際には以下のようにinitial begin ~ end
で囲った部分で利用します。
initial begin
$dumpfile("wave.vcd"); // 波形ファイル
$dumpvars(0, DUT); // DUTインスタンスの全信号を出力
end
この記述は回路の生成の前に記述してください。
クロックの生成
基礎知識のD-FFの部分で説明しましたが、回路が情報を記録するためにはクロックという信号がしばしば必要になります。先程の練習問題ではクロックを用いませんでしたが、今後は殆どの場合においてクロックを必要とする回路を作ることになりますので、今のうちにテストベンチでクロックを生成する方法を学んでおきましょう。
テストベンチにおいてはクロック信号の生成に以下の記述を用います。以下の記述では先程定義した入力用信号であるclk
の値を1秒毎にNOT演算子で反転し、再びclk
に入力しています。この結果、clk
の値は0, 1, 0, 1, 0, 1と振動するようになります。これをクロック信号として利用しています。
always #1 begin
clk <= ~clk;
end
入力信号を制御
テスト対象の回路に対してずっと同じ信号を入力するのはあまり効果的なテストとは言えません。そこで時間に応じて入力信号を変化させる必要があります。そのために以下の記述を用います。
以下のinitial begin ~ end
で囲まれた記述では、入力信号のdata_i
に値を格納してから#2
という記述をしています。この#
は遅延と呼び、#数値
で指定した秒数だけ待機する事が出来ます。待っている間にもクロックは動作します。つまり以下の例では回路に8'h11
という値を入力して2秒待機しています。
その後何回か信号を変化させ、最終的にシステムタスクの$finish
が実行されます。この$finish
が実行されるとシミュレーションが終了します。テストベンチには$finish
を必ず記述する必要があります。
initial begin
data_i = 8'h11;
#2 // 2秒待機
data_i = 8'h20;
#2
data_i = 8'h30;
#10 // 10秒待機
$finish; // 終了
end
なぜ2秒待機しているのか一応説明すると、先程のクロック生成でclk
は#1
で0 -> 1と変化しますが、これでは半クロック分しか待機できていません。そこで#2
とするとclk
は0 -> 1 -> 0と変化し、1クロック待機することが可能となるためです。
シミュレーションを実行
シミュレーションを実行するためにいくつかコマンドを実行しました。これらのコマンドの使い方とシミュレーションの実行方法をここで説明します。
シミュレーションにおいて主にiverilog
、vvp
、gtkwave
の3つのコマンドを利用します。
- iverilog : シミュレータ用のVerilogコンパイラ
- vvp : シミュレーション用バーチャルマシン(実行環境)
- gtkwave : オープンソースの波形ビューワー
これらのコマンドを使う流れとしては、書いたVerilogファイルとテストベンチをiverilog
に与えてa.out
を生成します。そして出力されたa.out
をvvp
に与えて波形ファイルを生成し、生成された波形ファイルをgtkwave
で閲覧します。
それぞれのコマンドの使い方は以下の通りです。
iverilog テストベンチファイル名 Verilogファイル名
vvp a.out
gtkwave 波形ファイル名
実際には以下のように使います。
iverilog test_tb.v test.v
vvp a.out
gtkwave wave.vcd
テストベンチを書くのは非常に面倒で退屈ですが、Verilog HDLでディジタル回路を設計する事とテストベンチを書くことは切っても切れない関係にあります。今後何度も書くことになりますので適宜参照するようにしてください。
Verilogの文法を学ぼう(基礎2)
さて、少し進んだ回路を作るための文法を学んでいきましょう。
定数
Verilogでもプログラミング言語と同じように、数値に別名を付けたい場合が存在します。そこで使うのが定数です。Verilogには定数としてdefine
とparameter
という2つの方法が存在しますが、define
はキモいのでparameter
を使うことを推奨します。
parameter
はparameter 定数名 = 数値;
のように使います。
module test_module(
// 省略
);
parameter TEISU1 = 123;
parameter TEISU2 = 32'h5555_5555;
parameter TEISU3 = 32;
wire [TEISU3-1:0] w1;
assign w1 = TEISU2;
endmodule
reg型
wire
は信号線を作成するためのキーワードでしたが、信号線はデータを保持する事が出来ません。基礎知識でも書きましたが、ディジタル回路がデータを保持するためにはフリップフロップ(以後レジスタ)が必要でしたね。
Verilogではreg
というキーワードでレジスタを生成することが可能です。reg
の使い方は以下のように。reg サイズ レジスタ名;
の順で記述します。
reg [31:0] test_32reg;
また以下のようにサイズの記述を省略すると1bitの幅を持ったレジスタになります。
reg test_1reg;
always文
reg
で作られたレジスタにはalways文というものを用いて信号を入力することが可能です。
基礎知識で述べたように、情報の記録とはクロックという信号に密接に関係してします。そこでalways文は殆どの場合においてクロック信号と一緒に用います。
always文はalways @(posedge クロック信号) begin ~ end
またはalways @(negedge クロック信号) begin ~ end
という文法で記述し、begin ~ end
の間に回路を記述します。posedge
の場合はクロック信号が立ち上がりエッジのタイミングで処理が実行され、negedge
の場合はクロック信号の立ち下がりのタイミングで処理が実行されます。
以下の例ではクロック信号としてi_clk
を定義しており、i_clk
の立ち上がりでi_data_a
とi_data_b
の加算結果がレジスタr_data
に格納されるようにしています。そしてr_data
の値をo_data
に出力しています。
module test_module(
input i_clk,
input [31:0] i_data_a,
input [31:0] i_data_b,
output o_data
);
reg [31:0] r_data;
assign o_data = r_data;
always @(posedge i_clk) begin
r_data <= i_data_a + i_data_b;
end
endmoudle
ここで注目してほしいのがalways文内の<=
です。Verilogではalways文内において、代入演算子として=
と<=
を使うことが可能であり、それぞれ異なった動作をします。<=
はノンブロッキング代入と呼ばれ、=
はブロッキング代入と呼ばれています。なんか2種類ありますが混在させるとバグの温床になりますので、本記事では<=
のノンブロッキング代入しか用いません。always文で代入は<=
です!そういう事にしましょう。
if文
if文とは条件に応じて処理を変える文法であり、always文と後述するfunction文で記述可能です。
if文の構文は以下の通りです。if(条件1) begin ~ end
とすると条件1が真の場合にbegin ~ end
で囲まれた部分が実行されます。またこの後ろにelse if(条件2) begin ~ end
を加えると条件1が偽で条件2が真の場合にbegin ~ end
で囲まれた部分が実行されます。そしてelse begin ~ end
を付け加えると、どの条件にも合致しない場合にelse begin ~ end
で囲まれた部分が実行されます。
if(条件1) begin
処理A
end else if(条件2) begin
処理B
end else begin
処理C
end
実際にif文を使った例が以下になります。以下の例ではi_condition
の値が4'h1
の場合は加算が実行され、4'hE
の場合は減算が実行され、それ以外の場合はOR演算が実行されます。
module test_module(
input [3:0] i_condition,
input [7:0] i_data_a,
input [7:0] i_data_b,
output reg [7:0] o_data
);
always @(posedge clk) begin
if(i_condition == 4'h1) begin
o_data <= i_data_a + i_data_b;
end else if(i_condition == 4'hE) begin
o_data <= i_data_a - i_data_b;
end else begin
o_data <= i_data_a | i_data_b;
end
end
endmodule
if文は非常によく構文ですので覚えておきましょう。
case文
case文は便利なif文のような構文であり、always文と後述するfunction文内で記述可能です。case()
の中に条件分岐に利用する信号名を記述し、case() ~ endcase
で囲んで利用します。case文の内部では数値 : 処理;
と記述し、case(信号名)
の信号の値が数値と一致した場合にその処理が実行されます。またdefault : 処理
と記述することで、どの数値にも当てはまらない場合はその処理が実行されるようになります。このdefault
を記述していない場合、処理系によってはエラーが発生します。
if文の説明で用いた回路をcase文で書き直したものが以下になります。
module test_module(
input [3:0] i_condition,
input [7:0] i_data_a,
input [7:0] i_data_b,
output reg [7:0] o_data
);
always @(posedge clk) begin
case(i_condition)
4'h1 : o_data <= i_data_a + i_data_b;
4'hE : o_data <= i_data_a - i_data_b;
default : o_data <= i_data_a | i_data_b;
endcase
end
endmodule
function文
always文では気軽にif文やcase文を使うことが出来ましたが、assign文ではそうもいきません。assign文でif文やcase文を使いたい場合はfunction文で関数を作成しましょう。
function文ではfunction 返り値の幅 関数名;
と記述して定義し、function ~ endfunction
で囲まれた部分で処理を記述します。入力信号は関数の定義後にinput
で定義し、関数の返り値はfunction名に入力する形で記述します。また返り値の幅を省略すると1bitになります。
以下の例では、test_function()
という名前の関数を新たに定義し、関数の処理結果をo_data
に入力しています。
module test_module(
input [3:0] i_condition,
input [7:0] i_data,
output [7:0] o_data,
);
assign o_data = test_function(i_condition, i_data);
function [7:0] test_function;
input [3:0] i_ctrl;
input [15:0] i_data;
begin
case(i_ctrl)
4'b0000 : test_function = i_data + 16'h0001;
4'b0000 : test_function = i_data & 16'h0505;
4'b0000 : test_function = i_data & 16'hff00;
default : test_function = 16'h0000;
endcase
end
endfunction
endmodule
function文もそれなりに使いますので覚えておきましょう。
三項演算子
Verilogには+
や&
など様々な演算子が存在してますが、その中でもある便利な演算子を紹介します。三項演算子です。
三項演算子は?
と:
の2つを使う演算子で、一行で書けるif文のような演算子であり、以下のような演算をします。
代入先 = (条件) ? (条件が真の時に代入する値) : (条件が偽の時に代入する値)
これは演算子ですのでassign文でもalways文でもfunction文でも使用可能です。
以下の例ではi_condition
の値が2'b11
の場合はo_data
にi_data_a
の値が格納され、それ以外の場合はi_data_b
が格納されます。
module test_module(
input [1:0] i_condition,
input [7:0] i_data_a,
input [7:0] i_data_b,
output reg [7:0] o_data
);
always @(posedge clk) begin
o_data <= (i_condition == 2'b11) ? i_data_a : i_data_b ;
end
endmodule
連結演算子
三項演算子に続いて便利な演算子をもう一つ紹介します。連結演算子です。これは以下のようにビットを結合する演算子です。
{4'h0011, 4'b1100} -> 8'b00111100
この連結演算子も演算子ですのでassign文やalways文やfunction文で利用可能です。
module test_module(
input [7:0] i_data_a,
input [7:0] i_data_b,
output reg [15:0] o_data
);
always @(posedge clk) begin
o_data <= {i_data_a, i_data_b};
end
endmodule
ビットの繰り返し
{数字{値}}
で値を数字の数だけ並べる事が可能です。
{10{1'b0}} -> 10'b0000000000
{10{1'b1}} -> 10'11111111111
モジュール呼び出し
Verilogを用いて作成したモジュールはモジュール名 インスタンス名
で利用する事が可能です。またインターフェイスは.インターフェイス名(信号線名)
で接続します。
また1つのモジュールから複数の回路を生成できます。
module test_module(
// 省略
);
wire [7:0] w_data1;
wire [7:0] w_data2;
wire [7:0] w_data3;
wire [7:0] w_data4;
wire [7:0] w_out1;
wire [7:0] w_out2;
small_module sm1(
.in1 (w_data1 ),
.in2 (w_data2 ),
.out1 (w_out1 )
);
small_module sm2(
.in1 (w_data3 ),
.in2 (w_data4 ),
.out1 (w_out2 )
);
endmodule
Verilogの文法を学ぼう(練習2)
練習問題1
以下の回路と同等のモジュールproblem4
をproblem4.v
という名前のファイルに作成してください。以下の回路は16bitのレジスタを持っており、クロック毎に1つづ値が増えていきます。またi_rst
はリセット信号であり、これが1になるとレジスタの値が0に戻ります。
入出力は画像の通りです。
またテストベンチとテスト用のコマンドは以下のものを使ってください。
テストベンチ、これをproblem4_tb.v
というファイル名で保存する。
module problem4_tb;
reg i_clk = 0;
reg i_rst = 0;
wire [15:0] o_p;
initial begin
$dumpfile("wave.vcd");
$dumpvars(0, DUT);
end
problem4 DUT(
.i_clk (i_clk ),
.i_rst (i_rst ),
.o_p (o_p )
);
always #1 begin
i_clk <= ~i_clk;
end
initial begin
i_rst = 1'b1;
#10
i_rst = 1'b0;
#100
$finish;
end
endmodule
テスト用コマンド
iverilog problem4_tb.v problem4.v
vvp a.out
gtkwave wave.vcd
波形ファイルを開き、i_rst
の値が0になってからo_p
の値が1づつ増えていけば正しい動作です。
練習問題2
以下の回路と同等のモジュールproblem5
をproblem5.v
という名前のファイルに作成してください。以下の回路は16bitのレジスタを持っており、i_ctrl
によって格納するデータを選択できます。
入出力は画像の通りです。
テストベンチは各自で作成してください。
練習問題3
以下の回路と同等のモジュールproblem6
をproblem6.v
という名前のファイルに作成してください。以下の回路は3bitのレジスタを持っており、i_data
の下位2bitによって格納するデータを選択できます。またi_rst
はリセット信号であり、これが1になるとレジスタの値が0に戻ります。
入出力は画像の通りです。
テストベンチは各自で作成してください。
模範解答1
module problem4(
input i_clk,
input i_rst,
output reg [15:0] o_p
);
always @(posedge i_clk) begin
if(i_rst) begin
o_p <= 16'h0000;
end else begin
o_p <= o_p + 16'h0001;
end
end
endmodule
模範解答2
module problem5(
input i_clk,
input [15:0] i_data_0,
input [15:0] i_data_1,
input [15:0] i_data_2,
input [15:0] i_data_3,
input [1:0] i_ctrl,
output reg [15:0] o_data
);
always @(posedge i_clk) begin
case(i_ctrl)
2'b00 : o_data <= i_data_0;
2'b01 : o_data <= i_data_1;
2'b10 : o_data <= i_data_2;
2'b11 : o_data <= i_data_3;
endcase
end
endmodule
模範解答3
module problem6(
input i_clk,
input i_rst,
input [15:0] i_data,
output reg [2:0] o_data
);
always @(posedge i_clk) begin
if(i_rst) begin
o_data <= 3'b000;
end else begin
case(i_data[1:0])
2'b00 : o_data <= i_data[4:2];
2'b01 : o_data <= i_data[7:5];
2'b10 : o_data <= i_data[10:8];
2'b11 : o_data <= i_data[13:11];
endcase
end
end
endmodule
実機向け:FPGA入門
本章ではVerilog HDLで書いたディジタル回路を実機で動かす方法を学んでいきます。自分で設計した回路が現実世界で動くというのはプログラミングとはまた違った喜びを味わえます。開発用ボードを積んでる変態を除けば、実際にボードを買い、動かすというのは非常にハードルが高く感じるかもしれません。一度買ってしまえば慣れます。ブレーキを外していきましょう。
FPGAボードを購入する
ではFPGAボードを購入しましょう。本記事ではTang Nano 9KというFPGAボードを使って開発を進めて行きます。Tang Nano 9kはGowin社のFPGAが搭載されているSipeed社が開発したFPGAボードです。FPGAボードは基本的に余裕で1万円を越しますが、このTang Nano 9kは非常に安価なFPGAボードであり、その値段はなんと2,500円です。チャイナパワーを感じますね。
このTang Nano 9kを秋月電子通商で購入してください。秋葉に買いに行ってもいいですしネット通販でも大丈夫です。ネット通販の場合は秋月のページに行ってTang Nano 9kを検索してください。通販コードはM-17448です。
送料が500円くらい掛かるのでまとめて買うといいです。
https://akizukidenshi.com/catalog/default.aspx
購入しましたらワクワクしながら待ちましょう。
開発環境構築
次にTang Nano 9kで開発を行うためのソフトウェアをインストールしていきましょう。
まずは以下のGowin社のサイトに飛び、アカウントを作成してください。知らんサイトで知らんアカウントを作るのは抵抗があるかもしれませんが、諦めてください。
https://www.gowinsemi.com/ja/support/home/
アカウントを作成しましたら、「Gowin® EDAのダウンロード」からGowin EDAのEducation Editionをダウンロードします。
そしてダウンロードしたファイルを解凍して、中のインストーラを実行してください。
インストーラを実行するとなんかスゲー怖い画面が出ます。臆せず詳細情報
から実行
をクリックしましょう。
インストールが開始した後は適当にNextなりYesなりポチポチしてけばいいです。たまに以下のようなスゲー怖い画面が出ますがはい
を押しとけばいいです。
インストールが成功したらデスクトップにGowin EDAのアイコンが出来ます。
プロジェクト作成
Gowin EDAのインストールが完了したので、次はプロジェクト作成します。Gowin EDAを起動し、New Projectをクリックしてください。
そうしましたら、Projects > FPGA Design Project
を選択し、OKをクリックします。
次にプロジェクトの名前を入力します。Nameにプロジェクト名、Create inではプロジェクトを作成する場所を選択します。今回はtangnano_test
という名前のプロジェクトをデスクトップに作成しましょう。
プロジェクトの名前を決定したら、次はプロジェクトで使うFPGAを選択します。
Tang Nano 9Kに載っているFPGAをよく見ると、チップの上にGW1NR-LV9QN88PC6/I5
と書いてあります。これがFPGAの型番です。
よってSelect DeviceではSeriesをGW1NR
にし、下に出てくるGW1NR-LV9QN88PC6/I5
を選択してからNextをクリックしてください。
全ての設定が完了したのを確認してからFinishをクリックしてください。
これでプロジェクトが作成されました。
ディジタル回路を書く
まずはプロジェクト内にVerilogファイルを作成します。
画像のようにDesign
のプロジェクト名の部分を右クリックし、New File..
をクリックします。
出てきたウィンドウのVerilog File
を選択してOKを押します。
そしてファイル名として今回はtest_module
と入力し、OKをクリックしてください。
そうしましたらVerilogを書く画面が出てきますので、ここではボタンを押すとLEDが光る簡単な回路を作成してみましょう。インターフェイスとしてボタン入力のi_button
、LED出力のo_led
を定義し、assign文でi_button
の入力をo_led
に出力します。
module test_module(
input wire i_button,
output wire o_led
);
assign o_led = i_button;
endmodule
これを書いた状態が以下の画像です。書き終わりましたら、必ずCtrl + S
を押して上書き保存を行ってください。
論理合成
Verilogが書き終わりましたら、論理合成を実行します。論理合成とはHDLをディジタル回路に変換する処理の事でしたね。
この論理合成、EDAではボタン一つで行うことが可能です。以下の画像のように右から3番目のボタンを押すことで、論理合成が開始されます。
論理合成が完了したら、下のConsole
にGowinSynthesis Finishと表示されます。また書いたVerilogに記述ミスがある場合はここで赤文字で表示されます。
ちなみにTool > Schematic Viewer > RTL Design Viewer
を押すと論理合成で生成された回路図を見ることが可能です。
今回はi_button
とo_led
を直結させているだけですので、非常にシンプルな回路図となっています。
ピンアサイン
論理合成が完了しましたら、次はピンアサインを行います。ピンアサインとは、FPGAの入出力ピンをVerilogで定義したモジュールの入出力に割り当てる作業の事を指します。
Gowin EDAでピンアサインを行うには、Process
からFloorPlanner
をクリックします。
そしたらピンアサインを行うためにツールであるFloorPlanner
が起動します。
FloorPlannerの下にあるタブでI/O Constraints
を選択してください。すると表が出てきます。
このI/O Constraints
の表で見るべき部分はPort
とLocation
2つです。Port
にはVerilogで書いたモジュールの入出力ポートが書かれており、Location
に数字を入力することでその行の入出力ポートにピンを割り当てることが可能です。
今回はボタンとLEDの割り当てが必要です。Tang Nano 9Kにおいて、ボタンはピン3に繋がっていますので、i_button
には3
を割り当て、またLEDはピン10に繋がっていますのでo_led
には10
を割り当てます。
Location
への入力が完了したら、必ずCtrl + S
を押して変更を保存してください。
Tang Nano 9KにはボタンやLEDの他にも、LCDやUARTなど様々なピンが存在しています。詳しくはSiPEED社のページにあるTang_Nano_9k_3672_Schematic.pdf
を参照してください。
https://dl.sipeed.com/shareURL/TANG/Nano%209K/2_Schematic
また以下の表に本記事で使うピンと、その番号を載せてきます。
ピン | ピン番号 |
---|---|
クロック(27MHz) | 52 |
ボタン(S1) | 4 |
ボタン(S2) | 3 |
LED0 | 10 |
LED1 | 11 |
LED2 | 13 |
LED3 | 14 |
LED4 | 15 |
LED5 | 16 |
ピンアサインが完了したら、FloorPlanner
は閉じて構いません。
配置配線
ピンアサインの次は配置配線を行います。配置配線とは加算器やAND回路、FFといった回路をFPGA内に配置し、それらを繋ぎ合わせる配線を行う処理です。
Gowin EDAで配置配線を行うためには上にある、右から二番目のRun Place & Route
というボタンをクリックします。
配置配線が成功すると、下のConsole
にBitstream generation completedと出力されます。
この配置配線は非常に重い処理です。今回は簡単な回路ですので短い時間で完了しましたが、複雑な回路を作ると30分以上掛かることも珍しくありません。
書き込み
配置配線が完了した次はFPGAに回路を書き込みましょう。ここはWindows限定です。Linuxの方はOpenFPGALoarderを使ってください。
まずはUSBケーブルでTang Nano 9KとPCを接続してください。その後、上のボタンからProgrammer
を起動します。
正常に起動すれば以下の画像のようにCable Settingのウィンドウが開きますので、Saveをクリックします。
たまにNo USB Cable Connectionと出る場合があります。その場合はとりあえずOKを押して、Query/Detect Cable
を押してください。
そしてProgram/Configure
をクリックするとFPGAへ書き込みが開始されます。
書き込み完了後、Tang Nano 9KのS2ボタンを押すとLEDが光ります。回路が正常に書き込めていますね。
コンピュータアーキテクチャ入門へ
これでディジタル回路とVerilog入門は終わりです。次からは今まで学んだ技術を使い、CPUを自作していきます。

Copyright (C) Cra2yPierr0t, ALL RIGHTS RESERVED