ディジタル回路と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つ存在しています。

  1. ロジックIC
  2. LSI
  3. 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)からダウンロードを行ってください。

https://code.visualstudio.com

Verilog HDLシミュレータのインストール

本記事ではシミュレータとしてIcarus Verilogを利用します。

このページ(http://bleyer.org/icarus/)のDownloadの一番上のリンクからダウンロードしてください。v12がv13とかv14とかになってるかもしれませんが、とりあえずDownloadの一番上のリンクです。

http://bleyer.org/icarus/

またインストール時の注意点として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.vadder8_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ボタンを押すと波形を見ることができます。実際にin0in1の加算結果がoutに入力されているのが確認できると思います。適当に触ってみて気合で慣れてください。

以上のVerilog HDLでディジタル回路とテストベンチを記述し、シミュレーションを実行。波形を見てデバッグが開発の流れになります。この一連の流れは今後何度も繰り返します。今全てを覚える必要はありませんので大丈夫です、そのうち手癖で開発を回せるようになります。

  1. Verilog HDLで回路を書く
  2. Verilog HDLでテストベンチを書く
  3. シミュレーションを実行
  4. 波形を見る

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_wire0test_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

以下の回路と同等のモジュールproblem1problem1.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の幅になります。

実際に入出力用信号を定義した例が以下になります。この場合、clkdata_iが入力用の信号で、data_oが出力を受ける信号になります。

  reg           clk     = 0;
  reg   [7:0]   data_i  = 8'h00;
  wire  [7:0]   data_o;

テスト用回路の生成

テストベンチで回路をテストするためにはテストベンチ内で回路を生成する必要があります。そこで行うのがテスト用回路の生成です。

Verilogにおいて、回路はモジュール名 インスタンス名で生成する事が可能です。以下の例ではtest_moduleという名前のモジュールからDUTという名前の回路(インスタンス)を生成しています。 また入出力信号は.インターフェイス名(信号線名)で接続します。以下の例ではtest_moduleclk, 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("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クロック待機することが可能となるためです。

シミュレーションを実行

シミュレーションを実行するためにいくつかコマンドを実行しました。これらのコマンドの使い方とシミュレーションの実行方法をここで説明します。

シミュレーションにおいて主にiverilogvvpgtkwaveの3つのコマンドを利用します。

これらのコマンドを使う流れとしては、書いたVerilogファイルとテストベンチをiverilogに与えてa.outを生成します。そして出力されたa.outvvpに与えて波形ファイルを生成し、生成された波形ファイルを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には定数としてdefineparameterという2つの方法が存在しますが、defineはキモいのでparameterを使うことを推奨します。

parameterparameter 定数名 = 数値;のように使います。

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_ai_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_datai_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

以下の回路と同等のモジュールproblem4problem4.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

以下の回路と同等のモジュールproblem5problem5.vという名前のファイルに作成してください。以下の回路は16bitのレジスタを持っており、i_ctrlによって格納するデータを選択できます。

入出力は画像の通りです。

テストベンチは各自で作成してください。

練習問題3

以下の回路と同等のモジュールproblem6problem6.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を秋月電子通商で購入してください。秋葉に買いに行ってもいいですしネット通販でも大丈夫です。ネット通販の場合は秋月のページに行って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_buttono_ledを直結させているだけですので、非常にシンプルな回路図となっています。

ピンアサイン

論理合成が完了しましたら、次はピンアサインを行います。ピンアサインとは、FPGAの入出力ピンをVerilogで定義したモジュールの入出力に割り当てる作業の事を指します。

チップの周りにピンが並んでいる

Gowin EDAでピンアサインを行うには、ProcessからFloorPlannerをクリックします。

そしたらピンアサインを行うためにツールであるFloorPlannerが起動します。

FloorPlannerの下にあるタブでI/O Constraintsを選択してください。すると表が出てきます。

このI/O Constraintsの表で見るべき部分はPortLocation2つです。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
Tang Nano 9Kのピンとピン番号(一部)

ピンアサインが完了したら、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を自作していきます。

VLSI.JP/Computer_Architecture.html

Copyright (C) Cra2yPierr0t, ALL RIGHTS RESERVED