<!-- 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. -->
> - 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].
| Kind | Values | Types |
|---|---|---|
| 4-state | 0, 1, X, Z | logic, reg, wire, integer, time |
| 2-state | 0, 1 | bit, 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 wirelogic 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;
| Type | Width | Signed? |
|---|---|---|
bit | 1 (or vector) | unsigned |
byte | 8 | signed |
shortint | 16 | signed |
int | 32 | signed |
longint | 64 | signed |
integer | 32 (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 enumEnums 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 unionA 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
string s = "hello";
s = {s, " world"}; // concatenate
$display("%s len=%0d", s, s.len());
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;
logic [7:0] mem [0:255];
[7:0] is packed: each element is an 8-bit vector[0:255] is unpacked: there are 256 separate elementsmem
├── mem[0] = 8 bits
├── mem[1] = 8 bits
├── mem[2] = 8 bits
…
└── mem[255] = 8 bits
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
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 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
[N-1:0]), not just a count, so the range is clear.parameter for widths instead of hard-coding them, so the width is set in one place.[0] is the LSB when the range is [N-1:0].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();
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
x and z in binary and hex (8'b10xx_zzzz).5 with no tick is a 32-bit signed decimal. '1 fills all ones, whatever the width.s for signed: 8'sd5.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.
| Assignment | Result (8-bit) | Behavior |
|---|---|---|
'h0 | 0000_0000 | zero extend |
'hZ | ZZZZ_ZZZZ | fill |
'hX | XXXX_XXXX | fill |
'h1 | 0000_0001 | zero-extend (not fill) |
'1 | 1111_1111 | fill with ones |
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
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
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
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)
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).
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
$clog2Ceiling 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;
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
module_name #(params) inst_name [range] (ports); The [N-2:0] after the instance name makes N-1 copies.{ } is how you wire neighbours together. The lowest piece goes to copy 0. In {ptl_res, inputs[0]}, inputs[0] is the lowest piece (copy 0) and ptl_res follows it.clock) is given to every copy.#(.M(M)) is applied to all copies.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).
assignassign 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 and initial blocks.wire tri-state).always blocks, use = (blocking, combinational) or <= (non-blocking, sequential) instead.always_comblogic [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
always @(). Don't use always @(), because it's easy to leave a signal out of the list.=). Each line finishes before the next one runs, like software.Use in assign (this is a mux).
assign y = sel ? a : b; // sel true -> a, else b
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
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
? (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: "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;
> 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
> 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
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
<=). All of them update together when the clock edge happens.@(posedge clock).if (reset) with a constant value and no other conditions or logic.#1 is an optional simulation delay that's ignored in synthesis. Using it keeps waveforms readable.= in an always_ff. It's allowed, but the handout says it's confusing and easy to get wrong.=) vs. non-blocking (<=)Blocking = | Non-blocking <= | |
|---|---|---|
| How it runs | The line finishes before the next line starts, like software | All 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 see | The new value | The old value |
| Use it in | always_comb | always_ff |
A hardware for loop makes copies in space. A software for loop repeats in time. That one idea explains everything below.
Generate for | Procedural for | |
|---|---|---|
| Where | Module level, inside generate | Inside always_comb / always_ff |
| Variable | genvar i | integer i |
| Body can hold | assign, instances, always | Statements only (=, <=, if, case) |
forA 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
genvar value only exists while the hardware is being built. It is not a signal.genvar outside the loop and use it only inside generate.for and breakA 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