Quartusでのpost-synthesis simulationの方法(GUI編)

QuartusのGUIモードでのpost-synthesis simulationの方法を紹介します。

ここで記載している手順は、基本的にここに載っています。
Quartus® Prime Standard Edition Handbook Volume 3: Verification
https://www.altera.com/en_US/pdfs/literature/hb/qts/qts-qps-5v3.pdf

事前インストール

下記の事前インストールが必要です。

  • Quartus Prime (Lite EditionでOK)
    • なんらかのデバイスファイル (e.g. MAX 10)
  • Modelsim-Altera (Starter EditionでOK)

デザインとテストベンチ

ここでは実験用として以下のファイルを用意します。counterディレクトリに置きます。

  • counter.v
module counter(
  input clk,
  input rst_n,
  output reg [3:0] count
);

always@( posedge clk )
  if( !rst_n )
    count <= 4'd0;
  else
    count <= count + 4'd1;

endmodule
  • test_counter.v
`timescale 1ns/1ps
module test_counter;

reg clk;
reg rst_n;
wire [3:0] count;

initial begin
  forever begin
    clk = 1;
    #5;
    clk = 0;
    #5;
  end
end

initial begin
  rst_n = 0;
  #16;
  rst_n = 1;
  @( posedge clk );
  $display( "cycle #0 %d %s", count, (count == 4'd0) ? "OK" : "NG" );
  @( posedge clk );
  $display( "cycle #1 %d %s", count, (count == 4'd1) ? "OK" : "NG" );
  @( posedge clk );
  $display( "cycle #2 %d %s", count, (count == 4'd2) ? "OK" : "NG" );
  @( posedge clk );
  $display( "cycle #3 %d %s", count, (count == 4'd3) ? "OK" : "NG" );
  @( posedge clk );
  $display( "cycle #4 %d %s", count, (count == 4'd4) ? "OK" : "NG" );
  @( posedge clk );
  $display( "cycle #5 %d %s", count, (count == 4'd5) ? "OK" : "NG" );
  $finish;
end

counter dut( .clk(clk), .rst_n(rst_n), .count(count) );

endmodule

手順

プロジェクト作成

f:id:kenjiwn:20170809235258p:plain
Quartusを起動し、File->New...->New Quartus Prime Project

f:id:kenjiwn:20170809234741p:plain
Nextをクリック

f:id:kenjiwn:20170809235409p:plain
counter.v, test_counter.vのある場所を指定する。Nextをクリック

f:id:kenjiwn:20170809234927p:plain
Nextをクリック

f:id:kenjiwn:20170809235007p:plain
Nextをクリック

f:id:kenjiwn:20170809235043p:plain
適当にデバイスを選択する。Nextをクリック

f:id:kenjiwn:20170809235507p:plain
SimulationでModelsim-Alteraを選択する。Finishをクリック

下準備

  • Tool->Options->General->EDA Tool OptionsでModelsim-Alteraのインストール場所を指定する

f:id:kenjiwn:20170809235636p:plain

  • Project->Add/Remove Files in Projectでcounter.vを追加

f:id:kenjiwn:20170809235841p:plain

  • 同ウィンドウ、EDA Tool Settings->Simulation->NativeLink settings で Compile testbench: を選択し、Test Benches...をクリック

f:id:kenjiwn:20170810000018p:plain

  • New...をクリック。test_counter.vを追加する。OKをクリック。

f:id:kenjiwn:20170810000126p:plain

  • Post-Synthesis Simulationで機能検証のみ行いたい(遅延モデル無しのシミュレーションをしたい)場合、同画面「More EDA Netlist Writer Settings...」でGenerate functional simulation netlistをOnにする

f:id:kenjiwn:20170810000315p:plain

合成

  • EDA Netlist Writerをダブルクリック

Modelsimの起動

  • Tools->Run Simulation Tools->Gate Level Simulation...をクリック

自動的にシミュレーションが走る。
f:id:kenjiwn:20170810000547p:plain
f:id:kenjiwn:20170810000553p:plain

Verilog HDLにおける演算記述の罠

最近はまった乗算の罠について

ビット幅の罠

まずは以下のVerilog記述を見ていただきたい。a*b と c、いずれもaとbの乗算を行っている。どのように出力されるでしょうか?

module test1;
reg [1:0] a = 2'd2;
reg [1:0] b = 2'd3;
reg [3:0] c;
initial begin
  c = a * b;
  $display( "a*b = %d", a*b );
  $display( "c = %d", c );
  $finish;
end
endmodule

結果は以下の通り。

a*b = 2
c =  6

a*b と直接記述した方はビット幅が削られているように見える。なぜなのか?

ビット幅の規則

SystemVerilogのLRMは以下からダウンロードできる。

IEEE Standard Association - IEEE Get Program

これの 11.6.1 Rules for expression bit lengths によると、式のビット幅は2種類あるとされる。

  • self-determined expression: 式そのものによってビット幅が決定される
  • context-determined expression: 文全体によってビット幅が決定される

前述の例では、「a*b」がself-determined expressions、「c = a*b;」 が context-determined expressionとなっている。self-determined expressionsでは、Table 11-21 Bit lengths resulting from self-determined expressionsによりビット幅が決定される。算術演算の場合、オペランドの大きい方のビット幅が演算結果のビット幅となる。代入文では、context-determined expressionとなるので演算結果のビット幅は左辺のビット幅となる。

したがって、a*bではビット幅2の演算と見なされ、a*bの結果6 "0110"のうち下位2ビットの"10"が演算結果となってしまう。

ビット幅の規則に関する注意点

11.6.2 Example of expression bit-length problem に興味深いことが記されている。以下の記述の場合、answerは期待通りに計算されるであろうか?

wire [15:0] a, b, answer;
assign answer = (a + b) >> 1;

何を期待通りとするかは判断の分かれるところだが、a+bの演算結果で桁上げが発生し一時的に17ビットとなってしまう場合、この演算結果は桁上げが捨てられてしまう。つまり、演算結果の最上位ビット answer[15]は常に0となる。なぜかというと、この文はcontext-determined expressionであるから、中間の演算結果は左辺answerの16ビットとなる。したがって、a+bが桁上げによって17ビットとなってしまった場合、最上位ビットは捨てられてしまう。これを防ぐためには以下のようにself-determined expressionsにより17ビットの演算となるようにしなければならない。

assign answer = ({1'b0, a} + b) >> 1;

上記の実証コードを以下に記載する。

module test3;
reg [2:0] a = 7;
reg [2:0] b = 1;
reg [2:0] c;
initial begin
  c = (a+b) >> 1;
  $display( "c = %d", c );
  c = ({1'b0,a}+b) >> 1;
  $display( "c = %d", c );
  $finish;
end
endmodule

実行結果は以下の通り。

c = 0
c = 4

また、連結演算 {} の中ではself-determined expressionsとなってしまう。したがって、以下の例はself-determined expressionsとなる。

module test1_2;
reg [1:0] a = 2'd2;
reg [1:0] b = 2'd3;
reg [3:0] c;
initial begin
  c = {a * b};
  $display( "c = %d", c );
  $finish;
end
endmodule

実行結果

c =  2

符号付き演算の罠

符号付きの数と符号無しの数を乗算したいとする。以下のような記述は期待通りの結果が得られるであろうか?

module test2;
reg [2:0] a = -2;
reg [2:0] b = 3;
reg [5:0] c;
initial begin
  c = $signed(a) * b;
  $display( "c = %d", $signed(c) );
  $finish;
end
endmodule

実行結果は以下のようになる。

c =  18

符号付き演算の規則

11.8.1 Rules for expression typesの最後の2つを見ると以下のような記述がある。

  • オペランドの中に符号無し数が含まれる場合、演算結果も符号無しになる
  • オペランド全てが符号付き数の場合、演算結果も符号付きになる(例外はあるが)

つまり、前述の乗算は符号無しオペランドが含まれるため、符号無しの乗算となってしまっていた。したがって、"110" * "011" = "010010" となり、18という結果となった。

これを回避する一つの方法として、符号拡張がある。

module test2_1;
reg [2:0] a = -2;
reg [2:0] b = 3;
reg [5:0] c;
initial begin
  c = $signed({ {3{a[2]}}, a} ) * { {3{b[2]}}, b };
  $display( "c = %d", $signed(c) );
  $finish;
end
endmodule

実行結果は以下のようになり、一見良さそうに見える。

c =  -6

しかし、この記法では、シミュレーションでは良くても論理合成まで含めて考えると良くない。論理合成の処理系によっては、この記述は6ビット同士の符号無し乗算と認識される。したがって、たとえばターゲットデバイスFPGADSPブロックが18ビット同士の乗算をサポートしており、a,bのビット幅が16、cのビット幅が32の場合、上記の記述ではDSPブロックを2つ以上消費してしまうことになってしまう。これは良くない設計である。

ベストな記述は以下のように符号付き数同士の乗算としてしまうことである。

module test2_2;
reg [2:0] a = -2;
reg [2:0] b = 3;
reg [5:0] c;
initial begin
  c = $signed( a ) * $signed( { 1'b0, b } );
  $display( "c = %d", $signed(c) );
  $finish;
end
endmodule

実行結果は以下のようになる。

c =  -6

このように記述すれば、論理合成処理系も乗算のビット幅を誤って認識することはない。

まとめ

演算のビット幅、符号付き演算のVerilog HDLによるRTL記述には落とし穴がある。筆者のおすすめとしては、落とし穴にはまらないようにするには以下のような方針で記述すると良さそうだ。

  • 乗算以外の場合、右辺のビット幅は桁上げに気をつけて基本的に左辺のビット幅にそろえてから演算する。
  • 乗算の場合、右辺は最小限のビット幅とする(合成処理系でのロスを防ぐため)。演算結果が符号付きの場合は両オペランドをsignedに変換してから演算する。

C++のコンストラクタ、デストラクタの呼び出し順序の研究(その1)

C++のコンストラクタ、デストラクタの呼び出し順序は非常に複雑なので、改めて復習・研究を行う。

コンパイラが自動生成する可能性がある特殊メンバ関数

C++のクラスのコンストラクタ等の関数は特にプログラマが記述しなくてもコンパイラによって自動生成される。自動生成される可能性がある特殊メンバ関数は以下の通りである。

名前 シグネチャ
コンストラクタ T()
デストラクタ ~T()
コピーコンストラクタ T(const T&)
コピー代入 T& operator=(const T&)
ムーブコンストラクタ(C++11以降) T(T&&)
ムーブ代入(C++11以降) T& operator=(T&&)

ここではコンストラクタ等がどのような呼び出し順序で行われるかを研究する。

例題

特殊関数が呼ばれた時に出力するクラス

コード(a.h)

#ifndef A_H_
#define A_H_
#include <iostream>
using namespace std;
class A{
public:
  A(){ cout << "A(" << this << ") ctor" << endl; }
  ~A(){ cout << "A(" << this << ") dtor" << endl; }
  A(const A&){ cout << "A(" << this << ") copy ctor" << endl; }
  A& operator=(const A&){ cout << "A(" << this << ") copy asgn" << endl; }
  A(A&&){ cout << "A(" << this << ") move ctor" << endl; }
  A& operator=(A&&){ cout << "A(" << this << ") move asgn" << endl; }
};
#endif

上記のクラスを作れば、どのオブジェクトのどの特殊関数がどのタイミングで呼ばれたかをトレースすることができる。
なお、コンパイル環境はMSYS2 gcc 5.3.0を使い、コマンドラインオプションは "g++ -std=c++11 -O0 " を使用する。

例1:コンストラクタ、デストラクタ

コード

#include "a.h"
int main() {
  A a; // ctor
}      // dtor

実行結果

A(0xffffcc0f) ctor
A(0xffffcc0f) dtor

例2:コピーコンストラクタ

コード

#include "a.h"
int main() {
  A a1;     // a1 ctor
  A a2(a1); // a2 copy
}           // a2/a1 dtor

実行結果

A(0xffffcbff) ctor
A(0xffffcbfe) copy ctor
A(0xffffcbfe) dtor
A(0xffffcbff) dtor

期待通り、"A a2(a1);"の行でコピーコンストラクタが呼ばれている。
なお、コンストラクタが呼ばれる順序はオブジェクトの宣言順で、デストラクタが呼ばれる順序はコンストラクタの順序の逆順のようだ。

例3:変数宣言と同時に代入を行う。

コード

#include "a.h"
int main() {
  A a1;       // a1 ctor
  A a2 = a1;  // a2 copy!!
}             // a2/a1 dtor

実行結果

A(0xffffcbff) ctor
A(0xffffcbfe) copy ctor
A(0xffffcbfe) dtor
A(0xffffcbff) dtor

驚くべきことに、コピー代入ではなく、コピーコンストラクタが呼ばれた。
変数宣言時に代入演算子を使って初期化する場合はコピーコンストラクタが呼ばれるらしい。

例4:変数宣言後に代入を行う。

コード

#include "a.h"
int main() {
  A a1;  // a1 ctor
  A a2;  // a2 ctor
  a2=a1; // a2 asgn
}        // a2/a1 dtor

実行結果

A(0xffffcbff) ctor
A(0xffffcbfe) ctor
A(0xffffcbfe) copy asgn
A(0xffffcbfe) dtor
A(0xffffcbff) dtor

期待通り、コピー代入が呼ばれた。

例5:関数の戻り値を使って初期化する

コード

#include "a.h"
A funcA() {
  cout << "funcA 1" << endl;
  A ret;
  cout << "funcA 2" << endl;
  return ret;
}
int main() {
  A a( funcA() ); // a ctor!!!
} // a dtor

実行結果

funcA 1
A(0xffffcc0f) ctor
funcA 2
A(0xffffcc0f) dtor

奇妙な実行結果となった。出力から考えると、funcA関数の中でmain関数内のローカル変数aのコンストラクタが呼ばれたと考えるのが妥当であると言える。

調べたところ、これはNRVO (Named Return Value Optimization)というコンパイラの最適化が働いた結果で、C++の言語仕様として規定されているらしい。
gccコンパイルオプションに-O0をつけていてもこの最適化機構は働くらしい。

例6:オブジェクト生成後に関数の戻り値を代入

コード

#include "a.h"
A funcA() {
  cout << "funcA 1" << endl;
  A ret;
  cout << "funcA 2" << endl;
  return ret;
}
int main() {
  A a; // a ctor
  a = funcA(); // ret ctor, a move, ret dtor
} // a dtor

実行結果

A(0xffffcbfe) ctor
funcA 1
A(0xffffcbff) ctor
funcA 2
A(0xffffcbfe) move asgn
A(0xffffcbff) dtor
A(0xffffcbfe) dtor

まず、"A a;" によってaのコンストラクタが呼ばれ、funcA関数内でretのコンストラクタが呼ばれる。
次に代入によってaのムーブ代入が呼ばれる。ムーブ代入後、retのデストラクタが呼ばれる。
最後に、aのデストラクタが呼ばれる。

つまりこういうことだと考えられる。
プログラマがムーブ演算やデストラクタを記述する際は、デストラクタ内ではムーブ演算によって所有権が他オブジェクトに移されたメンバ変数がある可能性も考慮しながら破棄しなければならない。
たとえば、ムーブ後はリソースを指すポインタはnullptrにしておき、デストラクタ内で誤って破棄されることがないようにする必要があるということだろう。

MNISTのデータファイルをC言語で読み込む

動機

私は現在「ゼロから作るDeep Learning」を読み進めています。しかし、この本ではPythonが使用されています。自分の理解のために、C言語、もしくはC++でこのディープラーニングを実装していきたいというのが動機です。

MNISTとは

機械学習の分野でよく用いられている数字の画像のデータセットです。
以下のページで公開されています。
MNIST handwritten digit database, Yann LeCun, Corinna Cortes and Chris Burges

MNISTのデータ書式

上記ページの "FILE FORMATS FOR THE MNIST DATABASE" で説明されています。
それによると、以下のような書式です。(3次元データの場合)

  • 最初の2バイト:マジックナンバー 0x0000
  • 次の1バイト:1要素のバイト数の情報。0x8なら1要素=1バイト
  • 次の1バイト:次元数。3なら3次元。
  • 次の4バイト:次元0のサイズ
  • 次の4バイト:次元1のサイズ
  • 次の4バイト:次元2のサイズ
  • 残り:データ(次元0×次元1×次元2のバイト数)

Stirlingなどのバイナリエディタで見ると、確かにそのような書式になっています。
f:id:kenjiwn:20170709200226p:plain

また、"Pixel values are 0 to 255. 0 means background (white), 255 means foreground (black)." と記載されています。
これは通常の画像形式とは逆なのでこれに従うのであれば反転が必要です。

これらを踏まえて、MNISTの画像をPGM形式に変換するプログラムをC言語で記述してみました。

github.com

出力されたPGMファイルをIrfanViewなどのビュアーで見ると、確かに数字っぽい画像が出現してきます。
f:id:kenjiwn:20170709201139p:plain

ついでに、ラベルデータをテキストで出すプログラムも作りました。

github.com

実行すると以下のような出力がされており、画像とラベルデータが一致していることが目視で確認できます。

50419213143536172869
40911243273869056076
18793985933074980941
44604561001716302117
90267839046746807831
57171163029311049200
20271864163459133854
77428586734619960372
82944649709295159123
23591762822507497832
11836103100172730465
26471899307102035465
86375809103122336475
06279859211445641253
93905965741340480436
87609757211689415229
03967203543658954742
73489192879187413110
23949216847744925724
42197287692238165110