Connecting hierarchical modules: struct vs interface in SystemVerilog

Ari picture Ari · Jan 24, 2014 · Viewed 17k times · Source

In SystemVerilog hierarchical modules can be connected by simple data types, complex data types (structs, unions, etc), or interfaces. The feature that I am interested in is aggregating all signals between two modules in one place which simplifies maintenance of the code.

For example in the following one change s_point's definition without changing the declarations of m1, m2, and top:

 typedef struct {
  logic [7:0] x;
  logic [7:0] y;
} s_point; // named structure


module m1 (output s_point outPoint);
//
endmodule

module m2 (input s_point inPoint);
//
endmodule

module top ();
    s_point point;
    m1 m1_inst (point);
    m2 m2_inst (point);
endmodule

Alternatively, this could have been done using interfaces. However, I believe using structures are easier and they are supported by more CAD tools, and they don't need to be instantiated as is the case for interfaces (although one still has to declare them in the top module).

My question is if only aggregating signals is required (i.e. no interest in interface's task, functions, modports, generic interface, internal always blocks etc) for a synthesizable design, which one is preferred?

Answer

Greg picture Greg · Jan 24, 2014

interface is preferred.

A struct okay to use only when all the signals within the struct all follow the same port direction; input, output, or inout wire. It becomes challenging to use structs when driving directions become mixed. Mixed direction is a allow using the ref keyword, however the ref keyword is not supported by many synthesis tools, yet. inout cannot be used because logic is considered a variable, IEEE Std 1800-2012 § 6.5 Nets and variables. However, inout wire can be used to cast the struct as a net. The components of a stuct can not be assigned within an always-block and needs an assign statement instead; just like a regular wire.

An interface should be grouping signals where the port direction is not consistent. Tri-states need to be defined as wire and the variable types do not require explicit port direction when used in conjunction with always_{ff|comb|latch}. It should also be used if the signals are part of a protocol. This way assertions can be added and it can be connected to a classes for an UVM test-bench or other SVTB environment.

Use a sturct when only passing a explicit direction data type. Use an interface for passing shared signals.

Example Senario:

Imagine there is a collection of signals x,y,&z, where module m_x drives x and reads y&z, module m_b drive y and reads x&z, and module m_z drives z and reads x&y. Each signals have only one driver and always_ff can be used to guarantee this.

If we try adding a bidirectional tri-state bus, to the mix then the struct cannot be used. A wire resolves conflicting drivers while logic/reg clobber and keep the scheduler running.

Sample code:

Using struct using ref (no tri-state allowed):

typedef struct {logic [7:0] x, y, z, bus; } s_point;

module m_x_st (ref s_point point, input clk);
  always_ff @(posedge clk)
    point.x <= func(point.y, point.z);
  //assign point.bus = (point.y!=point.z) ? 'z : point.x; // NO tir-state
endmodule
module m_y_st (ref s_point point, input clk);
  always_ff @(posedge clk)
    point.y <= func(point.x, point.z);
  //assign point.bus = (point.x!=point.z) ? 'z : point.y; // NO tir-state
endmodule
module m_z_st (ref s_point point, input clk);
  always_ff @(posedge clk)
    point.z <= func(point.x, point.y);
  //assign point.bus = (point.x!=point.y) ? 'z : point.z; // NO tir-state
endmodule

module top_with_st (input clk);
    s_point point;
    m_x_st mx_inst (point,clk);
    m_y_st my_inst (point,clk);
    m_z_st mz_inst (point,clk);
endmodule

Using struct using inout wire (nets must be driven with assign, loses single driver guarantee):

typedef struct {logic [7:0] x, y, z, bus; } s_point;

module m_x_wst (inout wire s_point point, input clk);
  logic [$size(point.x)-1:0] tmp;
  assign point.x = tmp;
  always_ff @(posedge clk)
    tmp <= func(point.y, point.z);
  assign point.bus = (point.y!=point.z) ? 'z : point.x; // tir-state
endmodule
module m_y_wst (inout wire s_point point, input clk);
  logic [$size(point.y)-1:0] tmp;
  assign point.y = tmp;
  always_ff @(posedge clk)
    tmp <= func(point.x, point.z);
  assign point.bus = (point.x!=point.z) ? 'z : point.y; // tir-state
endmodule
module m_z_wst (inout wire s_point point, input clk);
  logic [$size(point.z)-1:0] tmp;
  assign point.z = tmp;
  always_ff @(posedge clk)
    tmp <= func(point.x, point.y);
  assign point.bus = (point.x!=point.y) ? 'z : point.z; // tri-state
endmodule

module top_with_wst (input clk);
    wire s_point point; // must have the 'wire' keyword
    m_x_wst mx_inst (point,clk);
    m_y_wst my_inst (point,clk);
    m_z_wst mz_inst (point,clk);
endmodule

Using interface (with tri-state):

interface if_point;
  logic [7:0] x, y, z;
  wire  [7:0] bus; // tri-state must be wire
endinterface

module m_x_if (if_point point, input clk);
  always_ff @(posedge clk)
    point.x <= func(point.y, point.z);
  assign point.bus = (point.y!=point.z) ? 'z : point.x;
endmodule
module m_y_if (if_point point, input clk);
  always_ff @(posedge clk)
    point.y <= func(point.x, point.z);
  assign point.bus = (point.x!=point.z) ? 'z : point.y;
endmodule
module m_z_if (if_point point, input clk);
  always_ff @(posedge clk)
    point.z <= func(point.x, point.y);
  assign point.bus = (point.x!=point.y) ? 'z : point.z;
endmodule

module top_with_if (input clk);
    if_point point();
    m_x_if mx_inst (point,clk);
    m_y_if my_inst (point,clk);
    m_z_if mz_inst (point,clk);
endmodule

Running code: http://www.edaplayground.com/s/6/1150