Topic 06 — RTL Design

SystemVerilog
for RTL Design

SystemVerilog is the industry-standard HDL for RTL design in ASIC and FPGA. Beyond Verilog, it adds intent-explicit procedural blocks, the unified logic type, enums, packed structs, interfaces, and synthesis-safe case statements — making RTL code safer, more readable, and easier to verify.

45 min read
IEEE 1800-2017
Interactive Lab

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.

Industry reality: All major ASIC design houses (ARM, Intel, AMD, Qualcomm, Apple) mandate SystemVerilog for RTL. Synopsys Design Compiler, Cadence Genus, Xilinx Vivado, and Intel Quartus all support the synthesizable SV subset fully.

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.

VerilogSystemVerilogSynthesis result
wire [7:0] buslogic [7:0] busNet / wire
reg [7:0] countlogic [7:0] countFF or combo
reg driven by assignlogic driven by assignAllowed in SV
wire in always blocklogic in always blockAllowed in SV
wire driven by 2 sourceslogic driven by 2 sourcesError — 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
Rule: In RTL, drive each 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
Verilog equivalent: 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
Synthesis note: Packed structs and packed multi-dimensional arrays are fully synthesizable. Unpacked arrays infer memories (registers or SRAM depending on access pattern). Never use 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.

KeywordMeaningSynthesis effectSimulation 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.

ConstructSynthesizable?Notes
logic, bit, int unsignedYesPreferred types in RTL
always_ff / always_combYesUse instead of always @
typedef enum / struct packedYesFully supported
interface / modportYesDC 2016+, Genus, Vivado
$clog2()YesElaboration-time function
class / objectNoOOP — testbench only
stringNoDynamic type — sim only
Dynamic arrays logic []NoUse static arrays in RTL
$display / $monitorSim onlyIgnored by synthesis
Tasks with timing controls #nNoNon-synthesizable
real / shortrealNoFloating-point — sim only
mailbox / semaphoreNoUVM verification primitives
Interactive Lab — SV Pattern Explorer
Click a pattern to see the SystemVerilog construct, its Verilog equivalent, and what the synthesis tool infers.
D Flip-Flop with async reset. The most fundamental RTL building block. SystemVerilog's always_ff adds synthesis-time verification that this block truly infers only flip-flops — no accidental latches.
Verilog
always @(posedge clk or
         negedge rst_n)
begin
  if (!rst_n)
    q <= 1'b0;
  else
    q <= d;
end
SystemVerilog
always_ff @(posedge clk or
            negedge rst_n)
begin
  if (!rst_n)
    q <= 1'b0;
  else
    q <= d;
end
Synthesis infers: One D flip-flop with asynchronous active-low reset. Area ≈ 1 DFF cell. No latch. The always_ff keyword causes the simulator to error if the block has any path that doesn't depend on a clock edge.
4-to-1 Multiplexer — combinational logic. Verilog requires manually listing every signal in the sensitivity list; a missed signal creates a latch. always_comb auto-generates the full sensitivity list and errors if a latch is accidentally inferred.
Verilog
// 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
SystemVerilog
// 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
Synthesis infers: 4-to-1 multiplexer (parallel decode). With 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.
3-state Moore FSM using 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.
Verilog
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
SystemVerilog
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
Synthesis infers: 2 flip-flops (for 3 states, 2-bit encoding). With binary encoding the tool needs a small decoder; with one-hot the tool uses 3 FFs but simpler next-state logic. Specify encoding with synthesis attributes or let the tool choose based on target technology.
Packed struct as a bus type. Instead of passing 10 individual ports, group them into a named struct. Field access by name eliminates off-by-one bit slicing errors. The entire struct can be passed as a single port.
Verilog — manual bit fields
// 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
SystemVerilog — named fields
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
Synthesis infers: Identical result to the Verilog version — wires with the same connectivity. Zero hardware overhead. The struct is purely a compile-time abstraction that disappears in the netlist. Tools like Verdi and DVE show struct field names in waveforms.
Interface with modport for a handshake bus. Without an interface, adding one signal to a bus requires editing every port list in the hierarchy. With an interface, you edit one file. Modports enforce direction — driving an input is a compile error.
Verilog — 30-line port lists
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
);
SystemVerilog — one port
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);
Synthesis infers: Exactly the same wire connections as the Verilog version — the interface is flattened to individual ports during elaboration. No area overhead. Supported by Design Compiler 2016+, Genus, Vivado 2017+, and Quartus Pro.

Frequently Asked Questions

The RTL subset is fully synthesizable — 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.
Functionally identical in simulation and synthesis. The difference is semantic checking — 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.
Yes. Synthesizable interfaces with modports are supported by Synopsys Design Compiler (2016+), Cadence Genus, Vivado 2017+, and Quartus Prime Pro. The interface must not contain tasks with timing delays, class constructs, or dynamic memory. During synthesis, the tool flattens the interface into individual signals — zero hardware overhead compared to explicit port lists.
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.
Packed arrays are stored as a contiguous bit vector: 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.
Use 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.
← Topic 05: CDC