The short answer: For RTL design, SystemVerilog gives you logic instead of wire and reg, always_ff, always_comb and always_latch to state intent, enums and packed structs for readable state and bus fields, unique and priority for safer case statements, packages for shared types and functions, and interfaces for buses. All of these are synthesisable and supported by mainstream tools. Classes, randomisation and dynamic types are testbench-only.
I write all new RTL in SystemVerilog. The language doesn’t change what hardware you can build, but it catches mistakes earlier and makes code shorter and easier to review. This post covers the synthesisable subset and what synthesis does with it, combining my older posts on SystemVerilog for design, synthesis and timing considerations, and packages.
- The features worth using
- always_ff, always_comb and always_latch
- unique and priority
- Packages
- Other small wins
- Synthesis and timing considerations
- Not synthesisable
Table of Contents
Part 1: The features worth using

Part 2: always_ff, always_comb and always_latch
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) state <= IDLE;
else state <= next;
always_comb begin
next = state;
case (state)
IDLE: if (start) next = RUN;
RUN: if (done) next = IDLE;
default: next = IDLE;
endcase
end
- always_comb has an automatic, complete sensitivity list (including signals read inside functions) and runs once at time zero.
- Tools warn if always_comb infers a latch or always_ff doesn’t infer flops.
- A variable written in an always_comb or always_ff block can’t be written anywhere else.
Part 3: unique and priority
always_comb begin
unique case (sel) // exactly one item should match
2'b00: y = a;
2'b01: y = b;
2'b10: y = c;
2'b11: y = d;
endcase
end
always_comb begin
priority if (irq[2]) id = 2; // first match wins; one must match
else if (irq[1]) id = 1;
else if (irq[0]) id = 0;
end
They replace the old full_case and parallel_case synthesis pragmas, which changed synthesis without telling the simulator. With unique and priority, the simulator reports violations, so a wrong assumption shows up as a warning instead of a silent mismatch. Still add a default unless you’re sure.
Part 4: Packages
package cpu_pkg;
typedef enum logic [3:0] {ADD, SUB, AND_, OR_} alu_op_t;
typedef struct packed {
alu_op_t op;
logic [4:0] rd, rs1, rs2;
} uop_t;
function automatic logic parity(input logic [31:0] d);
return ^d;
endfunction
endpackage
module alu import cpu_pkg::*; (input uop_t uop, ...);
Packages replace `include files for shared types, parameters and functions. Compile them before the modules that import them.
Part 5: Other small wins
'0,'1,'xfill any width:count <= '0;.$bits(x)returns the width of a signal or type.++,+=and friends work in procedural code (use them carefully in RTL for readability).- Port connections with
.*connect every port to a same-named signal; handy at the top level, but explicit names are clearer elsewhere. - Assertions can live right next to the RTL they check. See SystemVerilog assertions.
Part 6: Synthesis and timing considerations
| Construct | Synthesis result | Watch out for |
|---|---|---|
| enum | Encoding chosen by you or the tool | Name-based debug only works in simulation |
| packed struct / union | Plain vectors | Field order sets bit order |
| unique case | Parallel mux, no priority logic | A violation in silicon is undefined behaviour |
| interface | Flattened into individual ports | Some tools limit modport and generic interface use at the top level |
| functions in always_comb | Inlined combinational logic | Deep functions become long timing paths |
| for loops | Unrolled | Large loops create large, slow logic |
The usual timing rules still apply: register outputs between blocks, keep combinational depth reasonable, and pipeline long arithmetic.
Part 7: Not synthesisable
Classes, randomisation, dynamic arrays, queues, associative arrays, strings, mailboxes, fork-join and delays are all testbench features. Keep them out of RTL files.
Practise this on VLSI Forge
I built VLSI Forge so you can write RTL in your browser, run it on a real simulator and check every signal in the waveform. Free, nothing to install.
Previous in my SystemVerilog series: SystemVerilog data types. Next: Tasks and functions.
FAQ
Is SystemVerilog synthesisable?
The design subset is: logic, always_ff, always_comb, enums, packed structs, interfaces, packages, unique and priority. Classes and dynamic types aren’t.
What is the difference between always_comb and always @(*)?
always_comb runs at time zero, includes signals read inside called functions, forbids other drivers of its outputs, and lets tools check for unintended latches.
Should I use unique case?
Use it when items really are mutually exclusive and you want the simulator to check that. Add a default for robustness.
What is a package in SystemVerilog?
A named scope for shared types, parameters, functions and tasks, imported into modules with import pkg::*;
