16.1 Verilog Basics: Modules, Ports, and Continuous Assignment
Write a complete, synthesizable Verilog module — declaring its ports and describing its behavior with a continuous assignment — and explain how that text description turns into a piece of hardware.
Every digital circuit you have designed so far in this course — gates, Boolean expressions, K-map-simplified logic — has been drawn as a schematic or written as an equation on paper. Hardware Description Languages (HDLs) like Verilog exist to close the gap between that paper design and an actual chip: Verilog is a text-based language that describes hardware structure and behavior in a form that both a simulator (to check correctness before manufacturing anything) and a synthesis tool (to turn the description into real gates on an FPGA or ASIC) can process. It is critical to understand from the very first line of Verilog you write that this is not a programming language in the software sense — a Verilog module does not execute a sequence of instructions on a CPU; it describes a piece of parallel, always-on hardware that exists all at once, the same way a schematic of AND and OR gates exists all at once with no notion of 'first do this, then do that.' This lesson covers the smallest unit of Verilog hardware description — the module — and the simplest way to describe combinational behavior inside one: the continuous assignment.
Why Digital Design Needs Its Own Language
A real digital chip can contain billions of transistors organized into millions of gates, flip-flops, and wires. Drawing that by hand as a schematic is not feasible past a few dozen gates, and even at that scale, a schematic captures nothing about timing, reuse, or verification. Verilog (and its modern superset, SystemVerilog) solves this by letting an engineer describe hardware textually and structurally: a design is built from modules, each module is a self-contained block of hardware with a well-defined interface (its ports), and modules can instantiate other modules to build up a hierarchy — exactly like a schematic built from sub-blocks, except captured as text that tools can parse, simulate, and synthesize.
This textual description is what lets the same design flow scale from a two-input AND gate to a multi-billion-transistor processor: the language constructs don't change, only how many of them you use and how they're composed. Understanding a tiny module like the one in this lesson is therefore not a toy exercise — it is literally the same syntax, scaled down, that describes a CPU's arithmetic logic unit or a memory controller.
Anatomy of a Verilog Module: Header, Ports, Body
A Verilog module has three parts: the module header (its name and port list), the port declarations (which of those ports are inputs and which are outputs, and how wide each one is), and the body, which describes what the hardware actually does. The keyword module opens the block and endmodule closes it — there is no semicolon after endmodule, and forgetting the endmodule keyword is one of the most common early syntax errors, since the compiler will keep reading the rest of the file as if it were still inside that module.
Inside the module, input and output declare the module's external connections — its pins, in hardware terms. wire is the default data type for a signal that is driven continuously by some piece of combinational logic (as opposed to reg/logic, which you'll meet when sequential storage is introduced). A signal declared as wire cannot 'remember' a value on its own; it simply reflects whatever the logic driving it currently computes, instantaneously and continuously — which is exactly the behavior a plain AND gate has in real hardware.
The body of a purely combinational module can be as simple as a single assign statement. assign creates a continuous assignment: the right-hand side is evaluated and drives the left-hand side wire at all times, automatically re-evaluating any time an input on the right-hand side changes. This is fundamentally different from a software assignment statement, which runs once, at a specific point in a sequence of instructions, and then is 'done.' A Verilog assign never finishes — it behaves like a physical wire connected through a gate, always live.
• module <name> ( <port list> ); — opens a hardware block and names its interface. • input, output — declare a port's direction; wire is the implicit default type for a combinational signal. • assign <wire> = <expression>; — a continuous assignment; re-evaluates instantly whenever any signal on the right changes. • endmodule — closes the module; no semicolon follows it.
From RTL Text to Real Hardware
The module you write in this lesson is called an RTL (Register-Transfer Level) description once it starts describing hardware using registers, wires, and expressions rather than individually instantiated gates (gate-level modeling, covered in 16.3, is more explicit). Even a purely combinational module like the AND gate below is RTL — the 'R' in RTL simply refers to the overall design style, not to the presence of registers in every single module.
In a real design flow, this exact style of module is what gets fed into a synthesis tool (Module 16.8 covers this in depth), which maps the assign y = a & b; statement onto an actual AND gate (or an equivalent structure) in a target FPGA's look-up tables or an ASIC's standard-cell library. The module boundary — its port list — also becomes the reusable unit: once you've written and verified and_gate, it can be instantiated by name inside a larger module exactly the way a library gate would be, which is the foundation of how large chips are built from small, individually verified blocks.
The Classic Beginner Trap: & Is Not &&
Verilog inherited much of its operator syntax from the C language, including the fact that it has two different-looking AND operators. & is the bitwise AND operator: it operates independently on every bit of its operands and produces a result the same width as its widest operand — this is what you want for describing an AND gate or building a bitwise Boolean expression. && is the logical AND operator: it treats each entire operand as a single true/false value (any nonzero value is 'true') and always produces a single 1-bit result — this is what you want in a conditional test, such as inside an if statement guarding two separate conditions.
For single-bit signals the two operators happen to produce the same result, which is exactly why the mistake is so easy to make and so easy to miss during a quick simulation with 1-bit test signals — the bug only reveals itself once the design is extended to multi-bit buses, where && silently reduces an entire bus down to a single bit instead of doing a per-bit AND.
Minimal Verilog Module Syntax
module / endmodule bracket exactly one hardware block; modules cannot be nested, only instantiated inside one another. The port list inside the parentheses only names the ports; input/output directions can be declared there directly (ANSI-style, shown here) or separately in the body (older, non-ANSI style). wire is optional in modern tools for a plain input/output but is good practice to write explicitly while learning, since it makes the signal's storage class unambiguous.
module <module_name> (
input wire <input_port_1>,
input wire <input_port_2>,
output wire <output_port>
);
assign <output_port> = <boolean_expression_of_inputs>;
endmoduleCode Examples
module and_gate (...) names the block and_gate and lists its three ports. input wire a and input wire b are the two gate inputs; output wire y is the gate output. The body has exactly one statement: assign y = a & b;, a continuous assignment that drives y with the bitwise AND of a and b at all times. There is no clock, no sequencing, and no notion of 'running' this code — it is a live piece of combinational hardware the instant it is powered. Expected result: y is 1 only when a=1 AND b=1, and re-computes instantly on any change to a or b.
assign y = a & b;
The only change from the 1-bit version is the [3:0] range added after wire on each port, declaring each signal as a 4-bit vector (bit 3 down to bit 0) instead of a single wire. Because & is bitwise, the exact same assign y = a & b; statement now performs four independent AND operations in parallel, one per bit position — this is the payoff of understanding & as truly bitwise rather than as a single logical test. If && had been used here by mistake, y would collapse to a single bit representing whether both buses are entirely nonzero — a completely different, and almost certainly unintended, circuit.
input wire [3:0] a,
module and_gate (
input wire a,
input wire b,
output wire y
);
assign y = a & b;
endmodulemodule and_gate_4bit (
input wire [3:0] a,
input wire [3:0] b,
output wire [3:0] y
);
assign y = a & b;
endmoduleand_gate module as a black-box hardware block
The module's ports are its entire public interface; everything inside is hidden implementation detail — just like a gate symbol on a schematic.
┌───────────────┐
a ───►│ │
│ and_gate ├───► y
b ───►│ │
└───────────────┘
module boundary = port list = the only
thing a caller ever needs to know aboutImportant Rules
Every module must be opened with module <name> (...) and closed with endmodule — no semicolon after endmodule.
input and output declare direction; a combinational output that is continuously driven should be a wire.
assign creates a continuous, always-active assignment — it is not a one-time statement and has no notion of 'when' it runs.
& is bitwise AND (operates per-bit, width-preserving); && is logical AND (reduces each operand to true/false, always produces 1 bit).
A module's port list is its complete interface — internal signal names and structure are invisible to anything instantiating it.
Reusable IP Blocks
In industry, a verified module like a well-tested AND/OR combination becomes a reusable building block ('IP', for intellectual property) that gets instantiated by name across many designs, exactly the way a standard library function is reused in software — except here, instantiating a module means placing an actual copy of that hardware. Chip teams build enormous designs by composing small, individually simulated modules into larger ones, and the module/port-list discipline you're practicing here is the exact mechanism that keeps a billion-transistor design manageable.
assign y = a && b; // intended as a 2-input AND gate assign y = a & b;
For single-bit a and b the two produce the same result, which is precisely what makes this mistake so easy to ship undetected. The moment a and b become multi-bit buses, && reduces each one to a single true/false value first and ANDs those, collapsing a 4-bit (or wider) bitwise AND into a 1-bit logical test — a completely different circuit than intended.
[tricky-problem] A student writes assign y = a && b; inside a module intended as a plain 2-input AND gate with 1-bit ports a, b, y, and it simulates correctly. Later the module is changed so a and b become 8-bit buses, and y starts behaving unexpectedly. What is the most likely cause? A. && is the logical AND operator, which reduces each multi-bit operand to a single true/false value before ANDing, instead of performing a per-bit AND like & B. Verilog does not support 8-bit signals in continuous assignments C. assign statements only work correctly on 1-bit wires D. The endmodule keyword needs to be updated when port widths change
A. && is the logical AND operator, which reduces each multi-bit operand to a single true/false value before ANDing, instead of performing a per-bit AND like & — & and && behave identically on single-bit operands, which hides the bug at 1-bit width. && is logical AND: it collapses each operand to 'is this nonzero' and ANDs those two 1-bit truth values, always yielding a single bit — not a per-bit AND across the bus. The fix is to use &, the bitwise operator, whenever the intent is a hardware AND gate (or array of AND gates) rather than a boolean test.
[interview] Why is it misleading to think of assign y = a & b; as 'running once' the way a line of Python or C would? A. Because a continuous assignment describes an always-active piece of hardware that re-evaluates instantly whenever a or b changes, with no notion of a single execution point B. Because Verilog assignments execute in a random order chosen by the simulator every time C. Because assign statements are only evaluated once, at compile time, and then discarded D. Because y is a variable that only updates when the module is re-instantiated
A. Because a continuous assignment describes an always-active piece of hardware that re-evaluates instantly whenever a or b changes, with no notion of a single execution point — A continuous assignment models a physical wire driven through combinational gates: the moment either input changes, the output responds, forever, for as long as the hardware exists. There is no 'line of execution' the way there is in an imperative programming language — every assign statement in a module is conceptually active in parallel with every other one.
What is the correct way to close the body of a Verilog module? A. end module; B. endmodule (no semicolon) C. } D. end;
B. endmodule (no semicolon) — Verilog closes a module block with the single keyword endmodule, written as one word with no trailing semicolon. end (with a semicolon or brace) is not Verilog syntax for closing a module — those belong to other constructs or other languages entirely.
Write a Verilog module named or_gate with two 1-bit inputs a and b and one 1-bit output y, using a single continuous assignment so that y is the OR of a and b.
module or_gate ( input wire a, input wire b, output wire y ); assign y = a | b; endmodule
Start from the and_gate module's structure exactly, and swap only the operator inside the assign statement — the port declarations don't need to change at all.