Interface(1)
Interfaceはverilogからsystemverilogへの拡張の中で、設計者にとってもっとも重要なものの一つだ。この機能により、モジュール間の接続が大幅に簡潔に行え、しかもミスの発生を抑えることができる。
本稿でのサンプルプログラムは、Quartus IIで合成できることを確認しているが、論理合成での対応状況はツールによって異なるようだ。interfaceは、複数のファイルやモジュールで共用されることが多く、ツールが非対応で修正が入った場合に影響が大きい。使う機能に関して、サンプルコードが合成できることを事前に確認しておくことをお勧めする。
基本的な使い方
モジュールのポートにinterfaceを使用することで、モジュール間の接続の記述量を大幅に減らすことができる。
interfaceは、複数のregやwire宣言を一つにまとめて接続する、構造体のようなものだ。
interfaceはインスタンス化してモジュールのポートに接続する。下の例では、interface myIfをmifというインスタンス名でインスタンス化し、各モジュールに接続している。これにより、個々のwireやlogic等を一つ一つ記述する必要がなくなる。
module TopModule(input wire ck);
myIf mif;
module ModuleA(.mif(mif), .ck(ck));
module ModuleB(.mif(mif), .ck(ck));
endmodule
interfaceの定義は以下のように行う。以下ではporta, portb, potcというレジスタ、ロジック、ワイヤを含めている。
reg/wireの区別は、接続先のモジュールでの値の代入方法で選択する。alwaysによって値が代入されるならreg、assignによって継続代入されるならwireを選択する。logicならどちらの状況でも使用できる。
interface myIf;
reg porta;
logic portb;
wire portc;
endinterface
接続先のモジュールでは、ポートの定義は以下のように行う。portの記述においては、interface自体にはinput/outputの属性はないので、指定は不要だ(後述するが、interfaceのメンバにはmodportによりinput/outputを指定することもできる)。また、接続先のモジュールでインタフェース内の各信号にアクセスするには。「ポート名.信号名」といった形で行う。
module ModuleA(myIf mif, input wire ck);
...
assign mif.portb = mif.porta;
...
endmodule
ポートには、使用するinterfaceの種類を指定しないで、interfaceのハンドルだけを宣言することもできる。これをgeneric interfaceという。どのinterfaceが接続されるかは、親モジュール委譲するということだ。ただ、どちらにせよinterface内の信号にアクセスするためには、使用するinterfaceを特定しなければならないため、あまり利点が無いような気がするのだが。。。一応、他人のコードを読んだ時にあわてないよう、知識としては覚えておいたほうがいいだろう。
module ModuleA(interface mif, input wire ck);
...
assign mif.portb = mif.porta;
...
endmodule
それでは、実際に動くコードを見てみよう。
以下の例は、SenderとReceiverというモジュール間をSrIfというinterfaceで接続している。
module test1();
reg clk, rst;
wire [7:0] finalData;
top dut(.finalData(finalData), .clk(clk), .rst(rst));
//Clock and Reset Generator
initial begin
clk = '0; rst = '1;
#10 rst = '0;
forever begin
#10 clk = ~clk;
end
end
//Monitor signal
always @(finalData) begin
$display("finalData = %x", finalData);
end
initial begin
#300 $finish();
end
endmodule
interface SrIf;
reg [7:0] rawData;
reg rawDataEnable;
reg [7:0] processedData;
reg processedDataEnable;
endinterface
module top(
output wire [7:0] finalData,
input wire clk, rst);
//Instantiate Interface
SrIf srif();
//Instantiate modules
Sender senderIns(.finalData(finalData), .srif(srif),
.clk(clk), .rst(rst));
Receiver receiverIns(.srif(srif), .clk(clk), .rst(rst));
endmodule
module Sender(output reg [7:0] finalData, SrIf srif,
input wire clk, rst);
always @(posedge clk or posedge rst) begin
if (rst) begin
srif.rawData <= '0;
srif.rawDataEnable <= '0;
end else begin
srif.rawData <= srif.rawData + 1'b1;
srif.rawDataEnable <= srif.rawData[0];
end
end
always @(posedge clk or posedge rst) begin
if (rst) begin
finalData <= '0;
end else if (srif.processedDataEnable) begin
finalData <= srif.processedData;
end
end
endmodule
module Receiver(SrIf srif, input clk, rst);
always @(posedge clk or posedge rst) begin
if (rst) begin
srif.processedData <= '0;
srif.processedDataEnable <= '0;
end else begin
if (srif.rawDataEnable) begin
srif.processedData <= srif.rawData + 8'h10;
srif.processedDataEnable <= 1'b1;
end else begin
srif.processedDataEnable <= '0;
end
end
end
endmodule
実行結果は以下の通り。
# 6.5e
# vsim -do {run -all; quit} -c test1
# Loading sv_std.std
# Loading work.test1
# Loading work.top
# Loading work.SrIf
# Loading work.Sender
# Loading work.Receiver
# run -all
# finalData = 00
# finalData = 12
# finalData = 14
# finalData = 16
# finalData = 18
# finalData = 1a
# finalData = 1c
ところで、interfaceの中にreg宣言があり、interface自体はtopの中で宣言されているが、これらinterfaceの中のregが、どの階層の中でFFとして合成されるかを確認しておく。特にフロアプランの階層の区切りでは、意図した部分でFFで切れていないとレイアウトに大きな問題を引き起こしてしまう。
LRMは文法と動作の規定のみで、合成結果については言及していない。論理合成ツールとしては、どのように解釈してもいいわけだ。
Qualtus IIでのtopの合成結果は以下の通り。SenderとReceiverのほかinterfaceの実体であるSrIfもインスタンス化されている(中身は空になっている)。
Sender側の合成結果は以下の通り。interfaceの中のregのうち、データを代入しているrowDataとrawDataEnableの2つがFFとして合成されていることがわかる。
Receiver側も同様に、代入を行っているprocessedDataとprocessedDataEnableがFFとして合成される。
以上のように、interfaceの中のregは、代入を行っている側のモジュールに組み入れられる。他のツールでも、私が確認した限りでは同様の結果となっている。
Port
上記の例では、clkとrstは別ポートとして宣言し、interfaceの中には含めなかった。これらの信号は、上位モジュールからくる信号が接続されるため、SenderとReceiverの親モジュールであるtopで接続する必要があったためだ。
このような信号は、intefaceの入力ポートとして定義することが可能だ。
interfaceの定義は以下のように変更になる。
interface SrIf(input wire clk, rst);
reg [7:0] rawData;
reg rawDataEnable;
reg [7:0] processedData;
reg processedDataEnable;
endinterface
topでは、interfaceの入力ポートにclkとrstを接続する
module top(
output wire [7:0] finalData,
input wire clk, rst);
//Instantiate Interface
SrIf srif(.clk(clk), .rst(rst));
//Instantiate modules
Sender senderIns(.finalData(finalData), .srif(srif));
Receiver receiverIns(.srif(srif));
endmodule
各モジュールでは、他の信号と同じようにsrif.clkでアクセスできる。
module Sender(output reg [7:0] finalData, SrIf srif);
always @(posedge srif.clk or posedge srif.rst) begin
if (srif.rst) begin
srif.rawData <= '0;
srif.rawDataEnable <= '0;
end else begin
srif.rawData <= srif.rawData + 1'b1;
srif.rawDataEnable <= srif.rawData[0];
end
end
...
Modport
上記の例では、interfaceの各要素には入出力の方向を定義していなかったが、実際にはモジュール毎に入出力の方向が存在する。上記例では、senderではrowDataとrowDataEnableが出力でprocessedDataとprocessedDataEnableが入力、receiverではその逆になる。
このようなinterfaceに対する、module毎の事情の違いに対応するのがmodportだ。interfaceの定義内で、modportによって使用する要素と入出力の方向を定義する。
これにより、誤接続のない、より安全なモジュール間の信号接続が可能になる。
早速上記例にmodportを追加したものを見てみよう。interfaceの定義は以下のようになる。例では、全てのregとwireをmodportに含めているが、使用しるものだけ記述すればいい。また、modport名はregやwire等の名前と同じにしてはならない。例だと、modportにrawData等の名前をつけることはできないということだ。
interface SrIf(input wire clk, rst);
reg [7:0] rawData;
reg rawDataEnable;
reg [7:0] processedData;
reg processedDataEnable;
modport sender(
output rawData, rawDataEnable,
input processedData, processedDataEnable,
input clk, rst);
modport receiver(
output processedData, processedDataEnable,
input rawData, rawDataEnable,
input clk, rst);
endinterface
接続先のモジュール側では、「インターフェース名.modport名」がポートの宣言になる。例を以下に示す。
module Sender(output reg [7:0] finalData, SrIf.sender srif);
これで、上記の入出力の定義に違反した操作はエラーとなる。
たとえば、Sender内で、入力であるprocessedDataに対して代入を行おうとするとエラーが表示される。
モジュール間の接続に関しては、特に変更点は無い。
module top(
output wire [7:0] finalData,
input wire clk, rst);
//Instantiate Interface
SrIf srif(.clk(clk), .rst(rst));
//Instantiate modules
Sender senderIns(.finalData(finalData), .srif(srif));
Receiver receiverIns(.srif(srif));
endmodule
ただし、interfaceのmodportを明示的に接続することも可能だ。
module top(
output wire [7:0] finalData,
input wire clk, rst);
//Instantiate Interface
SrIf srif(.clk(clk), .rst(rst));
//Instantiate modules
Sender senderIns(.finalData(finalData), .srif(srif.sender));
Receiver receiverIns(.srif(srif.receiver));
endmodule
Modport expressions
実際にinterfaceの使用場面では、接続する信号のビット幅が異なることが考えられる。たとえば、アドレス空間が32Bitのバスでも、そのバスに接続するコンポーネントは20Bitのアドレスしか入力されず、上位ビットのデコード結果が、セレクト信号として入力されるといった具合だ。
このような場合、modport expressionを使う。modportの信号に別名をつける機能だ。上記の例だと、20Bitのアドレスに対して別の名前をつければ解決する。
上記のサンプルプログラムのデータ幅をmodportで4ビットに変更してみよう。
interface SrIf(input wire clk, rst);
reg [7:0] rawData;
reg rawDataEnable;
reg [7:0] processedData;
reg processedDataEnable;
modport sender(
output .rawData(rawData[3:0]), rawDataEnable,
input .processedData(processedData[3:0]), processedDataEnable,
input clk, rst);
modport receiver(
output .processedData(processedData[3:0]), processedDataEnable,
input .rawData(rawData[3:0]), rawDataEnable,
input clk, rst);
endinterface
上記例ではビットの選択を行ったが、ビットの連結や別名への変更も可能だ。
output .newsig(oldsig[3:0]),
input .newsig2({oldsig[3], oldsig[0]}),
といった具合になる。
parameter
interfaceはパラメータをとることも可能だ。書き方は、moduleの時のパラメータと同様だ。上記例をパラメータを使うように変更してみよう。
(注)LRMの例ではparameterというキーワードが使われておらず、#(DWIDTH = 4)のような記述になっている。BNFから判断するとこれはミスだと思われる。ModelSimはこのような記述も許すが、Quartusはエラーとして処理している。
interface SrIf #(parameter DWIDTH = 4) (input wire clk, rst);
reg [DWIDTH - 1:0] rawData;
reg rawDataEnable;
reg [DWIDTH - 1:0] processedData;
reg processedDataEnable;
modport sender(
output rawData, rawDataEnable,
input processedData, processedDataEnable,
input clk, rst);
modport receiver(
output processedData, processedDataEnable,
input rawData, rawDataEnable,
input clk, rst);
endinterface
インスタンス化する時の記法もmoduleと変わらない。
//Instantiate Interface
SrIf #(.DWIDTH(8)) srif(.clk(clk), .rst(rst));