System Verilog

System Verilog (SV) :


DUOLOS has tons of docs on everything: http://www.doulos.com/

For SV tutoria on Duolosl: http://www.doulos.com/knowhow/sysverilog/tutorial/

This link has very nicely put tutorial on SV: https://verificationguide.com/systemverilog/

SV has gone thru lots of updates since it's initial version. SV 2009 is the one most widely supported by all tools.
New features in SV2009: http://www.testbench.in/SystemVerilog_2009_enhancements.html

SV is both a design and test language, It's design constructs are mostly from Verilog, so we won't cover those here. We are going to cover the test constructs which were all added from multiple languages. SystemVerilog language components are:

  • Concepts of Verilog HDL
  • Testbench constructs based on Vera
  • OpenVera assertions
  • Synopsys’ VCS DirectC simulation interface to C and C++
  • A coverage application programming interface that provides links to coverage metrics

Testbench or Verification Environment is used to check the functional correctness of the Design Under Test (DUT) by generating and driving a predefined input sequence to a design, capturing the design output and comparing with-respect-to expected output.

The verification environment can be written by using SystemVerilog concepts. SV added object oriented programming (OOP) to Verilog to aid in Verification. Before we get into Writing TB with SV, let's look into some basic concepts.


SV data types:


NOTE: verilog data types are bit vectors and arrays, no other data type for storing complex structures. We create static arrays of bits for storing char, string, etc in verilog. Struct, union were added later to verilog. SV uses OOP to create complex data types with routines to operate on them. In OOP, there is a handle pointing to every object. These handles may be saved in arrays, queues, mailbox, etc.

data_type: wire/reg/logic are structural data type, while others are behavioural data types.

Structural Data Type:

wire, reg, logic are structural data types

Integer type:

We can have 2 state or 4 state integer type.
2 state integer type (0 or 1), default value is "0", so any unintialized variables start with "0" (which might mask startup errors)

bit = 0,1. unsigned. ex: bit[3:0] a;
byte/shortint/int/longint = 8/16/32/64 bits, signed. ex: byte a,b;=> a,b can be from -128 to +127. longint a;

NOTE: 2 state var save memory in SV and improve simulator performance. To check o/p ports of DUT which can have 4 values, use this
if ($isunknown(iport) $display("@%0d: 4-state value detected on input port",$time, iport); => returns 1 if any bit of expr is x or z

4 state integer type (0,1,X,Z), default valus is "x" so that any unintialized variables start with x (useful in detecting errors).

reg, logic = identical to reg, can be used for both wire and reg. unsigned. ex: logic[7:0] a;
NOTE: logic[7:0] can't be replaced with byte as logic[7:0] is unsigned and so it ranges from 0 to 255, while byte is from -128 to 127
integer = 32 bits signed. It's diff than "int" as all 4 vales allowed. "integer" is the one that is allowed in verilog for defining integers. integer in SV(4 values) is different than integer in verilog(2 values). "int" is only allowd in SV.
wire = identical to verilog wire, but default value is "z". unsigned. We use logic instead of wire, as it can be used for both reg/wire.
time = 64 bit unsigned. All 4 values (0,1,X,Z) allowed in SV, but only 2 values (0,1) allowed in verilog.

NOTE: when we assign a 4 state variable to 2 state variable, then Z,X get assigned to 0.

Non integer type:

shortreal, real, realtime = like float/double/double in C. ex: realtime now; (realtime allows us to specify time in real number)

string: to avoid using reg for storing strings as that was cumbersome. uses dynmaic array for allocation. Each char is of type byte
-----
string s; s = "SystemVerilog";

wreal:
----
wire can also be assigned to real numbers to model for voltage/current. just using "real" doesn't allow signals to flow. This type needed in DMS (digital mixed signal) to model analog blocks. However, since wreal is sv keyword, you need to make file as .sv (i.e file is named xy.sv) so that we can use wreal.
wreal q;
assign q = 1.279 * a; //assignes the real value to wire

wrealsum => if 2 drivers driving, value on wire is sum of 2.
wrealmax => if 2 drivers driving, value on wire is max of 2.

wreal generally used for pwr supplies:
input VDD;
wrealsum VDD;
out = (VDD > MIN_SUPPLY) ? 1'b1 ? 1'bx; //assigns out to "x", if pwr supply too low.

NOTE: if gate netlist is generated using this RTL, then VDD may be defined as wire, which will cause compilation issues as VDD is wreal inside model. To get around that, change "wire" to "interconnect". In connecting models to digtop also, we use "interconnect" as it allows any signal to flow on it.

wreal model for Low pass filter:
---------------
module LPF (input wreal Vin, output wreal Vout);
initial begin
 #1 int_clk = 0;
 #1 int_clk = 1;
 Vout = Vin;
end

always @(int_clk) begin
 Vout = Vout + Vin/100; //put more conditions to check that Vout doesn't exceed Vin.
 int_clk <= #1 ~int_clk;
end

endmodule

 



Typedef: allows to create own names for type definitions that they will use frequently in the code

typedef reg [7:0] octet_t; => define new type. *_t usually used for user defined types
octet_t b; => creating b with that type
above 2 lines same as => reg [7:0] b;

enum: enumaerated data types allows us to define a data type whose values have names.
-----
enum { circle, ellipse, freeform } curve; => named values circle, etc here act like constants. The default type is int. So, anything defined to be of curve data type can take any value circle, ellipse, freeform all of which are int. so int 0=circel, 1=ellipse and so on. default basetype can be changed to anything as follows:
enum byte { circle, ellipse, freeform } curve; => here basetype is byte so byte 00=circle, 01=ellipse and so on.

Typedef is commonly used together with enum, like this:

typedef enum { circle, ellipse, freeform } ClosedCurve;
ClosedCurve c; => here c can take any value of datatype ClosedCurve (circle, ellipse, freeform). Here c is an integer, so any usage appr for int would be appr here.
c = freeform; => correct
c = 2; => however, this is incorrect as enum are strongly typed, so cpoying numeric value into a variable of enumeration type is not allowed, unless you use a type-cast:
c = ClosedCurve'(2); => casts int 2 to ClosedCurve type. So, c = freeform, as circle=0, ellipse=1 and so on. when using "c" anywhere, we are still working with c's integer equiv.
If we display "c" using %d, we can see it's int value. So, we can do comparison, arithmetic, etc with "c".

built in methods for enum: (i.e methods defined as "function enum first ();".
f = c.first(); =>  f gets c's first element = circle
f = c.name(); => f gets c's name (i.e circle, ellipse, freeform)
f = c.num(); => f gets c's integer value (it's 0 for circle, 1 for ellipse and so on)
f = c.next(); =>  f gets c's next element
f = c.last(); =>  f gets c's last element = freeform

ex: typedef enum shortint { circle, ellipse, freeform } ClosedCurve; => here ClosedCurve type can only take shortInt values and not 32 bit int.

 



package: similar to vhdl. container for shared declarations. NOT processes so no assign/always/initial blocks can be inside package.

package types_pkg; => declaring a pkg of name types_pkg
 typedef enum {ACTIVE,INACTIVE} mode_t; => defines a type mode_t
 class base; ... endclass
endpackage

module a;
 types_pkg::mode_t mode; => calling type mode_t from pkg types_pkg, and assigning it to variable mode. "::" is class scope resolution operator. If types_pkg is in file file1.sv, then that file has to be as one of the files to be compiled, else this pkg can't be found.
 import types_pkg::*; => can also do this. This imports all class/defn etc from package types_pkg, so that can be used in module "a" below
 ...
endmodule



struct and union: similar to C

struct {
  int x, y;
} p;

p.x = 1; => Struct members are selected using the .name syntax:
p = {1,2}; => Structure literals and expressions may be formed using braces.
p = {x:1,y:2} => named assignments

typedef struct {bit [7:0] r, g, b;} pixel_s; => defines typedef so that this struct can be shared across routines.
pixel_s my_pixel;

union: they save mem, are useful when you frequently need to read and write a register in several different formats.
ex:
typedef union { int i; real f; } num_u;
num_u un;
un.f = 0.0; // set n in floating point format

##########################

 



class:

user-defined data type. Classes consist of data (called properties) and tasks and functions to access the data (called methods). class instances are dynamic objects, declaring a class instance does not allocate memory space for object. Calling built in new() function creates memory for object. When we call methods, we pass handle of object, not the object itself. This is similar to using ref in SV, where address of arg is passed (if ref is not used in SV, then a copy is made of local var, and that is passed).
structs are static objects, so take up mem from start of pgm itself.

class declaration: class can be defined in program, module, package or outside of all of these. Good approach is to declare them outside of program or module and in a package. Class can be used (instantiated) in program and module only.
------------------
class C;
  int x; => by default all class member are publicly visible in SV (in contrast to pgm language, where they are are private by default. To hide it, it must be declared local: local int x; However, if declared local, it can't be accessed in extended classes, so we can declare it as protected int x; we can also use rand to any varaible to randomize it: protected rand integer x;
  stats_class stats; => here class C contains an inst of another class "stats_class". stats is handle to object stats. If stats_class is defined later, then to prevent compilation error use typedef before calling this class "typedef class stats_class;"
  static int count = 0; // count is static var, so is shared with all inst of class . It can be used to count Number of objects created by incrementing it in new() fn everytime it's called. static var are stored with class, and NOT the object, that's why they are able to retain value.
  function new (int a, ...); x=a; endfunction => initializes variables when new object is created. If new fn not defined, then default new constructor used.
  function new (logic [31:0] a=5, b=3...); x=b; endfunction => This assigns default values to args. So, if new(5,7) called then a=5, b=7, but if new() called then a=5, b=3.
 NOTE: this new in SV is diff than new[] used in verilog for dynamic array
  task set (int i);
    x = i;
  endtask
  function int get; => If we want to declare this fn outside of this class, use "function int C::get;". then use "extern function int get;" inside the class to tell the compiler.
    return x;
  endfunction
endclass => can also write as endclass:C => labels are useful in viewing, as we know which end corresponds to which start.

create object:
-------------
 C c1; => declares c1 to be C, i.e c1 can contain a handle to object of class C. It is init to NULL.
 c1 = new; => new assigns space for object of type C, initializes it (based on whatever default is for reg, bit, int, etc is in SV for that data type )and assigns its handle (returns it's address) to c1
or above 2 can be combined in one as:
 C c1 = new;

delete object:
------------
 c1 = null; => this assigns NULL pointer so object is deallocated by SV.
 Garbage collection is automatically run, and GC checks periodically of how many handles point to an object. If objects are no longer referenced, it implies object is unused, and hence freed.

using class:
-----------
initial
begin
  c1.set(3); => or we can say c1.x=3 (if we used local in x, then x can't be accessed this way).  
  $display("c1.x is %d", c1.get()); => In strict OOP, we should use use private methods get() and put()for accessing var of obj. However in SV, we may relax the rule and access them directly as it's more convenient.
end

extending class (inheritance):
-----------------------------
class ShiftRegister extends Register;
  task shiftleft;  data = data << 1; endtask
  task shiftright; data = data >> 1; endtask
endclass

scoping rules in SV:
------------------
scope = block of code such as a module, program, task, function, class or being-end block.The for and foreach loops automatically create
a block so that an index variable can be declared or created local to the scope of the loop.
You can define new variables in a block. New in SV is the ability to declare a variable in an unnamed begin-end block. A name can be relative to the current scope or absolute starting with $root. For a relative name, SV looks up the list of scopes until it finds a match. So, if we forget to declare var in local scope, then SV will use one from higher scope w/o warning if it finds one with same name. If you want to be unambiguous, use $root at the start of a name. "this" allows access to var from that class. So, this.name refers to "name" var within that class. If it's not present, SV will error out.
ex: int limit; // this can be accessed using $root.limit
 program p; int limit; ... endprogram // this can be accessed using $root.p.limit


constraints in class: All constraints in class need to be satisfied with randomize() call, or randomize() will fail.
-------------------
class c;
 ... rand struct packed {...} message; ...
 static constraint c1 {clk >= min_clk;} => NOTE: no semicoln at end
 static constraint c2 {message.DLC <= 8;} => or {message.DLC inside {[0:8]};}. forces DLC rand value to be in between 0 to 8
endclass

coverage: functional and code.
---------
1. covergroup:  is like a user defined type that encapsulates and specifies the coverage.  It can be defined in a package, module, program,  interface or class. covergroup has one or more coverpoint (each of which may have multiple bins)
ex:
class test_cov extends uvm_seq;
 apb_seq_item t;
 covergroup i2c_cg; [or additional cond can be specified for sampling => covergroup i2c_cg @(posedge clk iff (resetn == 1'b0)); //samples automatically on+ve clk while resetn=1]. sampling can also be done by explicitly calling .sample method, when sampling is reqd based on some calculations rather than events.
  i2cReg: coverpoint (t.data) iff (t.addr == `I2C_CTRL) { //if bins not specified, default bins are created based on all possible values of coverpoint. In this case, for all values of t.data
         wildcard bins i2cEn         ={16'b???????????????1}; //bin count is inc every time t.data LSB=1.
         wildcard bins i2cMode       ={16'b??????????????1?, 16'hFF0F}; //inc if any of these 2 values matches
                  bins invalid       =default;
      }
  CtrlReg: coverpoint digtop.porz; ...
  option.comment = "CG for I2c";
 endgroup : i2c_cg
 ...
 //function to sample
 function my_sample(apb_seq_item t);
  this.t=t;
  i2c_cg.sample; //sample is std keyword to sample variable. this triggers sampling of i2c_cg, when automatic sampling is not anabled above.
 endfunction
 //function to create new inst of i2c_cg
 function new (string name="i2c_cov",uvm_component parent=null) //string is valid type in SV
  super.new(name,parent);
  i2c_cg = new();
 endfunction
endclass

cg cg_inst = new; => creates inst. could have also created it by calling function new.
initial begin
 cg_inst.sample();//this causes it to sample all coverpoint in cg. or this sample can be called within a read task or something, when we know that sampling of this only needs to happen when we are in read task.
 //my_sample(t); //alt way of sampling by calling func my_sample
end

 



mailbox:

A mailbox is a communication mechanism that allows messages to be exchanged between processes. Data can be sent to a mailbox by one process and retrieved by another.
Mailbox is a built-in class that provides the following methods:
--Create a mailbox: new()
--Place a message in a mailbox: put()
--Try to place a message in a mailbox without blocking: try_put()
--Retrieve a message from a mailbox: get() or peek()
--Try to retrieve a message from a mailbox without blocking: try_get() or try_peek()
--Retrieve the number of messages in the mailbox: num()

 



program:

similar to module, but we do not want module to hold tb as it can have timing problems with sampling/driving, so new "program" created in SV. Just as in module, it can contain port, i/f, initial, final stmt. However it cannot contain "always" stmt. It was introduced so that full tb env can be put into program instead of in module, which is mostly used for describing h/w. Thus it separates tb and design, and provides an entry point to exe of tb.
NOTE: program can call task/func inside module but NOT vice-versa.

ex:
program my;
 mailbox my_mail; int packet;
  initial begin
   my_mail = new(); //creates inst of mailbox
   fork
    my_mail.put(0); //puts masg "0" in mailbox
    my_mail.get(packet); //gets whatever is in mailbox, and assigns it to int packet. here packet gets value of 0
   join_any
  end
endprogram

ex:
program simple (input clk, output logic Q);
 env env_inst; //env is a class defined somewhere
 initial begin //tb starts exec
   ... @(posedge clk); ...
   env_inst = new(); //call class/methods
   tb.do_it(); //calling task in module
 end
 task ack(..); ..
endprogram

module tb();
  ... always @(clk) ...
  simple I_simple(clk,reset); //inst the program above. Not necessary, as even w/o instantiating program, it will still work.
  task ack(..); //same name task as above, but still considered separate
endmodule

 



randomize: in testbench
---------
SV provides randomize() fn, which can randomize args passed to it. optional constraints can be provided. randomize returns "1" if successful.
ex: int var1,var2; if (randomize(var1,var2) with {var1<100; var2>200;}) $display("Var=%d",var);

For classes, built in randomize() method is called to randomize var which have rand or randc attribute. "rand" distributes equally, while "randc" is cyclic random  which randomly iterates over all the values in the range and no value is repeated with in an iteration until every possible value has been assigned. pre_randomize() and post_randomize() fn are also available. when randomize() is called, firstly pre_randomize is called, then randomize() and finally post_randomize(). All contraints within class will also need to be satisfied whenever randomize() is called, else it will fail.

c test_msg;
test_msg.randomize(); => randomizes all variables in class which have attribute rand.
test_msg.randomize() with {message.DCL==4;} => randomizes all except that DCL is fixed to 4

assert: in testbench. used to verify design, as well as provide func coverage
------
In SV, 2 kinds of assertions (aka SVA=system verilog assert):
1. immediate (assert): procedural stmt mainly used in sims
2. concurrent ("assert property", "cover property", "assume property" and "expect"): these use sequence and properties that describes design's behaviour over time, as defined by 1 or more clocks

1. procedural stmt similar to if. It tests a boolean expr and anything other than 1 is failure, which then reports severity level, file name, line num and simulation time of failure.
ex: A1: assert !(wr_en && rd_en); => will display error if wr_an and rd_en are "1" at same time.
ex: assert (A==B) $display("OK"); else $warning("Not OK"); //we can write our won pass/fail msg. $fatal, $error(default), $warning and $info are various severity levels.
ex: similar code written in verilog will take couple of lines.
begin: A1
 if (wr_en && rd_en) $display ("error");
end

2. concurrent stmt: here properties are built using sequences which are then asserted.
ex: assert property !(rd && wr); => This checks for this assertion at every tick of sim time.
ex: A1: assert property (@(posedge clk) !(rd_en && wr_en));  => A1 is label, property is a design behaviour that we want to hold true. event is the posedge clk, and on that event we look for expr !(rd_en && wr_en) to be true. Here assertion is checked for only at +ve clk. It samples rd_en and wr_en at +ve clk, and then checks at that clk edge. Usually it's temporal, i.e with some delay (#5), so that any delays in gate sims are accounted for.
ex: assert property (@(posedge Clock) Req |-> ##[1:2] Ack); => here Req and Ack are sampled at +ve clk, then whenever Req goes high, Ack should go high in next clk or following clk

system tasks $assert, $asseroff, $asserton etc also available (however these may not work on all simulators, so better to stick to SV assert cmd, and use tasks to turn asserts off, etc)

implication construct: allows to monitor seq. If LHS seq matches, then RHS seq is evaluated. So, this adds coditional matching of seq.
1. overlapped: |-> if there is match of LHS, then RHS is evaluated on same clk tick.
ex: req |-> ack //expr is true if req=1 results in ack=1 in same cycle
2. non overlapped: |=> if there is match of LHS, then RHS is evaluated on next clk tick
ex: req |=> ack //expr is true if req=1 results in ack=1 in next cycle (i.e ack is delayed by a cycle)

sequence delay:
ex: req ##[1:3] ack //expr is true if req=1 results in ack=1 in 1 to 3 cycles later
ex: req ##2 ack |-> ~err //expr is true if req=1 results in ack=1 2 cycles later (i.e ack is delayed by 2 cycles exactly). Then in that cycle that ack goes high, err=0 for implication to be true. ##2 equiv to ##[2:2]

functions:
1. $past(A) => past func. returns val of A on prev clk tick.
ex: req ##[2:4] ack |=> ~$past(err) //after ack goes high, err=0 the same cycle (|=> implies next cycle, but $past implies prev cycle, so effectively it's current cycle) for implication to be true.
2. $rose(A), $fell(A), $stable(A) => assess whether a signal is rising, falling or is stable b/w 2 clk ticks.
ex: req ##[2:4] ack |=> $stable(data) //data should be stable the next cycle after ack goes high

------
sequence request //seq is list of bool expr in order of inc time.
    Req; => Req must be true on current tick/clk (since no ## specified)
endsequence

sequence acknowledge
    ##[1:2] Ack; //##1=> Ack must be true on next tick/clk. ##[1:4]=> Ack must be true on 1st to 4th tick/clk anytime
endsequence

property handshake;
    @(posedge Clock) request |-> acknowledge; //ack must be true within 1-2 tick after req is true. implication const here adds if stmt, saying only if "req" is true, go ahead and eval "ack"
 // @(posedge Clock) request acknowledge; //here implication const not used. It means that on every +ve clk, both seq must be true, i.e: Req must be true and within 1-2 cycles after first +ve edge, ack must be true.
//  @(posedge Clock) disable iff (Reset) not b ##1 c; //here check is disabled if Reset=1. If Reset=0, then seq "b ##1 c" should never be 1.
endproperty

assert property (handshake); //property asserted here

---

bind: Assertions can be added in RTL, but when we want to keep assertions separate from RTL file, we need bind stmt to bind assertions to specific instances in RTL. bind works just like other module instantiation where we connect 1 module ports to other module ports:

bind digtop assertion_ip U_binding_ip (.clk_ip (clk), .... ); //here module "digtop" is binded to "assertion_ip" which has all assertions, and then vip_ports are connected to RTL ports. "digtop" is RTL DUT module, while "assertion_ip" is assertion module (with assertions ike property in it). Both have ports which are connected here. This bind stmt can be put in top level Tb file.


-----------



###################################
tasks/functions:
--------------
In verilog, args to tasks/functions can only be passed thru value. This involves making a local copy of args and then working on local copy, without modifying the original. However, sometimes we need to modify args globally within task/func. SV provides "pass args by reference" too to solve this problem.
1. pass by value: function int crc( logic signal1);
2. Pass by reference:  function int crc(ref logic signal1); => ref implies pass args by reference.

###################################



Arrays: In verilog, arrays are static (fixed size of array), however, SV allows dynamic arrays similar to malloc in C.

Static array:


Both packed and unpacked  arrays are available in both verilog/SV.
Ex: reg [7:0] a_reg_array; => packed array since array upper and lower bounds are declared between the variable type and the variable name. packed array in SV is treated both as array and a single value. It is stored as a contiguous set of bits with no unused space, unlike an unpacked array.
ex: bit [3:0] [7:0] bytes_4; // 2D packed array. 4 bytes packed into 32-bits
bytes_4 = 32¡¯hdead_beef; //here bytes_4[3]=8'hde, bytes_4[0][1]=1'b1. so bytes[3] to bytes[0] are in 32 bit single longword.
Ex: reg a_reg_array [7:0]; => unpacked array since array bounds are declared after the variable name

an array may have both packed and unpacked parts.
Ex: reg [7:0] reg_array [3:0][7:0];
Ex: bit [3:0] [7:0] barray [3]; // Packed: 3x32-bit, barry[0],[1],[2] are all 32 bit longword
barray[0] = 32'h0123_4567; barray[0][3] = 8'h01; barray[0][1][6] = 1¡¯b1; //Any bit/byte/word can be accessed

NOTE: reg a_reg_array [8]; => this is a compact form and has array [7:0]


Dynamic array:

Dynamic arrays in SV have limitation that dynamic part of the array must be of unpacked nature, and be 1 dimensional.
Ex: reg [7:0][3:0]      a_reg_array []; // dynamic array of 2 packed dim.
Ex: a_reg_array = new[4]; //here we allotted size of 4 to a_reg_array. i.e a_reg_array[3] to  a_reg_array[0]

NOTE: In verilog, we can't have 2D arrays as ports, but in SV, we can have that. So, during simulation, we provide +sv option for irun, so that verilog files are treated as SV files, and so 2D ports don't throw out an error.
Ex:    
 wire [8:0]     tb_2d_table[15:0]; => 2D array defined
 digtop dut (.y_2d_table(tb_y_2d_table), ... ); => In verilog, this would be illegal, but works in SV.

 

Queue:

in queues, size is flexible. It's single dim array, with all queue op permitted (sort, search, insert, etc). queues are similar to dynamic arrays, but grow/shrink of queue doesn't have performance penalty similar to dyn array, so better. No new() needed for queue.
ex: int q[$] = {0,1,3}; //defines queue with q[0]=0. q[1]=1, q[2]=3. q[$] defines queue.
q.insert(1,5); //inserts 5 at [1]st position, so new q={0,5,1,3}. displaying out of bound will cause error (i.e q[9])

associative array:

generally used for sparse mem. dynamically allocatted and are non-contiguous
ex: int aa[*]; //defines asso array

Foreach:

For looping thru array elements, we use for loop in verilog. However, we need to know array bounds, or we may go over the bounds and cause issues down the line (most of the tools do not report this as error). foreach in sv solves this issue.
ex: in verilog:
int arr[2][3]; => valid range from arr[0][0] to arr[1][2]
for (int i=0; i<=2; i++)
 for (int j=0; j<=3; j++)
   $display("%x",arr[i][j]); => NOTE: arrays arr[2][3] accesssed which is out of bound.

ex: in sv:
int arr[2][3];
foreach (arr[i][j]) => will automatically go thru all valid range. can also use for queue => foreach (q[i])
 $display("%x",arr[i][j]); => NOTE: This will display all arr vlues from arr[0][0] to arr[1][2]

foreach (md[i,j]) // NOTE: we don't have m[i][j] as in verilog code above.
 $display("md[%0d][%0d] = %0d", i, j, md[i][j]);

 



Interface:

An interface is a new sv construct. An interface is a named bundle of wires, similar to a struct, except that an interface is allowed as a module port, while a struct is not. So, when there are bunch of wires to be connected at multiple places, we can define them in an interface, and use that interface to make connections, saving typing. Adding/deleting signals is easy as only interface definition needs to be modified. Connections b/w modules remains the same.

The group of signals defined within i/f are declared as type logic, as they are just wires/nets. All of these nets are put inside a "interface" (similar to module). Now this interface can be inst just like module, and can also be connected to port like signal. To connect it as port, we just treat this instantiated interface as a net, and connect it to module port, just as we connect other nets to module ports. Interface has lot more capabilities than just being a substitute for a group of signals. It can have parameters, constants, variables, functions, and tasks inside it too. It can have assign stements, initial, always, etc similar to what modules can have.

Interface was inteneded to connect design and testbench, as that's where we had to either manually type all port names or use SV .* (provided port names were same on 2 sides) to connect all ports from TB to DUT. However, interface is now used within DUT and all Synthesis tools understand how to synthesize interface correctly.

Link => https://verificationguide.com/systemverilog/systemverilog-interface-construct/

Duolos 1 pager => https://www.doulos.com/knowhow/systemverilog/systemverilog-tutorials/systemverilog-interfaces-tutorial/

Interface definition:

interface intf; //signal addition/deletion is easy as it's done only at this place. Everywhere else, intf is just instantiated, and called as 1 entity.
  logic [3:0] a;
  logic [3:0] b;
  logic [6:0] c;
endinterface

Interface instantiation:

intf i_intf(); //this i_intf handle can now be passed to various modules.

Interface connections:

adder DUT ( //Here adder module has 3 separate ports. It may also have intf port as "intf intf_add" instead of having 3 separate ports. In that case, we can do connections as "adder DUT (.intf_add(i_intf))"
  .a(i_intf.a),
  .b(i_intf.b),
  .c(i_intf.c)
);

We can access/assign values of a,b etc as follows:
i_intf.a = 6;
i_intf.b = 4;
display("sum is %d", i_intf.c)

Virtual Interface:

SV interface above is static in nature, whereas classes are dynamic in nature. Because of this reason, it is not allowed to declare the interface within classes, but it is allowed to refer to or point to the interface. A virtual interface is a variable of an interface type that is used in classes to provide access to the interface signals.

Good example here: https://verificationguide.com/systemverilog/systemverilog-virtual-interface/

syntax: virtual interface_name inst_name => We just prepend the keyword "Virtual" before the defintition.

virtual intf i_intf()


Modport:

A new construct related to interface which provides direction information (input, output, inout or ref) to wires/signals declared within the interface. The keyword modport indicates that the directions are declared as if inside the module to which the modport is connected (i.e if modport i/f is connected to RAM, then modport dirn is one seen from the RAM). It also controls the use of tasks and functions within certain modules. Any signal declared as i/p port in modport can't be driven and will cause compilation error. So, using modport, we can restrict driving access by specifying direction as input. We can access wires declared in the modport in same way as in interface, except that there's one more hier in the name.

Ex here: https://verificationguide.com/systemverilog/systemverilog-modport/

My understanding is that if there was no modport, we wouldn't be able to assign "interface" sigmals to module defn, as module defn has i/p and o/p ports, while Interface only has logic or nets. By having modport with appr i/p, o/p defn, we can now assign these to module defn as shown below for TestRAM.

ex:
interface MSBus (input Clk); //interface may not have any i/p, o/p ports
  logic [7:0] Addr, Data; //nets used internally
  logic RWn;
  modport Slave (input Addr, inout Data); //internal nets declared as i/p and i/o for "Slave" instance. We can't drive/assign this net "Addr" anymore, and any attempt to do this will lead to compilation error - "Error-[MPCBD] Modport port cannot be driven"
  modport Master (output Addr); //internal net "Addr" decalred as o/p for "Master" instance.
endinterface

interface trim_if();
  logic clk, read, ...; //all dig/ana ports are connected to these logic signals "clk, read". This would not be true if dig,ana would be module (since then connectivity would have to be provided using .clk(clk) connections), but since these are modport, connectivity is assumed if same name for port/logic provided.
  modport dig (input clk, output read, ...); //all ports here used below in TestRam
  modport ana (...);
endinterface

module RAM (MSBus.Slave MemBus); // Here, MemBus is defined of type MSBus.Slave which is a modport, and has Addr=i/p and Data=inout which are assigned to RAM ports. MemBus is the port (Addr and Data are within MemBus modport i/f)

endmodule


module TestRAM (input a, trim_if.dig ram_if, output b,...); //module port defined as interface that has all other I/O ports. So, i/f port behaves as a bus with appropriate direction of bits, and can be accessed in simvision by expanding the i/f.
  logic Clk;
  trim_if my_if(); //defined my_if as of type trim_if. So my_if contains everything defined within trim_if. If we use my_if.dig to connect any other interface which is also of type trim_if.dig, then those i/o pins defined within dig get connected. So, saves typing as multiple signals get connected as single bus i/f.
  mod1(my_if); mod2(my_if); //here mod1 and mod2 modules are connected via bunch of wires (clk, read, etc) in my_if interface.  
  assign ram_if.clk = MCLK; //i/f signals can be assigned.
  MSBus i_bus(.Clk(Clk)); //instance the i/f. Now any signal from MSBus can be accessed.
  RAM TheRAM (.MemBus(i_bus.Slave)); //connect the i/f (Here MemBus needs to be defined of type i/f inside RAM module, or be defined as 8 bit addr and 8 bit data).
  assign i_bus.Slave.data = signal3; //signals can be assigned to i/f.
  ...
endmodule

Tasks/Functions in Interfaces:

Tasks and functions can be defined in interfaces to model to allow a more abstract level of modelling. We can call this from inside the TB module, and drive values on i/f to test the DUT.

 



Clocking Block:

A clocking block specifies timing and synchronization for a group of signals. It's usually for testbenches to synchronize clocking events b/w DUT and TB. Clocking block can be declared in interface, module or program block. We have i/p, o/p signals in a clocking block, along with optional skews that specify the delay of these signals. These delay are called skew and must be a constant expression and can be specified as a parameter. In case if the skew does not specify a time unit, the current time unit is used.

ex: In below ex, cb is defined as clocking block. It's synchronized on +ve edge of "clk", which is called the clocking event. "from_Dut" is i/p signal that is sampled at #1 time units before the +ve edge of clk (it's modeling setup time of 1 time unit). "to_Dut" is o/p signal that is driven #2 time units after the the +ve edge of clk (it's modeling c2q delay of 2 time units).

clocking cb @(posedge clk);
  default input #1 output #2; //Instead of defining default skew here, we may also specify skew with each i/p, o/p signal.
  input  from_Dut; //to specify skew here, we may write "input#1ps from_Dut;"
output to_Dut;
endclocking

@(cb); //this is the clocking block event that waits on "cb" block. This is equiv to @(posedge clk); We don't have to specify clk explicitly

Cycle Delay ##: #delay is used to specify the skew or delay in absolute time units. ## is a new one used to specify delay in terms of clk cycles. i.e ##8 means "wait 8 clk cycles".

 



create sine wave in system verilog:

module tb();
import "DPI" pure function real sin (input real rTheta);
   initial begin
      for (int i = 0; i < 120000; i++) begin
     time_ns = $time * 1e-9; //In this case timescale is in ns. But $time just gives the raw number. So we convert it into ns.
     sine_out = (sin(2*3.14*1000*time_ns));//sine_out is a real number. freq=1000=1KHz. So, in 10^-3sec=1ms, this should go from 0 to 2pi. So, $time will need to goto 10^6 units. Here 10^6ns or 1ms, which is what is expected. So, everything consistent. If timescale changes, we have to change the multiplying factor, or else freq will be off by orders of magnitude. Try using $realtime
     #5; //delay of 5 time units (here 5ns)
      end
   end // initial begin

NOTE: if we don't multiple $time by anything, then 2*pi*1000*time_ns will be very large number, and a multiple of 2*pi. That would imply that sine_out will always be 0. However, in reality, we are using 3.14 instead of pi, so, arg of sine func is not exactly 2*pi, but has little residue. That residue keeps on increasing in each loop, and generates it's own sine wave with a very large freq. That's a bogus sine wave, and has nothing to do with the frequency we are targetting.

------------------------------
to get access to shell variables from within verilog code, do this (only valid in sv)

import "DPI-C" function string getenv(input string env_name);
string sdf_path;
initial begin
 sdf_path = {getenv("HOME"), "/software/file.sdf"};   
 $write("sdf_path = %s \n",sdf_path); => prints sdf_path as /home/kagrawal/software/file.sdf
end

------------------------------
randcase: case statement that randomly selects one of its branches.prob of taking branch is based on branch weight.
---
ex:
randcase
 3 : x = 1; //prob=3/(3+1+4)=3/8 of assigning 1 to x
 1 : x = 2; //prob=1/8 of assigning 2 to x
 4 : x = 3;
endcase

Each call to randcase statement will return a random number in the range from 0 to SUM. $urandom_range(0,SUM) is used to generate a random number. As the random numbers are generated using $urandom are thread stable, randcase also exhibit random stability.

-----------------------------
$system task added in SV-2009 to call system cmd directly.

$system: to call unix cmds (called within any module):
-----
$system("rm -rf dir1/*.v");
To call some system cmd after finish of test, do:
final $system("rm -rf dir1/*.v");