SystemVerilog cheat sheet and guide

<!-- The SystemVerilog guide shown on the Guide page and in the Guide tab of every problem. Edit this file to change the guide; it is bundled into the frontend at build time. Fenced code blocks (``systemverilog, ``text, ...) get syntax colouring and a Copy button. Each "#" and "##" heading becomes an entry in the guide's table of contents ("###" does not). A blockquote ("> ...") is shown as a highlighted rule box. -->

Golden rules

> - Use logic for signals, always_comb for combinational logic and always_ff for flip-flops. > - always_comb uses blocking =; always_ff uses non-blocking <=. Never mix them. > - In always_comb, assign every output on every path (set a default at the top), or you get a latch. > - Any feedback path (an output used to compute itself) must go through an always_ff. > - Give every case a default and every if chain a final else. > - Size widths with a parameter and write both bounds: [N-1:0].

1. Data types

Four-state vs. two-state

KindValuesTypes
4-state0, 1, X, Zlogic, reg, wire, integer, time
2-state0, 1bit, byte, shortint, int, longint

Use 4-state for hardware signals (so you can see X/Z). Use 2-state for testbench variables and counters where speed matters.

logic and wire

logic is the all-purpose type. It can be driven by one continuous assignment or by procedural blocks, but not by multiple drivers.

logic        valid;
logic [7:0]  data;
always_ff @(posedge clk) data <= data_in;
assign valid = (data != '0);

wire is a net type that supports multiple drivers and resolves them (needed for tri-state buses). (We don’t really use this that much)

wire [7:0] bus;
assign bus = oe ? tx_data : 8'hZZ;

Integer types

TypeWidthSigned?
bit1 (or vector)unsigned
byte8signed
shortint16signed
int32signed
longint64signed
integer32 (4-state)signed
int      count = 0;
byte     b = -5;
bit [3:0] nibble = 4'hA;
int unsigned addr;          // override signedness
logic signed [7:0] s;

typedef and enum

Enums give readable state machines. Specifying the base type (logic [1:0]) keeps encoding and X-propagation under control.

typedef logic [7:0] byte_t;
byte_t payload;

typedef enum logic [1:0] {IDLE, LOAD, RUN, DONE} state_t;
state_t state, next_state;

always_comb begin
  case (state)
    IDLE: next_state = LOAD;
    LOAD: next_state = RUN;
    default: next_state = IDLE;
  endcase
end

struct and union

A packed struct is a vector you can slice by field name. An unpacked struct is a plain grouping.

// Packed struct: fields side by side (sizes add up), first field = MSBs
typedef struct packed {
  logic [3:0]  opcode;     // bits 15:12
  logic [11:0] operand;    // bits 11:0
} instr_t;

instr_t instr;
instr = 16'hA123;          // assign as a whole vector
instr.opcode;              // 4'hA  (access by field name)
instr.operand = 12'hFFF;   // changing one field leaves the other alone
// Packed union: same bits, different views (all members same width)
typedef union packed {
  logic [31:0]     word;   // whole 32 bits
  logic [3:0][7:0] bytes;  // same bits as 4 bytes
} word_u;

word_u w;
w.word = 32'hDEAD_BEEF;
w.bytes[3];                // 8'hDE (top byte)
w.bytes[0] = 8'h00;        // word is now DEAD_BE00

Strings

string s = "hello";
s = {s, " world"};         // concatenate
$display("%s len=%0d", s, s.len());

2. Vectors and arrays

Packed vs. unpacked

logic [3:0][7:0] word;     // 4 bytes packed into 32 bits
word[2] = 8'hFF;
word = 32'hDEAD_BEEF;      // assign whole thing
logic [7:0] mem [0:255];   // 256 bytes of memory
mem[0] = 8'h12;

Example: one packed + one unpacked dimension

logic [7:0] mem [0:255];
mem
 ├── mem[0]   = 8 bits
 ├── mem[1]   = 8 bits
 ├── mem[2]   = 8 bits
       …
 └── mem[255] = 8 bits

Example: both dimensions unpacked

logic mem [7:0][0:255];
mem
├── mem[0][0]   = 1 bit
├── mem[0][1]   = 1 bit
      …
├── mem[0][255] = 1 bit
├── mem[1][0]   = 1 bit
      ...
└── mem[7][255] = 1 bit

Buses: indexing, slicing, concatenation

Packed arrays (buses) hold multibit data. Dimensions go in brackets as [high:low], before the name.

module exampleArr #(parameter N = 8)(     // parameter sets the width
  input  logic [N-1:0] signal_in,         // N-bit bus, [N-1] = MSB, [0] = LSB
  output logic [N-1:0] signal_out
);
  assign signal_out[0]   = signal_in[0];       // single bit (index 0 = LSB)
  assign signal_out[2:1] = signal_in[2:1];     // slice (part-select)
  assign signal_out[7:3] = {signal_in[2:1],    // concatenation rearranges wires
                            signal_in[0],
                            {2{signal_in[3]}}};  // replication: 2 copies of bit 3
endmodule

Pack the slices so the widths match: [7:3] is 5 bits, and 2 + 1 + 2 = 5.

Multi-dimensional arrays

Multi-dimensional arrays are arrays of arrays, read as [row][col] (or [d][r][c]). You can assign at any level.

module exampleMD #(parameter N = 8, M = 4)(
  input  logic [N-1:0]        arr_in,
  output logic [M-1:0][N-1:0] arr_out     // M rows of N bits
);
  assign arr_out[0][1] = 1'b0;            // one bit
  assign arr_out[1]    = {8{1'b1}};       // one whole row
endmodule

Notes

Dynamic arrays, associative arrays, queues

These are mostly for testbenches.

Dynamic arrays have a size set at runtime.

int da[];
da = new[10];
da = new[20](da);          // resize, keep contents

Associative arrays are sparse and indexed by any type.

int scoreboard [string];
scoreboard["pkt1"] = 5;
if (scoreboard.exists("pkt1")) ...

Queues are variable-length with easy push/pop.

int q[$];
q.push_back(3);
q.push_front(1);
int x = q.pop_front();

3. Literals

Format

Format: <size>'<base><value>

8'b1010_0101     // binary, 8 bits (underscores are ignored, for readability)
8'd249           // decimal
8'o251           // octal
8'hAF            // hex
1'bx             // unknown, 1 bit
4'bz             // high impedance
'b1101           // size omitted: taken from context (risky)
'1  '0  'x  'z   // no base: fill every bit of the target

Padding

A value narrower than its size is padded on the left. It zero-extends, except that a leading x or z extends with x or z. 'h1 is 0000_0001, not all ones.

AssignmentResult (8-bit)Behavior
'h00000_0000zero extend
'hZZZZZ_ZZZZfill
'hXXXXX_XXXXfill
'h10000_0001zero-extend (not fill)
'11111_1111fill with ones

4. Operators

Logic gates and reductions

assign q_or   = a | b;         // OR
assign q_and  = a & b;         // AND
assign q_xor  = a ^ b;         // XOR
assign q_nor  = ~(a | b);      // NOR
assign q_nand = ~(a & b);      // NAND
assign q_not  = ~a;            // NOT
assign q_xnor = ~(a ^ b);      // XNOR (1 when a == b)
a && b         // logical AND (1-bit result)
&example_vec   // reduction AND (all bits 1?)
|example_vec   // reduction OR  (any bit 1?)
{a, b}         // concatenation
{4{1'b1}}      // replication -> 4'b1111

Arithmetic and carry

assign s = a + b;          // add
assign d = a - b;          // subtract
assign p = a * b;          // multiply (big hardware)
assign q = a / b;          // divide  (very big, avoid)
assign r = a % b;          // modulo  (very big, avoid)
assign e = a ** 2;         // power (constant exponent)

To keep the carry out, make the left side 1 bit wider than the operands. If the left side is only 8 bits, the carry is silently dropped.

logic [7:0] a, b, sum;
logic       cout;
assign {cout, sum} = a + b;   // 9-bit result split into carry + sum

Shifts

assign l  = a << 2;        // logical left,  fills with 0
assign rl = a >> 2;        // logical right, fills with 0
assign ra = a >>> 2;       // arithmetic right: copies the sign bit, but ONLY if a is signed
                           // (<<< is the same as <<)
// ex: 8'b1000_0000 >> 2 = 0010_0000,  signed >>> 2 = 1110_0000

Comparison and equality

The result is 1 bit.

a <  b    a <= b    a >  b    a >= b
a == b    a != b                // X/Z in an operand -> result is X
a === b   a !== b               // exact match, X/Z compared literally (sim only)
!a        a || b                // logical NOT / OR (1-bit result, any nonzero = true)

5. Modules and parameters

Modules and instances

A module is the basic building block of hardware: a named block with ports and internal logic. Everything lives in a module.

module adder #(parameter WIDTH = 8)(   // parameter = overridable constant
  input  logic [WIDTH-1:0] a, b,       // ports: input / output / inout
  output logic [WIDTH:0]   sum
);
  assign sum = a + b;
endmodule
// Instantiate (use it inside another module)
adder #(.WIDTH(8)) u_add (.a(x), .b(y), .sum(z));   // by name (preferred)
adder u_add2 (.*);                       // implicit: connects same-named signals

Instantiate by name rather than by position to avoid wiring mistakes. An instance needs both a module name and an instance name (adder u_add).

Macros: ` define ``

// Text substitution, global
`define N 8                        // no '=' and no ';'
`define M 4
`define CLOCK_PERIOD 10

logic [`M-1:0][`N-1:0] sig_in;     // use with a backtick: `N becomes 8
sig_in = `N'b0;                    // `N'b0 expands to 8'b0
#(`CLOCK_PERIOD);                  // `CLOCK_PERIOD expands to 10

parameter and localparam

// Per-module constant, can be overridden
module example #(parameter N = 8, M = 4)(
  input  logic [M-1:0][N-1:0] sig_in,
  output logic [M-1:0][N-1:0] sig_out
);
  localparam W = N * M;            // derived constant, can't be overridden
endmodule

example #(.N(16)) u_ex 
    ( .sig_in(in_signal), .sig_out(out_signal) )  // override N at instantiation

$clog2

Ceiling of log2, for index widths.

logic [$clog2(N)-1:0] ptr;         // N=6 -> 3 bits; N=8 -> 3 bits
output logic [$clog2(N)-1:0] argmin_out;

Arrays of instances

An instance array creates several copies of one module in a single statement. This is the handout's ripple-style "min of N inputs" example.

module minN #(parameter N = 4, M = 1)(
  input  logic [N-1:0][M-1:0] inputs,
  output logic        [M-1:0] min_out
);
  logic [N-3:0][M-1:0] ptl_res;            // partial results between stages

  minAB #(.M(M)) minArr [N-2:0] (          // N-1 copies: minArr[0] .. minArr[N-2]
    .inputA  (inputs[N-1:1]),              // split across copies: copy i gets inputs[i+1]
    .inputB  ({ptl_res, inputs[0]}),       // copy 0 gets inputs[0], copy i gets ptl_res[i-1]
    .min_out ({min_out, ptl_res})          // copy N-2 drives min_out, copy i drives ptl_res[i]
  );
endmodule

How the connections are split

6. Combinational logic

Procedural blocks are software-like, line-by-line code that still describes hardware. The block type decides what hardware you get. always_comb gives gates and always_ff gives flip-flops (see chapter 7).

assign

assign is a continuous assignment for combinational logic: the right side is always evaluated, and the left side updates whenever an input changes.

assign y   = a & b;           // simple logic
assign mux = sel ? a : b;     // ternary mux
assign any = |a;              // reduction operator
assign bus = oe ? a : 4'hZ;   // tri-state (target must be wire)

wire [3:0] inv = ~a;          // declare and assign in one line
always_comb y = a & b;        // equivalent procedural form

always_comb

logic [1:0] s, ns;

// Combinational logic: an alternative to assign
always_comb begin
  if (!s[0] && !s[1]) signal_out = `N'b0;   // blocking assignment (=)
  else                signal_out = `N'b1;
  ns = (s + 2'b01) & 2'b11;                 // lines run in order, like software
end

Conditionals: ternary, if/else, case

Ternary

Use in assign (this is a mux).

assign y = sel ? a : b;                  // sel true -> a, else b

if / else

Only inside procedural blocks.

always_comb begin
  if (sel == 2'd0)      y = a;
  else if (sel == 2'd1) y = b;
  else                  y = c;           // always end with else (see latch note)
end

case

Like a switch, but no break needed.

always_comb begin
  case (sel)
    2'd0:    y = a;
    2'd1:    y = b;
    2'd2,
    2'd3:    y = c;                      // several values, one branch
    default: y = '0;                     // always include default
  endcase
end

casez

? (or z) in a case item means don't care.

always_comb begin
  casez (req)
    4'b1???: grant = 4'b1000;            // highest bit wins
    4'b01??: grant = 4'b0100;
    4'b001?: grant = 4'b0010;
    4'b0001: grant = 4'b0001;
    default: grant = 4'b0000;
  endcase
end

unique and priority

// unique: "exactly one item will match, never two"
// Simulator warns if sel matches NO item, or MORE THAN ONE item. 
unique case (sel) 
  2'd0: y = a; 
  2'd1: y = b; 
  2'd2: y = c; 
  2'd3: y = d;
endcase
// priority: "at least one will match, and if several do, the first one wins"
// Tool builds a priority chain (req[0] beats req[1] beats req[2]). 
// Simulator warns if NONE match. 
priority if (req[0]) grant = 2'd0; 
         else if (req[1]) grant = 2'd1; 
         else if (req[2]) grant = 2'd2;

Avoiding latches

> NB: in always_comb, every output must be assigned on every path. The easiest habit is a default at the top of the block.

A latch is an unintended memory. Combinational logic should have none, and tools usually warn "latch inferred."

// BAD: if en = 0, y keeps its old value, so the tool builds memory (a latch)
always_comb begin
  if (en) y = a;
end
// FIX 1: default value first, then override
always_comb begin
  y = '0;                  // y is always assigned
  if (en) y = a;
end

// FIX 2: complete else / default
always_comb begin
  if (en) y = a;
  else    y = '0;
end
// case has the same trap: no default and a missing value -> latch
always_comb begin
  case (sel)
    2'd0: y = a;
    2'd1: y = b;
    default: y = '0;       // covers everything else
  endcase
end

Avoiding combinational loops

> NB: any feedback path (output used to compute itself) must pass through an always_ff.

// BAD: count feeds itself through gates only, so it never settles
always_comb begin
  count = count + 1;
end
// FIX: put a flip-flop in the feedback path
always_ff @(posedge clock) begin
  if (reset) count <= '0;
  else       count <= count + 1;
end

7. Sequential logic

always_ff

// Sequential logic: flip-flops
always_ff @(posedge clock) begin
  if (reset) s <= #1 '0;                    // reset on its own, constant value
  else       s <= #1 ns;                    // non-blocking assignment (<=)
end

Blocking (=) vs. non-blocking (<=)

Blocking =Non-blocking <=
How it runsThe line finishes before the next line starts, like softwareAll right-hand sides are read first, then every left-hand side updates together at the end of the time step (the clock edge)
Later lines seeThe new valueThe old value
Use it inalways_combalways_ff

8. Loops

A hardware for loop makes copies in space. A software for loop repeats in time. That one idea explains everything below.

Generate forProcedural for
WhereModule level, inside generateInside always_comb / always_ff
Variablegenvar iinteger i
Body can holdassign, instances, alwaysStatements only (=, <=, if, case)

Generate for

A for loop in hardware makes multiple copies of the same logic in space. Software repeats logic over time. The loop sits in a generate block, and the loop variable is a genvar.

module exampleFor #(parameter N = 8, M = 4)(
  input  logic [M-1:0][N-1:0] sig_in,
  output logic [M-1:0][N-1:0] sig_out
);

  genvar i;                              // loop variable (exists only at build time)
  generate
    for (i = 0; i < M; i++) begin        // bounds must be constants (parameters)
      assign sig_out[i] = sig_in[i];     // this line is generated M times
    end
  endgenerate

endmodule

This is the same as writing the assign M times, once for each value of i: sig_out[0] = sig_in[0], sig_out[1] = sig_in[1], and so on.

Generate can also create instances (not possible in procedural for):

genvar i;
generate
  for (i = 0; i < M; i++) begin
    minAB u_min (.inputA(a[i]), .inputB(b[i]), .min_out(m[i]));
  end
endgenerate

Notes

Procedural for and break

A procedural loop sits inside an always_comb and uses a normal variable (integer i) instead of a genvar. It looks like C, but it works differently.

`define N 8
`define M 4

always_comb begin
  integer i;                              // loop variable declared in the block
  for (i = 0; i < `M; i++) begin          // bounds must be constants
    sig_out[i] = sig_in[i];               // M copies of this logic, side by side
  end
end

With break, later copies depend on earlier ones:

always_comb begin
  integer i;                         // declarations first in the block
  sig_out = '0;                      // default, so every output is always assigned

  for (i = 0; i < 4; i++) begin
    sig_out[i] = sig_in[i];
    if (stop[i]) break;              // later copies depend on this one
  end
end