RTL で依存性の注入(っぽいこと)をする
石谷 @PEZY Computing です。SystemVerilog Advent Calendar 2020 の2回目です。1回目 は inside
演算子の解説を行いました。
今回は RTL 中で依存性の注入(っぽいこと)をやってみようと思います。依存性の注入 (Dependency Injection: DI) については、
- https://qiita.com/okadabasso/items/066efc2e728a666b8732
- https://qiita.com/hshimo/items/1136087e1c6e5c5b0d9f
当たりを参考にしてください。
はじめに
RTL を書いていると、「ほとんどの機能は同じだが、微妙に違う」module が多く出てくると思います。
バスの MUX を例にとると、
- ラウンドロビンな MUX
- 固定優先順位な MUX
- ランダムな調停を行う MUX
のように、調停方法によって、微妙に違う MUX を実装する必要があります。
以下のように、parameter
でどの調停方法を使うかを指定できるようにし、generate
文でその実装を切り替えれるようにすれば、一応の解決を見ます。
module bus_mux #(
parameter ARBITRATION = 0
);
if (ARBITRATION == 0) begin : g_roundrobin
// ラウンドロビンを実装
end
else if (ARBITRATION == 1) begin : g_fixed_priority
// 固定優先順位を実装
end
else begin : g_invalid
// 該当なし
// エラー
end
endmodule
ただし、この方法には、
- 調停方法が増えるたびに
bus_mux
を変更する必要がある - 一品ものの特殊な調停方法に対応しにくい
などの欠点があります。解決するには、調停方法の実装を bus_mux
外部に追い出す必要があります。
依存性の注入(っぽいこと)を適用してみる
「依存性の注入」とは?
使われ方によって変化する機能を、どの実装を使うかの選択も含め、外部に追い出して、機能間を疎結合にし、拡張性やメンテナンス性、テスト容易性を向上させる設計パターンの1つです。
例えば、ある機能 A がログ出力する場合を考えます。ログの出力先として、
- ファイル
- 端末
- テストの用のモック
等々が考えられます。パラメータによって出力先を切り替える場合、先ほどの bus_mux
の場合と同じ問題にあたってしまいます。
そこで DI の登場です。DI では、
- 出力先ごとのログ出力を実装したログ出力オブジェクトを定義する
- 各ログ出力オブジェクトは共通のインターフェースを持つ
- 機能 A インスタンス時に、所望のログ出力オブジェクトを与える
ようにすることで、機能 A の実装とログ出力の実装を切り離すことができます。
ログの出力先が増えた場合、所望のログ出力オブジェクトを実装し、機能 A に与えることで実現できます。
これは、機能 A の実装を変えることなく実現できます。
RTL 上でやってみる
SystemVerilog の RTL 上では、
- 機能オブジェクトは interface を使って実装
- 接続には generic interface を使用
することで、依存性の注入(っぽこと)を実現します。
先ほどの bus_mux
の例を考えます。
調停方法が bus_mux
から分離したい機能です。なので、interface を使って各調停方法(調停オブジェクトと呼ぶことにします)を実装します。
interface roundrobin_arbiter #(
parameter int ENTRIES = 2
)(
input var i_clk, input var i_rst_n
);
logic [ENTRIES-1:0] request;
logic [ENTRIES-1:0] grant;
modport arbiter (
output request,
input grant
);
// ラウンドロビンを実装
endinterface
interface fixed_priority_arbiter #(
parameter int ENTRIES = 2
);
logic [ENTRIES-1:0] request;
logic [ENTRIES-1:0] grant;
modport arbiter (
output request,
input grant
);
// 固定優先度での調停を実装
endinterface
各調停オブジェクトは、共通のインターフェースとして、modport arbiter
を持ちます。
次に、調停オブジェクトの接続についてです。これは generic interface
を使って実現します。
generic interface
を使えば、interface を介して参照する信号や function が存在しさえすれば、任意の interface のインスタンスを接続することができます。
なので、bus_mux
の調停オブジェクト使用周辺の実装は、以下のようになります。
module bus_mux (
interface.arbiter arbiter
foo_if.slave slave_if[2],
foo_if.master master_if
);
logic [1:0] grant;
always_comb begin
arbiter.request[0] = slave_if[0].request;
arbiter.request[1] = slave_if[1].request;
grant = arbiter.grant;
end
endmodule
最後に bus_mux
と調停オブジェクトの接続についてです。それぞれをインスタンスして、普通に繋ぐだけです。
fixed_priority_arbiter u_arbiter();
bus_mux u_mux (
.arbiter (u_arbiter ),
// 省略
);
調停方法をラウンドロビンにする場合は、fixed_priority_arbiter
の代わりに、roundrobin_arbiter
をインスタンスするだけです。
実装例
依存性の注入(っぽい)を適用した例を GitHub のリポジトリに置いてあります。
https://github.com/taichi-ishitani/rtl_with_di
bus_mux.sv が MUX の実装です。
調停オブジェクトへの参照を、interface.arbiter arbiter
として、generic interface
を使って宣言しています。
調停オブジェクトの要件は、
- 名前が
arbiter
のmodport
を持つこと - 上記
modport
は、出力request
と、入力grant
を持つこと
があげられます。
調停オブジェクトとの接続があるだけで、それ以外の調停に関する記述は MUX の記述中には一切ありません。
fixed_prioriy_arbiter.sv と roundrobin_arbiter.sv が調停オブジェクトの実装になります。
別々の interface として定義されていますが、上記の要件に合致する modport
を持っているので、bus_mux
の調停オブジェクトとして使うことができます。
bus_mux_top.sv が使用例を示すトップモジュールになります。
2つの bux_mux
のインスタンスがありますが、どの調停オブジェクトを使うかを切り替えることで、調停の種類の切り替えを行っています。
まとめ
上記の例では、MUX の実装と、調停の実装とを分離できました。
他の調停方法を使いたい場合は、所望の調停方法を実装した、調停オブジェクトを実装するだけでよく、MUX の実装には影響はありません。MUX の実装が変化に対して強くなった、と言えます。
また、調停オブジェクトは MUX だけではなく、他のモジュールにも使えるので、より再利用性が高まったともいえます。
再度の宣伝
YAML とか Excel で記述したレジスタマップから、RTL 等を自動生成するツール (RgGen) を作っています。
面倒な CSR のコーディングをせずに済むので、手前味噌ですが、便利なツールです。
よかったら、使ってみてください。
Qiita にも紹介記事を載せてあります。
- https://qiita.com/taichi-ishitani/items/5155b2928b7d85370ae6
- https://qiita.com/taichi-ishitani/items/d89738b5376503c813d8
UVM で記述した AMBA AXI/APB の VIP (モドキ) も公開しています。
こちらも、よければ、見てみてください。
SystenVerilog で記述した Network On Chip (NoC) も公開しています。
Design Compiler および Vivado でエラボレーションできるところまでは確認しています。
RTL 中で SystemVerilog をどこまで使えるのかの参考になると思います。
(ただし Quartus は知らない。)
UVM を使った検証環境も同梱しているので、UVM を使った検証環境の参考にもなると思います。