This is not a complete guide!
This is just here to give some glimpses at some SystemVerilog
additional features.
This is included here more for interest: you don't need to learn this material but you are welcome to use what you can if you wish.
You can find the full IEEE standard for SystemVerilog (legitimately!) via the University's subscription to IEEE Xplore
SystemVerilog
expands Verilog in numerous ways. A very useful addition is the
declaration (data type)
logic,
which is almost the same as
reg
(which, remember, is not always driven by a physical register)
but can be driven with
assign
too. This is one of the extensions you're most likely to encounter
first if you do this sort of thing in future.
Another new type is
bit
(it can still come in arrays to form variables); these bits always
have binary values and can't adopt
x or
z states.
| type | Language | Effect | States |
|---|---|---|---|
| wire aaa; | Verilog | Declare a net: a combinatorial logic value. | {0, 1, X, Z} |
| reg bbb; | Verilog | A signal which may (or may not) be state holding. | {0, 1, X, Z} |
| logic … | SystemVerilog | Declare either a net or a variable, depending on usage. | {0, 1, X, Z} |
| bit … | SystemVerilog | Like logic but can only adopt one of two states. Can still be a multibit variable. | {0, 1} |
These can be assigned by ‘Procedural Blocks’ to help avoid pitfalls resulting from potential ambiguities in its predecessor. For example always_comb can help to avoid accidental omissions in a sensitivity list, thus:
always_comb a = b & c;
can replace
always @ (*) a = b & c;
(although there are some subtle differences here, such as the former also executing at t = 0).
always_comb blocks are also sensitive to changes in inputs to any functions called from within, while always @ (*) blocks are not.
This is restricted to combinatorial blocks. Presumably the
idea is to ensure only ‘proper’ code is enclosed, so
providing an additional security check. With
always_comb
the sensitivity list is inferred by the tools (plus a
‘bonus’ execution after
initialisation).
Plus, any particular assigned (written) value can only be assigned
within one such block.
(The Verilog language allows a variable to be written to in multiple
always
blocks: this is generally Bad Practice and will be rejected by
most/all(?) logic synthesizers.)
There is a nasty ‘trap’ in ‘old-fashioned’ Verilog style which, presumably, arises from it beginning as a Hardware Description Language: this is the inadvertent introduction of latches.
always @ (*)
if (X) a = b + c;
else if (Y) a = b - c;
Verilog acts as a programming language, where a will not change if neither X nor Y is ‘true’. This is ‘natural’ in programming but assumes variable storage: when synthesized this means including an extra latch structure, which is (almost certainly) unwanted, and (definitely) expensive in area, power, timing and may introduce timing (glitch) problems in implementation.
Logic synthesizers typically spot this issue and produce ‘Warning’ messages — which developers typically ignore!
To avoid problems always include a definition for every output state in a combinatorial logic description: if/when you don't care, make this explicit and the synthesizer will (hopefully!) choose the cheapest possible implementation. Thus:
always @ (*)
if (X) a = b + c;
else if (Y)
a = b - c;
else
a = 'hx;
// Don't care
It's strongly suggested this is applied as a default: in all case statements too, as a good habit.
Similarly there are also always_ff and always_latch for registers and latches respectively, although these are (arguably) less of a boon.
Finally (here) there is final which is the scheduler's complement to initial. This(/these) run(s) after a simulation has ‘$finished’. They can be useful for (e.g.) outputting results and statistics or closing files. This is only useful for test code, of course.
initial is used in verification code to run a thread at start-up time. When a design is synthesised into an FPGA the synthesiser can interpret some initial statements and set up the contents of memories and the state of registers and flip-flops. This is because these values are downloaded when the FPGA is configured.
In an ASIC there is no downloading! Registers will not be initialised and (in Verilog terms) start with a value ‘X’. This can be very important when implementing state machines, et al. where an active ‘reset’ is usually needed. RAM must also be actively loaded; ROM contents must have been preprogrammed when the appropriate macrocell was specified.
There are explicit enumerated types which save listing definitions by hand.
For large hierarchies the inter-block wiring can become very complex with bundles of buses all heading in the same direction. In old-style Verilog this gets complicated/messy/error-prone. Interfaces provide a means of ‘wrapping’ associated signals into more complex buses which can be connected as single units.

module top();
logic clk;
logic reset;
logic bits_0;
logic bobs_0;
logic pieces_0;
logic bits_1;
logic bobs_1;
logic pieces_1;
source source_0(.clk(clk),
.reset(reset),
.bits(bits_0)),
.bobs(bobs_0)),
.pieces(pieces_0));
source source_1(.clk(clk),
.reset(reset),
.bits(bits_1)),
.bobs(bobs_1)),
.pieces(pieces_1));
destination dest(.clk(clk),
.reset(reset),
.bits_0(bits_0),
.bobs_0(bobs_0),
.pieces_0(pieces_0),
.bits_1(bits_1),
.bobs_1(bobs_1),
.pieces_1(pieces_1));
endmodule : top
module source(input logic clk,
input logic reset,
output logic bits,
output logic bobs,
output logic pieces);
...
endmodule : source
module destination(input logic clk,
input logic reset,
input logic bits_0,
input logic bobs_0,
input logic pieces_0,
input logic bits_1,
input logic bobs_1,
input logic pieces_1);
...
endmodule : destination

interface bundle;
logic bits;
logic bobs;
logic pieces;
endinterface
module top();
logic clk;
logic reset;
bundle stuff_0;
bundle stuff_1;
source source_0(.clk(clk),
.reset(reset),
.stuff_out(stuff_0));
source source_1(.clk(clk),
.reset(reset),
.stuff_out(stuff_1));
destination dest(.clk(clk),
.reset(reset),
.stuff_in_0(stuff_0),
.stuff_in_0(stuff_1));
endmodule : top
module source(input logic clk,
input logic reset,
bundle stuff_out);
...
endmodule : source
module destination(input logic clk,
input logic reset,
bundle stuff_in_0,
bundle stuff_in_1);
...
endmodule : destination
Access to the fields within an interface follows the convention you might guess at. Thus, for example, in the instance dest above, fields can be broken out as ‘stuff_in_0.bits’, ‘stuff_in_0.bobs’ etc.
Interfaces bundle together associated wiring so multiple buses can be handled as one entity. Structures serve a similar role for other elements, such as associated registers. The syntax is very C-like and should be more or less intuitive for anyone used to programming.
struct { bit [7:0] opcode; bit [23:0] addr; } IR;
(Nicked definitions from standard.)
typedef struct {
bit [7:0] opcode;
bit [23:0] addr;
} instruction;
// Type defined
instruction IR;
// Stucture variable defined
Verilog syntax can be both somewhat confusing and somewhat restrictive in this area. SystemVerilog at least alleviates some of the restrictions! First, some terminology:
In both cases the index is expanded across the specified range. The
index can be ascending or descending although it is typically
conventional to number bits-in-a-bus, at least, in a descending
(little-endian) sequence.
A one dimensional packed array (as
here) is sometimes called a ‘vector’. Only certain
data types may be ‘packed’ although these are the elements
you might expect, such as
reg,
logic etc.
The two types can be combined, such as:
logic [31:0] mem [0:1023];
which declares a 1 KiW memory of 32-bit words. Access is then available with statements such as:
data = mem[addr];
… and this is sometimes referred to explicitly as a
‘memory’.
SystemVerilog allows more detailed indexing so that (for example) a
byte could be extracted from the memory:
data = mem[addr][15:8];
(Note the position of the indices!)
It is possible to have – and combine – multiple packed and
unpacked dimensions in arrays. This can get quite confusing, so
don't be too tempted and take care (and read the documentation!)
before getting in too deeply.
SystemVerilog allows modules to be declared within other modules, limiting their scope accordingly.
Verilog can ‘get at’ any signal using the
appropriate hierarchical name
(with
‘.’
separators).
local_signal = module_X.submodule_Y.signal_of_interest;
This can be handy in verification to ‘probe’ the insides of nested modules – perhaps with some logic functions.
The improvement is testbench facilities is probably the most significant addition. These include (presumably) familiar concepts such as object-oriented programming and string handling.
Debugging is eased with assertions (also present in some earlier Verilog extensions, but not as formalised).
Typically bus signals ‘hang around’ even when not in use:
for example a memory address will possibly remain on the address bus
after a transfer is complete. For display and
verification (only) it can be convenient to highlight
the value only during the actual transfer.
A means of doing this is to create (within the testbench) some extra
signals for display purposes. Something like:
assign disp_address = (read || write) ? address : 'hXXXX;
This makes the signal ‘undefined’ (you could choose Z if preferred) when not in use which makes the transfers much more obvious in a trace.

Lots of syntactic features (such as ‘x++’) now work the way you might expect; loops can break or continue and so on. There's plenty more detailed stuff if interested and not enough time/space here to delve into it all.
Up to Verilog index
Onward to assertions.