You may have heard about SystemVerilog. And if you have, maybe you have also heard that SystemVerilog is simply an extension of Verilog, focused on testing and verification. That is both partly true and partly false. Let’s explore this a bit further to learn why that is the case.
A brief history of SystemVerilog
As you may know, Verilog is a Hardware Description Language (HDL) used to describe and model digital circuitry. Verilog was primarily developed for gate-level and register-transfer-level (RTL) design. It was developed in the 1980’s and transferred to the IEEE. The Verilog standard was ratified in 1995 (Verilog-1995) and then later significantly improved in 2001 (Verilog-2001). In general, when most people talk about Verilog, we specifically mean Verilog-2001. However Verilog did not include modern verification features, things like assertions, functional coverage, or random testing with constraints.
To solve this problem, a new extension to Verilog was proposed that added a number of verification features. These extensions were ratified by the IEEE in 2005 and the standard became known as SystemVerilog-2005.
Finally, in 2009 Verilog and SystemVerilog were combined into a single unified standard. The merged standard is, somewhat confusingly, called SystemVerilog-2009. It is reasonable to say that SystemVerilog-2009 is a proper superset of both Verilog-2001 and SystemVerilog-2005. When I say SystemVerilog I am generally referring to SystemVerilog-2009.
It is important to note that SystemVerilog is a large and complex language. And there are a number of differences between Verilog and SystemVerilog. However, for this article I am only focusing on the changes that affect the circuits we are exploring. Specifically, those focused on hardware description at the gate-level and register-transfer-level. The subset of the language focused on synthesizable hardware definitions, that is, designs that can be transformed into a physical device is much more limited.
Let’s be logical here
One of the most visible and important changes involves replacement of the wire and reg data types. Yes, you heard that right! You now no longer have to think about whether and where to use a wire vs. a reg. Instead, both data types can in most cases be replaced by a single data type called logic. We can then safely rely on the compiler to infer the right underlying signal type based on usage.
There are some cases, however, where the logic type is insufficient. One such example is that a logic type cannot correctly model a signal that is driven by multiple circuits. An example is shown below.
logic y;
always @*
y = foo & bar;
always @*
y = foo | bar;
In the above example, the SystemVerilog compiler is not able to properly choose a correct behavior using the logic type. In this case we must fall back to using the wire type.
It is always about the blocks
Another change to the language introduced by SystemVerilog is the addition of the always_ff and always_comb blocks. These are used as more explicit replacements for always @(…) and always @* blocks typically that are used for sequential and combinational circuits respectively. An always_ff block tells the compiler to infer a flip-flop or register. And an always_comb block signals an intention to infer a combinational circuit. Note that an always_comb block determines its own sensitivity.
As a quick example, let’s look at a simple D-type Flip-flop (D-FF). As you may recall, that code looked like the following in Verilog.
module dff
(
input clk,
input d,
output reg q
);
// each clock sample d and output as q
always @(posedge clk)
q <= d;
endmodule
In SystemVerilog that same design would look like the following.
module dff
(
input logic clk,
input logic d,
output logic q
);
// each clock sample d and output as q
always_ff @(posedge clk)
q <= d;
endmodule
There is also an always_latch block, which as you might expect, signals that a latching logic circuit should be inferred. However I have not yet needed to use that particular construct. So we won’t discuss it further beyond that an always_latch block determines its own sensitivity similar to an always_comb block.
Localparam challenges
Another useful and commonly used SystemVerilog addition is enumerated types. These can be a convenient replacement for explicit localparam definitions. For example, it is common in Verilog to explicitly define state values for a state-machine using localparam.
// define some state values
localparam idle = 2'b00, transmit = 2'b01, done = 2'b10;
// define a state register
reg [1 : 0 ] state;
While this works, it is prone to errors. In particular it is possible to duplicate values resulting in subtle run-time errors. A common source of such errors is the occasional need to insert new states and the resultant need to re-number following states. Another hard-to-find error related to the addition of new states is improperly sizing the state register.
Let’s revisit the example above looking at what can happen when adding new states to an existing design. There are two errors, can you spot them? The first is not too difficult to locate. The state values for transmit and receive are duplicated. Depending on the behavior of your Verilog compiler either you will receive an error for a duplicated case, or the code for the receive case may never be executed.
// define some state values
localparam idle = 2'b00, transmit = 2'b01, receive = 2'b01, ready = 2'b11, done = 2'b100;
// define a state register
reg [1 : 0 ] state_reg;
The second error is a bit more subtle. In the course of adding more states, the bit-width of our state register needs to increase from 2 bits to 3. However, in our haste to update the code we may have neglected to make that change. The result is that attempts to put a state value with more than 2 bits into the state register will be truncated. And that will result in erroneous state transitions.
Enumerated Types
The enumerated type helps to avoid both of these issues. In SystemVerilog we can rewrite the above example as follows.
// define some state values
typedef enum { idle, transmit, receive, ready, done } state_type;
// define a state register
state_type state_reg;
In this example we note two things. First, SystemVerilog will assign values to our states. And in that process determines the bit-width to properly represent all of the defined state values. Adding new states is simple and the compiler updates the underlying values and representation as appropriate.
Second, in this case SystemVerilog will also define a new type for us, in this case state_type, that is sized properly to hold all of the defined state values. If the number of states changes state_type is updated by the compiler as needed.
If it is unnecessary to define a type for re-use elsewhere we can combine defining the states with declaring a state register to hold those states.
// define our states AND declare a state register
enum { idle, transmit, receive, ready, done } state_reg;
Why continue to use Verilog?
Given the clear advantages that SystemVerilog provides over Verilog you might reasonably ask why use of Verilog continues. The answer is simple, there is a huge investment in legacy Verilog both in terms of training and in code. For development of entirely new designs, it makes good sense to use SystemVerilog. Since SystemVerilog is a proper superset of Verilog, that is generally the approach that I will follow. At the same time, there is still a very large set of consumers using Verilog. All significant language transitions take time to propagate.
Whether it makes sense to invest time and incur the risk of bugs in order to upgrade existing designs to SystemVerilog is a decision best left to the owners of that code.
And that is it for this time! There is a lot more to SystemVerilog than I was able to cover in a single post. As I discover more useful features I will share what I find.
Remember to please like and share this post. Have a comment, question or suggestion regarding this post? Please leave me a comment!