SystemVerilog RTL Tutorial
This tutorial introduces some the new features in SystemVerilog that will make RTL design easier and more productive.
SystemVerilog adds a number of new operators, mostly borrowed from C. These include increment (++) and decrement (--), and assignment operators (+=, -=, ...). The wild equality operators (=== and !==) act like the comparisons in a casex statement, with X and Z values meaning “don’t care”.
New loop statements
Also from C is the do-while loop statement and break and continue. The new foreach loop is used with array variables. The for loop has been enhanced, so that the following is permitted:
for (int i = 15, logic j = 0 ; i > 0 ; i--, j = ~j) ...
In Verilog, you may label begin and fork statements:
begin : a_label
In SystemVerilog the label may be repeated at the end:
end : a_label
This is useful for documenting the code. The label at the end must be the same as the one at the beginning. Modules, tasks and functions may also have their names repeated at the end:
module MyModule ...
endmodule : MyModule
In SystemVerilog any procedural statement may be labelled:
loop : for (int i=0; ...
This is especially useful for loops, because they can then be disabled. Despite enhancing named blocks in this way, one reason for using them is removed: in SystemVerilog variables may be declared in unnamed blocks!
Relaxed Assignment Rules
Perhaps the hardest Verilog feature for beginners (and even experienced Verilog users are tripped up by it from time to time) is the difference between variables and nets. SystemVerilog consigns the confusion to history: variables may be assigned using procedural assignments, continuous assignments and be being connected to the outputs of module instances. Unfortunately, you still can'’t connect variables to inout ports, although you can pass them using ref ports.
This means that, in SystemVerilog, you would tend to use the logic data type most of the time, where in Verilog you would sometimes use reg and sometimes wire. In fact reg and logic are completely interchangeable, but logic is a more appropriate name.
There are some restrictions, though. You are not allowed to assign the same variable from more than one continuous assignment or output port connection. This is because there is no resolution for variables like there is for nets in the case of multiple drivers. Also, if you assign a variable in one of these way, you may not assign the same variable using procedural assignments.
Port Connection Shorthand
Suppose you are using Verilog-2001 and are writing a testbench for a module which has the following declaration:
module Design (input Clock, Reset, input [7:0] Data, output [7:0] Q);
In the testbench, you might declare regs and wires:
reg Clock, Reset;
reg [7:0] Data;
wire [7:0] Q;
and you would instance the module like this:
Design DUT ( Clock, Reset, Data, Q );
or, better, like this:
Design DUT ( .Clock(Clock), .Reset(Reset), .Data(Data), .Q(Q) );
But this is a bit repetitive. SystemVerilog allows you to use the following shorthand notation:
Design DUT ( .Clock, .Reset, .Data, .Q );
where appropriately named nets and variables have previously been declared, perhaps like this:
logic Clock, Reset;
logic [7:0] Data;
logic [7:0] Q;
If even this is too verbose, you can also write this:
Design DUT ( .* );
which means “connect all ports to variables or nets with the same names as the ports”. You do not need to connect all the ports in this way. For example,
Design DUT ( .Clock(SysClock), .* );
means “connect the Clock port to SysClock, and all the other ports to variables or nets with the same names as the ports.”
Verilog is very widely used for RTL synthesis, even though it wasn’t designed as a synthesis language. It is very easy to write Verilog code that simulates correctly, and yet produces an incorrect design.For example, it is easy unintentionally to infer transparent latches. One of the ways in which SystemVerilog addresses this is through the introduction of new always keywords: always_comb, always_latch and always_ff.
always_comb is used to describe combinational logic. It implicitly creates a complete sensitivity list by looking at the variables and nets that are read in the process, just like always @* in Verilog-2001.
f = x;
f = y;
n addition to creating a complete sensitivity list automatically, it recursively looks into function bodies and inserts any other necessary signals into the sensitivity list. It also is defined to enforce at least some of the rules for combinational logic, and it can be used as a hint (particularly by synthesis tools) to apply more rigorous synthesis style checks.Finally, always_comb adds new semantics: it implicitly puts its sensitivity list at the end of the process, so that it is evaluated just once at time zero and therefore all its outputs take up appropriate values before simulation time starts to progress.
always_latch and always_ff are used for infering transparent latches and flip-flops respectively. Here is an example of always_ff:
always_ff @(posedge clock iff reset == 0 or posedge reset)
q <= 0;
else if (enable)
The advantage of using all these new styles of always is that the synthesis tool can check the design intent.
Unique and Priority
Another common mistake in RTL Verilog is the misuse of the parallel_case and full_case pragmas. The problems arises because these are ignored as comments by simulators, but they are used to direct synthesis. SystemVerilog addresses this with two new keywords: priority and unique.
Unlike the pragmas, these keywords apply to if statements as well as case statements. Each imposes specific simulation behaviour that is readily mapped to synthesised hardware. unique enforces completeness and uniqueness of the conditional; in other words, exactly one branch of the conditional should be taken at run-time. If the specific conditions that pertain at run-time would allow more than one branch of the conditional, or no branch at all, to be taken, there is a run-time error. For example, it is acceptable for the selectors in a case statement to overlap, but if that overlap condition is detected at runtime then it is an error. Similarly it is okay to have a unique case statement with no default branch, or an if statement with no else branch, but at run time the simulator will check that some branch is indeed taken. Synthesis tools can use this information, rather as they might a full_case directive, to infer that no latches should be created.
priority enforces a somewhat less rigorous set of checks, checking only that at least one branch of the conditional is taken. It therefore allows the possibility that more than one branch of the conditional could be taken at run-time. It licenses synthesis to create more extravagant priority logic in such a situation.