LoginSignup
5
2

More than 3 years have passed since last update.

Verilog未経験者がATLYSを動かしたときのメモ【FPGA】

Last updated at Posted at 2019-09-12

0.まとめた目的

今日(令和元年9月13日)Verilogの授業があってソースコード予習するように夏休み前から言われてたのに、昨日気がついた(´;ω;`)

だから死ぬほど慌てて書いてます正確性はゼロだと思ってください。
ちなみに昨日は僕の21の誕生日です。人生で最悪な誕生日だった(´;ω;`)

1. vファイルとucfファイル[1][2]

1-1. vファイル

ハードウェア(特に回路)記述言語。デジタル回路だけじゃなくアナログ回路もできるみたいですごいなと思った。遺伝子回路って何?

1-2 ucfファイル

vファイルで記述されたIOピンを実際のFGPAピンに対応させる。

以下、基本的にvファイルの文法について解説しようと思う。家出るまであと1時間しかない泣泣泣

2. vファイルの書き方[3]4

基本的に「モジュール」と呼ばれるものを書く。モジュールとは回路の「部品」のようなもの。正しい言い方をすると「verilogではモジュールを組み合わせて回路を設計していく」のである。

モジュールの書き方は次のようになる。参考サイト[3]から引用。(インデントは改編)

module 
モジュール名
(
    ポート・リスト
);
    input    入力信号名;
    output   出力信号名;
    wire     内部信号名;
    reg      内部信号名;

        回路機能定義

endmodule

回路機能定義(動作記述と呼ぶことも)は、具体的にはassgn文always文といって「小さな回路」を作る処理だったり、if文、case文で条件分岐を行うものだったりする。

また[4]によれば各モジュールは上位モジュール・下位モジュールに分けられるよう。
上位モジュール内で宣言したinputでは下位モジュールへの進入が可能。
このことから親子関係はオブジェクト指向でいうなら part of 関係に近いだろう。
wireは配線を意味し、配線(=wire変数(=論理ゲート(!?))を組み合わせることで組み合わせ回路を作れる。
一方regはレジスタを意味するから組み合わせ回路はもちろん、順序回路を作ることだってできる。
変数については2-4で解説。

2-1. 回路を作る文

2-1-1. assign文

ワイヤ(wire)同士の接続を行うための文。(regに対しても使用可能なようだが順序回路は書けない。)当たり前だがワイヤの接続により回路が成立しうる。従って「回路を作る文」ということも出来る。bit演算で論理式を書くことで論理回路(組み合わせ回路のみ。順序回路不可)を設計できる。
たとえば
assign A = ( B & C ) | ( D & E ) ;
と書けば、論理式 ( B × C ) + ( D × E ) と等しい論理関数(に対応する変数)Aを生成できる。

2-1-2. always文

always文もまた「回路を作る文」である。assign文と違って順序回路も可能。

次のような書き方をする。

always@(センシティビティリスト)
begin
    動作記述
end 

センシティビティリストとは入力の列挙であり、例えば posedge CLK と書くと「クロックの立ち上がり」を入力とするalways文となる。したがって「クロックの立ち上がりで動作を開始する回路」の宣言となるわけだ。

2-1-2-1. 同時処理と逐次処理

c言語, java, php, ... などの馴染み深いプログラミング言語では、コードは基本的に上から順に処理される。
こういうのを逐次処理という。逐次処理ではない処理(の仕方、特にタイミング)のには同時処理というものがある。
同時処理はすべて同じタイミングで処理されて、「処理の順番」という概念が存在しないもののことをいう。
すべてのalways文は同時処理であり、処理の順番は存在しない。

2-2. 分岐文[4]

if文とcase文がある。どちらもalways文の中でしか使用できない。

2-2-1. if文

分岐後の各処理(c言語等のif文で例えるなら各ブロック内の処理)が複数の場合、beginendの間に挟む。
逆に言えば、処理が一つだけならbeginendは不要。

(要は、c言語等のif文でいう{beginで、}endだ。)
ifelse ifelse が使用可能で、これらの意味や関係はc言語等のif文と同じ。

if文も同時処理されるため、代入は一斉に行われる。

2-2-2. case文

c言語でいうswitch文のようなもの。
次のように書く。

case(条件変数)
    数値その1 : 動作記述
    数値その2 : 動作記述
    数値その3 : 動作記述
    数値その4 : 動作記述
    default : 動作記述
endcase 

参考までに、対応するc言語のswitch文を書いておく。

switch(条件変数)
{
    case 数値その1: 動作記述; break;
    case 数値その2: 動作記述; break;
    case 数値その3: 動作記述; break;
    case 数値その4: 動作記述; break;
    default 動作記述;
}

複数の処理を各箇所で行いたい場合は、2-2-1と同じようにbegin,endを使う。(ここはc言語等のswitch文(ブロック無くても複数処理できる)と違うところ)

2-3. 定数

定数の意味はそのまま「数字」の意味である。
bit 数 ' 基数 数値
という書き方をする。bit数は10進自然数。
基数はb,d,hの三種類があり、順番にbinary(2進数),decimal(10進数),hex(16進数)を意味する。
数値は、奇数で示した通りに書く。(但し、0による左詰めはしなくていいようだ。(勘違いかも))
例:5'b010105'd105'haに等しい。

2-4. 変数[4]

wire変数とreg変数がある。
wire変数は配線を意味する。保持とかはできない(だから組み合わせ回路しか作れないのだろう)。そのため「値の流れている線」と思うべき。
それに対してreg変数はc言語等同様、値を保持する(普通の)変数だ。

3.ucf

時間切れなので省略(´;ω;`)

4.サンプル

大学の講義資料から引用。「これは7セグのプログラムだが、中途半端なので完成させなさい」という課題が与えられている。

`timescale 1ns / 1ps /* 第1節 */

module seg7test
(
    input wire[3:0] sw, // タクトスイッチ /* 第2節 */
    input wire[3:0] ssw, // スライドスイッチ
    output wire[6:0] seg. // 7セグ
    output wire dp,
    output wire[3:0] line // 4ライン制御
)
    wire[6:0] code;
    assign seg = code; //atlys 
    assign seg = ~code; // basys2
    decoder7seg decoder0(.data(ssw),.code(code));//7セグデコード /* 第3節 */
    assign line[3:0] = sw[3:0]^4'b1111; // atlys /* 第2節 */
    //assign line[3:0] = sw[3:0]; // basys2
    assign dp = 1'b1;
endmodule

module decoder7seg( input wire [3:0] data, output wire [6:0] code );
    assign code
    =
    (data==4'd0) ? 7'b1111110 : //0
    (data==4'd1) ? 7'b0110000 : //1
    (data==4'd2) ? 7'b1101101 : //2
    7'b1000111; //F
endmodule

4-1.timescale[5][6]

シミュレーション時刻を記述するためのもの。
‘timescale<1タイムスケールあたりの実時間>/ <丸めの精度>
サンプルでは1タイムスケールを1ns(10^-9s)とし、1ps(10^-12s)の精度で丸めている(10^-12s以下を切り上げるか切り捨てるかは環境依存らしい)。
例えば
#(10/3) clock = 1;
あるいは
clock = #(10/3) = 1
とすると、
3.3333...n秒経ったときにclock = 1;を実行する。実際には丸めがあって、小数第12-9=3位より小さな時間は考えない。したがって、
3.333n秒後または3.334秒後に実行する。

4-2.wire[n:0] [7]

例えば wire[3:0] = data とすると、4つのwire変数をまとめて宣言することができる。
(regで同じことができるかどうかは未調査)
各変数はdata[3] data[2] data[1] data[3] のようにしてアクセスできて、0か1いずれかの値を持つ。
(前述の通り、wireは導線なのだから、今データが流れているか否かが1と0に対応している)
これだけみると、その本質は真偽値配列であるが、他にも次のようなアクセスが可能。
data = 4'b0100;
このようにすると、data[2]だけ1、それ以外は0となるわけだ。
当然、次のようなことも可能。

case(data)
    4'b0000 : (処理);
    4'b0001 : (処理);
    4'b0010 : (処理);
    default : (処理);
endcase

その他、サンプル中にも
sw[3:0]^4'b1111;とあるように、bit毎の論理演算(この場合は排他的論理和)をまとめて計算し、その値を得ることも可能。

4-3.モジュール利用

モジュールは、c言語やphpにおける関数のように宣言・呼び出しすることができる。
但し関数と異なり、戻り値はないし、引数(output宣言を付けたもの)に対して積極的に副作用を与える。
また、呼び出すときの引数はそれらの順番で対応させるのではなく、.引数名(その引数に与えたい変数名) のように、それぞれがどの引数に対応するのかを明示することになっている。
サンプルにおけるdecoder7segモジュールの宣言・呼び出しをjavaのメソッドっぽく書くと次のようになる。

code = decoder7seg(ssw);

boolean[] decoder7seg( boolean[] data )
{
    if(data.length!=4) throw new IllegalDataLengthError();//アーリーリターン
    byte data_b = (data[3]?8:0)
                + (data[2]?4:0)
                + (data[1]?2:0)
                + (data[0]?1:0);
                + 
    String code_str = 
    (data_b==0) ? "1111110" : //0
    (data_b==1) ? "0110000" : //1
    (data_b==2) ? "1101101" : //2
    1000111;//F

    boolean[] code = new boolean[7];
    for(int i=0; i < code.length; i++)//蛇足:拡張forでは書き込み不可
        code[i] = code.charAt(i)=='1';
    return code;
}

よくみるとdecoder0 という「名前」のようなものが消えている。実はdecoder0decoder7segの「インスタンス」なのだ。このようにverilogのモジュールはメソッドとクラスの中間的な存在のようで、ちょっと迷う。

4-4. サンプルの解説

4-4-1. seg7testモジュール

第1節~3節の解説を元に、サンプルコードを読んでみよう。
まず最初に小数点以下第3位まで有効なタイムスケール#1=1nsを宣言している。

メインとなるモジュールseg7testでは入力用タクトスイッチ、スライドスイッチを4個ずつ宣言している。
出力としてはseg(7セグ)の他、dpとlineを用意している。

処理としてはdecoder7segという別モジュールを呼び出すことで、次のことをしている。
「segに7セグをどう光らせるべきか」を「スライドスイッチ」を元に書き込んでいる。

dpは処理を行ったら(本当は「同時」に)1を立てている。
このことから、「不具合があったときにそもそもモジュールseg7test自体が動作しているのか」を確かめるためのものなのだろうと推察される。(dpが1⇔プログラムが動作している)

一方lineについてはよくわからなかった。(atlysとかbasys2とかで検索すると開発ボードがそれぞれでてくる。それぞれの種類を識別するための信号なのだろうか)

4-4-2. dacoder7segモジュール

decoder7segモジュールは前述の通り、データを受け取ったら7セグのどこを光らせどこを消すか判断するモジュールである。

コメントを見るだけでも、どこが中途半端なのかは明らかになっている。3~Eが抜けてるではないか。
2^6の位をA, 2^5の位をB,...2^0の位をGと呼ぶとして、7'bABCDEFGのように書くことで、7セグの各セグメントに点灯・消灯を切り替えることができる。
7seg.png
図4.4.2.1 7セグの各セグメントに名前を付けたもの

4-5. サンプルの改良

0~2とFはあらかじめ与えられている。
3についてはFとEのみを消灯すればよい。(図4-4でFとEを指で隠すと3が浮かび上がる)
したがって7'b1111001とする。
4~Eも同様に考えることができる。
結局、decoder7segモジュールは次のように宣言しなおせばよいわけだ。

module decoder7seg( input wire [3:0] data, output wire [6:0] code );
    assign code
    =
    (data==4'd0) ? 7'b1111110 : //0
    (data==4'd1) ? 7'b0110000 : //1
    (data==4'd2) ? 7'b1101101 : //2
    (data==4'd3) ? 7'b1111001 : //3
    (data==4'd4) ? 7'b0110011 : //4
    (data==4'd5) ? 7'b1011011 : //5
    (data==4'd6) ? 7'b1011111 : //6
    (data==4'd7) ? 7'b1110010 : //7
    (data==4'd8) ? 7'b1111111 : //8
    (data==4'd9) ? 7'b1111011 : //9
    (data==4'd10) ? 7'b1110111 : //A
    (data==4'd11) ? 7'b0011111 : //b
    (data==4'd12) ? 7'b1001110 : //C
    (data==4'd13) ? 7'b0111110 : //d
    (data==4'd14) ? 7'b1001111 : //E
    7'b1000111; //F
endmodule

5. 講義でやったこと

※5ー1以外は僕が自分で考えてやったものではありません。あくまでも講義内の実験でやったことを自分でまとめなおしたものです。

また講義用のPCにはすでにISEがダウンロードされていましたが、入手方法については独自で調べて5-1にまとめています。
※5ー2やそれ以降について、ソースコードは第7章の吟味にて掲載・解説する。

5-1. ATLYSへのFPGA組み込みプログラミングの環境用意

5-1-1. ISE Design Suiteをダウンロード

https://japan.xilinx.com/support/download.html からダウンロードできる。[8]
ただしこのページは過去にレイアウトが大きく変わったことがある。レイアウトが大きく変わり得る以上、サイトからISEをダウンロードする詳細な方法を述べても無駄になるから述べない。
ダウンロードしようとするとログインを求められる。そのためアカウントを作成する必要がある。
その際「勤務先Eメール」の入力を求められるが、大学から支給されたメールアドレスを用いた。
アカウントを作成すると、xilinx.comから「勤務先Eメール」へメールが届く。そのメールに含まれるアクティベート用リンクをクリックする必要がある。
すると、ユーザIDとパスワードの入力を求められた後、「購読設定の管理」の画面へと遷移する。とりあえず使用する言語を日本語として、その他の設定はいじらず、「アップデート」を押した。

その後、ISEのダウンロードを試みると、名前や勤務先住所の確認を求められた。名前は自分の名前、住所は大学の住所を入力した。英語でないと怒られるので大学名も含め英語で記述。
仕事内容(「プロフィールの編集」から予め入力している場合は「職務内容」)は「学生」が選択肢にあったのでそれにした。
(このことから、xilinxは学生として大学に通うことを仕事・職務とみなしていることがわかる。したがって勤務先Eメールに大学のアドレスを入力することは不正に当たらないと考えてよいだろう。)

入力を終えて「次」をクリックすると、いきなり6.9GBのzip形式ファイルのダウンロードが始まるため、地味に注意が必要。
ダウンロード先フォルダに日本語を含めると、5-1-3の時にfatal errorが出てしまうので、日本語を含まないパスにすること。

巨大なファイルをダウンロードし、しかも解凍する必要があるので、待っている間に5-1-2に述べたAdeptのダウンロードを行うのがよい。

5-1-2. Adeptのダウンロード

https://store.digilentinc.com/digilent-adept-2-download-only/ からダウンロードできる。[9]
Download Hereをクリックすると、ものすごく時間がかかった後、次の画面へ遷移した。
その画面で右のメニュー(?)から最新版のファイルをダウンロードできる。
やはりこちらでも個人情報の入力を求められるが、ISEをダウンロードした時ほど面倒なものではない。
ISEの時同様素直に入力。(こちらもOCCUPATIONでStudentが選べる)
最後の質問「WHERE DID YOU PURCHASE YOUR DIGILENT SYSTEM BOARD?」(Digilentシステムボードはどちらで購入しましたか?)ばかりは素直に答えることができない。(購入したのは大学だから)otherを選択した。
その後SUBMITを押すと、ダウンロードが始まった。こちらは解答の必要はないし十数MBの実行ファイル形式なのですぐに完了した。

5-1-3. ISE Design Suiteのインストール

(まだISEのダウンロードや展開が終わっていない場合、先に5-1-4をやってしまいましょう)
5-1-1の手順が終わったら、展開したフォルダからxsetup.exeを選んで実行した。
「Next >」、2つの「I agree」をチェックして「Next >」、「Next >」、インストールフォルダを選択して「Next >」、「Install」、インストール中にOracle Corporation ユニバーサル シリアル バス コントローラ... のデバイスをインストールしてよいか聞かれるのでインストールした。すると無事にインストールが終わった。

5-1-4. Adeptのインストール

5-1-2でダウンロードした実行ファイルを実行すると、Adeptのインストールが始まる。
ウィザードに対し、「Next >」、「I agree」、「Next >」、「Next >」、インストール先フォルダを決めて「Install」、インストールが終わったら「Next >」、「Finish」をした。

5-1-5. ISE Design Suiteの動く仮想PCの初期設定

ISE Design Suiteを開くと、なぜかOracle VM virtualboxによる仮想PCが起動し、その中でISE Design Suiteが起動した。
このままだとホストOS(本当のPC)とゲストOS(仮想PC)の間でのファイルのやり取りがしづらいから、必要なら仮想PCの設定を変更する必要がある。
また、コメントなどで日本語を利用する場合、日本語入力の設定がかなり面倒。

5-1-5-1. ゲスト・ホスト間でのファイルのやりとりを可能にする

ファイルのやりとりについては、少なくとも以下の3つの方法が確認できた。

  • ホストOSとゲストOSで共有するフォルダを作る方法
  • ホストOSとゲストOS間でクリップボードを共有する方法
  • ホストOSとゲストOS間でドラッグドロップを可能にする方法

ホストOSとゲストOSで共有するフォルダを作る方法は、
図5.1.5.1.1のように「デバイス」→「共有フォルダー」、「共有フォルダーの設定...」を選択し、
自動マウントと永続化の設定を有効にし、仮想PCを再起動したら、仮想PCデスクトップ上と、ホストPCの指定したディレクトリの双方に同名のディレクトリができ、ファイルの共有に成功していることが確認できた。

1.png
図5.1.5.1.1 仮想マシンの設定

なお、クリップボードの共有や、ドラッグドロップはうまくいかなかった。

5-1-5-2. 日本語入力の設定

闇雲に行ったため、理論的な正しさは保証しない。

  1. 仮想PCの画面の上部にあるメニューバーで「System」、「Preferences」、「Keyboard」の順に進み、「Keyboard Preferences」画面を表示した。
  2. 「Layouts」タブを選ぶと、Selected layoutsが「English (US)」一つとなっていた。「Add...」をクリックし、「Choose a Layout」画面を表示した。
  3. 上部に「Country」とあるから「Japan」を選んだ。Variantsは「Japanese」とした。
  4. 「Keyboard Preferences」画面へ戻ると、Layoutの一覧には「English (US)」と「Japanese」の2つが表示されていた。そのうち「English (US)」の方をRemoveした。
  5. 次に、仮想PC上部のメニューバーで「System」、「Preferences」、「Input Method」と進んだ。
    Input MethodとはIMEのことであると考えられる。
  6. Enable input method featureにチェックを入れ、Input MethodのUse IBus (recommended)を選択し、「Input Method Preferences...」をクリックし、「IBus Preferences」画面が表示されるのを待った。
  7. 「IBus Preferences」画面の「General」タブで「Enable or disable」という項目の中に「Zenkaku_Hankaku」があることを確認した。
    これは「半角/全角」キーでIMEの無効/有効が切り替えられると言う意味で、理に適っている。(逆に言えば、その他のショートカットキーは無効にしてしまってもよいだろう)
  8. 「Input Method」タブをクリックし、「Select an input method」をクリックすると、各国の言語がかかれた選択肢が出てきた。「Japanese」を選ぶと、その中に「Anthy」と言うものがあったのでそれをクリックした。

以上で(IME有効時に)「A」キーを押すと「あ」が表示され、そのまま変換キーを押すと「亜」が表示されるようになった。

5-2. 実験1 簡単プログラムでFPGA組み込み開発の流れをつかもう

  1. ISE Design Suiteを開いた。
  2. 「File」→「New Project」を選択し、working directoryを5-1-5で作った共有フォルダに設定し、Nameを「test」として「Next >」を押した。
  3. 図5.2.1のような画面になった。そのうち赤く示した項目を正しく(図5.2.1と同じになるように)設定して、「Next >」、「Finish」と進んだ。
    2.png
    図5.2.1 プロジェクトの設定
  4. 「Project」→「New Source」を選択し、次の1,2を選択し、FileNameをそれぞれプロジェクト名と同じtestとした。
    1. 「Verilog Module」(test.vが作成される)
    2. 「Implementation Constraints File」(test.ucfが作成される)
  5. ISEを再起動した。(合成時のバグを防ぐ)
  6. test.v,test.ucfへLチカプログラムを書き、Generate Programing Fileを右クリックし、Rerun Allにてtest.vとtest.ucfを合成し、ボードへ流し込むためのtest.bitを生成した。
  7. 3つともチェックマーク(あるいはエラーではなく警告マークであり、その警告を無視してよい理由が説明できる)になったことを確認した。
    自宅の環境ではここまで(ボードがないので)。以下、大学でボードへビットを流し込んだ手順を述べる。
    5ー2以降ではこのことは断らない。
  8. パソコンとATLYSボードを接続し、Adeptを起動し、「Browse..」でtest.bitを選択し、「Program」で実際にボードへ流し込んだ
  9. すると、ボード上のLEDが1秒間隔で点滅した。

5ー3. 実験2 7セグメントLED表示回路の作成

  1. 図5.3.1のようにLED表示回路を構成した。(GNDは忘れやすいので要注意)
  2. ISEを立ち上げ、「File」→「New Project」を選択し、7セグ表示プロジェクト(Nameが「seg7test」)を作った。ISEを再起動したのちに7セグ表示プログラム(seg7test.vおよびseg7test.ucf)を書き込み、seg7test.bitファイルを合成した。
    5ー3以降では同様の操作を「hogeプログラムのfuga.bitを生成した」のように簡略化して説明する。
  3. パソコンとATLYSボードを接続し、Adeptを起動し、「Browse..」でseg7test.bitを選択し、「Program」で実際にボードへ流し込んだ。
    5-3以降では同様の操作を「Adeptでfuga.bitをボードへ転送した」のように簡略化して説明する。
  4. すると、スライドスイッチ4つを4bit(On→1/Off→0)とみて、その4bitに対応する16進数を7セグ上に表示することができた。
    例えば4つのスライドスイッチを「On, Off, On, Off」とするとAが表示され、「On, On, On, On」とするとEが表示された。

5-4. 実験3 ストップウォッチの作成

  1. ストップウォッチプログラムのstopwatch.bitを生成し、Adeptでボードへ流し込んだ。
  2. すると、図5.4.1に示すパターンで状態(0000)から始まり、状態が0.01秒毎にインクリメントされるようなストップウォッチが完成した。(スイッチ操作も正常に確認できた)ちなみに状態(9999)の次は状態(0000)へ戻った。
  3. 第一修正ストップウォッチプログラムに変更しなおしたstopwatch.bitを生成し、Adeptでボードへ流し込んだ。
  4. すると、7セグ表記の数字で4桁の10進数を表したパターンで状態(0000)から始まり、状態が0.01秒毎にインクリメントされるようなストップウォッチが完成した。(スイッチ操作も正常に確認できた)
  5. 第二修正ストップウォッチプログラムに変更しなおしたstopwatch.bitを生成し、Adeptでボードへ流し込んだ。
  6. すると、手順4で確認した動作が0.1倍速になったストップウォッチが完成した。(状態が0.1秒毎にインクリメントされた)

5-5. 実験4 組み合わせ回路(1) 半加算器

  1. 半加算器プログラムのhalf_adder.bitを生成し、Adeptでボードへ流し込んだ。
  2. すると、表5.5.1の通りの真理値表にしたがう回路が構成された。ただしAはSW1の状態(On→1, OFF→0)、BはSW0、Y[1]はLD1,Y[0]はLD0の状態をそれぞれ表す。

表5.5.1 半加算器プログラムの真理値表

A B Y[1] A and B Y[0] A xor B
0 0 0 0
0 1 0 1
1 0 0 1
1 1 1 0

5-6. 実験5 組み合わせ回路(2) 全加算器

  1. 全加算器プログラムのfull_adder_by_elements.bitを生成し、Adeptでボードへ流し込んだ。
  2. すると、表5.6.1の通りの真理値表にしたがう回路が構成された。ただしx_はSW_(「_」は0~2の整数)の、Y[1]はLD7,Y[0]はLD6の状態をそれぞれ表す。
  3. 修正全加算器プログラムに変更したfull_adder_by_elements.bitを生成し、Adeptでボードへ流し込んだ。
  4. すると、手順2で確認したものと全く同じ動作をする回路が構成された。

表5.6.1 全加算器プログラムの真理値表

x2 x1 x0 Y[1] Y[0]
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

5-7. 実験6 組み合わせ回路(3) 2ビット加算器

  1. 2ビット加算器プログラムのbit2_adder.bitを生成し、Adeptでボードへ流し込んだ。
  2. 被加数x1、加数x0がそれぞれ2bitとなる加算(2^4=)16通りがすべて正しく行われることを確認できた。x1はSW3(2の位)とSW2(1の位)が、x0はSW1(2の位)とSW0(1の位)がそれぞれ対応している。
  3. 修正2ビット加算器プログラムに変更したbit2_adder.bitを生成し、Adeptでボードへ流し込んだ。
  4. すると、手順2で確認したものと全く同じ動作をする回路が構成された。

5-8. 実験7 順序回路(1) Dフリップフロップ,2分周器

  1. 2分周器プログラムのd_ff_s.bitを生成し、Adeptでボードへ流し込んだ。
  2. すると、BTNDを押す度にLD0が点灯消灯を繰り返すボードが実現した。このボードはチャタリングの問題を抱えていて、一押しで点灯と消灯を繰り返すことがしばしば確認された。
  3. 2分周器を直列に4つ接続した回路プログラムのd_ff.bitを生成し、ボードへ流し込んだ。
  4. すると、4bitカウンタが構成された。しかしチャタリングの問題を抱えていて、一押しで4ほどカウントが進むこともあった。

5-9. 実験8 チャタリング防止回路

  1. ff_no_chatteringプロジェクトを新しく作り、5-8の2分周器を直列に4つ接続した回路プログラムと同じ内容にした。
  2. anti_chatteringモジュールを追記した。
  3. reg xwire xに変更し、always @(negedge clk) x>=a;
    anti_chatter anti_chatter1( .clk(clk), .sw_in(a), .sw_out(x) );
    に変更した。
  4. ff_no_chattering.bitを合成し、Adeptへ流し込んだ。
  5. すると、チャタリングの起こらない4bitカウンタが構成された。一押しで1だけカウントが進んだ。

5-10. 実験9 順序回路(2) 単相同期回路

  1. 3ビットカウンタプログラムのoctal_counter.bitを生成し、ボードへ流し込んだ。
  2. すると、一押しで1だけカウントが進み、7の次は0へ戻るカウンタの動作が確認できた。
  3. 図5.10.1で示すように「Floorplan Area/IO/Logic(PlanAhead)」をダブルクリックし、PlanAheadを起動した。
    3.png
    図5.10.1 PlanAheadを開く直前の画面
  4. PlanAheadが起動したらSchematicタブを選択し、図5.10.2 のように画面を操作してLUT3(Mcount_Q_xor<2>11)の真理値表を確認した。すると、表5.10.1のような真理値表が得られた。
    4.png
    図5.10.2 LUTの真理値表を確認する手順
  5. 3ビットカウンタプログラムを改造し、4bitカウンタプログラムに変更し、hexadecimal_counter.bitを生成し、ボードへ流し込んだ。
  6. すると、一押しで1だけカウントが進み、15の次は0へ戻るカウンタの動作が確認できた。
  7. 手順4と同様の手順でLUT4(Mcount_Q_xor<3>11)の真理値表を確認すると、表5.10.2の通りだった。

表5.10.1 3ビットカウンタでQ(2)の次の値を決めるLUTの真理値表
Q(2)以外がすべて1なら次のQ(2)は変化するし、そうでないなら次のQ(2)は変化しない。

Q(2) Q(1) Q(0) 次のQ(2)
0 0 0 0
0 0 1 0
0 1 0 0
0 1 1 1
1 0 0 1
1 0 1 1
1 1 0 1
1 1 1 0

表5.10.2 4ビットカウンタでQ(3)の次の値を決めるLUTの真理値表
Q(3)以外がすべて1なら次のQ(3)は変化するし、そうでないなら次のQ(3)は変化しない。

Q(3) Q(2) Q(1) Q(0) 次のQ(3)
0 0 0 0 0
0 0 0 1 0
0 0 1 0 0
0 0 1 1 0
0 1 0 0 0
0 1 0 1 0
0 1 1 0 0
0 1 1 1 1
1 0 0 0 1
1 0 0 1 1
1 0 1 0 1
1 0 1 1 1
1 1 0 0 1
1 1 0 1 1
1 1 1 0 1
1 1 1 1 0

5-11. 実験10 順序回路(3) 10進カウンタ

  1. 10進カウンタプログラムのdecimal_counter.bitを生成し、ATLYSボードへ転送した。
  2. すると、一押しで1だけカウントが進み、9の次は0へ戻るカウンタの動作が確認できた。
  3. 5ー10の手順4と同様の手順で回路図やLUTの真理値表を入手した。
  4. 10進カウンタを1秒パルスで動かすプログラムのauto_decimal_counter.bitを生成し、ボードへ流し込んだ。
  5. すると、1秒で1ずつカウントする10進カウンタの動作が確認できた。
  6. auto_decimal_counter.vのwire [3:0] max=4'd9;とあるところで、右辺を適宜変えることで6進カウンタ、3進カウンタにそれぞれ改造し、bitファイルを生成してボードへ流し込み、動作を確かめた。

5-12. 実験11 順序回路(4) 60進・24進カウンタ

注:この実験は時間の都合上、大学では実際に行っていない。

  1. 10進カウンタと6進カウンタを統合して60進カウンタを実現するプログラムのauto_counter_60_ary.bitを生成した。
  2. これをボードへ転送すれば、1秒で1つカウントが進む10進カウンタが1周期を迎える毎に6進カウンタが1進むことで1分(=60秒)を周期とする60進カウンタが実現したと考えられる。
  3. 60進カウントを7セグで実現するプログラムのauto_counter_60_ary_7seg.bitを生成した。
  4. これをボードへ転送すれば、手順2に加えて、カウントを表す数値を7セグ上することが実現できると考えられる。
  5. 60進カウントを7セグで実現するプログラムを修正し、24進カウンタに改造した。
  6. 24進カウンタと60進カウンタを併せて24時間時計に改造した。

6. 検討事項(書きかけ)

大学のレポート課題。

6-1. 表5.10.2 を散光にして10進カウンタ(実験10)のすべてのLUTの真理値表をつくり、その動作を説明せよ(書きかけ)

まず実験10で作ったdecimal_counter.vの回路図は、図6.1.1のようになっていた。

6-2. 「24時間表示ディジタル時計」のブロックダイアグラムを作成せよ

7-1-11-4で作ったプログラムclock24.bitのブロックダイアグラムを作成した。
すると、図6.2.1のようになった。この図を作成しながらプログラムの問題を一部修正したり、改めてプログラムの各モジュールやalways文の関係性を広い視野で理解するなどできた。特に3つのクロックclk,clk_m,clk_hの間の関係のように、「一度わかれば単純なロジックであるが、癖があるために始めに理解することや、他人に口で説明することが比較的難しい」ようなロジックを驚くほど簡潔に説明できることを発見した。このようにブロックダイアグラムを作ることは全体俯瞰に役立つとともに細部を理路整然と説明する役割を持つ。
6.png
図6.2.1 24時間時計のブロックダイアグラム

6-3. チャタリング防止回路のソースコードを読み、図を用い動作を説明せよ(書きかけ)

文章による動作の説明は7-1-8-1で行った。ここではタイムテーブルとフローチャートを組み合わせてチャタリング回避の様子を示した図6.3.1、および7-1-8-1-5で示した「潜在的な問題」を説明する図6.3.2を示す。

6-4 ATLYSで0.1秒周期のパルスを発生させる回路をVerilogの10行以内のコードで書け(書きかけ)

module pulse0_1(input wire clk, output reg clk0_1);
    reg [23:0] counter10M;
    always @(posedge clk) counter10M[23:0] <= (counter10M[23:0]>=24'd9999999)? 24'b0 : counter10M[23:0]+24'b1;
     always @(counter10M[23]) clk0_1 <= counter10M[23];   
endmodule

5行。(コンパイルに成功)
可読性を無視するなら改行を取り払い、1行にまとめても問題なくコンパイルが通る。

7. 吟味

7-1. 各プログラムのコードと解説

ここでは第5章に示した実験における各プログラムのコードとその解説を示す。
また解説に当たってFPGA端子名と部品名の対応付けが必要となるため、表7.1.1にATLYSにおける部品名とFPGA端子名の対応表を示す。この表は大学のテキスト及び参考資料[11]をもとにまとめられた。

表7.1.1 ATLYSにおける部品名とFPGA端子の対応表
(後で)

7-1-1. 実験1

7-1-1-1. Lチカプログラム

Lチカプログラムのtest.vは次の通り。

`timescale 1ns / 1ps
module test(input wire clk0, output wire led);
    reg[26:0] c=0;
    assign led=c[26];

    always @( posedge clk0 )
        if( c==27'd99999999 )
            c <= 0;
        else
            c <= c + 1'b1;
endmodule

test.ucfは次の通り。

NET "clk0"  LOC = L15;  // CLK
NET "led"   LOC = U18;  // LD0
7-1-1-1-1. testモジュールの入出力

vファイルより、testモジュールでは1bitワイヤclk0の入力を受け付け、1bitワイヤledを出力していることがわかる。ucfファイルよりclk0はATLYSにおけるFPGA端子L15に対応していることが分かる。表7.1.1より、L15は100MHzのクロックに対応していることがわかる。
またledはLEDに対応していることがわかる。
よってtestモジュールは100MHzのクロックを受け取り、LEDに対する出力を行うことがわかる。

7-1-1-1-2 testモジュールの内容

100MHzクロックのアップエッジに合わせて27bitレジスタcをインクリメントしていっている。
但しcが99999999であった場合、0に書き換えている。

これにより、cは100000000(=100M)進カウンタとして動作する。
クロックの周波数と照らし合わせて考えると、cの状態は1秒を1周期として変化する。(以下、これを「cの周期は1秒である」のように表現する)
また、assign led=c[26]より、LEDはcの226の位(MSBである)が1であるとき光る。
100M進カウンタで226の位が1となるのは、カウント値が226以上100M=108以下の時である。
したがって、周期に対するLED点灯時間の割合は
1-(226÷108)
で求められる。ここで、210=1.024×103より、
230÷109=1.0243≒1だから、
1-(226÷108)≒1-(2-4×10)=1-(5/8)=3/8
と求められる。

7-1-2. 実験2

7-1-2-1. 7セグ表示プログラム

7セグ表示プログラムのseg7test.vは次の通り。

`timescale 1ns / 1ps

module seg7test(input wire[3:0] sw, input wire[3:0] ssw, output wire[6:0] seg, output wire dp, output wire[3:0] line);
    wire[6:0] code;
    assign seg = code;

    decoder7seg decoder0(.data(ssw),.code(code));//7セグデコード
    assign line[3:0] = sw[3:0]^4'b1111; 
    assign dp = 1'b1;
endmodule

module decoder7seg( input wire [3:0] data, output wire [6:0] code );
    assign code
    =
    (data==4'd0) ? 7'b1111110 : //0
    (data==4'd1) ? 7'b0110000 : //1
    (data==4'd2) ? 7'b1101101 : //2
    (data==4'd3) ? 7'b1111001 : //3
    (data==4'd4) ? 7'b0110011 : //4
    (data==4'd5) ? 7'b1011011 : //5
    (data==4'd6) ? 7'b1011111 : //6
    (data==4'd7) ? 7'b1110010 : //7
    (data==4'd8) ? 7'b1111111 : //8
    (data==4'd9) ? 7'b1111011 : //9
    (data==4'd10) ? 7'b1110111 : //A
    (data==4'd11) ? 7'b0011111 : //b
    (data==4'd12) ? 7'b1001110 : //C
    (data==4'd13) ? 7'b0111101 : //d
    (data==4'd14) ? 7'b1001111 : //E
    7'b1000111; //F
endmodule

seg7test.ucfは次の通り。

NET "seg(6)" LOC = V13; // A
NET "seg(5)" LOC = V15; // B
NET "seg(4)" LOC = N8; // C
NET "seg(3)" LOC = V10; // D
NET "seg(2)" LOC = T10; // E
NET "seg(1)" LOC = V16; // F
NET "seg(0)" LOC = V8; // G
NET "dp" LOC = T8; // DP

NET "line(3)" LOC = U10; // IO11
NET "line(2)" LOC = R8; // IO12
NET "line(1)" LOC = M8; // IO13
NET "line(0)" LOC = U8; // IO14

NET "sw(3)" LOC = P4; // BTNL
NET "sw(2)" LOC = F5; // BTNC
NET "sw(1)" LOC = F6; // BTNR
NET "sw(0)" LOC = P3; // BTND

NET "ssw(3)" LOC = P15; // sw3
NET "ssw(2)" LOC = C14; // sw2
NET "ssw(1)" LOC = D14; // sw1
NET "ssw(0)" LOC = A10; // sw0
7-1-2-1-1. seg7testモジュールの入出力

vファイル、ucfファイル、および表7.1.1より、seg7testモジュールではタクトスイッチ(プッシュボタン)上、中、下、左、右、および4つのスライドスイッチの入力を受け付け、出力seglineをVHDCコネクタへ送出していることがわかる。
実際は7セグのどこを光らせるかに関わっている。(7セグへのつなぎ方はエンジニアの配線次第なわけだから、ATLYS公式ドキュメントではVHDCコネクタへ送出した先のことは説明していない)
lineにより何桁目の7セグを制御するかを決定し、segにより7セグのどこを点灯するかを決定する。

よってseg7testモジュールはプッシュボタンの押下状態やスライドスイッチの状態を受け取り、VHDCへ出力を行い7セグを制御することがわかる。

7-1-2-1-2. seg7testモジュールの中身

lineはタクトスイッチ4列の否定となっている。
実験中に動作を確認した時はタクトスイッチをすべてOFFにしていたため、7セグには4桁すべてに同じ数字が表示されていた。
どの数字が(どのようにして)表示されるかは、decoder7segモジュールのdecoder0インスタンスが決めている。

7-1-2-1-3. decoder7segモジュール

スライドスイッチの示す数字を受け取り、それに対応する7segコードを出力する。
このモジュールに関しては4-4-2で詳しく解説している。

7-1-3. 実験3

7-1-3-1. ストップウォッチプログラム

ストップウォッチプログラムのstopwatch.vは次の通り。

`timescale 1ns / 1ps

module stopwatch(input wire clk0, start_sw, reset_sw, output wire[1:0] led, output wire[6:0] seg, output wire[3:0] line, output wire dp);

    wire enable_deci, enable_sec1, enable_sec10, enable_sec100, enable_sec1000;
    reg state=1'b0;
    reg[19:0] cc0=20'b0;
    reg[20:0] cc1=21'b0;
    wire[3:0] decisec, sec1, sec10, sec100;
    reg[3:0] ten = 4'b0;
    assign led={ reset_sw|enable_sec1000, start_sw };

    parameter MAX = 24'd1_000_000;

    always@ (posedge clk0 or posedge reset_sw) begin // 0.1秒信号作成
        if(reset_sw==1'b1)
            cc0<=20'b0;
        else if(cc0==MAX-1'b1)
            cc0<=20'b0;
        else 
            cc0<=cc0+state;
    end
    assign enable_deci=(cc0==MAX-1'b1)?1'b1:1'b0; // 0.1秒イネーブル信号生成
    //10進カウンタモジュール接続
    counter10 cntr0( .clk(clk0), .rsw(reset_sw), .e_in(enable_deci), .dat(decisec), . e_out(enable_sec1) );
    counter10 cntr1( .clk(clk0), .rsw(reset_sw), .e_in(enable_sec1), .dat(sec1), . e_out(enable_sec10) );
    counter10 cntr2( .clk(clk0), .rsw(reset_sw), .e_in(enable_sec10), .dat(sec10), . e_out(enable_sec100) );
    counter10 cntr3( .clk(clk0), .rsw(reset_sw), .e_in(enable_sec100), .dat(sec100), . e_out(enable_sec1000) );

    reg[4:0] shift_ff=4'b0;
    reg sw_old=1'b0;
    wire enable_chattering;
    reg no_chattering_sw = 1'b0;

    always@(posedge clk0) begin // チャタリング除去
        if(enable_chattering==1'b1)
            shift_ff<={shift_ff[3:0], start_sw };
        no_chattering_sw = no_chattering_sw?(|shift_ff):(&shift_ff);
    end

    always@( posedge clk0 or posedge reset_sw )begin // スタート/ストップ
        if(reset_sw==1'b1)
            state<=1'b0;
        else begin
            if({sw_old,no_chattering_sw}==2'b01)
                state<=state+1'b1;
            sw_old <= no_chattering_sw;
        end
    end

    wire[6:0] seg_0, seg_1, seg_2, seg_3;
    decoder7seg decoder0( .data(decisec),.code(seg_0) ); //デコーダ接続
    decoder7seg decoder1( .data(sec1),.code(seg_1) );
    decoder7seg decoder2( .data(sec10),.code(seg_2) );
    decoder7seg decoder3( .data(sec100),.code(seg_3) );

    always@( posedge clk0 ) cc1<=cc1+1'b1; // ダイナミック点灯用カウンタ
    assign dp = (cc1[20:19]==2'b01)?1'b1:1'b0; // ATLYS // 小数点制御
    //assign dp = (cc1[20:19]==2'b01)?1'b0:1'b1; // BASYS2
    assign seg = 
    cc1[20:19]==2'b00 ? seg_0: // 00
    cc1[20:19]==2'b01 ? seg_1: // 01
    cc1[20:19]==2'b10 ? seg_2: // 10
    seg_3; // 11

    assign enable_chattering = (cc1[19:0]==20'b0)? 1'b1: 1'b0;
    assign line = 4'b1<<cc1[20:19]; // 4ライン制御


endmodule
//数値→7セグデコーダ
module decoder7seg( input wire [3:0] data, output wire [6:0] code );
    wire[6:0] c =
        data==4'd0 ? 7'b1111110 : //0
        data==4'd1 ? 7'b1000000 : //1
        data==4'd2 ? 7'b0100000 : //2
        (data==4'd3) ? 7'b0010000 : //3
        (data==4'd4) ? 7'b0001000 : //4
        (data==4'd5) ? 7'b0000100 : //5
        (data==4'd6) ? 7'b0000010 : //6
        (data==4'd7) ? 7'b1100011 : //7
        (data==4'd8) ? 7'b0011101 : //8
        7'b1111011; //9
    assign code = c;
endmodule

//10進カウンタ
module counter10(input wire clk, input wire rsw, input wire e_in, output wire[3:0] dat, output wire e_out );
    reg[3:0] tt=4'b0;
    always@( posedge clk or posedge rsw ) begin
        if( rsw==1'b1 )
            tt <= 4'b0;
        else if( e_in==1'b1 )
            tt <= (tt==4'd9)?4'b0:(tt+4'b1);
    end
    assign e_out=(e_in&&tt==4'd9)?1'b1:1'b0;
    assign dat=tt;
endmodule

stopwatch.ucfは次の通り。

NET "clk0"  LOC = L15;

NET "seg(6)"    LOC = V13;  // A
NET "seg(5)"    LOC = V15;  // B
NET "seg(4)"    LOC = N8;   // C
NET "seg(3)"    LOC = V10;  // D
NET "seg(2)"    LOC = T10;  // E
NET "seg(1)"    LOC = V16;  // F
NET "seg(0)"    LOC = V8;   // G
NET "dp"    LOC = T8;   // DP

NET "line(3)"   LOC = U10;
NET "line(2)"   LOC = R8;
NET "line(1)"   LOC = M8;
NET "line(0)"   LOC = U8;

NET "reset_sw"  LOC = P4;
NET "start_sw"  LOC = P3;

NET "led(1)"    LOC = M14;
NET "led(0)"    LOC = U18;
7-1-3-1-1. stopwatchモジュールの入出力

100MHzクロック、スタート/ストップボタンやリセットボタンに対応するスイッチを受け取り、7セグを制御するsegline(7-1-2を参照されたい)、7セグの小数点に対応するdpを出力している。

7-1-3-1-2. stopwatchモジュールの中身

このプログラムは、10秒、1秒、0.1秒、0.01秒を数えて7セグに表示させるストップウォッチの機能を実現している。

まずassign文で2つのLEDの光るタイミングを決めている。
1つ目のLEDはリセットボタンが押されたとき、あるいはカウント溢れが発生した時に光る。
2つ目のLEDはスタートボタンが押されたときに光る。

次にparameter宣言により定数MAXを1000000(=1M)と定めている。
これを100MHz(1秒間が100M周期に相当する)のクロックが数えるのにかかる時間は0.01秒である。
実際、100MHzクロックとMAXを利用して0.01秒に1度パルス出力するクロックenable_deciを作っている。
これをcounter10モジュール(7-1-3-1-9と7-1-3-1-10で解説)に入れると、10倍遅い(0.1秒に1度pulse出力するクロック)enable_sec1が実現する。これを4回やることで、10秒、1秒、0.1秒、0.01秒を数えるストップウォッチの機能が出来上がる。各カウンタ値を7セグで表示可能な形式に変換するのがdecoder7segモジュールであり、これを時分割多重して実際に7セグに表示させる。

7-1-3-1-3. 0.1秒生成

注意:実際には0.01秒を生成している。
vファイル中に「//0.1秒生成」とコメントを付した箇所へ注目されたい。
ここでは100MHzクロックのアップエッジの度に20bitレジスタcc0を「stateにより」インクリメントし、
1M進カウンタを作っている。この周期は0.01秒である。
リセットスイッチがされたら20bitレジスタcc0を0へ戻している。
またcc0が999999のときも0に戻している。

7-1-3-1-4. 0.1秒イネーブル信号生成

注意:実際には0.01秒イネーブル信号を生成している。
vファイル中に「// 0.1秒イネーブル信号生成」とコメントを付した箇所(enable_deci)へ注目されたい。
前述した通りcc0が0.01秒周期のカウンタとなっているため、
ある一定値になるという事象が0.01秒に1度の割合で起こる。
これを利用して、enable_deciは0.01秒に1度だけ1になるクロックの役割を果たしている。

7-1-3-1-5. チャタリング除去

(7-1-8-1で説明しているものと同じである。)

7-1-3-1-6. スタート/ストップ

vファイル中に「// スタート/ストップ」とコメントを付した箇所へ注目されたい。
リセットボタンが押されたとき、1bitレジスタstateを0にしている。

0.1秒生成の見出しに示した通り、stateは0.01秒を数えるためのカウンタcc0をインクリメントするのに使われる。
それが0になるということは、インクリメントが行われなくなることを意味する。
このことにより、リセットボタンを押すとストップウォッチが止まるのである。

ストップボタンが押されたときはstateを切り替えている。

sw_old <= no_chattering_sw;により、sw_oldにはストップボタンが短絡しているときに1が、開放時には0が入る。
したがって、if({sw_old,no_chattering_sw}==2'b01)の文は「ストップボタンが今まで開放されていて、今まさに短絡された」という場合に実行される。
これにより、ユーザがボタンを長押しすることによる誤動作を防ぐことができる。

7-1-3-1-7. ダイナミック点灯

vファイル中に「//ダイナミック点灯用カウンタ」とコメントを付した箇所とassign segの文へ注目されたい。
ここでは時分割多重により7セグ表示を実現している。時分割多重については7-1-11-2-2で詳しく解説している。

7-1-3-1-8. decoder7segモジュール

数値を、7セグの光らせ方の情報に置き換えている。4-4-2で詳しく説明している。

7-1-3-1-9. counter10モジュールの入出力

100MHzクロック、リセットボタン、適切なクロック(enable_decienable_sec1enable_sec10enable_sec100のどれか)をe_inとして受け取り、カウント値と、e_outを出力している。
e_outは、e_inよりも一つ(10倍)周期の遅いクロックを作るものである。

7-1-3-1-10. counter10モジュールの中身

リセットボタンが押されたとき、4bitレジスタttをリセットする。このttは10進カウンタとして使われている。
適切なクロックがパルスを発信しているとき、ttをインクリメントする。
これにより、自分より10倍周期の遅いクロックe_outを作ることができる。

7-1-3-2. 修正ストップウォッチプログラム

カウント値の7セグ上での表現方法を改めるため、decoder7segモジュールを次のように変更した。

module decoder7seg( input wire [3:0] data, output wire [6:0] code );
    wire[6:0] c =
        data==4'd0 ? 7'b1111110 : //0
        data==4'd1 ? 7'b0110000 : //1
        data==4'd2 ? 7'b1101101 : //2
        (data==4'd3) ? 7'b1111001 : //3
        (data==4'd4) ? 7'b0110011 : //4
        (data==4'd5) ? 7'b1011011 : //5
        (data==4'd6) ? 7'b1011111 : //6
        (data==4'd7) ? 7'b1110010 : //7
        (data==4'd8) ? 7'b1111111 : //8
        7'b1111011; //9
    assign code = c;
endmodule

各bitは、左から順に、図4.4.2.1のa,b,c,d,e,f,gを意味している。

7-1-3-3. 第二修正ストップウォッチプログラム

ストップウォッチを10倍遅くするため、「// 0.1秒信号作成」のコメントを付したalways文を次のように書き替えた。

    reg [3:0] ten = 4'b0;
    always@ (posedge clk0 or posedge reset_sw) // 0.1秒信号作成
    begin 
        if(reset_sw==1'b1)
            cc0<=20'b0;
        else if(cc0==MAX-1'b1)
            cc0<=20'b0;
        else begin
            if(ten>=4'd9) begin
                cc0<=cc0+state;
                ten<=4'd0;
            end else
                ten<=ten+state;
        end
    end

これは、もともとcc0がインクリメントされるタイミングで、代わりに10進カウンタtenをインクリメントし、tenのカウント値が9になったときにcc0をインクリメントすることで、cc0インクリメントの機会をちょうど10分の1に減らしたものである(別の言い方をするなら、1M進カウンタcc0とカウントアップ用100MHzクロックの間に10進カウンタを新たに挟み、10M進カウンタを合成した)。こうすることによりcc0の周期が10倍になり、しかも7セグ用の4つのクロックはみなcc0に依存しているから、ストップウォッチ全体も10倍だけ遅くなる。

ちなみにタイムスケールを変更してもストップウォッチの速さは変化しない。その理由はいたって単純で、そもそもタイムスケールを一切利用せずにプログラムを書いているからである。(つまり、#n(nは待ち時間を表す実数)付きの文が存在しない)

7-1-4. 実験4

7-1-4-1. 半加算器プログラム

半加算器プログラムのhalf_adder.vは次の通り。

`timescale 1ns / 1ps

module half_adder(input wire x1, input wire x0, output wire [1:0] y);
    assign y[1] = x1 & x0;
    assign y[0] = (!(x1&x0)) & (x1|x0);
    //assign y[0] = x1 ^ x0; も可
endmodule

または、次のようにしてもよい。

`timescale 1ns / 1ps

module half_adder(input wire x1, input wire x0, output wire [1:0] y);
    assign y = x1 + x0;
endmodule

half_adder.ucfは次の通り。

net "x1" loc=D14;
net "x0" loc=A10;

net "y[1]" loc=M14;
net "y[0]" loc=U18;

vファイルを2通り示したが、前者は半加算器の論理回路を明示して記述し、後者は半加算器の機能を記述している。

数値計算といった、一見ハードウェアで実現を目指すと膨大な手間がかかりそうな、ソフトウェア向きの機能であっても、ハードウェア記述言語を用いれば、あたかもソフトウェアを記述するかのようにハードウェアを設計してしまうことができるのである。(回路図やLUTの真理値表を取り出すことも出来るため、「ソフトウェア向きのメソッドや関数を、ハードウェアの概念であるデジタル回路のモジュールに変換する機能がある」と言ってもよいのかもしれない)

7-1-5. 実験5

7-1-5-1. 全加算器プログラム

全加算器プログラムのfull_adder_by_elements.vは以下の3通りが挙げられる。

1通り目

`timescale 1ns/ 1ps

module full_adder_by_elements(input wire x2, x1, x0, output wire [1:0] y);
    assign y[1] = (x2&x1)|(x1&x0)|(x0&x2);
    assign y[0] = (x2&x1&x0)|(x2&!x1&!x0)|(!x2&x1&!x0)|(!x2&!x1&x0);
    //実現すべき真理値表(表5.6.1と一致する)を基に積和標準形で記述
endmodule

2通り目
大学では試していないが、レポート作成時にplanAheadで回路図と真理値表を確認した。

`timescale 1ns/ 1ps

module full_adder_by_elements(input wire x2, x1, x0, output wire [1:0] y);
    wire [1:0] a,b;//半加算器(half_adderモジュールを呼び出す方法も可能)
    assign a[1] = x2 & x1;
    assign a[0] = x2 ^ x1;
    assign b[1] = a[0] & x0;
    assign b[0] = a[0] ^ x0;
    assign y[1] = a[1] | b1;
    assign y[0] = b[0];
    //全加算器の回路図(図7.1.5.1.1)を基に論理式を記述
endmodule

3通り目(修正全加算器プログラム)

`timescale 1ns/ 1ps

module full_adder_by_elements(input wire x2, x1, x0, output wire [1:0] y);
    assign y = x0 + x1 + x2;
endmodule

full_adder_by_element.ucfは次の通り。

net "x2" loc=C14;
net "x1" loc=D14;
net "x0" loc=A10;
net "y[1]" loc=N12;
net "y[0]" loc=P16;

9.png

図7.1.5.1.1 全加算器の回路図(大学の実験テキストに加筆したもの)

7-1-6. 実験6

7-1-6-1. 2bit加算器プログラム

2bit加算器プログラムのbit2_adder.vは次の通り。
(他にもカルノー図を用いた方法や、真理値表を用いた方法もあるだろう)

`timescale 1ns / 1ps
module bit2_adder(input wire [1:0] x1, input wire [1:0] x0, output wire [2:0] y);
    assign y[2] = ((x1[1]^x0[1])&(x1[0]&x0[0])|(x1[1]&x0[1]));
    assign y[1] = ((x1[1]^x0[1])^(x1[0]&x0[0]));
    assign y[0] = x1[0] ^ x0[0];
    //2bit加算器の回路図(図7.1.6.1.1)を基に論理式を記述
endmodule

2bit以上の加算器は、半加算器(LSB(最下位)のみ)と全加算器(LSB以外)により構成されるから、多少冗長だが次のように書くことも出来る。
(長くなるが、可読性は高くなる)
(大学では試していないが、レポート作成時にplanAheadで回路図と真理値表を確認した。)

`timescale 1ns / 1ps
module bit2_adder(input wire [1:0] x1, input wire [1:0] x0, output wire [2:0] y);
    wire [1:0] 
    sum1,//1の位の加算結果
    sum2;//2の位の加算結果

    half_adder ha1( .x1(x1[0]), .x0(x0[0]), .y(sum1));
    //1の位同士の加算。結果はsum1へ

    full_adder fa2( .x2(sum1[1]), .x1(x1[1]), .x0(x0[1]), .y(sum2));
    //キャリsum1[1]を考慮しつつ2の位同士の加算。結果はsum2へ

    assign y = {sum2, sum1[0]};
endmodule

module full_adder_by_elements(input wire x2, x1, x0, output wire [1:0] y);
    assign y = x0 + x1 + x2;
endmodule

module half_adder(input wire x1, input wire x0, output wire [1:0] y);
    assign y = x1 + x0;
endmodule

bit2_adder.ucfは次の通り。

net "x1[1]" loc=P15;
net "x1[0]" loc=C14;
net "x0[1]" loc=D14;
net "x0[0]" loc=A10;

net "y[2]" loc=N12;
net "y[1]" loc=P16;
net "y[0]" loc=D4;

後者のvファイルでは、今回はbit数の少ない2bit加算器を生成したため、前者との違いが感じにくいが、例えば10bit加算器を考えてみると違いは歴然とする。前者の場合は複雑な回路を読んでその式を立てなければならないが、後者の場合、規則性が高いため、ほとんど同じことを反復するだけでよい。そのため、可読性も高く、さらにコードが書きやすい。次のようになる。

`timescale 1ns / 1ps
module bit2_adder(input wire [15:0] x1, input wire [15:0] x0, output wire [16:0] y);
    wire [1:0] sum1, sum2, sum4, sum8, sum16, sum32, sum64, sum128, sum256, sum512;

    half_adder ha1( .x1(x1[0]), .x0(x0[0]), .y(sum1));
    full_adder fa2( .x2(sum1[1]), .x1(x1[1]), .x0(x0[1]), .y(sum2));
    full_adder fa4( .x2(sum2[1]), .x1(x1[2]), .x0(x0[2]), .y(sum4));
    full_adder fa8( .x2(sum4[1]), .x1(x1[3]), .x0(x0[3]), .y(sum8));
    full_adder fa16( .x2(sum8[1]), .x1(x1[4]), .x0(x0[4]), .y(sum16));
    full_adder fa32( .x2(sum16[1]), .x1(x1[5]), .x0(x0[5]), .y(sum32));
    full_adder fa64( .x2(sum32[1]), .x1(x1[6]), .x0(x0[6]), .y(sum64));
    full_adder fa128( .x2(sum64[1]), .x1(x1[7]), .x0(x0[7]), .y(sum128));
    full_adder fa256( .x2(sum128[1]), .x1(x1[8]), .x0(x0[8]), .y(sum256));
    full_adder fa512( .x2(sum256[1]), .x1(x1[9]), .x0(x0[9]), .y(sum512));

    assign y = {sum512, sum256[0], sum128[0], sum64[0], sum32[0], sum16[0], sum8[0], sum4[0], sum2[0], sum1[0]};
endmodule

module full_adder_by_elements(input wire x2, x1, x0, output wire [1:0] y);
    assign y = x0 + x1 + x2;
endmodule

module half_adder(input wire x1, input wire x0, output wire [1:0] y);
    assign y = x1 + x0;
endmodule

10.png

図7.1.6.1 2bit加算器の回路図(大学の実験テキストから引用)

7-1-7. 実験7

7-1-7-1. 2分周器プログラム

2分周器プログラムのd_ff_s.vは次の通り。

`timescale 1ns / 1ps
module d_ff_s(input clk, input wire a, output reg q);
    reg x;

    always @(negedge clk ) x <= a;
    always @(negedge x )q <= !q;
endmodule

d_ff_s.ucfは次の通り。

net "clk" loc=L15;
net "a" loc=P3;
net "q" loc=U18;
7-1-7-1-1. d_ff_sモジュールの入出力

100MHzクロックとボタンの押下状態を受け取り、LEDを出力する。

7-1-7-1-2. d_ff_sモジュールの中身

100MHzクロックのダウンエッジでボタンの押下状態をレジスタxに記憶している。
これによりxはダウンエッジ型d-ffとして振舞う。
xが1から0に代わるタイミングでLEDの点灯/消灯を司るqの値が切り替わる。
クロック周波数がとてもはやいので、xにはボタンの押下状態がそのまま記憶されると考えてよい。
xが1から0に代わるタイミングとは、ボタンを押した状態から離した瞬間のことである。
この回路はチャタリングの問題を抱えている。

7-1-7-2. 2分周器4直列プログラム

2分周器を4つ直列につなげたプログラムのd_ff.vは次の通り。

`timescale 1ns / 1ps
module d_ff(input clk, input wire a, output reg[3:0] q);
    reg x;

    always @(negedge clk) x <= a;
    always @(negedge x) q[0] <= !q[0];
    always @(negedge q[0]) q[1] <= !q[1];
    always @(negedge q[1]) q[2] <= !q[2];
    always @(negedge q[2]) q[3] <= !q[3];
endmodule

d_ff.ucfは次の通り。

net "clk" loc=L15;

net "a" loc=P3;
net "q(3)" loc=L14;
net "q(2)" loc=N14;
net "q(1)" loc=M14;
net "q(0)" loc=U18;
7-1-7-2-1. d_ffモジュールの入出力

100MHzクロックとボタンの押下状態を受け取り、LEDを出力する。

7-1-7-2-2. d_ffモジュールの中身

d-ffを直列に接続すると、(接続した個数)bitカウンタとして動作する。その理由は7-1-8-1-2で述べている。

7-1-8. 実験8

7-1-8-1. チャタリング防止回路

チャタリング防止回路のff_no_chattering.vは次の通り

`timescale 1ns / 1ps
module ff_no_chattering(input wire clk, input wire a, output reg [3:0] q);
    wire x;
    anti_chatter anti_chatter1( .clk(clk), .sw_in(a), .sw_out(x) );

    always @(negedge x ) q[0] <= !q[0];
    always @(negedge q[0] ) q[1] <= !q[1];
    always @(negedge q[1] ) q[2] <= !q[2];
    always @(negedge q[2] ) q[3] <= !q[3];
endmodule

module anti_chatter(input wire clk, input wire sw_in, output wire sw_out);
    assign sw_out=anti_chatter_sw;
    reg[4:0] shift_ff=0;
    reg[15:0] cc=0;
    reg anti_chatter_sw=0;
    always@(posedge clk)
    begin
        cc <= cc+1'b1;
        if(&cc)
        begin
            shift_ff <= {shift_ff[3:0], sw_in};
            anti_chatter_sw <= anti_chatter_sw?(|shift_ff):(&shift_ff);
        end
    end
endmodule

ff_no_chattering.ucfは次の通り

net "clk" loc=L15;
net "a" loc=P3;

net "q(3)" loc=L14;
net "q(2)" loc=N14;
net "q(1)" loc=M14;
net "q(0)" loc=U18;
7-1-8-1-1. ff_no_chatteringモジュールの入出力

vファイルより、ff_no_chatteringモジュールではclk, aの入力を受け付け、4bitレジスタqを出力していることがわかる。ucfファイルよりclkは100MHzのクロックに対応していることがわかる。
またaはBTND(プッシュボタン(下))に、qはLEDに対応していることがわかる。
よってff_no_chatteringモジュールは100MHzのクロックとプッシュボタン(下)の押下状態を受け取り、LEDに対する出力を行うことがわかる。

7-1-8-1-2. ff_no_chatteringモジュールの中身

ff_no_chatteringモジュールではプッシュボタン(下)が押されて離された瞬間に、LED列がアップカウンタとして動作するようにしている。

anti_chatterのモジュールを呼ぶことでプッシュボタン(下)が押されたとき、チャタリングなしでxの電気の有無を切り替えることができる。

xをOFFにするタイミングでq[0]のON/OFFを切り替え、
q[0]をOFFにするタイミングでq[1]のON/OFFを切り替え、
q[1]をOFFにするタイミングで... をq[3]までやっている。

xをOFFにするタイミングというのは「プッシュボタン(下)」を指から離した瞬間を意味する。
実験中もプッシュボタン(下)を長押しした結果、押している間はLEDの状態が変化せず、離した瞬間に変化した。
また、初期のLEDの状態(ボタンを押した回数)={q(3), q(2), q(1), q(0)}は0回目={0, 0, 0, 0}であった。(0は消灯、1は点灯)
プッシュボタン(下)を1回目に押すと、q[0]を0から1にするので1回目={0, 0, 0, 1}となる。
プッシュボタン(下)を2回目に押すと、q[0]を1から0にするので、q[0]のダウンエッジで2つ目のalways文が起動し、q[1]も切り替わり、2回目={0, 0, 1, 0}となる。
プッシュボタン(下)を3回目に押すと、q[0]を0から1にするので3回目={0, 0, 1, 1}となる。
以降も同様にして、LED4つによるbit列がインクリメントされる。q[1]q[3]が、q[0]に連動して切り替わるのは、bit列をインクリメントした際の繰り上がりを表現するものである。インクリメントにより下位bitが1から0に切り替わるということは、繰り上がりが発生することを意味する。これが、2つ目から4つ目のalways文が、下位bitのダウンエッジにより起動する仕組みになっている理由である。

7-1-8-1-3. anti_chatterモジュールの入出力

モジュール宣言より、anti_chatterモジュールではclk, sw_inの入力を受け付け、sw_outを出力していることがわかる。モジュール呼び出し(anti_chatter1)よりclkは100MHzのクロックに対応していることがわかる。
またsw_inはBTND(プッシュボタン(下))に、sw_outxに対応していることがわかる。
よってanti_chatterモジュールは100MHzのクロックとプッシュボタン(下)の押下状態を受け取り、xに対する出力を行うことに使われていることが分かる。

7-1-8-1-4. anti_chatterモジュールの中身

まず、100MHzクロックのアップエッジごとに16bitレジスタccがインクリメントしている。
またccのインクリメント直後に記述されているif(&cc)ccは、インクリメントされる前のccである(∵verilogでは「コードを処理する順番」の概念がなく、特にalways文中の処理はすべて同時に実行される)
さらに&ccのように単項演算子としての&はリダクション演算子と呼ばれる。ものであり、各bitに対して論理演算を施し、その結果を1bitで評価するものである。
&ccccの各bitに対してand演算をするのだから、ccのすべてのbitが1である場合のみ1となり、それ以外の場合は0となるわけだ。
したがって、if(&cc)内の処理は、ccがインクリメントで111...1から000...0へと戻る瞬間のみ実行される。これは216×10-8秒に1度、つまりおよそ0.0625秒に一度のペースで実行される。
5bitレジスタshift_ffを左に1bitだけシフトし、空いた最下位bitに、その瞬間におけるボタンの状態sw_inを代入している。およそ0.0625×5=0.3125秒間の間にチャタリングと「潜在的な問題」が起こらなければshift_ffは5'b11111か5'b00000のいずれかになるはずだし・・・①、起きた場合はそれ以外の数値となる・・・②。
①の場合、anti_chatter_swでは適切な1bitが確実に代入される。
具体的に言うとshift_ffが5'b11111なら1が、5'b00000なら0が代入される。
②の場合、anti_chatter_swは状態遷移を躊躇い保守的に振舞う。今anti_chatter_swが0だったとするならば、次の状態は&shift_ffで決まり、これはshift_ffが5'b11111でない限り0のままである。②の場合を考えているため、実際にはこのようなことは起こり得ず、確実に0であり続ける。今anti_chatter_swが1である場合についても同様に1であり続ける。

ちなみに、shift_ffに代入されるbitがチャタリングによって5連続で符号誤りを起こす場合、確かに誤動作が起こる。
しかしそのようなことが起こる確率pmissSwは、「その瞬間にチャタリングによる符号誤りが発生する確率pmissFf」を5乗して求められるので、0に十分近いと考える。例えばpmissFf=0.5としても、pmissSwは0.55=0.03125である。

7-1-8-1-5. 潜在的な問題

一見、(一定の確率で起こる誤動作以外は)非の打ち所がないように見えるチャタリング防止回路であるが、例外的に高確率で誤動作する場合がある。それはおよそ0.0625+(0.3125×α×2.5)秒以内に利用者がボタンを押したり離したりした場合である。(αはチャタリングを検知する回数の期待値。2.5はチャタリングが何回目に検知されるかの平均値)
例えば4回までshift_ffに正しいbit(ここでは1としよう)が書き込まれたにも関わらず、4回目と5回目の間のタイミングで利用者がボタンを離し、ここでチャタリングが発生することにより、0となるべき5bit目を1で記録してしまった場合、anti_chatter_swには(比較的)高い確率で誤りが記録されてしまう。

7-1-9. 実験9

7-1-9-1. 単相同期回路

3bitカウンタプログラム(単相同期回路)のoctal_counter.vは次の通り。

`timescale 1ns / 1ps
module octal_counter(input wire clk, input wire a, output reg [2:0] q);
//module octal_counter(input wire clk, input wire a, output reg [2:0] q); 4bitカウンタに変更する場合
    wire x;
    anti_chatter anti_chatter1( .clk(clk), .sw_in(a), .sw_out(x) );
    always @(negedge x) q <= q + 1'b1;
endmodule

module anti_chatter(input wire clk, input wire sw_in, output wire sw_out);
(実験8と同様)
endmodule

octal_counter.ucfは次の通り

net "clk" loc=L15;
net "a" loc=P3;

net "q(3)" loc=L14;
net "q(2)" loc=N14;
net "q(1)" loc=M14;
net "q(0)" loc=U18;

実験7や実験8で示した、d-ffによるカウンタでは、xのダウンエッジでq[0]が1から0へ切り替わり、それによって生じるq[0]のダウンエッジでq[1]が1から0へ切り替わり、それによって生じるq[1]のダウンエッジでq[2]が1から0へと切り替わり...という連動的な動作をしている。この連動が起こるには電気が伝わるのにかかる時間や、素子の反応にかかる時間などの原因で時間差がどうしても生じ、それは累積してしまう。
例えば電流が伝わる速さは光速であるといわれる(電子の移動速度そのものはカタツムリよりも遅いが、導線中に詰まっている無数の電子へ移動する作用が伝わっていくのは高速である。これは電磁波そのものであるから光速と考えるのが筋である)。[14]
光速は30万km/sであるから、100MHzクロック1周期に相当する10-8秒あたりでは3mしか進まない。
例えば、15cm四方の回路を5周するたびに1クロック分の遅れが生じることになる。素子の応答速度を無視してこれなのだから、実際にはさらに深刻な遅れが生じるのは想像に難くない。
そこで、回路内のすべての素子を単一のクロックで制御する必要が出てくる。こうすれば「回路5週分の遅れ」は原理的にあり得ない。このような回路を単相同期回路という。
単相同期回路をverilogで実装するためには、一つのalways文で動作させるなり、全く同じ条件で呼び出される複数のalways文で動作させるなりをする必要がある。

7-1-10. 実験10

7-1-10-1. 10進カウンタプログラム

10進カウンタプログラムのdecimal_counter.vは次の通り。

`timescale 1ns / 1ps

module decimal_counter(input wire clk, input wire a, output reg [3:0] q);
    wire x;
    anti_chatter anti_chatter1( .clk(clk), .sw_in(a), .sw_out(x) );

    always @(negedge x)
    begin
        if(q==4'd9)
            q <= 0;
        else
            q <= q + 1'b1;
    end
endmodule

module anti_chatter(input wire clk, input wire sw_in, output wire sw_out);
(実験8と同様)
endmodule

decimal_counter.ucfは7-1-9-1と同じものでよい。

7-1-10-1-1. decimal_counterモジュールの入出力

100MHzクロック、プッシュボタン(下)の押下状態を受け取り、LED4列へ出力している。

7-1-10-1-2. decimal_counterモジュールの中身

プッシュボタン(下)の押下状態から、チャタリングを除去したxを考える。
そのダウンエッジ(押された状態から、指が離された瞬間)で、レジスタqを用いて10進カウントをしている。

7-1-10-2. 10進カウンタを1秒パルスで動かすプログラム

10進カウンタを1秒パルスで動かすプログラムのauto_decimal_counter.vは次の通り。

`timescale 1ns / 1ps

module auto_decimal_counter(input wire clk, input wire a, output reg [3:0] q);
    reg x;
    reg [26:0] cc=0;
    wire [26:0] chk=(a)?(27'd9999999):(27'd99999999);
    always @(posedge clk)
    begin //1秒生成
        cc <= (cc>=chk)? 1'b0 : cc+1'b1;
        x <= (cc>=chk)? 1'b1 : 1'b0;
    end

    wire [3:0] max=4'd2;
    always @(posedge clk )
    begin //10進カウンタ
        if(x)
        begin
            if(q==max)
                q <= 0;
            else
                q <= q + 1'b1;
        end
    end
endmodule

auto_decimal_counter.ucfは7-1-9-1と同じものでよい。

7-1-10-2-1. auto_decimal_counterモジュールの入出力

7-1-10-1-1に同じ

7-1-10-2-2. auto_decimal_counterモジュールの中身

「1秒生成」とコメントを付した部分では、100MHzクロックのアップエッジでインクリメントするchk=100M進カウンタにより1秒毎にパルスを出すクロックxを作っている。
但しchkはプッシュボタン(下)の押下状態を示すaによって値が異なる。これにより、プッシュボタン(下)が押されているときだけは10M進カウンタとなるから、xは0.1秒毎にパルスを発信することになる。
以上の議論より、プッシュボタン(下)は早回しの機能を提供することが分かる。

また「10進カウンタ」とコメントを付した部分は。7-1-10-1とほとんど同じコードが書かれている。(always文の呼び出すタイミングが異なるが、これも単相同期を実現するための工夫と言えそうだ)

7-1-11. 実験11

7-1-11-1. 60進カウンタプログラム

60進カウンタプログラムのauto_counter_60_ary.vは次の通り。

`timescale 1ns / 1ps
module auto_counter_60_ary(input wire clk, input wire a, output reg [3:0] q0, output reg [2:0] q1);
    reg x0=0, x1=0;

    wire [26:0] chk=(a)?(27'd9999999):(27'd99999999);
    reg [26:0] cc=0;

    always @(posedge clk)//1秒生成
    begin
        cc <= (cc>=chk) ? 1'b0 : cc+1'b1;
        x0 <= (cc>=chk) ? 1'b1 : 1'b0;
    end

    always @(posedge clk)//10進カウンタ
    begin
        if(x0)
        begin
            q0 <= (q0>=4'd9) ? 1'b0 : q0+1'b1;
            x1 <= (q0>=4'd9) ? 1'b1 : 1'b0;
        end
        else
            x1 <= 0;
    end

    always @(posedge clk)//6進カウンタ
        if(x1)
            q1 <= (q1>=3'd5) ? 1'b0 : q1+1'b1;
endmodule

auto_counter_60_ary.ucfは次の通り。

net "clk" loc=L15;
net "a" loc=P3;

net "q1(2)" loc=P16;
net "q1(1)" loc=D4;
net "q1(0)" loc=M13;

net "q0(3)" loc=L14;
net "q0(2)" loc=N14;
net "q0(1)" loc=M14;
net "q0(0)" loc=U18;
7-1-11-1-1. モジュールの入出力

vファイルより、auto_counter_60_aryモジュールではclk, aの入力を受け付け、4bitレジスタq0と3bitレジスタq1を出力していることがわかる。ucfファイルよりclkは100MHzのクロックに対応していることがわかる。
またaはBTND(プッシュボタン(下))に、q0やq1はLEDに対応していることがわかる。
よってauto_counter_60_aryモジュールは100MHzのクロックとプッシュボタン(下)の押下状態を受け取り、LEDに対する出力を行うことがわかる。

7-1-11-1-2. 1秒生成

vファイル中に「//1秒生成」とコメントを付した箇所へ注目されたい。
ここでは「1秒に1度のペースで(プッシュボタン(下)押下なら10倍速)1となる1bitレジスタx0」を構成している。
always @(posedge clk)は100MHzクロックが0から1へと変化したとき(アップエッジ)に文の中身が実行されることを意味する。
文の中身は27bitレジスタccおよび1bitレジスタx0への代入を行うだけのものである。
ccchk以上になると、次のccには0が代入され、chk未満の場合はインクリメントされる。
つまりccはクロックのアップエッジに合わせてカウントするchk進カウンタであるといえる。
chkaつまりプッシュボタン(下)が押されているときは9999999であるし、押されていないときは99999999である。
これをccがカウントする所要時間は、クロックの周波数100MHzを根拠に考えれば、順に0.1秒、1秒である。
このようにしてccは「1周期1秒(プッシュボタン(下)が押されている場合は0.1秒)のカウンタ」になるし、
x0は「1秒(プッシュボタン(下)が押されている場合は0.1秒)に1回だけ1となる1bitレジスタ」となる。

7-1-11-1-3. 10進カウンタ

vファイル中に「//10進カウンタ」とコメントを付した箇所へ注目されたい。
ここではx0に1が入っているときだけ(つまり1秒に1度)q0(4bitのLED列)に10進カウンタの動作をするように定義している。
そして9から0へ戻るタイミングでx1に1を代入している。(それ以外のタイミングでは常に0を代入)
(基本的な)動作はx0に1が入っているときだけなのだから、always @(x1)のほうが完結ではないかという考えもあろうが、それをしてしまうと同期がとれなくなってしまい、時を刻む機能に狂いが出てくる恐れがある。
これを防ぐため、always文は常に単一のクロックに従って呼び出される。(単相同期回路の考え方)

7-1-11-1-4. 6進カウンタ

vファイル中に「//6進カウンタ」とコメントを付した箇所へ注目されたい。
ここではx1に1が入っているときだけ(つまり10秒に1度)q1(3bitのLED列)に6進カウンタの動作をするように定義している。

7-1-11-2. 7セグによる60進カウンタプログラム

60進カウントを7セグで実現するプログラムのauto_counter_60_ary_7seg.vは次のように作ることができる。

  1. auto_counter_60_ary.vと同じコードを書く(モジュール名は「_7seg」を最後尾に追加する)
  2. 下記のコードを追加する。
/* 
モジュール宣言の括弧内に追加
*/
, output wire[7:0] seg, output wire[3:0] line

/*
モジュール内の一番下に追加
*/
    reg[20:0] dd=0;
    always @(posedge clk) 
        dd <= dd+1'b1;
    wire[1:0] nn = dd[20:19];
    wire[6:0] code[0:9];
    assign code[0] = 7'b1111110;
    assign code[1] = 7'b0110000;
    assign code[2] = 7'b1101101;
    assign code[3] = 7'b1111001;
    assign code[4] = 7'b0110011;
    assign code[5] = 7'b1011011;
    assign code[6] = 7'b1011111;
    assign code[7] = 7'b1110000;
    assign code[8] = 7'b1111111;
    assign code[9] = 7'b1111011;    

    assign seg = 
    (
        nn == 2'b00 ? {code[{1'b0, cc[23:21]}], 1'b0 } :
        nn == 2'b01 ? {code[{1'b0, cc[26:24]}], 1'b0 } :
        nn == 2'b10 ? {code[q0], q0[0] } :
      /*nn == 2'b11*/ {code[{2'b0, q1}], 1'b0}
    );

    assign line = (4'b0001 << nn);

auto_counter_60_ary_7seg.ucfはauto_counter_60_ary_7seg.ucfに次のコードを追加して作ることができる。

NET "seg(7)" LOC = V13; // A
NET "seg(6)" LOC = V15; // B
NET "seg(5)" LOC = N8; // C
NET "seg(4)" LOC = V10; // D
NET "seg(3)" LOC = T10; // E
NET "seg(2)" LOC = V16; // F
NET "seg(1)" LOC = V8; // G
NET "seg(0)" LOC = T8; // 小数点

NET "line(3)" LOC = U10;
NET "line(2)" LOC = R8;
NET "line(1)" LOC = M8;
NET "line(0)" LOC = U8;
7-1-11-2-1. モジュールの入出力

今回追加した8bitワイヤsegと4bitワイヤline。ucfファイルと照らし合わせるとVHDCコネクタへ送出していることがわかる。
実際は7セグのどこを光らせるかに関わっている。(7セグへのつなぎ方はエンジニアの配線次第なわけだから、ATLYS公式ドキュメントではVHDCコネクタへ送出した先のことは説明していない)
lineにより何桁目の7セグを制御するかを決定し、segにより7セグのどこを点灯するかを決定する。

7-1-11-2-2. 7セグ表示

今回モジュールの一番下に追加したコードに注目されたい。
codeは各数字を7セグでどう表現するかを指示していて、実際にそれらの内どれをどこにいつ表示するかはseglineが時分割多重を行いながら決めている。
7bitワイヤcode[0]code[9]を定義することで、数字0~9に対応する7セグの光らせ方(点灯/消灯)を記述している。
例えば「8」に対応する数字はすべてのセグメントを点灯させる必要があるから当然7'b1111111となる。
但し、7-1-11-2-1で述べた通り、7セグを制御しているのはsegであるから、この変数にcodeを適切な方法で代入する必要がある。それを行っているのがassign seg =...の文である。
この文ではnnの値によって代入される内容が異なるという仕様になっている。
そしてnnは21bitレジスタddの一部の2bitを抽出した内容の入るワイヤとなっていてddは100MHzクロックのアップエッジ毎にインクリメントされている。2^21≒2Mであることより、ddの周期はおよそ0.02秒である。
nnddの最上位2bitから値をコピーしているため、こちらも周期はおよそ0.02秒である。これを状態数4で割れば、nnの値が更新される頻度がおよそ0.005秒であると求められる。
assign line =...の文もみてみよう。
1がnnbitだけ左にシフトされている。
このことからlineのbit列はおよそ0.005秒毎に「0001」→「0010」→「0100」→「1000」→(「0001」へ戻る)を繰り返しているとわかる。これはつまり、「何桁目のセグを光らせるか」をおよそ0.005秒毎に切り替えているのである。
1秒におよそ200回というハイペースで切り替えているわけだから、人間が実際に7セグを見てみると、あたかも4桁が同時に表示されているように見えるわけである。これは時分割多重である。
以下、segに代入する内容をnn毎にみていく。ちなみにコード中の中括弧は変数のbit列を繋げる意味がある。

nn==2'b00のとき(line4'b0001)

百分の一秒を再現するものであると考えられるが大学側のミス(仕様かもしれない)で五十分の一秒を再現している。またいずれにせよ、コードを簡単にする都合上、不正確な表示となっている。(厳密にいえば、8や9の表示され得ない8進数でカウントしている)

ccはクロックのアップエッジの度に、つまり10-8秒に1度インクリメントする。
従ってcc[m:n](m>=n)は2n×10-8秒間だけ同じ値であり続け、周期はおよそ2m+1×10-8秒である。状態数は2m+1-nである。
m=23, n=21を代入すると、cc[23:21]は221×10-8秒、つまり221-20×10-8+6=およそ0.02秒間だけ同じ値であり続け、周期は224×10-8秒、つまり224-20×10-8+6=およそ0.16秒であるとわかる。状態数は8(0から7)である。
同じ値であり続ける時間がおよそ0.02秒であることから分かるが、100分の1秒ではなく50分の1秒を数えている。
100分の1秒を数えるためには{code[{1'b0, cc[22:20]}], 1'b0}に修正する必要がある。

nn==2'b01のとき(line4'b0010)

同様にコードを吟味すると、cc[26:24]は224×10-8秒、つまり224-20×10-8+6=およそ0.16秒間だけ同じ値であり続け、周期は227×10-8秒、つまり227-20×10-8+6=およそ1.28秒であるとわかる。状態数は8である。

nn==2'b10のとき(line4'b0100)

10進カウンタq0に対応するコードを代入している。またq0[0]は小数点が1秒ごとに点滅する仕様を実現している。

nn==2'b11のとき(line4'b1000)

6進カウンタq1に対応するコードを代入している。

7-1-11-3. 7セグによる24進カウンタプログラム

7-1-11-2の60進プログラムを24進プログラムに改造する。使用する入出力環境に変わりはないのだから明らかにucfファイルは7-1-11-2と同じでよい。vファイルの方は、23の時、次が0に戻るよう改造すればよい。(6進カウンタを3進カウンタへ変えるのは、やってもいいが無駄である)
以下24進カウンタプログラムのauto_counter_24_ary.vの作り方を述べる。

  1. auto_counter_60_ary.vと同じコードを書く。モジュール名は60を24に置き換える。
  2. モジュール内の「//10進カウンタ」や「//6進カウンタ」のコメントがついたalways文を以下のコードのように書き加える。
    always @(posedge clk)//10進カウンタ
    begin
        if(x0)
        begin
            if(q1>=3'd2 && q0>=4'd3)//23(かそれ以上)なら
                q0 <= 1'b0;
            else
                q0 <= (q0>=4'd9) ? 1'b0 : q0+1'b1;
            x1 <= (q0>=4'd9) ? 1'b1 : 1'b0;
        end
        else
            x1 <= 0;
    end
    always @(posedge clk)//6進カウンタ
    begin
        if(x0)
            if(q1>=3'd2 && q0>=4'd3)//23(かそれ以上)なら☆
                q1 <= 1'b0;
            else
                q1 <= (q1>=3'd5) ? 1'b0 : q1+1'b1;
              //q1 <= (q1>=3'd3) ? 1'b0 : q1+1'b1;としてもよいが、無意味
    end

☆について、10進カウンタの方でq0を0にしているからそれが原因で誤作動するのではないかという懸念の声もあるかもしれないが。verilogではalways文は常に同時に実行されるため、「q0が0に変更された後に、6進カウンタがq0を評価する」ということにはならない。

7-1-11-4. 24進カウンタと60進カウンタによる24時間時計

7-1-11-2や7-1-11-3で作成したカウンタを利用して24時間時計を作る課題。
以前に作成したコードを別のプログラムで再利用するためには、モジュール化してしまうのが便利である。

次のようにしてclock24.vを作る。(clock24.ucfファイルは7-1-11-2と同じものから、q0やq1に関係する行を消去すればよい)

  1. `timescale 1ns / 1ps(タイムスケール宣言)だけ取り除き、7-1-11-2と7-1-11-3のvファイルをそれぞれコピーし、clock24.vへ貼り付ける。
  2. 先頭に、以下のように追記する。

`timescale 1ns / 1ps
//どうせ1分に1度しか動かないので、単相同期は崩してしまった。

module clock24(input wire clk, input wire a, output reg [7:0] seg, output wire [3:0] line);
//alwaysするのでsegをwireからregへ変更
    reg [5:0] dummy=0, dummy2=0;

    reg clk_h=0, clk_m=0;
    wire [15:0] sa32, sa10;


    always @(posedge clk)//クロックを60進で数える。60倍周期の疑似クロックclk_mを実現する
    begin
        dummy <= (dummy>=6'd59) ? 1'b0 : dummy+1'b1;
        clk_m <= (dummy>=6'd59) ? 1'b1 : 1'b0;
    end

    always @(posedge clk_m)//clk_mを60進で数える。clk_mの60倍(clkの3600倍)周期の疑似クロックclk_hを実現する
    begin
        dummy2 <= (dummy2>=6'd59) ? 1'b0 : dummy2+1'b1;
        clk_h <= (dummy2>=6'd59) ? 1'b1 : 1'b0;
    end

    auto_counter_60_ary minute( .clk(clk_m), .a(a), .seg(sa10), .line(line) );//分を数える
    auto_counter_24_ary hour( .clk(clk_h), .a(a), .seg(sa32));//時を数える

    always @(posedge clk)
     begin
        if(line[3:2]!=0)//時
              if(line[3])
                    seg <= sa32[1];
                else
                    seg <= sa32[0];
          else//分
              if(line[1])
                    seg <= sa10[1];
                else
                    seg <= sa10[0];
    end


endmodule

3.7-1-11-2と7-1-11-3から来たコードについて、次のように変更する

  • segについて、output wire[15:0] segとする(モジュール宣言)
  • segへの代入を行うassign文について、次のように改める。
    assign seg[0] = {code[q0], q0[0] };
    assign seg[1] = {code[{2'b0, q1}], 1'b0};

以上のようにすることで、24時間時計が構成される。
追加・改編したコードの意味はコメントに付した通りであるが、軽く説明する。
まずはalways @(posedge clk)文の中身について。
レジスタdummyclkをカウントする60進カウンタとして用い、59になったときだけclk_mを1にする。
これにより、「clkより60倍遅いクロック」clk_mが実現する。
同様の要領でalways @(posedge clk_m)文では「clk_mより60倍遅いクロック」clk_hを作っている。
これにより、「clkを使えば1秒を数えるはずのカウンタ」はclk_mを使うと60秒即ち1分を数えるし、
clk_hを使うと1時間を数えるようになる。
あとは、segの出力方法を修正し、時分割多重によらずいつでも10秒の位、1秒の位2桁を上位モジュールclock24に対して出力するようにした。これにより、clock24モジュールではいつでも容易に24進カウンタと60進カウンタの保持する値にアクセスできるようにした。これを時分割多重することで、時間と分を表示する24時間時計が実現する。

7-2. 特殊相対性理論の実証(時間の遅れの観測)

FPGAボードで、時間の遅れを観測できるかもしれない。

特殊相対性理論によれば、運動している物体に流れる「時」は、静止している物体に流れる「時」よりも緩やかに経過する。
しかし私たちがそれを目で見てわかるように観測するためには光速に近い速度で運動する物体を見つけるなり、強い重力場を作り出すなどしなければならず、ほとんど不可能と言えるだろう。
時間の遅れに関する式は次の通りである[15]
Δt運動する物体/Δt静止している物体=√(1-(v運動する物体の速度/光速)2)
式から分かる通り、日常的に私たちが観測しているあらゆる物体の速度では、光の速度と比べてあまりにも遅いため、時間の遅れる割合が1に限りなく近いのである。
しかし、日常とはかけ離れた高周波といえよう100MHzクロックでは、時間の遅れ(誤差アと呼ぶことにする)を観測できるかもしれない。
ATLYSボードを3台用意し、それぞれ運動ボード、静止ボード1,2と名前を付けよう。
静止ボード1は固定し、運動ボード(※)は静止ボード1を中心に等速円運動をさせる。これはモータで容易に実現するだろう。
(※運動エネルギー節約のため、質量は軽いほうが良い。それならば、運動ボードを運動させると述べたが実際には水晶発振器だけを取り出して運動させた方がよいだろう。)
そして3台それぞれのクロックによる100M進カウンタのカウンタ値の差分を調べ、7セグ等で表示しよう。
水晶発振器の精度は最大でも10-9と言われている[16]から、時間の遅れと関係なく10秒に1カウントずつ以上の期待値で狂いが出る(誤差イ)。カウント値の誤差のア、イ成分比を統計的手法で検証するため、3台用意する必要があるのである。
水晶発振器は36個でおよそ18gである[17]ため、1つで0.5gと考えてよいだろう。
地球上(g=9.8m/s/s)では102gの質量の物体に働く力が1Nであるため、
0.5gの水晶発振器の重力は0.005Nといえる。
次に、回転半径を例えば10cm=0.1mとして、この回転半径を実現するための「腕」の重量を考えよう。
縦10cm、横1cm、高さ1cmの直方体状の段ボールの箱を作って、ここに水晶発振器を閉じ込めれば、遠心力により回転半径は10cmで維持される。直方体の表面積は10×4+1×2=42cm2≒0.00420m2である。
比較的軽量な段ボールは1m2当たり180gである[18]から、円柱の質量は0.756g、重力は0.00741Nとなる。
したがって、全体の重力は0.012Nであり、半径が0.01mであることからモータの負荷トルクは0.00012N・mつまり0.12mN・mとわかる。
これはマブチDCモーター規格表[19]にあるどのトルクよりも小さいため、モータの回転数は、無負荷時~適正負荷時の間にあると考えてよい。例えばFA130RAというモータを3Vで駆動すれば、1分間で13200~16400回転、つまり毎秒220~273回転することが期待できる。議論を簡単にするため、以下では毎秒250回転するとみなそう。
このとき水晶発振器の速さは250×2π×0.1m/s=157m/sとなる。
時間の遅れの式に代入し、wolfram alphaで解くと
0.99999999999986306111111110173498146604809875927370957103...
と求められる。これを100M倍すると
99999999.9999863...である。
これにより、静止ボードで100M回カウント(=1秒経過)するたびに、運動ボードではおよそ0.0000137カウントだけ遅れが生じる。1カウントの遅れがでる時間の期待値は有効数字3桁で73000秒となる。
1日の長さが86400秒であることも考えれば、20時間少々の観測で1カウントの遅れが生じるといえ、
1週間当たりに、誤差ア(特殊相対性理論に起因するカウント遅れ)が8.29カウントだけ期待される。
東京電機大学3ECの実験科目のVerilogの実験は2週間かけて行われるから、1週目の実験でこの装置を組み立て、2週目の実験で結果を観測することもできるのかもしれない。
一方前述の通り、誤差イは10秒=109カウントに1カウントの期待値で発生するが、遅れと進みが同様に確からしく発生する。
誤差イによって1週間=6.048×1013カウントの間でnカウント進む(nは負も可)確率を求めよう。

この問題を統計学的に扱うと、次のようになる。

0から始まるカウンタ(カウンタ値には上限も下限もない)を考える。
カウンタは確率pで1進み、
確率pで1戻り、
確率1-2pで変化しない。
このようなカウンタの試行をN回繰り返した時、
カウンタ値がxとなったとする。
確率変数Xの累積分布関数を求めよ。
但し、p=10-9、N=6.048×1013

カウンタ値がxになることは、x+over回進み、over回戻り、stay回そのままであったことと捉えることができる。
但しstay=N-(x+2×over)である。
そのようなことが起こる確率は反復試行の公式と和の法則より
P(x)=Σover=[0,N-x][NCx+over×N-x-overCover×px+over×pover×(1-2p)stay]
である。これをもとにして[20]を参考にしながら、確率密度関数を求めよう。
実数全体(k×P(x))dx=1を満たすようにkを採れば、k×P(x)が確率密度関数となり、それを積分したものが累積分布関数となる。(累積分布関数は、確率変数を横軸とし、確率変数がある値以下になる確率を縦軸としたグラフである)

8. 参考

[1]https://ja.wikipedia.org/wiki/Verilog
[2]http://bluefish.orz.hm/sdoc/verilog.html

[3]https://www.mtl.t.u-tokyo.ac.jp/~jikken/cpu/wiki/Verilog%20%E3%81%AE%E6%96%87%E6%B3%95%E8%A7%A3%E8%AA%AC/
[4]https://qiita.com/thtitech/items/8cc898dda7a10780f495
[5]https://www.macnica.co.jp/business/semiconductor/articles/intel/110605/
[6]http://zakii.la.coocan.jp/hdl/52_timescale.htm
[7]https://qiita.com/rikitoro@github/items/ab3a734b4e19df0ad19f
[8]https://japan.xilinx.com/support/download.html
[9]https://store.digilentinc.com/digilent-adept-2-download-only/
[10]https://qiita.com/delicious-locomoco/items/b83c9078d8b553c03778
[11]https://reference.digilentinc.com/_media/atlys:atlys:atlys_rm.pdf ATLYSの公式ドキュメント
[12]http://zakii.la.coocan.jp/hdl/14_equation.htm
[13]https://stackoverflow.com/questions/3224553/does-verilog-support-short-circuit-evaluation
[14]https://hegtel.com/denryu-speed.html
[15]https://www.mk-mode.com/blog/2013/07/08/time-dilation-on-special-relativity/
[16]https://toragi.cqpub.co.jp/Portals/0/backnumber/2010/12/p147.pdf
[17]https://www.amazon.co.jp/Newone-%E5%90%88%E8%A8%8836%E5%80%8B%EF%BC%8812%E7%A8%AE%E9%A1%9E-%E3%82%AF%E3%83%AA%E3%82%B9%E3%82%BF%E3%83%AB%E7%99%BA%E6%8C%AF%E5%99%A8-%E6%B0%B4%E6%99%B6%E6%8C%AF%E5%8B%95%E5%AD%90HC-49S-%E3%83%91%E3%83%83%E3%82%B7%E3%83%96%E7%B5%90%E6%99%B6/dp/B072LRQXJ4
[18]https://www.taiyoushiki.com/products/material/
[19]http://www.picfun.com/motor01.html
[20]http://konamih.sakura.ne.jp/Stats/Text/Statistics.pdf
[全体において参考とした]http://www.icrus.org/machida/product/verilog.pdf (大学の教材)

5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2