Chapter 2 of 16
Basic Syntax: Modules, Literals, and Printing
Learn to read and write a module's declaration and instantiation, SystemVerilog's numeric literal format, $display/$sformatf and message severity levels, parameter/localparam module configuration, and the ternary (conditional) operator — the connective tissue every later chapter's examples assume you already know.
The mux2 example in the last chapter already used a fair amount of syntax: a module's declaration and instantiation, literals like 1'b0, the %0b inside $display, and the question-mark pattern sel ? b : a — none of it formally explained yet. This chapter covers all of that basic syntax in one place, since every later chapter's examples assume you already recognize it.
Modules: declaration, ports, and instantiation
A module is wrapped in module...endmodule, with a port list and internal logic in between:
module mux2 (
input logic sel,
input logic a,
input logic b,
output logic y
);
always_comb begin
y = sel ? b : a;
end
endmodulemodule mux2 (...)declares a module namedmux2; the parentheses hold its port list.- Every port has a direction:
input(read-only, driven from outside),output(write-only, driven by the module), and a less commoninoutfor bidirectional signals (like a real I/O pin) — this track won't useinout. - A port's type (
logichere) gets covered in detail in the data types chapter.
Once a module is defined, instantiating it means creating an instance of it — the clearest way is to connect each port by name:
mux2 dut (
.sel(sel),
.a(a),
.b(b),
.y(y)
);mux2 is the module name; dut is the instance name given to this particular instantiation (a module can be instantiated multiple times, each with a different instance name). .portName(signalName) is called named port connection — it spells out exactly which port connects to which signal, instead of relying on position/order in the port list. It's less error-prone than positional connection and is the style this track sticks with throughout.
Numeric literals: <width>'<base><value>
1'b0 and 8'hFF are SystemVerilog's numeric literal syntax. Their format is:
<width>'<base><value>
<width>: how many bits this literal occupies; defaults to 32 bits if omitted.<base>:b/B(binary),o/O(octal),d/D(decimal, the default base),h/H(hexadecimal).<value>: the digits in that base; for 4-state types,x/X(unknown) andz/Z(high-impedance) are also valid.
4'b1010 // 4 bits, binary 1010, equals decimal 10
8'hFF // 8 bits, hex FF, equals decimal 255
8'd10 // 8 bits, decimal 10
1'bx // 1 bit, unknown state (only meaningful for 4-state types)
32'h1000_2000 // underscores in the digits are purely for readability and are ignored_ can be inserted anywhere between digits (commonly used to separate bytes, as in 1000_2000 above) purely for readability — it has no effect on the literal's actual value. Writing a bare number with no width or base (like 10) is treated as a 32-bit signed decimal value.
$display and format specifiers
$display prints a line during simulation, similar to formatted printing in other languages:
$display("sel=%0b -> y=%0b (expected %0b)", sel, y, a);A %X inside the string is a format specifier, telling $display which base to print the corresponding argument in:
| Specifier | Meaning |
|---|---|
%b | Binary |
%d | Decimal |
%h | Hexadecimal |
%s | String |
%t | Simulation time |
Adding a 0 right after the % (like %0d, %0b) means "don't pad to a fixed width — print only as many digits as actually needed." Plain %d pads with fixed-width spaces/zeros based on the signal's bit width, while %0d prints just enough digits, which reads more compactly — that's why this track's examples almost always use %0d/%0b/%0h instead of the unpadded-zero versions. $display automatically appends a newline after each call; $write is the equivalent without one (not used in this track).
$sformatf and message severity: $display, $warning, $error, $fatal
$sformatf works exactly like $display, but instead of printing, it returns the formatted result as a string — handy for building a message before deciding what to do with it:
string msg;
msg = $sformatf("addr=%0h data=%0h", 8'h20, 8'hFF);
$display(msg); // equivalent to $display("addr=%0h data=%0h", 8'h20, 8'hFF);Beyond $display, SystemVerilog provides three more printing tasks graded by severity, which later chapters' verification code uses constantly:
| Task | Severity | Stops simulation? |
|---|---|---|
$display | None (plain info) | No |
$warning | Warning | No |
$error | Error | No (but most simulators count it as a failure) |
$fatal | Fatal | Yes — internally calls $finish to stop immediately |
if (y != expected) begin
$error("mismatch: expected=%0b actual=%0b", expected, y);
end$error/$warning don't stop simulation on their own — their job is to tag a message's severity, so simulation logs and coverage tools can tell whether a run actually failed, rather than just being ordinary output like $display. $fatal is for errors severe enough that continuing doesn't make sense, and it terminates the simulation immediately.
Giving a module configurable parameters: parameter, localparam, and #(...)
Sometimes the same module needs to be configurable for different situations — a generic FIFO that might need to be 8 bits wide in one place and 32 bits wide in another — without writing a separate module for every configuration. parameter is designed for exactly this: a compile-time constant that can be overridden at instantiation:
module fifo #(
parameter int WIDTH = 8,
parameter int DEPTH = 16
) (
input logic [WIDTH-1:0] data_in,
output logic [WIDTH-1:0] data_out
);
localparam int ADDR_BITS = $clog2(DEPTH); // derived from DEPTH, can't be overridden on its own
logic [WIDTH-1:0] mem [DEPTH];
// ...
endmoduleInstantiation overrides parameter values with #(...), which looks a lot like named port connection:
fifo #(.WIDTH(32), .DEPTH(64)) my_fifo (.data_in(din), .data_out(dout));Omit #(...) and the declared defaults apply (WIDTH=8, DEPTH=16). parameter can be overridden via #(...); localparam is a read-only derived constant (like the address width computed from DEPTH) that can't be overridden at instantiation. Later in this track, class can be parameterized the same way (like mailbox #(int)), to say "this class can hold this type of data" — that part waits until after class itself is covered.
The ternary (conditional) operator ?:
mux2's y = sel ? b : a; uses the ternary operator (also called the conditional operator), with the syntax:
condition ? value-if-true : value-if-false
sel ? b : a means: "if sel is true, the result is b; otherwise, the result is a" — exactly what a 2-to-1 multiplexer should do, which is why this one line fully describes mux2's behavior. The ternary operator is extremely common in SystemVerilog, and shows up in nearly every chapter from here on.
A quick note on time units
A delay statement like #10 advances simulation time forward by 10 "time units." How much real time one time unit corresponds to is decided by a setting like `timescale — the next chapter covers this in full. For now, it's enough to know that #N means "advance simulation time by N units, then continue executing," a pattern you'll see again with delays and clocks later on.
Summary
- A module is declared with
module...endmodule; ports haveinput/outputdirection (and the less-commoninout); instantiation connects ports by name with.portName(signalName), which is clearer and less error-prone than positional connection. - A numeric literal's format is
<width>'<base><value>, like8'hFF; the width defaults to 32 bits if omitted, and the base defaults to decimal if omitted. %b/%d/%h/%s/%tinside$displayare format specifiers; a0right after%(like%0d) means print without fixed-width padding.$sformatfworks like$displaybut returns a formatted string instead of printing it;$display/$warning/$error/$fatalare graded by severity, and only$fatalstops simulation.parameter(overridable via#(...)) andlocalparam(a read-only derived constant) let the same module be configured with different widths/depths/etc. at instantiation.- The ternary operator
condition ? true-value : false-valueis "pick one of two," and it's what describesmux2's core logic in a single line. #Nadvances simulation time by N time units; the actual duration depends on a setting like`timescale, covered in full next chapter.
In the instantiation mux2 dut (.sel(sel), .a(a), .b(b), .y(y));, what are dut and mux2 respectively?
What does the literal 8'hFF represent?
What does the expression sel ? b : a mean?
What's the main difference between %0d and %d inside $display?
What's the main difference between $error and $display?
In fifo #(.WIDTH(32), .DEPTH(64)) my_fifo (...);, what does the #(...) part do?