型のパラメータ化と interface based typedef

# 型のパラメータ化と interface based typedef

石谷 @[PEZY Computing](https://www.pezy.co.jp/) です。[SystemVerilog Advent Calendar 2020](https://qiita.com/advent-calendar/2020/systemverilog) の第３篇目です。
第１篇目、第２篇目はこちら。

* [出木杉君演算子 inside](https://qiita.com/taichi-ishitani/items/d5bb34273fce8d4b0385)
* [RTL で依存性の注入（っぽいこと）をする](https://qiita.com/taichi-ishitani/private/03b13589431c5213b968)

SystemVerilog では、

* 型のパラメータ化
* interface 内で定義された型の参照

をすることができます。
今回は、これらを紹介し、応用例として、ハンドシェイクプロトコルを実装する、より一般化した interface および機能モジュールを例示していきます。

## 型のパラメータ化

SystemVerilog では、数値だけではなく、型をパラメータとして宣言することができます。宣言の際は、`type` キーワードを使います。

```verilog
module foo_bar #(
  parameter type  DATA_TYPE = logic
)(
  input   DATA_TYPE i_d,
  output  DATA_TYPE o_d
);
endmodule
```

インスタンスする際に、`DATA_TYPE` に所望の型を与えます。

```verilog
typedef enum logic [1:0] {
  FOO_0,
  FOO_1
} foo_enum;

foo_enum  foo_0;
foo_enum  foo_1;

foo_bar #(
  .DATA_TYPE  (foo_enum )
) u_foo (
  .i_d  (foo_0  ),
  .o_d  (foo_1  )
);

logic [1:0] bar_0;
logic [1:0] bar_1;

foo_bar #(
  .DATA_TYPE  (logic[1:0] )
) u_bar (
 .i_d (bar_0  ),
 .o_d (bar_1  )
);
```

`enum` では、厳密な型チェックがされます。なので、`enum` を使う場合は、

* 出力ポートの型と、接続先の型
* 代入の両辺の型

を合わせる必要があります。上記の例の場合では、以下のコードは文法エラーになります。

```verilog
foo_enum    foo_0;
foo_enum    foo_1;
logic [1:0] bar_0;
logic [1:0] bar_1;

assign  foo_0 = bar_0;  //  Syntax Error

foo_bar #(
  .DATA_TYPE  (logic[1:0] )
) u_bar (
  .i_d  (bar_1  ),
  .o_d  (foo_1  ) //  Syntax Error
);
```

なので、FIFO 等の共通モジュールは、`enum` の使用を考慮して、型のパラメータ化は必須と言えます。

## interface based typedef

interface 内で定義された (typedef された) 型は、そのインスタンスの接続先のモジュール内で使うことができます。これは `interface based typedef` と呼ばれ、LRM (IEEE 1800-2017) では `6.18 User-defined types` にその説明があります。

```verilog
interface foo_if #(int W = 1);
  typedef logic [W-1:0] foo_type;
  foo_type  foo_data;
  modport mp (input foo);
endinterface

module bar (
  foo_if.mp foo_port
);
  typedef foo_port.foo_type foo_type; //  これ
  foo_type  foo_data;
  assign  foo_data  = foo.foo_data;
endmodule

module baz;
  foo_if #(1) foo_if_1();
  bar u_bar_1 (foo_if_1);

  foo_if #(2) foo_if_2();
  bar u_bar_2 (foo_if_2);
endmodule
```

上記の例では、`foo_if` 内で定義された `foo_type` を `foo_port` を介して、`module bar` 内に取り込んでいます。
`module bar` 自体はパラメータ化されていませんが、

* u_bar_1 の foo_type は `logic [0:0]`
* u_bar_2 の foo_type は `logic [1:0]`

となります。

応用例として、型定義を一か所にまとめるために使うことができます。
コンフィグレーションパラメータから導かれる型を、それぞれのモジュールで定義するのは、非常に面倒です。
そこで、interface 内に型を定義し、その interface を介して型を取り込むことで、型の定義を一か所に纏められるようになります。以下が使用例になります。

* https://github.com/taichi-ishitani/tnoc/blob/master/rtl/common/tnoc_types.sv
* https://github.com/taichi-ishitani/tnoc/blob/master/rtl/common/tnoc_flit_if.sv#L21

## 応用例

応用例として、valid/ready 方式のハンドシェイクを実装する interface のより一層の一般化を行います。

まずは interface の定義です。当該 interface は valid/ready/data を持ち、data の型が `DATA_TYPE` としてパラメータ化されています。

```verilog
interface handshake_if #(
  type  DATA_TYPE = logic
);
  typedef DATA_TYPE __data_type;

  logic     valid;
  logic     ready;
  DATA_TYPE data;

  function automatic logic ack();
    return valid && ready;
  endfunction

  modport master (
    output  valid,
    input   ready,
    output  data,
    import  ack
  );

  modport slave (
    input   valid,
    output  ready,
    input   data,
    import  ack
  );
```

型をパラメータ化しているので、`enum` を直接使えますし、data が構造体の場合、そのメンバーフィールドを直接参照することも可能です。

```verilog
typedef enum logic {
  FOO_0,
  FOO_1
} foo_enum;

handshake_if #(foo_enum)  foo_if();
assign  foo_if.data = FOO_0;

typedef struct packed {
  logic bar_0;
  logic bar_1;
} bar_struct;

handshake_if #(bar_struct)  bar_if();
assign  bar_if.data.bar_0 = '0;
assign  bar_if.data.bar_1 = '1;
```

型パラメータ `DATA_TYPE` を `__data_type` として `typedef` しているところも、注目点です。
こうすることで、`interface based typedef` を用いて、接続先に `DATA_TYPE` を取り込むことができるようになります。

次の機能モジュールです。ここでは FIFO を実装し、以下のようになります。

```verilog
module handshake_fifo #(
  parameter int DEPTH = 2
)(
  input var           i_clk,
  input var           i_rst_n,
  handshake_if.slave  slave_if,
  handshake_if.master master_if
);
  typedef slave_if.__data_type  __data_type;

  logic push;
  logic pop;
  logic empty;
  logic full;

  always_comb begin
    slave_if.ready  = !full;
    master_if.valid = !empty;
  end

  always_comb begin
    push  = slave_if.ack();
    pop   = master_if.ack();
  end

  common_fifo #(
    .DEPTH  (DEPTH        ),
    .TYPE   (__data_type  )
  ) u_fifo (
    .i_clk    (i_clk          ),
    .i_rst_n  (i_rst_n        ),
    .o_empty  (empty          ),
    .o_full   (full           ),
    .i_push   (push           ),
    .i_data   (slave_if.data  ),
    .i_pop    (pop            ),
    .o_data   (master_if.data )
  );
endmodule
```

`interface based typedef` を用いて、`DATA_TYPE` を `__data_type` として取り込んでいます。なので、`handshake_fifo` 自体には型に関するパラメータを持たせる必要はありません。

そして、以下が使用例になります。

```verilog
typedef enum logic {
  FOO_0,
  FOO_1
} foo_enum;

handshake_if #(foo_enum)  foo_if[2]();
handshake_fifo u_foo_fifo (
  .i_clk      (i_clk      ),
  .i_rst_n    (i_rst_n    ),
  .slave_if   (foo_if[0]  ),
  .master_if  (foo_if[1]  )
);

typedef struct packed {
  logic bar_0;
  logic bar_1;
} bar_struct;

handshake_if #(bar_struct)  bar_if[2]();
handshake_fifo u_bar_fifo (
  .i_clk      (i_clk      ),
  .i_rst_n    (i_rst_n    ),
  .slave_if   (bar_if[0]  ),
  .master_if  (bar_if[1]  )
);
```

## まとめ

`型のパラメータ化` と `interface based typedef` を用いれば、汎用性と利便性が高いモジュール定義を行うことが可能となります。
ただ、`interface based typedef` は、ツールの対応がこなれていない為か、バグを踏むことがあるので、要注意です。
例えば、[これ](https://forums.xilinx.com/t5/Synthesis/Vivado-2019-1-Synth-8-448-named-port-connection-error-on/m-p/991115)。