[SystemVerilog] interface と function を組み合わせると便利ですよ、という話

# [SystemVerilog] interface と function を組み合わせると便利ですよ、という話

石谷 @[PEZY Computing](https://www.pezy.co.jp) です。[HDL Advent Calendar 2021](https://qiita.com/advent-calendar/2021/hdl) と言うことで、SystemVerilog の interface と function を組み合わせると便利ですよ、と言う話をしていこうと思います。

## はじめに

interface は SystemVerilog で導入された機能の１つで、複数の信号やポートを纏めて取り廻せるようにする事が主な機能となります。信号やポート宣言の簡略化、ポート接続の簡略化をすることができ、複数の信号やポートで構成されているバスなどを取り扱う際に、大幅な省力化が期待できます。

interface 内に function や task を定義することができて、定義された function や task は interface のインスタンスやポートリスト上の modport を介して呼び出すことができます。バスの interface を例に、これの応用をいくつか紹介します。

## 例題のインターフェースについて

[OCP](https://accellera.org/downloads/standards/ocp/) のようなバスプロトコルで、以下の信号で構成されています。

* リクエスト側
    * **mcmd**
        * リクエストの種類を示します。
        * 0b00: Idle
        * 0b01: Write
        * 0b10: Read
    * **mid**
        * リクエストの識別子です。
    * **maddr**
        * アドレスです。
        * 幅はパラメータ `ADDRESS_WIDTH` で指定します。
    * **mdata**
        * 書き込みデータです
        * 幅はパラメータ `DATA_WIDTH` で指定します。
    * **scmd_accept**
        * slave 側でリクエストを受領可能かを示します。
* レスポンス側
    * **sresp**
        * レスポンスが有効であることを示します。
    * **sid**
        * レスポンスの識別子です。
    * **sdata**
        * 読み出しデータです。
        * 幅はパラメータ `DATA_WIDTH` で指定します。
    * **mresp_accept**
        * Master 側でレスポンスを受領可能かを示します。

`mcmd`/`scmd_accept` の組み合わせ、`sresp`/`mresp_accept` の組み合わせのハンドシェイクプロトコルになっています。
名前を `qiita_bus_if` として、interface の実装は以下のようになります。

```verilog:qiita_bus_pkg.sv
package qiita_bus_pkg;
  typedef struct packed {
    int id_width;
    int address_width;
    int data_width;
  } qiita_bus_config;

  typedef enum logic [1:0] {
    QIITA_IDLE  = 2'b00,
    QIITA_WRITE = 2'b01,
    QIITA_READ  = 2'b11
  } qiita_bus_command;
endpackage
```

```verilog:qiita_bus_if.sv
interface qiita_bus_if
  import  qiita_bus_pkg::*;
#(
  parameter qiita_bus_config  BUS_CONFIG  = '0
);
  logic                                 scmd_accept;
  qiita_bus_command                     mcmd;
  logic [BUS_CONFIG.id_width-1:0]       mid;
  logic [BUS_CONFIG.address_width-1:0]  maddr;
  logic [BUS_CONFIG.data_width-1:0]     mdata;
  logic                                 mresp_accept;
  logic                                 sresp;
  logic [BUS_CONFIG.id_width-1:0]       sid;
  logic [BUS_CONFIG.data_width-1:0]     sdata;

  modport master (
    input   scmd_accept,
    output  mcmd,
    output  mid,
    output  maddr,
    output  mdata,
    output  mresp_accept,
    input   sresp,
    input   sid,
    input   sdata
  );

  modport slave (
    output  scmd_accept,
    input   mcmd,
    input   mid,
    input   maddr,
    input   mdata,
    input   mresp_accept,
    output  sresp,
    output  sid,
    output  sdata
  );
endinterface
```

### 注記

関連するパラメータが複数個ある場合、構造体に纏めておくと、取り回しが楽で便利です。

## 状態の取得

以下の様に、ハンドシェイクの成立 (ack) はよく使用する論理です。

```verilog
always_comb begin
  access_start  = (slave_if.mcmd != QIITA_IDLE ) && slave_if.scmd_accept;
  write_start   = (slave_if.mcmd == QIITA_WRITE) && slave_if.scmd_accept;
  read_start    = (slave_if.mcmd == QIITA_READ ) && slave_if.scmd_accept;
end
```

`slave_if` が 1 行につき 2 回も出てきて、長ったらしいコードです。と言うことで、function にしてしまいましょう。

以下の function 定義を `qiita_buf_if` の定義中に追加します。

```verilog
function automatic logic request_ack();
  return (mcmd != QIITA_IDLE) && scmd_accept;
endfunction

function automatic logic write_ack();
  return (mcmd == QIITA_WRITE) && scmd_accept;
endfunction

function automatic logic read_ack();
  return (mcmd == QIITA_READ) && scmd_accept;
endfunction

function automatic logic response_ack();
  return sresp && mresp_accept;
endfunction
```

また、`import` を用いて、modport を介して呼び出せるようにしておきます。

```verilog
modport master (
  //  省略
  input   sdata,
  import  request_ack,
  import  write_ack,
  import  read_ack,
  import  response_ack
);

modport slave (
  //  省略
  output  sdata,
  import  request_ack,
  import  write_ack,
  import  read_ack,
  import  response_ack
);
```

呼び出し側は、以下のようになります。

```verilog
always_comb begin
  access_start  = slave_if.request_ack();
  write_start   = slave_if.write_ack();
  read_start    = slave_if.read_ack();
end
```

すっきりとしましたし、コードの意図がより分かりやすくなったかと思います。

## 接続の簡略化 (interface 間接続編)

interface を使えば、module 間の接続は簡略ができますが、interface 間の接続は、存外、面倒です。

```verilog
always_comb begin
  slave_if.scmd_accept  = master_if.scmd_accept;
  master_if.mcmd        = slave_if.mcmd;
  master_if.mid         = slave_if.mid;
  master_if.maddr       = slave_if.maddr;
  master_if.mdata       = slave_if.mdata;
end
```

interface 同士を繋ぐ module なりマクロを導入すれば、それなりの解決を見ます。が、アドレスなど一部だけを変更したい場合、この解決策を使えず、他の信号も繋ぎなおす必要があります。

そこで、リクエスト/レスポンスを纏めた構造体を導入します。パラメータ化された型の導入は少々面倒なので、アドレスや ID は最大幅を持つ全体で共通の構造体を定義します。

```verilog:qiita_bus_pkg.sv(抜粋)
`ifndef QIITA_BUS_MAX_ID_WIDTH
  `define QIITA_BUS_MAX_ID_WIDTH  8
`endif

`ifndef QIITA_BUS_MAX_ADDRESS_WIDTH
  `define QIITA_BUS_MAX_ADDRESS_WIDTH 32
`endif

`ifndef QIITA_BUS_MAX_DATA_WIDTH
  `define QIITA_BUS_MAX_DATA_WIDTH  32
`endif

typedef struct packed {
  qiita_bus_command                         mcmd;
  logic [`QIITA_BUS_MAX_ID_WIDTH-1:0]       mid;
  logic [`QIITA_BUS_MAX_ADDRESS_WIDTH-1:0]  maddr;
  logic [`QIITA_BUS_MAX_DATA_WIDTH-1:0]     mdata;
} qiita_bus_request;

typedef struct packed {
  logic [`QIITA_BUS_MAX_ID_WIDTH-1:0]   sid;
  logic [`QIITA_BUS_MAX_DATA_WIDTH-1:0] sdata;
} qiita_bus_response;
```

interface 中に、

* interface 中の信号の値を構造体に設定して、その構造体を返す function
* 構造体を受け取って、interface 中の信号に設定する function

を定義します。

```verilog:qiita_bus_if.sv(抜粋)
function automatic qiita_bus_request get_request();
  qiita_bus_request request;
  request = '{mcmd: mcmd, mid: mid, maddr: maddr, mdata: mdata};
  return request;
endfunction

function automatic void put_request(qiita_bus_request request);
  mcmd  = request.mcmd;
  mid   = request.mid;
  maddr = request.maddr;
  mdata = request.mdata;
endfunction

function automatic qiita_bus_response get_response();
  qiita_bus_response  response;
  response  = '{sid: sid, sdata: sdata};
  return response;
endfunction

function automatic void put_response(qiita_bus_response response);
  sid   = response.sid;
  sdata = response.sdata;
endfunction

modport master (
  //  省略
  import  put_request,
  import  get_response
);

modport slave (
  //  省略
  import  get_request,
  import  put_response
);
```

これらの function を使えば、信号を1本ずつ繋ぐことなく、interface 間の接続を行うことができます。

```verilog
always_comb begin
  slave_if.scmd_accept  = master_if.scmd_accept;
  master_if.put_request(slave_if.get_request());
end
```

一部を変更したい場合、変更したい箇所だけ変更すればよく、他の信号を明示的に繋ぎなおす必要はありません。

```verilog
qiita_bus_request request;
always_comb begin
  request       = slave_if.get_request();
  request.maddr = convert_address(request.maddr);

  slave_if.scmd_accept  = master_if.scmd_accept;
  master_if.put_request(request);
end
```

## 接続の簡略化 (ビット列に詰める編)

FIFO など既存の module を流用する等、interface 中の信号をビット列に纏めたい場合があります。この場合、無駄なビットが生じてしまうので、上記の共通の構造体を使った方法はあまり宜しくありません。なので、ビット列を取得する function、ビット列から interface 中の信号を設定する function を定義して対応します。

まず、package にビット列化したリクエスト、レスポンスの幅を取得する function を定義します。

```verilog:qiita_bus_pkg.sv(抜粋)
function automatic int get_packed_request_width(qiita_bus_config bus_config);
  int width = 0;
  width += $bits(qiita_bus_command);
  width += bus_config.id_width;
  width += bus_config.address_width;
  width += bus_config.data_width;
  return width;
endfunction

function automatic int get_packed_response_width(qiita_bus_config bus_config);
  int width = 0;
  width += bus_config.id_width;
  width += bus_config.data_width
  return width;
endfunction
```

次に、interface 中に、

* interface 中の信号をビット列に詰めて、そのビット列を返す function
* ビット列を受け取って、interface 中の信号に設定する function

を定義します。ポイントは、各信号のビット列の位置を localparam として計算しておくことです。

```verilog:qiita_bus_if.sv(抜粋)
localparam  int MCMD_LSB    = 0;
localparam  int MCMD_WIDTH  = $bits(qiita_bus_command);
localparam  int MID_LSB     = MCMD_LSB + MCMD_WIDTH;
localparam  int MID_WIDTH   = BUS_CONFIG.id_width;
localparam  int MADDR_LSB   = MID_LSB + MID_WIDTH;
localparam  int MADDR_WIDTH = BUS_CONFIG.address_width;
localparam  int MDATA_LSB   = MADDR_LSB + MADDR_WIDTH;
localparam  int MDATA_WIDTH = BUS_CONFIG.data_width;
localparam  int SID_LSB     = 0;
localparam  int SID_WIDTH   = BUS_CONFIG.id_width;
localparam  int SDATA_LSB   = SID_LSB + SID_WIDTH;
localparam  int SDATA_WIDTH = BUS_CONFIG.data_width;

localparam  int PACKED_REQUEST_WIDTH  = get_packed_request_width(BUS_CONFIG);
localparam  int PACKED_RESPONSE_WIDTH = get_packed_response_width(BUS_CONFIG);

function automatic logic [PACKED_REQUEST_WIDTH-1:0] get_packed_request();
  logic [PACKED_REQUEST_WIDTH-1:0]  request;
  request[MCMD_LSB+:MCMD_WIDTH]   = mcmd;
  request[MID_LSB+:MID_WIDTH]     = mid;
  request[MADDR_LSB+:MADDR_WIDTH] = maddr;
  request[MDATA_LSB+:MDATA_WIDTH] = mdata;
  return request;
endfunction

function automatic void put_packed_request(logic [PACKED_REQUEST_WIDTH-1:0] request);
  mcmd  = qiita_bus_command'(request[MCMD_LSB+:MCMD_WIDTH]);
  mid   = request[MID_LSB+:MID_WIDTH];
  maddr = request[MADDR_LSB+:MADDR_WIDTH];
  mdata = request[MDATA_LSB+:MDATA_WIDTH];
endfunction

function automatic logic [PACKED_RESPONSE_WIDTH-1:0] get_packed_response();
  logic [PACKED_RESPONSE_WIDTH-1:0] response;
  response[SID_LSB+:SID_WIDTH]      = sid;
  response[SDATA_LSB+:SDATA_WIDTH]  = sdata;
  return response;
endfunction

function automatic void put_packed_response(logic [PACKED_RESPONSE_WIDTH-1:0] response);
  sid   = response[SID_LSB+:SID_WIDTH];
  sdata = response[SDATA_LSB+:SDATA_WIDTH];
endfunction

modport master (
  //  省略
  import  put_packed_request,
  import  get_packed_response
);

modport slave (
  //  省略
  import  get_packed_request,
  import  put_packed_response
);
```

以下は、使用例です。

```verilog
localparam  PACKED_REQUEST_WIDTH  = get_packed_request_width(BUS_CONFIG);

logic                             fifo_empty;
logic                             fifo_full;
logic                             fifo_push;
logic                             fifo_pop;
logic [PACKED_REQUEST_WIDTH-1:0]  slave_request;
logic [PACKED_REQUEST_WIDTH-1:0]  master_request;

always_comb begin
  slave_if.scmd_accept  = !fifo_full;
  slave_request         = slave_if.get_packed_request();
end

always_comb begin
  if (!fifo_empty) begin
    master_if.put_packed_request(master_request);
  end
  else begin
    master_if.put_packed_request('0);
  end
end

always_comb begin
  fifo_push = slave_if.command_ack();
  fifo_pop  = master_if.command_ack();
end

common_fifo #(
  .WIDTH  (PACKED_REQUEST_WIDTH )
) u_fifo (
  .i_clk    (i_clk          ),
  .i_rst_n  (i_rst_n        ),
  .o_empty  (fifo_empty     ),
  .o_full   (fifo_full      ),
  .i_push   (fifo_push      ),
  .i_data   (slave_request  ),
  .i_pop    (fifo_pop       ),
  .o_data   (master_request )
);
```

### 注記

ビット列に詰める操作や、ビット列を分解する操作は、`BUS_CONFIG` から幅を決めた構造体や、連接を使うことで実装可能です。

```verilog
function automatic logic [PACKED_REQUEST_WIDTH-1:0] get_packed_request();
  return {mdata, maddr, mid, mcmd};
endfunction
```

ただし、サイドバンド信号など、パラメータによって有効な信号の数が増減する場合は、この方法は使えません。
なので、本稿では、各信号のビット列上の位置を予め計算する方法で実装しました。

## 注意

今回定義した function は、always_comb 文や順序回路記述の always 文中で使用してください。
特に 無引数 function (reuest_ack 等) を assign 文や組み合わせ回路記述の always 文中使うと、シミュレーションが実行できません。
無引数 function の場合、センシビリティリストの特定ができないためです。

```verilog
//  イベントを見つけられないので、各文は実行されない
assign  request_ack = slave_if.request_ack();

always @* begin
  response_ack  = master_if.response_ack();
end
```

always_comb 文の場合、function の中で参照している信号も合わせて、センシビリティリストの推定が行われるので、このような問題は起きません。


## 最後に

今回使用したサンプルコードは GitHub 上で公開しています。

https://gist.github.com/taichi-ishitani/d703099a0c8c1ec05b22d96e540000d3

本サンプルコードは、実際に業務で書いている SystemVerilog のコードを簡略化したものです。少なくとも、以下の EDA ツールでは対応している記述です。

* VCS
* Design Compiler
* Formality
* Vivado