[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 側でリクエストを受領可能かを示します。
-
mcmd
- レスポンス側
-
sresp
- レスポンスが有効であることを示します。
-
sid
- レスポンスの識別子です。
-
sdata
- 読み出しデータです。
- 幅はパラメータ
DATA_WIDTH
で指定します。
-
mresp_accept
- Master 側でレスポンスを受領可能かを示します。
-
sresp
mcmd
/scmd_accept
の組み合わせ、sresp
/mresp_accept
の組み合わせのハンドシェイクプロトコルになっています。
名前を qiita_bus_if
として、interface の実装は以下のようになります。
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
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 は最大幅を持つ全体で共通の構造体を定義します。
`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
を定義します。
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 を定義します。
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 として計算しておくことです。
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