SystemVerilog for Design (IEEE 1800)
Volume 01 called reg the worst-named keyword in the language and promised
that SystemVerilog fixes it. It does - but the real theme of this volume is bigger than
naming. Every feature here exists to convert a class of bug that Verilog would let you
ship into an error the compiler catches before you run a single test.
8.1 2-state vs 4-state data types
Verilog had one value system: four states (0, 1, X,
Z). SystemVerilog adds a second, faster family that holds only
0 and 1. Choosing between them is a real decision with real
consequences.
| Type | States | Width | Signed | Typical use |
|---|---|---|---|---|
| logic | 4 | 1 (packable) | no | Everything in RTL |
| wire | 4 | 1 (packable) | no | Only where multiple drivers are needed |
| integer | 4 | 32 | yes | Legacy loop counters |
| bit | 2 | 1 (packable) | no | Testbench flags, counters |
| byte | 2 | 8 | yes | Testbench data |
| int | 2 | 32 | yes | Loop variables, scoreboard maths |
| longint | 2 | 64 | yes | Large counters, addresses |
The temptation is to reach for 2-state types everywhere - they simulate roughly twice as fast and use half the memory. In RTL that is a mistake, and here is why.
X is not a nuisance; it is a free bug
detector. A 2-state variable quietly initialises to 0, which happens to match what
the test expected - so a genuine missing-reset bug sails through simulation and shows up
in the lab, where real flops power up unpredictably.
logic) for all RTL and everything on the DUT boundary.
You want X propagation there - it is the mechanism from Volume 1.3 that makes missing
resets and bus conflicts visible. Use 2-state types for testbench internals:
loop counters, scoreboard arithmetic, array indices - places where an X would be
meaningless anyway.
Why logic is safer than reg and wire
logic can be driven procedurally or continuously, which removes the
wire/reg decision entirely. But the real win is what it
forbids: a logic variable may have only one driver, and the
compiler enforces it.
module dual_drive (
input wire a, b, sel,
output wire y
);
// TWO continuous drivers on one wire.
// Verilog allows this. The result is
// resolved by strength - in practice
// X whenever they disagree.
assign y = a;
assign y = b;
// No error. No warning in many tools.
// You find it in the waveform, days later.
endmodule
module dual_drive (
input logic a, b, sel,
output logic y
);
assign y = a;
assign y = b;
// ^^^
// ERROR: variable 'y' driven by more
// than one continuous assignment.
//
// Caught at COMPILE time, with a file
// and line number, before any test runs.
endmodule
wire
Exactly one case: a genuine multi-driver net - a tri-state bus where
several devices take turns driving, as in Volume 1.3. That is the whole point of a net
type, and logic deliberately cannot express it. Inside a modern chip those
are rare; at I/O pads they are still the norm.
8.2 Enums, structs & user-defined types
Volume 04 built state machines out of localparam constants. That works, but the
state variable is just a number - nothing stops you assigning 7 to a 5-state machine, and
your waveform viewer shows 3'b011 instead of S101.
// The base type is NOT optional in practice. Without "logic [2:0]" an
// enum defaults to a 32-bit 2-state int - wasteful, and it loses X
// propagation on your state register.
typedef enum logic [2:0] {
S0 = 3'd0,
S1 = 3'd1,
S10 = 3'd2,
S101 = 3'd3,
S1011 = 3'd4
} state_e;
state_e state, next_state;
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) state <= S0;
else state <= next_state;
always_comb begin
next_state = state;
unique case (state) // "unique" asks the tool to check overlap
S0 : next_state = din ? S1 : S0;
S1 : next_state = din ? S1 : S10;
S10 : next_state = din ? S101 : S0;
S101 : next_state = din ? S1011 : S10;
S1011: next_state = din ? S1 : S10;
endcase
end
// Enums carry methods. This prints "S101", not "3".
always_ff @(posedge clk)
if (trace_en) $display("state = %s", state.name());
| What you gain | Why it matters |
|---|---|
| Waveforms show names | Debugging an FSM by reading 3'b011 is how afternoons disappear |
| Type checking | Assigning a raw integer to an enum is an error, not a silent illegal state |
.name() | Log messages that a human can read |
.next() .first() .num() | Iterate every state in a testbench without a hand-written list |
| One place to add a state | The typedef. Not five localparam lines across three files |
Packed structs: a bundle that is still a bus
packed is the keyword that keeps a struct
usable as hardware. The first field declared becomes the most significant bits, so the
layout is predictable and every field is simultaneously a named member and a bit range.
typedef struct packed {
logic valid; // bit 36 - first declared = most significant
logic [3:0] id; // bits 35:32
logic [31:0] data; // bits 31:0
} pkt_t; // 37 bits total
// A whole bundle crosses a module boundary as ONE port.
module sink (
input logic clk,
input pkt_t pkt_in
);
always_ff @(posedge clk)
if (pkt_in.valid)
$display("id=%0d data=%h", pkt_in.id, pkt_in.data);
endmodule
// Because it is packed, all of these are legal and mean the same thing:
// pkt.data pkt[31:0] pkt_t'(37'h1_0000_00FF)
//
// Adding a field to pkt_t updates every module that uses it. No port
// lists to edit, nothing to get out of step.
8.3 Procedural blocks with synthesis intent
Volume 1.5 spent a whole section on accidental latches, and ended with a promise: there is a way to make the tool catch them for you. This is it.
always @(*) tells the tool what to be sensitive to.
always_comb tells it what you meant - and a tool that knows
your intent can check it.
always_* family.
always @(*) |
always_comb |
|
|---|---|---|
| Latch inference | Warning, if you are lucky | Error |
| Runs at time 0 | Not guaranteed - waits for an input to change | Always |
| Sensitive to function contents | No - a classic silent bug | Yes |
| Another block may assign the same var | Allowed - race conditions follow | Compile error |
// COMBINATIONAL - the tool errors if this could infer a latch.
always_comb begin
next_state = state; // default first (Volume 1.5 still applies)
case (state)
IDLE: if (start) next_state = RUN;
RUN : if (done) next_state = IDLE;
default: next_state = IDLE;
endcase
end
// SEQUENTIAL - the tool errors if this is not a flip-flop.
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) count <= '0; // '0 fills the whole width with zeros
else if (en) count <= count + 1'b1;
end
// LATCH - deliberately. Declaring it makes review trivial: an
// always_latch is a decision, an inferred latch is an accident.
always_latch begin
if (gate) q_held <= d;
end
'0 and '1 fill any width with zeros or ones, so
{W{1'b0}} becomes '0 and stays correct when the width changes.
unique case asks the tool to check that branches do not overlap and that one
always matches - the safe, checked version of the parallel_case pragma
Volume 4.5 told you never to use.
8.4 Interfaces, modports & clocking blocks
A bus is a dozen signals that always travel together. In Verilog you write those twelve names in every port list, every instantiation, and every wire declaration between them - and when the bus gains a thirteenth signal, you edit all of it.
pprot to the bus becomes a one-line change in one file.
interface apb_if #(
parameter int AW = 32,
parameter int DW = 32
) (
input logic pclk,
input logic presetn
);
logic [AW-1:0] paddr;
logic psel, penable, pwrite;
logic [DW-1:0] pwdata, prdata;
logic pready, pslverr;
// A modport is a VIEW of the same signals with directions applied.
modport master (
input pclk, presetn, prdata, pready, pslverr,
output paddr, psel, penable, pwrite, pwdata
);
modport slave (
input pclk, presetn, paddr, psel, penable, pwrite, pwdata,
output prdata, pready, pslverr
);
endinterface
// The module declares a ROLE, not eight ports.
module apb_slave (apb_if.slave bus);
always_ff @(posedge bus.pclk or negedge bus.presetn) begin
if (!bus.presetn) begin
bus.prdata <= '0;
bus.pready <= 1'b0;
end else begin
bus.pready <= bus.psel && bus.penable;
if (bus.psel && bus.penable && !bus.pwrite)
bus.prdata <= regfile[bus.paddr[7:2]];
end
end
endmodule
Clocking blocks - solving a race you already know about
A testbench driving and sampling at the same clock edge as the DUT hits exactly the race from Volume 1.2: two processes triggered by one edge, with no defined order. Clocking blocks fix it by moving testbench sampling into a later region of the stratified event queue.
input #1step samples in the Postponed region of
the previous time step - the settled value the hardware itself would have seen.
Deterministic, every run, on every simulator.
8.5 Packages, compilation units & $cast
Types are only useful if every module agrees on them. Verilog's answer was
`include and a forest of macros. SystemVerilog's answer is the
package: a proper namespace holding types, parameters and functions.
package chip_pkg;
localparam int ADDR_W = 32;
localparam int DATA_W = 64;
typedef enum logic [1:0] { IDLE, BUSY, DONE, ERROR } state_e;
typedef struct packed {
logic valid;
logic [3:0] id;
logic [DATA_W-1:0] data;
} pkt_t;
// Functions in packages must be `automatic` - a static function would
// share one set of locals across every concurrent call.
function automatic logic [3:0] parity_nibble(input logic [15:0] v);
return {^v[15:12], ^v[11:8], ^v[7:4], ^v[3:0]};
endfunction
endpackage
// Three ways to reach into it - pick one and be consistent.
import chip_pkg::*; // everything (convenient, can collide)
import chip_pkg::state_e; // one name (explicit, verbose)
// chip_pkg::state_e s; // fully qualified, no import at all
module core (
input logic clk,
input chip_pkg::pkt_t pkt_in
);
chip_pkg::state_e state;
...
endmodule
$cast: converting into an enum safely
A static cast state_e'(raw) converts without checking, so an out-of-range value
lands your FSM in an encoding that does not exist - precisely the illegal-state problem from
Volume 4.5. $cast checks first and tells you whether it worked.
import chip_pkg::*;
state_e s;
logic [1:0] raw;
// STATIC cast - no check. If raw is not a legal encoding you now hold an
// enum variable containing a value the type says cannot exist.
s = state_e'(raw);
// DYNAMIC cast - returns 1 on success, 0 on failure. Used as a function
// it lets you handle the bad case instead of corrupting state.
if (!$cast(s, raw))
$error("illegal state encoding: %0d", raw);
// Reading a register written by software is exactly this situation: the
// value came from outside the design and cannot be trusted to be legal.
Interview grilling - "You have a legacy Verilog codebase. Which SystemVerilog features would you introduce first, and why?"
They are testing judgement, not vocabulary. The good answer is ordered by bugs prevented per line changed, and acknowledges risk.
-
always_comb/always_ff. Highest value, near zero risk - a pure find-and-replace that immediately converts latent latch bugs into build failures. Expect it to break the build on day one. That is the feature working. -
logicinstead ofwire/reg. Mechanical, and it buys single-driver checking. Leave genuine tri-state nets aswire. -
Typed enums for state machines. Small change, enormous debug payoff
the first time a waveform shows
WAIT_ACKinstead of3'b101. -
Packages for shared types and parameters. Kills the
`include/macro layer, at the cost of touching the build file order. - Packed structs for buses that keep gaining signals.
- Interfaces - last, and cautiously. Biggest structural change, and the one most likely to trip lint rules, legacy IP integration or an older synthesis flow.
The line that shows seniority: "I would take them in that order because the first three are locally verifiable - I can convert one module, diff the synthesized netlist, and prove nothing changed. Interfaces alter module boundaries, so they cannot be validated that cheaply."
Volume 08 recap
| Concept | The one thing to remember |
|---|---|
| 4-state vs 2-state | logic in RTL - X propagation is a free bug detector, not a nuisance. |
logic |
Drivable procedurally or continuously, and limited to one driver. |
wire |
Still required for genuine multi-driver tri-state nets. Nothing else. |
| Enums | Always give a base type: enum logic [2:0]. Names in waveforms. |
| Structs | packed or it is not a bus. First field declared = most significant. |
always_comb |
Turns the Volume 1.5 latch warning into a build-stopping error. |
'0 / unique case |
Width-independent fills, and the checked replacement for parallel_case. |
| Interfaces | One file owns the bus. Modports enforce direction. Adopt last. |
| Clocking blocks | Verification-only. #1step samples the pre-edge value, race-free. |
$cast |
Checked conversion into an enum. A static cast checks nothing. |