17
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

HDL (SystemVerilog/Verilog/VHDL/Chisel/etc.)Advent Calendar 2021

Day 1

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

Last updated at Posted at 2021-11-30

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

石谷 @PEZY Computing です。HDL Advent Calendar 2021 と言うことで、SystemVerilog の interface と function を組み合わせると便利ですよ、と言う話をしていこうと思います。

はじめに

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

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

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

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 の実装は以下のようになります。

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
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) はよく使用する論理です。

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 の定義中に追加します。

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 を介して呼び出せるようにしておきます。

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
);

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

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 間の接続は、存外、面倒です。

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 は最大幅を持つ全体で共通の構造体を定義します。

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

を定義します。

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 間の接続を行うことができます。

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

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

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 を定義します。

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 として計算しておくことです。

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
);

以下は、使用例です。

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 から幅を決めた構造体や、連接を使うことで実装可能です。

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 の場合、センシビリティリストの特定ができないためです。

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

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

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

最後に

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

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

  • VCS
  • Design Compiler
  • Formality
  • Vivado
17
6
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
17
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?