Why SystemVerilog over Verilog?
Verilog (IEEE 1364) was designed in the 1980s for simulation. Its wire/reg ambiguity, manual sensitivity lists, and absence of type safety cause entire classes of RTL bugs that only surface at synthesis or in silicon. SystemVerilog (IEEE 1800) is a strict superset that fixes these issues without breaking any existing Verilog code.
Unified logic type
Replaces both wire and reg. No more guessing which to use — the context determines whether it becomes a net or a variable in synthesis.
Intent-explicit always blocks
always_ff, always_comb, always_latch tell the tool and the reader exactly what is being described. The simulator checks for violations.
Type safety with enums
FSM states defined as typedef enum catch illegal state assignments at compile time and produce readable waveforms in simulation.
Interfaces reduce bugs
Group related signals into an interface with modport direction rules. A 20-signal AXI bus becomes one port — and direction errors are caught by the tool.
The logic Type
In Verilog, wire is a net (driven continuously, required for port connections) and reg is a variable (driven from an always block). This distinction causes confusing errors. SystemVerilog introduces logic — a 4-state variable (0, 1, X, Z) that works in both contexts.
| Verilog | SystemVerilog | Synthesis result |
|---|---|---|
wire [7:0] bus | logic [7:0] bus | Net / wire |
reg [7:0] count | logic [7:0] count | FF or combo |
reg driven by assign | logic driven by assign | Allowed in SV |
wire in always block | logic in always block | Allowed in SV |
wire driven by 2 sources | logic driven by 2 sources | Error — multi-driver |
// Verilog — two separate types, easy to confuse
wire [7:0] w_data; // net — must be driven by assign or output
reg [7:0] r_count; // variable — driven from always block
// SystemVerilog — single type for everything
logic [7:0] data; // works as wire (assign) or reg (always)
logic [7:0] count; // context determines wire vs flip-flop
logic valid, ready; // single-bit signalsSystemVerilog
logic signal from exactly one source — one always block or one assign statement. Multiple drivers cause X in simulation and a short circuit in synthesis.Intent-Explicit Procedural Blocks
Verilog's always @(...) is ambiguous — the same syntax describes flip-flops, latches, and combinational logic. SystemVerilog adds three dedicated keywords that document intent and enable semantic checking by the simulator and synthesis tool.
always_ff
Sequential logic driven by a clock edge. The tool errors if the block does not infer flip-flops. Sensitivity list is explicit and checked.
always_comb
Combinational logic. Equivalent to always @(*) but the tool also verifies full case coverage and warns on latches.
always_latch
Level-sensitive latch. Explicitly marks intentional latch inference — a signal to reviewers that the latch is deliberate, not a coding mistake.
// ─── always_ff : D flip-flop with async reset ───
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= 8'b0;
else
count <= count + 1'b1;
end
// ─── always_comb : combinational decoder ───
always_comb begin
// No sensitivity list needed — tool infers it automatically
case (opcode)
2'b00: result = a + b;
2'b01: result = a - b;
2'b10: result = a & b;
default: result = 8'b0;
endcase
end
// ─── always_latch : level-sensitive enable latch ───
always_latch begin
if (en)
q = d; // blocking assignment — latch transparent when en=1
endSystemVerilog
always_ff @(posedge clk) is identical to always @(posedge clk) in simulation and synthesis. The difference is the semantic check — SystemVerilog simulators (VCS, Questa, Xcelium) issue an error if the block does not actually describe flops.Enumerated Types for FSM Design
Defining FSM states as integer literals is error-prone and produces unreadable waveforms. SystemVerilog typedef enum gives states symbolic names, enables compile-time checks for illegal state assignments, and makes waveform debugging trivial — the debugger shows state names, not bit patterns.
// Define state encoding type — tool respects the bit-width
typedef enum logic [1:0] {
IDLE = 2'b00,
FETCH = 2'b01,
EXEC = 2'b10,
DONE = 2'b11
} state_t;
module fetch_ctrl (
input logic clk, rst_n, start,
output logic done
);
state_t state, next_state;
// Sequential state register
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) state <= IDLE;
else state <= next_state;
end
// Combinational next-state + output logic
always_comb begin
next_state = state; // default: hold state
done = 1'b0;
unique case (state)
IDLE: if (start) next_state = FETCH;
FETCH: next_state = EXEC;
EXEC: next_state = DONE;
DONE: begin done = 1'b1; next_state = IDLE; end
endcase
end
endmoduleSystemVerilog
The unique case keyword tells the synthesis tool that exactly one branch matches — enabling parallel decode optimization. Assigning an out-of-range value like state = 3'b100 is a compile-time error because the enum only holds 2-bit values.
Packed Structs and Arrays
SystemVerilog structs let you group related signals into a named type. Packed structs are stored as a contiguous bit vector — fully synthesizable, sliceable, and usable in arithmetic. This replaces error-prone manual bit-field extraction and makes bus definitions self-documenting.
// Define a packed AXI-like data struct
typedef struct packed {
logic [31:0] data;
logic [3:0] strb; // byte write strobes
logic valid;
logic last;
} axi_data_t; // total: 38 bits, contiguous
module packet_filter (
input axi_data_t rx,
output axi_data_t tx
);
// Access fields by name — no manual bit slicing
always_comb begin
tx = rx; // copy entire struct
tx.valid = rx.valid & rx.strb[0]; // byte 0 must be valid
tx.data = rx.data & 32'hFFFF_00FF;
end
endmodule
// Packed arrays — contiguous bit vectors
logic [3:0][7:0] word; // 4 bytes packed = 32 bits
assign word[2] = 8'hAB; // access byte 2 by index
assign out = word[31:16]; // can still slice as a flat vector
// Unpacked arrays — memory-like, one element per address
logic [7:0] mem [0:255]; // 256-byte RAM arraySystemVerilog
string, real, or dynamic arrays (logic []) in RTL — they are simulation-only.Interfaces and Modports
An interface bundles a group of related signals into a single named object. Modports define directional views of the interface — master vs slave, or initiator vs target — enforced at the port boundary. This eliminates the copy-paste port lists that cause connection bugs in large designs.
// Define the interface
interface simple_bus #(parameter W = 8) (
input logic clk
);
logic [W-1:0] data;
logic valid;
logic ready;
// Modport directions — master drives data/valid, reads ready
modport master (output data, valid, input ready, input clk);
modport slave (input data, valid, output ready, input clk);
endinterface
// Producer module uses master modport
module producer (simple_bus.master bus);
always_ff @(posedge bus.clk) begin
bus.valid <= 1'b1;
bus.data <= bus.data + 1'b1;
end
endmodule
// Consumer module uses slave modport
module consumer (simple_bus.slave bus);
always_comb
bus.ready = 1'b1; // always accept
endmodule
// Top-level instantiation — one port per module
module top (input logic clk);
simple_bus #(.W(16)) bus (.clk(clk)); // interface instance
producer p (.bus(bus));
consumer c (.bus(bus));
endmoduleSystemVerilog
Modport direction violations are caught by the tool at elaboration — if the consumer accidentally drives bus.data, it is an error. Without interfaces, this would be a silent multi-driver bug discovered only in simulation.
unique and priority Case Statements
Plain case in Verilog requires synthesis pragmas (// synthesis parallel_case) to get efficient decode logic. SystemVerilog adds unique case and priority case as first-class keywords that achieve the same result with formal simulation semantics.
| Keyword | Meaning | Synthesis effect | Simulation check |
|---|---|---|---|
case |
Standard case — priority encoder inferred | May generate priority mux chain | None |
unique case |
Exactly one branch matches. Full coverage guaranteed. | Parallel decode — smallest area | Error if 0 or >1 branch matches |
priority case |
First matching branch wins. May have overlapping conditions. | Priority encoder — like casex/casez | Warning if no branch matches |
unique if |
Applies to if-else chains — same as unique case | Parallel decode on if/else tree | Error if >1 condition true |
// unique case — tool generates optimal parallel decode
always_comb begin
unique case (sel) // exactly one branch matches
2'b00: out = a;
2'b01: out = b;
2'b10: out = c;
2'b11: out = d;
endcase
end
// priority case — first matching branch wins (like casex)
always_comb begin
priority case (1'b1) // equivalent to priority encoder
irq[3]: grant = 3;
irq[2]: grant = 2;
irq[1]: grant = 1;
irq[0]: grant = 0;
endcase
endSystemVerilog
Parameters, Localparams & Typedefs
SystemVerilog adds typed parameters and a richer type system for constants. Typed parameters catch dimension mismatches at elaboration time — passing a string where an integer is expected becomes an error rather than a silent truncation.
// Verilog — untyped, width can mismatch silently
parameter DEPTH = 16;
parameter WIDTH = 8;
// SystemVerilog — typed parameters (preferred)
parameter int unsigned DEPTH = 16; // integer type enforced
parameter int unsigned WIDTH = 8;
localparam int unsigned ADDR_W = $clog2(DEPTH); // derived, not overridable
// Typedef for reusable types across modules
typedef logic [WIDTH-1:0] data_t;
typedef logic [ADDR_W-1:0] addr_t;
module fifo #(
parameter int unsigned DEPTH = 16,
parameter int unsigned WIDTH = 8
) (
input logic clk, rst_n,
input logic wr_en, rd_en,
input logic [WIDTH-1:0] din,
output logic [WIDTH-1:0] dout,
output logic full, empty
);
localparam int unsigned AW = $clog2(DEPTH);
logic [WIDTH-1:0] mem [0:DEPTH-1];
logic [AW:0] wr_ptr, rd_ptr; // extra bit for full/empty distinction
assign full = (wr_ptr == {~rd_ptr[AW], rd_ptr[AW-1:0]});
assign empty = (wr_ptr == rd_ptr);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
wr_ptr <= '0; rd_ptr <= '0;
end else begin
if (wr_en && !full) begin mem[wr_ptr[AW-1:0]] <= din; wr_ptr <= wr_ptr + 1'b1; end
if (rd_en && !empty) begin dout <= mem[rd_ptr[AW-1:0]]; rd_ptr <= rd_ptr + 1'b1; end
end
end
endmoduleSystemVerilog
Synthesis Pitfalls & What to Avoid
SystemVerilog has two worlds: the RTL/synthesizable subset used in design, and the verification subset (OOP, dynamic memory, randomization) used in testbenches. Never mix them in a synthesizable module.
| Construct | Synthesizable? | Notes |
|---|---|---|
logic, bit, int unsigned | Yes | Preferred types in RTL |
always_ff / always_comb | Yes | Use instead of always @ |
typedef enum / struct packed | Yes | Fully supported |
interface / modport | Yes | DC 2016+, Genus, Vivado |
$clog2() | Yes | Elaboration-time function |
class / object | No | OOP — testbench only |
string | No | Dynamic type — sim only |
Dynamic arrays logic [] | No | Use static arrays in RTL |
$display / $monitor | Sim only | Ignored by synthesis |
Tasks with timing controls #n | No | Non-synthesizable |
real / shortreal | No | Floating-point — sim only |
mailbox / semaphore | No | UVM verification primitives |
always_ff adds synthesis-time verification that this block truly infers only flip-flops — no accidental latches.always @(posedge clk or
negedge rst_n)
begin
if (!rst_n)
q <= 1'b0;
else
q <= d;
end
always_ff @(posedge clk or
negedge rst_n)
begin
if (!rst_n)
q <= 1'b0;
else
q <= d;
end
always_ff keyword causes the simulator to error if the block has any path that doesn't depend on a clock edge.always_comb auto-generates the full sensitivity list and errors if a latch is accidentally inferred.// Manual sensitivity list
// Miss one signal = latch bug
always @(sel or a or b
or c or d)
begin
case (sel)
2'b00: y = a;
2'b01: y = b;
2'b10: y = c;
2'b11: y = d;
endcase
end
// Auto sensitivity list
// unique = parallel decode
always_comb begin
unique case (sel)
2'b00: y = a;
2'b01: y = b;
2'b10: y = c;
2'b11: y = d;
endcase
end
unique case: ~3 LUTs (FPGA) or balanced mux tree (ASIC). Without unique: priority encoder chain (~5 LUTs). The default branch is not needed when unique case guarantees full coverage.typedef enum. State names appear in waveform viewers by name (IDLE, ACTIVE, DONE) rather than binary values. Assigning an undefined state constant is a compile-time error.parameter IDLE = 2'b00;
parameter ACTIVE = 2'b01;
parameter DONE = 2'b10;
reg [1:0] state;
always @(posedge clk)
case (state)
IDLE: if (go)
state <= ACTIVE;
ACTIVE: if (done_i)
state <= DONE;
DONE: state <= IDLE;
endcase
typedef enum logic [1:0] {
IDLE = 2'b00,
ACTIVE = 2'b01,
DONE = 2'b10
} state_t;
state_t state;
always_ff @(posedge clk)
unique case (state)
IDLE: if (go)
state <= ACTIVE;
ACTIVE: if (done_i)
state <= DONE;
DONE: state <= IDLE;
endcase
// 10-bit bus: {valid,last,data[7:0]}
module consumer (
input [9:0] pkt
);
wire valid = pkt[9];
wire last = pkt[8];
wire [7:0] data = pkt[7:0];
// Easy to get indices wrong
endmodule
typedef struct packed {
logic valid;
logic last;
logic [7:0] data;
} pkt_t; // 10-bit packed
module consumer (
input pkt_t pkt
);
// Access by name — no indices
if (pkt.valid && pkt.last)
process(pkt.data);
endmodule
module producer (
output logic valid,
output logic [7:0] data,
input logic ready
// ... repeat at every level
);
module consumer (
input logic valid,
input logic [7:0] data,
output logic ready
// ... repeat at every level
);
interface bus_if;
logic valid;
logic [7:0] data;
logic ready;
modport src (output valid, data,
input ready);
modport dst (input valid, data,
output ready);
endinterface
module producer (bus_if.src b);
module consumer (bus_if.dst b);
Frequently Asked Questions
always_ff, always_comb, logic, enums, packed structs, interfaces with modports, unique/priority case, and typed parameters are all supported by Synopsys DC, Cadence Genus, Xilinx Vivado, and Intel Quartus. The verification subset (classes, dynamic arrays, mailboxes, strings, real numbers, $random) is simulation-only — never use it inside synthesizable modules.always_ff requires the simulator (VCS, Questa, Xcelium) to verify that the block truly infers only flip-flops. If you accidentally write combinational logic inside an always_ff, the simulator raises an error. It also communicates design intent clearly to the next engineer who reads the code.unique case tells the tool that exactly one branch will match (no overlapping conditions, full coverage guaranteed). Synthesis generates an optimal parallel decode instead of a priority mux chain — reducing area and improving timing. The simulator raises a runtime error if multiple branches match or no branch matches. It replaces the old Verilog synthesis pragmas // synthesis full_case parallel_case which had no simulation semantics.logic [3:0][7:0] word is a 32-bit packed array of 4 bytes — you can slice it as word[31:16] or access a byte as word[2]. Unpacked arrays are collections of separate elements stored like a C array: logic [7:0] mem [0:255] is 256 independent bytes — used for register files and RAMs. Only packed arrays support bit-level operations and arithmetic.logic in RTL. logic is 4-state (0, 1, X, Z) — it propagates X in simulation, which correctly models uninitialized registers and bus conflicts. bit is 2-state (0, 1 only) — X collapses to 0, hiding initialization bugs. Using logic means your simulation catches problems your synthesis tool would hide.