• Home   /  
  • Archive by category "1"

Systemverilog Struct Assignment Discovery

The difference between Verilog reg and Verilog wire frequently confuses many programmers just starting with the language (certainly confused me!). As a beginner, I was told to follow these guidelines, which seemed to generally work:

  • Use Verilog reg for left hand side (LHS) of signals assigned inside in always blocks
  • Use Verilog wire for LHS of signals assigned outside always blocks

Then when I adopted SystemVerilog for writing RTL designs, I was told everything can now be “type logic”. That again generally worked, but every now and then I would run into a cryptic error message about variables, nets, and assignment.

So I decided to find out exactly how these data types worked to write this article. I dug into the language reference manual, searched for the now-defunct Verilog-2005 standard document, and got into a bit of history lesson. Read on for my discovery of the differences between Verilog reg, Verilog wire, and SystemVerilog logic.

Verilog data types, Verilog reg, Verilog wire

Verilog data types are divided into two main groups: nets and variables. The distinction comes from how they are intended to represent different hardware structures.

A net data type represents a physical connection between structural entities (think a plain wire), such as between gates or between modules. It does not store any value. Its value is derived from what is being driven from its driver(s). Verilog wire is probably the most common net data type, although there are many other net data types such as tri, wand, supply0.

A variable data type generally represents a piece of storage. It holds a value assigned to it until the next assignment. Verilog reg is probably the most common variable data type. Verilog reg is generally used to model hardware registers (although it can also represent combinatorial logic, like inside an always@(*) block). Other variable data types include integer, time, real, realtime.

Almost all Verilog data types are 4-state, which means they can take on 4 values:

  • 0 represents a logic zero, or a false condition
  • 1 represents a logic one, or a true condition
  • X represents an unknown logic value
  • Z represents a high-impedance state

Verilog rule of thumb 1: use Verilog reg when you want to represent a piece of storage, and use Verilog wire when you want to represent a physical connection.

Assigning values to Verilog reg, Verilog wire

Verilog net data types can only be assigned values by continuous assignments. This means using constructs like continuous assignment statement (assign statement), or drive it from an output port. A continuous assignment drives a net similar to how a gate drives a net. The expression on the right hand side can be thought of as a combinatorial circuit that drives the net continuously.

Verilog variable data types can only be assigned values using procedural assignments. This means inside an always block, an initial block, a task, a function. The assignment occurs on some kind of trigger (like the posedge of a clock), after which the variable retains its value until the next assignment (at the next trigger). This makes variables ideal for modeling storage elements like flip-flops.

Verilog rule of thmb 2: drive a Verilog wire with assign statement or port output, and drive a Verilog reg from an always block. If you want to drive a physical connection with combinatorial logic inside an always@(*) block, then you have to declare the physical connection as Verilog reg.

SystemVerilog logic, data types, and data objects

SystemVerilog introduces a new 2-state data type—where only logic 0 and logic 1 are allowed, not X or Z—for testbench modeling. To distinguish the old Verilog 4-state behaviour, a new SystemVerilog logic data type is added to describe a generic 4-state data type.

What used to be data types in Verilog, like wire, reg, wand, are now called data objects in SystemVerilog. Wire, reg, wand (and almost all previous Verilog data types) are 4-state data objects. Bit, byte, shortint, int, longint are the new SystemVerilog 2-state data objects.

There are still the two main groups of data objects: nets and variables. All the Verilog data types (now data objects) that we are familiar with, since they are 4-state, should now properly also contain the SystemVerilog logic keyword.

There is a new way to declare variables, beginning with the keyword var. If the data type (2-state or 4-state) is not specified, then it is implicitly declared as logic. Below are some variable declaration examples. Although some don’t seem to be fully supported by tools.

Don’t worry too much about the var keyword. It was added for language preciseness (it’s what happens as a language evolves and language gurus strive to maintain backward-compatibility), and you’ll likely not see it in an RTL design.

I’m confused… Just tell me how I should use SystemVerilog logic!

After all that technical specification gobbledygook, I have good news if you’re using SystemVerilog for RTL design. For everyday usage in RTL design, you can pretty much forget all of that!

The SystemVerilog logic keyword standalone will declare a variable, but the rules have been rewritten such that you can pretty much use a variable everywhere in RTL design. Hence, you see in my example code from other articles, I use SystemVerilog logic to declare variables and ports.

When you use SystemVerilog logic standalone this way, there is another advantage of improved checking for unintended multiple drivers. Multiple assignments, or mixing continuous and procedural (always block) assignments, to a SystemVerilog variable is an error, which means you will most likely see a compile time error. Mixing and multiple assignments is allowed for a net. So if you really want a multiply-driven net you will need to declare it a wire.

In Verilog it was legal to have an assignment to a module output port (declared as Verilog wire or Verilog reg) from outside the module, or to have an assignment inside the module to a net declared as an input port. Both of these are frequently unintended wiring mistakes, causing contention. With SystemVerilog, an output port declared as SystemVerilog logic variable prohibits multiple drivers, and an assignment to an input port declared as SystemVerilog logic variable is also illegal. So if you make this kind of wiring mistake, you will likely again get a compile time error.

If you follow this rule, you can pretty much forget about the differences between Verilog reg and Verilog wire! (well, most of the time)

Conclusion

When I first wondered why it was possible to always write RTL using SystemVerilog logic keyword, I never expected it to become a major undertaking that involved reading and interpreting two different specifications, understanding complex language rules, and figuring out their nuances. At least I can say that the recommendations are easy to remember.

I hope this article gives you a good summary of Verilog reg, Verilog wire, SystemVerilog logic, their history, and a useful set of recommendations for RTL coding. I do not claim to be a Verilog or SystemVerilog language expert, so please do correct me if you felt I misinterpreted anything in the specifications.

References

wire my_wire;                       // implicitly means "wire logic my_wire"

wire logic my_wire;                 // you can also declare it this way

wire[7:0]my_wire_bus;             // implicitly means "wire logic[15:0] my_wire_bus"

wire logic[7:0]my_wire_logic_bus;// you can also declare it this way

reg[15:0]my_reg_bus;              // implicitly means "reg logic[15:0] my_reg_bus"

//reg logic [15:0] my_reg_bus;        // but if you declare it fully, VCS 2014.10 doesn't like it

  // From the SV-2012 LRM Section 6.8

  varbytemy_byte;    // byte is 2-state, so this is a variable

  //  var v;           // implicitly means "var logic v;", but VCS 2014.10 doesn't like this

  varlogicv;         // this is okay

  //  var [15:0] vw;   // implicitly means "var logic [15:0] vw;", but VCS 2014.10 doesn't like this

  varlogic[15:0]vw;// this is okay

  varenumbit{clear,error}status;// variable of enumerated type

  varregr;                          // variable reg

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

module my_systemverilog_module

(

  input  logic       clk,

  input  logic       rst_n,

  input  logic       data_in_valid,

  input  logic[7:0]data_in_bus,

  output logic       data_out_valid,// driven by always_ff, it is a variable

  output logic[7:0]data_out_bus,   // driven by always_comb, it is a variable

  output logic       data_out_err    // also a variable, driven by continuous assignment (allowed in SV)

);

 

  assign data_out_err=1'b1; // continuous assignment to a variable (allowed in SV)

//  always_comb data_out_err = 1'b0;// multiple drivers to variable not allowed, get compile time error

 

  always_comb data_out_bus=<data_out_bus logic expression>;

  always_ff@(posedge clk,negedge rst_n)

    if(!rst_n)

      data_out_valid<=1'b0;

    else

      data_out_valid<=<data_out_valid logic expression>;

  

endmodule

Verilog generate statement is a powerful construct for writing configurable, synthesizable RTL. It can be used to create multiple instantiations of modules and code, or conditionally instantiate blocks of code. However, many Verilog programmers often have questions about how to use Verilog generate effectively. In this article, I will review the usage of three forms of Verilog generate—generate loop, if-generate, and case-generate.

Types of Verilog Generate Constructs

There are two kinds of Verilog generate constructs. Generate loop constructs allow a block of code to be instantiated multiple times, controlled by a variable index. Conditional generate constructs select at most one block of code between multiple blocks. Conditional generate constructs include if-generate and case-generate forms.

Verilog generate constructs are evaluated at elaboration, which occurs after parsing the HDL (and preprocessor), but before simulation begins. Therefore all expressions within generate constructs must be constant expressions, deterministic at elaboration time. For example, generate constructs can be affected by values from parameters, but not by dynamic variables.

A Verilog generate block creates a new scope and a new level of hierarchy, almost like instantiating a module. This sometimes causes confusion when trying to write a hierarchical reference to signals or modules within a generate block, so it is something to keep in mind.

Use of the keywords generate and endgenerate (and begin/end) is actually optional. If they are used, then they define a generate region. Generate regions can only occur directly within a module, and they cannot nest. For readability, I like to use the generate and endgenerate keywords.

Verilog Generate Loop

The syntax for a generate loop is similar to that of a for loop statement. The loop index variable must first be declared in a genvar declaration before it can be used. The genvar is used as an integer to evaluate the generate loop during elaboration. The genvar declaration can be inside or outside the generate region, and the same loop index variable can be used in multiple generate loops, as long as the loops don’t nest.

Within each instance of the “unrolled” generate loop, an implicit localparam is created with the same name and type as the loop index variable. Its value is the “index” of the particular instance of the “unrolled” loop. This localparam can be referenced from RTL to control the generated code, and even referenced by a hierarchical reference.

Generate block in a Verilog generate loop can be named or unnamed. If it is named, then an array of generate block instances is created. Some tools warn you about unnamed generate loops, so it is good practice to always name them.

The following example shows a gray to binary code converter written using a Verilog generate loop.

Another example from the Verilog-2005 LRM illustrates how each iteration of the Verilog generate loop creates a new scope. Notice wire t1, t2, t3 are declared within the generate loop. Each loop iteration creates a new t1, t2, t3 that do not conflict, and they are used to wire one generated instance of the adder to the next. Also note the naming of the hierarchical reference to reference an instance within the generate loop.

Generate loops can also nest. Only a single generate/endgenerate is needed (or none, since it’s optional) to encompass the nested generate loops. Remember each generate loop creates a new scope. Therefore the hierarchical reference to the inner loop needs to include the label of the outer loop.

Conditional If-Generate

Conditional if-generate selects at most one generate block from a set of alternative generate blocks. Note I say at most, because it may also select none of the blocks. The condition must again be a constant expression during elaboration.

Conditional if-generate may be named or unnamed, and may or may not have begin/end. Either way, it can contain only one item. It also creates a separate scope and level of hierarchy, like a generate loop. Since conditional generate selects at most one block of code, it is legal to name the alternative blocks of code within the single if-generate with the same name. That helps to keep hierarchical reference to the code common regardless of which block of code is selected. Different generate constructs, however, must have different names.

Conditional Case-Generate

Similar to if-generate, case-generate can also be used to conditionally select one block of code from several blocks. Its usage is similar to the basic case statement, and all rules from if-generate also apply to case-generate.

Direct Nesting of Conditional Generate

There is a special case where nested conditional generate blocks that are not surrounded by begin/end can consolidate into a single scope/hierarchy. This avoids creating unnecessary scope/hierarchy within the module to complicate the hierarchical reference. This special case does not apply at all to loop generate.

The example below shows how this special rule can be used to construct complex if-else if conditional generate statements that belong to the same hierarchy.

This generate construct will select at most one of the generate blocks named u1. The hierarchical name of the gate instantiation in that block would be test.u1.g1. When nesting if-generate constructs, the else always belongs to the nearest if construct. Note the careful placement of begin/end within the code Any additional begin/end will violate the direct nesting requirements, and cause an additional hierarchy to be created.

Named vs Unnamed Generate Blocks

It is recommended to always name generate blocks to simplify hierarchical reference. Moreover, various tools often complain about anonymous generate blocks. However, if a generate block is unnamed, the LRM does describe a fixed rule for how tools shall name an anonymous generate block based on the text of the RTL code.

First, each generate construct in a scope is assigned a number, starting from 1 for the generate construct that appears first in the RTL code within that scope, and increases by 1 for each subsequent generate construct in that scope. The number is assigned to both named and unnamed generate constructs. All unnamed generate blocks will then be given the name genblk[n] where [n] is the number assigned to its enclosing generate construct.

It is apparent from the rule that RTL code changes will cause the unnamed generate construct name to change. That in turn makes it difficult to maintain hierarchical references in RTL and scripts. Therefore, it is recommended to always name generate blocks.

Conclusion

Verilog generate constructs are powerful ways to create configurable RTL that can have different behaviours depending on parameterization. Generate loop allows code to be instantiated multiple times, controlled by an index. Conditional generate, if-generate and case-generate, can conditionally instantiate code. The most important recommendation regarding generate constructs is to always name them, which helps simplify hierarchical references and code maintenance.

References

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

Example of parameterized gray tobinary code converter

 

Module gray2bin

#(parameter SIZE = 8)

(

  input[SIZE-1:0]gray,

  output[SIZE-1:0]bin

)

 

Genvar gi;

// generate and endgenerate is optional

// generate (optional)

  for(gi=0;gi<SIZE;gi=gi+1)begin:genbit

    assign bin[i]=^gray[SIZE-1:gi];

  end

// endgenerate (optional)

endmodule

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

module addergen1

#(parameter SIZE = 4)

(

  input  logic[SIZE-1:0]a,b,

  input  logic            ci,

  output logic            co,

  output logic[SIZE-1:0]sum

);

 

wire[SIZE:0]c;

genvari;

 

assignc[0]=ci;

 

// Hierarchical gate instance names are:

// xor gates: bitnum[0].g1 bitnum[1].g1 bitnum[2].g1 bitnum[3].g1

// bitnum[0].g2 bitnum[1].g2 bitnum[2].g2 bitnum[3].g2

// and gates: bitnum[0].g3 bitnum[1].g3 bitnum[2].g3 bitnum[3].g3

// bitnum[0].g4 bitnum[1].g4 bitnum[2].g4 bitnum[3].g4

// or gates: bitnum[0].g5 bitnum[1].g5 bitnum[2].g5 bitnum[3].g5

// Gate instances are connected with nets named:

// bitnum[0].t1 bitnum[1].t1 bitnum[2].t1 bitnum[3].t1

// bitnum[0].t2 bitnum[1].t2 bitnum[2].t2 bitnum[3].t2

// bitnum[0].t3 bitnum[1].t3 bitnum[2].t3 bitnum[3].t3

 

for(i=0;i<SIZE;i=i+1)begin:bitnum

  wire t1,t2,t3;

  xorg1(t1,a[i],b[i]);

  xorg2(sum[i],t1,c[i]);

  andg3(t2,a[i],b[i]);

  andg4(t3,t1,c[i]);

  org5(c[i+1],t2,t3);

end

 

assign co=c[SIZE];

 

endmodule

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

module test;

  parameterp=0,q=0;

  wirea,b,c;

 

  //---------------------------------------------------------

  // Code to either generate a u1.g1 instance or no instance.

  // The u1.g1 instance of one of the following gates:

  // (and, or, xor, xnor) is generated if

  // {p,q} == {1,0}, {1,2}, {2,0}, {2,1}, {2,2}, {2, default}

  //---------------------------------------------------------

 

  if(p==1)

    if(q==0)begin:u1// If p==1 and q==0, then instantiate

      andg1(a,b,c);// AND with hierarchical name test.u1.g1

    end

    elseif(q==2)begin:u1// If p==1 and q==2, then instantiate

      org1(a,b,c);// OR with hierarchical name test.u1.g1

    end

    // "else" added to end "if (q == 2)" statement

    else;// If p==1 and q!=0 or 2, then no instantiation

  elseif(p==2)

    case(q)

      0,1,2:

        begin:u1// If p==2 and q==0,1, or 2, then instantiate

          xorg1(a,b,c);// XOR with hierarchical name test.u1.g1

        end

      default:

        begin:u1// If p==2 and q!=0,1, or 2, then instantiate

          xnor g1(a,b,c);// XNOR with hierarchical name test.u1.g1

        end

    endcase

 

endmodule

Jason Yu

SoC Design Engineer at Intel Corporation

Jason has 10 years' experience in the semiconductor industry, designing and verifying Solid State Drive controller SoC. His areas of workinclude microarchitecture and RTL design, dynamic and formal verification using UVM and Cadence JasperGold, and full-chip low power verification with UPF. Thoughts and opinions expressed in articles are personal and do not reflect that of Intel Corporation in any way.

Latest posts by Jason Yu (see all)

Categories Design, VerilogTags case generate, generate, if generate

One thought on “Systemverilog Struct Assignment Discovery

Leave a comment

L'indirizzo email non verrĂ  pubblicato. I campi obbligatori sono contrassegnati *