Tuesday, May 13, 2014

Interface in SystemVerilog

Interface simplifies hooking up modules/ programs.
There are 2 methods of hooking up stuff using interface.
Hardwire method:
RTL: portname is interface_name.modport_name choose_a_name.
all outputs are assigned as choose_a_name = value.
At top level: create a instance of interface_name.
interface_name instance_of_interface_name (.clk(clk));
and hook up choose_a_name with instance_of_interface_name.

An example will make it clear. I am using an example from my current assignment at the UCSC extension coursework.
module memory_core (memory_interface.core_port deepika_coreif);
   always @ (posedge deepika_coreif.clk)
   ....
endmodule

module memory_ctrl(memory_interface.ctrl_port deepika_ctrlif);
....
endmodule

program testcase(memory_interface.testcase_port deepika_tcif,
input call_finish);
............
endprogram

module top();

   bit         clk;
   memory_interface imif (.clk (clk));
   ..........
   initial begin
clk = 0;
   end

   always #5 clk = !clk;
   ....

   memory_core icore     (imif);
   memory_ctrl         ictrl       (imif);
   testcase   itest      (.deepika_tcif (imif),
     .call_finish (call_finish) );
endmodule


Generic method: 
Here at the RTL end, the porttype is simply interface. What gets hooked up is instance_of_interface.specific_modport.
module memory_core (interface deepika_coreif);
..
endmodule

module memory_ctrl(interface deepika_ctrlif);
...
endmodule

program testcase(interface deepika_tcif,
input call_finish);
..
endprogram

module top();
   bit         clk;
   ....
   memory_interface imif (.clk (clk));
 
   initial begin
clk = 0;
   end
   always #5 clk = !clk;

   memory_core icore     (imif.core_port);
   memory_ctrl ictrl     (imif.ctrl_port);
   testcase   itest   (.deepika_tcif (imif.testcase_port),
     .call_finish (call_finish) );
endmodule

The code for interface itself remains unaffected whichever way you to connect stuff.
interface memory_interface (input bit clk);
logic reset;
.....
//modports
modport core_port(
input clk, reset,...,
output ... );

modport ctrl_port(
input clk, reset, ...
        output ... );
);

modport testcase_port(
input clk, reset...,
       output ...,
       import init_mem,
       import write_mem,
       import read_mem
);

task init_mem();
reset = 1; ....
#100 reset = 0;
endtask

task write_mem(ref int passed_addr,
               ref bit [7:0] passed_data  );
...
endtask

task read_mem( ref int passed_addr);
....
endtask
endinterface

Advantages of using tasks:
In my example, I could use the tasks and fork-join to test what happens when write and read occurs simultaneously  (This could never happen because there is a single read/writebar pin but its fun to check it anyways.. Pretty cool stuff.)

Note that task only has whatever is "passed" as the inputs/outputs. Task is actively driving the outputs but enumerating that is already taken care of in the modport. A good way of simplifying the code.
Interface basically gives you a overview of stuff. Since you consolidate all wires in one file, all worry about data_width mismatch, or whether you connected little/ big endianess correctly is taken care of.