チラシの裏は意外と白くない

最近忘れっぽくなったので調べたことをチラシの裏に書きます

クラスに関して②:コンストラクタとデストラクタ

クラスの特殊なメンバ関数としてコンストラクタとデストラクタがある。

コンストラク

クラスをインスタンス化するときに自動的に呼ばれる。先の例ではコンストラクタの記述をしていないが、その場合デフォルトコンストラクタが呼ばれる。コンストラクタは、

  • コンストラクタの名前はクラス名と同じ
  • 戻り値はなし

例えば、以下のようにコンストラクタを定義することができる。この例ではインスタンスしたときにm_height=-1で初期化する。

man.h

        CMan(); // publicで

man.cpp

CMan::CMan() : m_height(-1){
    cout << "CManオブジェクト生成" << endl;
}

デストラク

インスタンスが解放されるときに自動的に呼ばれる。こちらも記述しないとデフォルトデストラクタが呼ばれる。

  • コンストラクタの名前は~+クラス名
  • 戻り値はなし

man.h

        ~CMan(); // publicで

man.cpp

CMan::~CMan(){
    cout << "CManオブジェクト破棄" << endl;
}

コンストラクタ・デストラクタが呼ばれるタイミング

を明確にするため、main.cppを以下のように書き換える。

int main(){
    cout << "this is first line" << endl;

    CMan suzuki;
    cout << "instanciated" << endl;

    suzuki.set_age(40);
    cout << "age of suzuki is " << suzuki.get_age() << endl;
    cout << "height of suzuki is " << suzuki.get_height() << endl;
//    cout << "(backdoor)age of suzuki is " << suzuki.m_age << endl;

    CMan ueda;
    ueda.set_age(50);
    cout << "age of ueda   is " << ueda.get_age() << endl;


    cout << "this is last line" << endl;
    return 0;
}

実行結果は、

$ ./a.out
this is first line
CManオブジェクト生成
instanciated
age of suzuki is 40
height of suzuki is -1
CManオブジェクト生成
age of ueda   is 50
this is last line
CManオブジェクト破棄
CManオブジェクト破棄

ということで、CMan Suzuki;の直後にコンストラクタが呼ばれる。デストラクタはreturn 0;の直前。

コンストラクタ・デストラクタ呼び出しタイミングの制御

main.cppを以下のように変更する。ポインタのため、メンバ関数の呼び出しは.から->に変更している。

int main(){
    cout << "this is first line" << endl;

    //CMan suzuki;
    CMan* suzuki;
    suzuki = new CMan();

    suzuki->set_age(40);
    cout << "age of suzuki is " << suzuki->get_age() << endl;
    cout << "height of suzuki is " << suzuki->get_height() << endl;
//    cout << "(backdoor)age of suzuki is " << suzuki.m_age << endl;
    delete suzuki;

    //CMan ueda;
    CMan* ueda;
    ueda = new CMan();
    ueda->set_age(50);
    cout << "age of ueda is " << ueda->get_age() << endl;
    delete ueda;

    cout << "this is last line" << endl;
    
    return 0;
}

実行結果は以下。newしたところでコンストラクタが呼ばれ、delしたところでデストラクタが呼び出される。

$ ./a.out
this is first line
CManオブジェクト生成
age of suzuki is 40
height of suzuki is -1
CManオブジェクト破棄
CManオブジェクト生成
age of ueda is 50
CManオブジェクト破棄
this is last line

クラスに関して①:基本的なところ

クラスの話。これも今更感があるけどメモ。

構成

基本の基本から。とりあえず”人”クラスで、年齢・身長・体重をprivateに、これらをセットする関数とゲットする関数を用意。

ファイル 内容
man.h CManクラスの定義
man.cpp CManの実装
main.cpp CManの利用

ソースコード

man.h

#ifndef __MAN_H__
#define __MAN_H__

// クラス宣言
class CMan
{
    public:
        void set_age(int age);
        void set_height(int height);
        void set_weight(int weight);
        int get_age();
        int get_height();
        int get_weight();
    private:
        int m_age;
        int m_height;
        int m_weight;
};

#endif // __MAN_H__

man.cpp

#include "man.h"

void CMan::set_age(int age){
    m_age = age;
}

void CMan::set_height(int height){
    m_height = height;
}

void CMan::set_weight(int weight){
    m_weight = weight;
}

int CMan::get_age(){
    return m_age;
}

int CMan::get_height(){
    return m_height;
}

int CMan::get_weight(){
    return m_weight;
}

main.cpp

#include <iostream>
#include "man.h"

using namespace std;

int main(){
    CMan suzuki;
    suzuki.set_age(40);
    cout << "age of suzuki is " << suzuki.get_age() << endl;
    cout << "height of suzuki is " << suzuki.get_height() << endl;

    CMan ueda;
    ueda.set_age(50);
    cout << "age of ueda   is " << ueda.get_age() << endl;

    return 0;
}

実行結果

$ g++ main.cpp man.cpp
$ ./a.out 
age of suzuki is 40
height of suzuki is 0
age of ueda   is 50

setしたageは読めているが、setしていない身長は初期値が呼ばれる。この場合は0。

get_age()を使用せず、直接m_ageを読むように以下のコードを追加。

    cout << "(backdoor)age of suzuki is " << suzuki.m_age << endl;

すると、コンパイル時に以下のように怒られる。

$ g++ main.cpp man.cpp
In file included from main.cpp:2:0:
man.h: In function ‘int main()’:
man.h:17:13: error: ‘int CMan::m_age’ is private
         int m_age;
             ^
main.cpp:13:53: error: within this context
     cout << "(backdoor)age of suzuki is " << suzuki.m_age << endl;
                                                     ^

privateメンバのm_ageにはアクセスできない。

C++あれこれ

今更感があるが、C++のいくつかの内容についてまとめておく。

関数のオーバーロード

例えば、以下のようなint型の引数2つの平均値をint型で返す関数を考える。

int average(int a, int b)
{
    return (a+b)/2;
}

Cでは、float型同士の平均を求めたい場合には新たに、

float average_float(float a, float b)
{
    return(a+b)/2;
}

のような関数を定義して、average_float()を呼び出す必要があった。

C++オーバーロードの仕組みを使えば、

int average(int a, int b)
{
    return (a+b)/2;
}
float average(float a, float b)
{
    return (a+b)/2;
}

のように同名の関数として定義することが可能で、呼び出し時に引数の型が一致したほうの関数が呼び出される。

関数テンプレート

関数のオーバーロードで呼び出し側は楽にかけることが分かったが、取り扱う型の種類だけ関数自体は定義してやる必要があった。関数の定義自体も1つにしてしまうのが関数テンプレート。

template <classT>
T average(T a, T b)
{
    return (a+b)/2;
}

のように記述する。呼び出し時にテンプレート引数Tを指定したデータ型に置き換えた関数を自動生成してくれる。

演算子オーバーロード

次のようなRGBクラスを考える。R,G,Bそれぞれのデータはprivateのため直接アクセスはできない。そのため各色に対して書き込みと読み出しを行うwrite,readというpublic関数を用意している。

class RGB
{
    private:
        unsigned int R;
        unsigned int G;
        unsigned int B;
    public:
        // データの書き込み
        void writeR(unsigned int a){
            if(a > 255) {
                R = 255;
            } else { 
                R = a;
            }
        }
        void writeG(unsigned int a){
            if(a > 255) {
                G = 255;
            } else { 
                G = a;
            }
        }
        void writeB(unsigned int a){
            if(a > 255) {
                B = 255;
            } else { 
                B = a;
            }
        }

        // データの読出し
        unsigned int readR(){ return R; }
        unsigned int readG(){ return G; }
        unsigned int readB(){ return B; }
};

例えば、以下のように使用する。

int main(){
    RGB rgb; // インスタンス

    unsigned int tmpR;
    unsigned int tmpG;
    unsigned int tmpB;

    tmpR = 100;
    tmpG = 200;
    tmpB = 300;

    // RGBの書き込み
    rgb.writeR(tmpR);
    rgb.writeG(tmpG);
    rgb.writeB(tmpB);

    // RGBを読みだして、標準出力
    cout << "R = " << rgb.readR() << endl;
    cout << "G = " << rgb.readG() << endl;
    cout << "B = " << rgb.readB() << endl;

    return 0;
}

RGBのオブジェクト同士の加算を考える。もちろん色ごとにrgb.writeR = rgb0.readR() + rgb1.readR()という感じで書いていくことも可能だが、rgb = rgb0 + rgb1のように一気に三色分の加算ができると便利になる。

int main(){
    RGB rgb0;

    unsigned int tmpR;
    unsigned int tmpG;
    unsigned int tmpB;

    tmpR = 100;
    tmpG = 200;
    tmpB = 300;

    rgb0.writeR(tmpR);
    rgb0.writeG(tmpG);
    rgb0.writeB(tmpB);

    RGB rgb1;
    
    tmpR = 10;
    tmpG = 20;
    tmpB = 30;
    
    rgb1.writeR(tmpR);
    rgb1.writeG(tmpG);
    rgb1.writeB(tmpB);

    RGB rgb;
    rgb = rgb0 + rgb1;

    cout << "R = " << rgb.readR() << endl;
    cout << "G = " << rgb.readG() << endl;
    cout << "B = " << rgb.readB() << endl;

    return 0;
}

実際にこれをコンパイルすると、

$ g++ rgb.cpp
rgb.cpp: In function ‘int main()’:
rgb.cpp:89:16: error: no match for ‘operator+’ (operand types are ‘RGB’ and ‘RGB’)
     rgb = rgb0 + rgb1;
                ^

こんな具合に、RGB型とRGB型の演算子+がない、というように怒られる。 そこで、RGB型同士の演算子"+"の挙動を定義してやる。これを演算子オーバーロードという。オーバーロードの記述は関数とほぼ同じ感じで、以下の通り(8bitでクリップしているが、本質的ではない)。

RGB operator+(RGB a, RGB b){
    RGB tmp;

    tmp.R = ((a.R + b.R) > 255) ? 255 : a.R + b.R;
    tmp.G = ((a.G + b.G) > 255) ? 255 : a.G + b.G;
    tmp.B = ((a.B + b.B) > 255) ? 255 : a.B + b.B;
    
    return tmp;
}

さて、これで再度コンパイルをすると、

$ g++ rgb.cpp
rgb.cpp: In function ‘RGB operator+(RGB, RGB)’:
rgb.cpp:8:22: error: ‘unsigned int RGB::R’ is private
         unsigned int R;
                      ^

今度はprivateメンバであるR,G,Bに直接アクセスしているため怒られてしまう。

こういう時にRGBのクラスの中でRGB operator+(RGB a, RGB b)をfriend指定してやればよいらしい。こうすることでRGBクラスのfriendであるoperator+(RGB a, RGB b)はRGBクラスのprivateメンバにアクセスできるようになる。

class RGB
{
    private:
        unsigned int R;    // 8bit
        unsigned int G;    // 8bit
        unsigned int B;    // 8bit
    public:
        // データの書き込み
        void writeR(unsigned int a){
            if(a > 255) {
                R = 255;
            } else { 
                R = a;
            }
        }
        void writeG(unsigned int a){
            if(a > 255) {
                G = 255;
            } else { 
                G = a;
            }
        }
        void writeB(unsigned int a){
            if(a > 255) {
                B = 255;
            } else { 
                B = a;
            }
        }

        // データの読出し
        unsigned int readR(){ return R; }
        unsigned int readG(){ return G; }
        unsigned int readB(){ return B; }

        // rgb同士の加算
        friend RGB operator+(RGB a, RGB b);

};

friendを使用しない書き方としては、operator+の記述を

RGB operator+(RGB a, RGB b){
    RGB tmp;

    unsigned int tmpR,tmpG,tmpB;
    tmpR = ((a.readR()+b.readR()) > 255) ? 255 : (a.readR()+b.readR());
    tmpG = ((a.readG()+b.readG()) > 255) ? 255 : (a.readG()+b.readG());
    tmpB = ((a.readB()+b.readB()) > 255) ? 255 : (a.readB()+b.readB());

    tmp.writeR(tmpR);
    tmp.writeG(tmpG);
    tmp.writeB(tmpB);
    
    return tmp;
}

のようにpublicメンバだけで書けばOK。

SystemC基本構文③

シミュレーション構文

4bitカウンタを作成した際に見てきた内容が多いが改めて。

時間単位の設定

  • sc_set_default_time_unit(値)でシミュレーションの時間単位を指定する
    • sc_set_default_time_unit(1, SC_PS); で1ps単位になる
    • デフォルトは1ns
    • sc_get_default_time_unit()で設定されている時間単位がわかる
  • sc_set_time_resolution(値)でシミュレーションの時間分解能を指定する
    • デフォルトは1ps

タイムスタンプ

  • sc_simulation_time()で設定されたシミュレーション時間単位に基づいた、現在のシミュレーション時刻がわかる
  • sc_time_stamp()は時間単位とともにシミュレーション時刻がわかる
  • sc_simulation_time()を用いてレイテンシ情報を把握することができる

sc_clock

sc_clock CLK("CLK", 10, SC_NS, 0.5, 1, SC_NS, true)で指定した場合、

属性 省略可否
周期 10 ×
単位 ns ×
デューティ比 50% 50%
最初のエッジ発生時刻 1 0
極性 true(立ち上がり) true

f:id:samurai375:20200414133659j:plain

sc_time

時間変数を表す。sc_clockやsc_startで使用する。

sc_time cycle_time(10, SC_NS);

sc_clock clock("clock", cycle_time);

sc_start/sc_stop

  • sc_start()でシミュレーションを開始
  • sc_start()に時間を指定すると指定時刻までsim実行して終了
  • sc_start(-1)でイベントがなくなるまでsim実行
    • イベントがなくなるとは?SC_CTHREADとかがあったら使用不可?
  • sc_start()で何も指定しない場合、sc_stop()で停止させる
sc_time finish_time(100, SC_NS);

sc_start(1, SC_US);    // 1usまでシミュレーションして終了
sc_start(finish_time); // finish_time=100nsまでシミュレーションして終了
sc_start(-1);          // イベントがなくなるまで実行

階層構造

すでにmain.cppにdutとtbをインスタンスしてシミュレーションを行っているのであまり目新しいことはない。

階層接続

Verilogと同じくポート名による接続と順序による接続がある。

SC_MODULE(rgb2yuv){
    //ports
    sc_in       < bool   > clock;
    sc_fifo_in  < rgb8_t > offset_in;
    sc_fifo_in  < rgb8_t > rgb_in;
    sc_fifo_out < yuv8_t > yuv_out;

SC_MODULE(tb) {
    // ports
    sc_in      < bool   > clock;
    sc_fifo_out< rgb8_t > offset_out;
    sc_fifo_out< rgb8_t > rgb_out;
    sc_fifo_in < yuv8_t > yuv_in;

を名前で接続する場合は

   // instanciate
    rgb2yuv i0("rgb2yuv");
    tb t0("tb");

    // conectivity
    i0.clock(clock);
    i0.offset_in(offset);
    i0.rgb_in(rgb);
    i0.yuv_out(yuv);

    t0.clock(clock);
    t0.offset_out(offset);
    t0.rgb_out(rgb);
    t0.yuv_in(yuv);

sc_main()

  • 最上位階層。C言語のmain()関数と同じ
  • 以下の内容を記述する
    • 下位インスタンスの呼び出し
    • 信号を宣言してインスタンスを接続
    • 波形ダンプの定義
    • sc_start()によるシミュレーション開始
    • 戻り値はreturn 0

SC_MODULEからインスタンスを呼び出す

インスタンス宣言による呼び出しとポインタ宣言による呼び出しの2種類がある。

インスタンス宣言による呼び出し
#include "tb.h"
#include "addr.h"

SC_MODULE(SYSTEM)
{
    sc_signal <bool> in1, in2, out;

    // モジュールのインスタンス
    addr addr0;
    tb tb0;

    SC_CTOR(SYSTEM) :
        addr0("addr0"),
        tb0("tb0")
    {
        addr0.i1(in1);
        addr0.i2(in2);
        addr0.o1(out);

        tb0.o1(in1);
        tb0.o2(in2);
    }
};
ポインタ宣言による呼び出し
#include "tb.h"
#include "addr.h"

SC_MODULE(SYSTEM)
{
    sc_signal <bool> in1, in2, out;

    // モジュールのインスタンス
    addr* addr0;
    tb* tb0;

    SC_CTOR(SYSTEM)
    {
        addr0 = new addr("addr0");
        addr0->i1(in1);
        addr0->i2(in2);
        addr0->o1(out);

        tb0 = new tb("tb0");
        tb0->o1(in1);
        tb0->o2(in2);
    }
};

SystemC基本構文②

データ型から続きを。

データ型

基本的にsc_***。C++のデータ型であるbool, int, char, doubleも使用可能。代表的なデータ型は、

データ型 内容
sc_int<N> N bit符号付き32bit整数
sc_uint<N> N bit符号なし32bit整数
sc_bit '0','1'の2値
sc_logic '0','1','x',z'の4値
sc_bv<N> sc_bitのN bitベクタ型
sc_lv<N> sc_logicのN bitベクタ型
sc_fixed<NW, NI> 符号付き固定小数点数
sc_ufixed<NW, NI> 符号なし固定小数点数

sc_bvやsc_lvの使いどころについては、今後出てきた時に詳しく見ることにしよう。あと、多分ライブラリが切り出された状態で提供されているとか、そういう都合があるのだろうがbit精度を規定したアルゴリズムCでACデータライブラリを使用している例をよく見かける。これの違いもいずれ見ておきたい。

www.mentorg.co.jp

信号と変数の使い分けについて

sc_signalで宣言した信号はデルタ遅延を持ち、sc_signal型からのリードにはread()メソッド、代入にはwrite()メソッドもしくは代入文(=)を使用する。sc_signalはVerilogでいうノンブロッキング代入になるということだろう。

sc_signal< sc_uint<8> > indata, outdata;
sc_uint<8> data, outvar;

data = indata.read();
outvar = data + 1;
outdata.write(outvar); // outdata = outvar;でも可

sc_signalがノンブロッキング代入になるのに対して、変数はブロッキング代入となり順序的に実行される。変数のほうがシミュレーションが速いので、プロセス内部の手続きには変数を使用するのが一般的。 プロセス内のローカル変数はデフォルトでauto(毎回初期化される)、初期化したくない場合はstatic、定数扱いはconst。この辺は普通のCと同じ。

イベントとwait()文

イベント

  • イベントは信号の変化を意味する
  • プロセス宣言時にsensitive << で指定するセンシティビティは静的なセンシティビティという
  • bool型の場合、0→1の変化を示すpos()、1→0の変化を示すneg()が使用可能

wait()

  • イベントが発生するとwait()までの記述を実行してプロセス停止、次のイベント発生を待つ
  • 高位合成では、wait()のタイミングでデータをレジスタに保持することを意味する

wait()の記述例:

wait();         // 1[cycle]待つ
wait(n);        // n[cycle]待つ。SC_CTHREADのみ有効
wait(a0|a1);    // a0もしくはa1が変化するまで待つ
wait(a0&a1);    // a0とa1の両方が変化するまで待つ。同時変化でなくてよく、順序も不問
wait(n, SC_NS); // n[ns]待つ

wait_until

これは以前調査したように、SystemC2.3.3では廃止されている。

sc_eventとnotify()

  • イベント型sc_eventで宣言。notify()でイベントを起動
  • イベントの起動を受けるために、センシティビティリストでの指定もしくはwait(イベント名)
  • プロセス内部でwait(イベント名)に到達すると、その他のセンシティビティリストに記載された信号変化では先に進まない
    • 動的なセンシティビティというらしい
  • イベント型は実際の信号変化を伴わないため、高位合成不可。検証用
sc_event eventA;
eventA.notify();         // 即時通知
eventA.notify(1, SC_NS); // 1ns後に通知

wait(eventA);

単方向ハンドシェイキングと双方向ハンドシェイキング

  • 双方向ハンドシェイキングでは、receiverがreadyをアサート、senderがready=Hを観測してからvalidとともにデータを送出する
  • 単方向ハンドシェイキングでは、senderはreceiverの状態を観測せず、validとともにデータを送出する

f:id:samurai375:20200412201129j:plain

ステートマシン①

ミーリタイプとムーアタイプ

順序回路の分類。これは今まで全く意識したことがなかった。

タイプ 次の状態
ミーリタイプ 保持している状態と、その時点での入力信号によって決定される
ムーアタイプ 保持している状態のみによって決定される

と言われてもわかったようなわからないような。教科書の例をそのままやってみる。

可変長符号デコーダの例

仕様

可変長符号化されたA~Gのデータをシリアルに(1bitずつ)、MSBファーストでクロック同期で受信して、デコードする回路を考える。符号化方法は、

文字 符号
A 00
B 01
C 100
D 101
E 110
F 1110
G 1111

エンコーディングに関して、絵もかいておこう。

f:id:samurai375:20200331145711j:plain:w300

ステートマシンの比較

ミーリタイプとムーアタイプそれぞれの状態遷移図を以下に示す。基本的な構造は同じだが、ムーアタイプでは文字を検出した状態(A~G)が加わっている。

f:id:samurai375:20200404115202j:plain

タイミングチャートを書くとこんな感じか。

f:id:samurai375:20200404121231j:plain

例えばa(dataが"0"→"0")を受け取った場合、

  • ミーリタイプではS0状態でのdata="0"を観測した瞬間に出力信号aをアサート
  • ムーアタイプではS0状態でのdata="0"を観測したら、次のクロックエッジで出力信号aをアサート&状態を"A"にする

という動作の違いがある。aがアサートされるタイミングがムーアタイプのほうが1cycle遅いことがわかる。また、aのアサート期間もクロックエッジ後にランダムロジックの遅延が入るか否かの違いによって、ムーアタイプのほうが安定期間が長くなる(図中で(5)とか(6)はマスの数。あくまでイメージ)。

次にe(dataが"1"→"1"→"0")の場合。aと違うのは、最後のdataが1→0に変化する点で、最後data="0"を受け取ったときに、

  • ミーリタイプではS4状態でのdata値によって、eをアサートしてS0に戻る or S5に遷移するの2種類がありえる。dataが"1"→"0"の変化に伴ってdataが安定するまでに時間がかかる。この遅延によって、aのアサート開始タイミングがaよりも遅くなる。最後の2bitが異なる場合、同じことが起こるので、a,c,gとb,d,e,fでは信号のアサート期間に差が発生する
  • ムーアタイプではS4状態でのdata="0"を観測したら、次のクロックエッジで出力信号eをアサート&状態を"E"にする。そのため、受信した文字によってアサート期間がばらつくことはない

という違いが起こる。ミーリタイプにおける、出力信号のアサート期間の違いは教科書にあっさりとした説明しかなくて理解するのに苦戦した。絵を書いたら分かった。

続いて、実際のVerilog記述を確認する。

順序回路とか

順序回路

組み合わせ回路の出力は過去の状態に影響されないのに対し、順序回路は過去の状態によって出力が変わる。always文を使うやつ。

D-FF

フリップフロップにはD,SR,JK,Tがあるが、D-FFが最も基本的なFFなので、これの動作を復習しておく。 クロックの立ち上がりエッジでデータラッチする場合はこんな感じ。

module dff(clock, in, out);
    input  clock;
    input  in;
    output out;

    reg q;

    always @(posedge clock) begin
        q <= in;
    end

    assign out = q;
endmodule

clockの立ち上がりエッジ(posede)でinをqに代入する。always @(***)のかっこの中身がセンシティビティリストで、複数信号、例えばclockと非同期リセットrst_nを入れる場合は、always @(posedge clock or negedge rst_n)みたいに書く。

メモリの記述

メモリはレジスタの配列として定義ができて、

reg [7:0] ram [0:1023]

と書くと、8bit x 1024 = 1024byteのメモリになる。[0:1023]は要素数であることに注意。

試しに同期式のSRAMとテストベンチを書いてみる。ポートは、

ポート bit幅 I/O 極性 機能
clock - I クロック
addr 10 I - アドレス
din 8 I - データ入力
cs - I L チップセレクト。Low-active
wen - I H ライトイネーブル
dout 8 O - データ出力

動作は、

  • 同期式
  • チップセレクトはLow-active
  • ライトイネーブルHでアクセスすると書き込み、Lで読み出し
  • データ入力ポートとデータ出力ポートは別々に持つ

とすると、

module singleport_ram(clock, addr, din, cs, wen, dout);
    input        clock;
    input  [9:0] addr;
    input  [7:0] din;
    input        cs;    // chip select, low-active
    input        wen;   // write enable
    output [7:0] dout;

    reg [7:0] mem [0:1023];
    reg [7:0] tmp_data;

    always @(posedge clock) begin
        if(!cs & wen) begin
            // write
            mem[addr] <= din;
        end
        else if(!cs & !wen) begin
            // read
            tmp_data <= mem[addr];
        end
    end

    assign dout = tmp_data;

endmodule

sim結果はこんな感じ。最低限の動作しか確認していないけど。 f:id:samurai375:20200330192913j:plain

always文を使った組み合わせ回路とラッチ回路

組み合わせ回路はassignやfunctionで記述するが、always文を使って書くことができる。よく見る記述がalways @(*) begin ~~ end。XORをalwaysを使って書くと、

module xor_always(in0, in1, out);
    input  in0;
    input  in1;
    output out;

    reg    tmp;

    always @(*) begin
        if(in0) tmp <= ~in1;
        else    tmp <=  in1;
    end

    assign out = tmp;
endmodule

のように書ける。一見XORらしくないが、ちゃんとXORになっている。always @(in0 or in1)としても同じ。

さて、このコードでelse節をコメントアウトした、以下のRTLの動作を考える。

module xor_always(in0, in1, out);
    input  in0;
    input  in1;
    output out;

    reg    tmp;

    always @(*) begin
        if(in0) tmp <= ~in1;
 //       else    tmp <=  in1;
    end

    assign out = tmp;
endmodule

in0=1の時の動作が規定されていない。真理値表を書くと、

in0 in1 out
0 0 x
0 1 x
1 0 1
1 1 0

こんな具合に、in0=1の時はx(不定)になる。simをやってみると、

f:id:samurai375:20200330204445j:plain

in0 in1 out 備考
0 0 x
0 1 x
1 0 1
1 1 0
0 1 0 前回の出力値が保持されている
0 0 0 前回の出力値が保持されている

というように、in=0の時、前回の値を保持するような回路になってしまっていて、これをラッチという。実際にschemaを書かせてみると、以下の通り。

f:id:samurai375:20200330204731j:plain

すべての入力の組み合わせにおける動作を定義しないと(今回のケースではin0=0が未定義)ラッチが生成されてしまう。例えばこのデザインをVivadoで合成すると以下のように警告が出る。

[Synth 8-327] inferring latch for variable 'tmp_reg' ["sandbox/verilog/xor/xor.v":9]

ブロッキング代入とノンブロッキング代入

"="がブロッキング代入、"<="がノンブロッキング代入。ブロッキング代入は記述順序に従って上からシーケンシャルに代入するプログラム的動作で、ノンブロッキング代入は右辺をすべて評価してから、左辺に代入する…という文章をよく見かける。で、あまり細かい意味を考えずに、ノンブロッキングで書いてきたが、改めて以下のようなコードで動作を確認してみる。

module tb();
    reg a;
    reg b;

    initial begin
            // initialize
            a = 1;
            b = 0;
        #10
            // blocking
            a = b;
            b = a;
        #10
            $display("blocking:    a = %d, b = %d", a, b);
        #10
            // initialize
            a = 1;
            b = 0;
        #10
            // non-blocking
            a <= b;
            b <= a;
        #10
            $display("nonblocking: a = %d, b = %d", a, b);
        #30
            $finish;
    end

endmodule

結果は以下、

blocking:    a = 0, b = 0
nonblocking: a = 0, b = 1

a,bをスワップする意図のコードだがブロッキング代入の場合、a=bを実行した時点で、a=0, b=0になってしまうので、その後にb=aを実行しても結果がa=0, b=0となってしまう。Cでスワップコードを書く場合は、ふつう退避用の変数を確保するが、同じことをしないといけない。

他方、ノンブロッキング代入の場合は一時変数を用意せずともスワップが成立している。a <= bでaにはb=0を代入する準備をして、さらにb <= aでbにa=1を代入する準備をして、それから時刻を進める(代入を行う)ため、スワップができる。右辺をすべて評価してから代入、という意味はこれ。

もう一つ、シフトレジスタの例を見ておく。2段のシフトレジスタ回路で、それぞれのFF出力を出力端子に接続している。b***はブロッキング代入の2段シフトレジスタ、nb***はノンブロッキング代入の2段シフトレジスタのつもり。

module shift(clock, in, b_out0, b_out1, nb_out0, nb_out1);
    input        clock;
    input  [1:0] in;
    output [1:0] b_out0,  b_out1;
    output [1:0] nb_out0, nb_out1;

    reg [1:0] tmp_b0,  tmp_b1;
    reg [1:0] tmp_nb0, tmp_nb1;

    always @(posedge clock) begin
        tmp_b0 = in;
        tmp_b1 = tmp_b0;
        tmp_nb0 <= in;
        tmp_nb1 <= tmp_nb0;
    end

    assign b_out0  = tmp_b0;
    assign b_out1  = tmp_b1;
    assign nb_out0 = tmp_nb0;
    assign nb_out1 = tmp_nb1;

endmodule

sim結果は以下で、ノンブロッキング代入のほうは正しくシフトレジスタになっているが、ブロッキング代入のほうはb_out0=b_out1になっている。

f:id:samurai375:20200331115916j:plain

schemaは以下の通りで、b_out0, b_out1ともに1段FFをたたいた結果が接続されていることがわかる。

f:id:samurai375:20200331120056j:plain