16.3 Gate-Level Modeling: Structural Instantiation of Hardware Primitives
Build a circuit by directly instantiating Verilog's built-in gate primitives (and, or, not, and related) and wiring their ports together, and explain how this structural style differs from writing RTL behavior.
Every previous lesson in this module described hardware behaviorally — writing an expression like a & b and letting the simulator and synthesis tool figure out what gate structure implements it. Gate-level modeling takes the opposite approach: it describes hardware structurally, by explicitly instantiating named gate primitives (and, or, not, and their relatives) and wiring their inputs and outputs together by hand, the same way you would draw a schematic gate by gate. Verilog has built this style in from the very beginning of the language, because it was originally meant to describe hardware at exactly the level a schematic would — this lesson covers that primitive instantiation syntax and clarifies when and why a designer would reach for it instead of behavioral RTL.
Verilog's Built-in Gate Primitives
Verilog defines a small library of built-in logic-gate primitives that exist in the language itself, with no module definition required: and, or, not, nand, nor, xor, xnor, and buffer (buf). Each primitive is instantiated the same way you'd instantiate any module, but with one important syntactic difference from behavioral modules: gate primitive ports are always ordered output first, then inputs — and g1(y, a, b); means 'g1 is an AND gate whose output is y, driven by inputs a and b,' in that fixed order.
These primitives can take more than two inputs where that makes sense (for example, a 3-input and gate), but always have exactly one output listed first. This output-first convention is specific to gate primitives; it is not the convention used for ordinary module instantiation with named port connections, which is one reason gate-level code is visually distinctive even without comments.
• and g1(y, a, b); — instantiate an AND gate named g1, output y, inputs a and b. • or g2(z, y, c); — instantiate an OR gate named g2, output z, inputs y and c. • not g3(out, z); — instantiate a NOT (inverter) gate named g3, output out, input z. • Gate instance names (g1, g2, g3) are required by good practice and by many tools' netlist/waveform annotation, even though the language permits omitting them in some contexts.
Wiring Gates Together: Structural Composition
The power of gate-level modeling comes from chaining instances together through shared wire names: notice in the spec's own example that g2's input y is the exact same signal name as g1's output — that shared name is the wire connecting the AND gate's output directly into the OR gate's input, precisely the way a wire on a schematic connects one gate's output pin to another gate's input pin. There is no assign statement anywhere in a purely structural description; the entire circuit's behavior emerges from how the gate instances are wired together, not from any written Boolean expression.
This means a gate-level description and a behaviorally equivalent RTL description of the same logic function can look completely different in source code while producing electrically identical hardware — the structural version explicitly names every gate and every internal wire, while the RTL version leaves that mapping up to the synthesis tool.
Structural Modeling vs. RTL: When Each Is Used
As the module's own tricky-problem note states directly: gate-level structural modeling describes instantiated hardware primitives, while RTL can describe behavior at a higher abstraction. In modern practice, engineers write the overwhelming majority of a design in behavioral RTL (as in 16.1, 16.2, 16.4, 16.5) precisely because it is faster to write, easier to modify, and lets the synthesis tool make gate-selection and optimization decisions that a human typically cannot match at scale.
Gate-level Verilog still matters for two major reasons. First, it is exactly the output format synthesis tools produce: when RTL is synthesized, the tool emits a gate-level netlist — a Verilog file built entirely from primitive (or standard-cell library) instantiations wired together — which is then simulated again to verify the synthesis didn't change behavior. Second, understanding gate-level instantiation syntax is what makes reading a synthesized netlist, or a vendor's gate-level simulation model, possible at all — it is the 'assembly language' layer underneath the RTL you normally write.
Gate Primitive Instantiation Syntax
Port order for built-in gate primitives is always output first, then one or more inputs — the reverse of reading direction on a schematic label but consistent across every primitive gate type. not/buf take exactly one input; and/or/nand/nor/xor/xnor can take two or more inputs. No assign or procedural block is used in a purely structural description — behavior comes entirely from the wiring between instances.
<gate_type> <instance_name> ( <output>, <input_1>, <input_2>, ... );
// Examples:
and g1(y, a, b);
or g2(z, y, c);
not g3(out, z);Code Examples
This is a structural rewrite of the classic half-adder truth table: xor g1(sum, a, b); instantiates an XOR gate whose output sum is 1 exactly when a and b differ (matching the half adder's sum column), and and g2(carry, a, b); instantiates an AND gate whose output carry is 1 exactly when both a and b are 1 (matching the carry-out column). No expression is written anywhere — the module's behavior is entirely defined by these two gate instances and how their ports connect to the module's own ports. Expected result: sum = a XOR b, carry = a AND b — identical truth table to the half adder built from discrete gates on paper.
xor g1(sum, a, b);
g1 computes y = a & b. That same wire y is then fed as an input into g2, which computes z = y | c, i.e. z = (a & b) | c — structurally, gate by gate, the exact same function as the behavioral SOP example from 16.2. Note the explicit wire y; declaration: any internal signal used only to connect two gate instances together (not an actual module port) must be declared as a wire before it is used.
and g1(y, a, b);
module half_adder_structural (
input wire a,
input wire b,
output wire sum,
output wire carry
);
xor g1(sum, a, b);
and g2(carry, a, b);
endmodulemodule chained_gates (
input wire a,
input wire b,
input wire c,
output wire z
);
wire y; // internal wire connecting g1's output to g2's input
and g1(y, a, b);
or g2(z, y, c);
endmoduleStructural netlist: two gates wired together
Every wire in a gate-level description corresponds to an actual physical net connecting two gate pins — there is no hidden behavior beyond this wiring.
a ──┐
├─►[ and g1 ]── y ──┐
b ──┘ ├─►[ or g2 ]── z
c ───────────────────────┘Important Rules
Gate primitives (and, or, not, nand, nor, xor, xnor, buf) are built into the language and need no module definition.
Primitive instance ports are always ordered output first, then input(s) — gate_type instance_name(output, input1, input2, ...);
Any internal signal connecting two gate instances (not a module port) must be declared with wire before use.
Structural modeling describes instantiated hardware directly; behavioral RTL describes function at a higher level and leaves gate selection to synthesis.
A synthesized RTL design is ultimately converted into a gate-level (or standard-cell) netlist that looks structurally like the code in this lesson.
Reading a Synthesized Netlist
When a design is synthesized (Module 16.8), the tool's output is precisely this style of code — thousands or millions of gate (or standard-cell) instances wired together with generated names — used for gate-level simulation, static timing analysis, and physical layout. An engineer who can read and g1(y, a, b); by hand can trace a real post-synthesis netlist to debug a timing violation or confirm that synthesis preserved the RTL's intended function.
and g1(a, b, y); // intending y as the output and g1(y, a, b);
Gate primitive ports are output-first — reversing the order silently redefines which signal is being driven. In this wrong version, a would be treated as the AND gate's output (being driven by b and y as inputs) rather than as an input, producing a circuit with a different, unintended structure and likely a multiple-driver conflict on a if it's also driven elsewhere.
[tricky-problem] How does gate-level structural modeling fundamentally differ from RTL behavioral modeling, as this lesson defines them? A. Structural modeling explicitly instantiates and wires individual hardware primitives by hand; RTL describes function/behavior at a higher abstraction and leaves gate selection to the synthesis tool B. Structural modeling can only be simulated, never synthesized, while RTL can only be synthesized, never simulated C. Structural modeling is written in SystemVerilog while RTL is written only in plain Verilog D. There is no real difference — both produce identical source code
A. Structural modeling explicitly instantiates and wires individual hardware primitives by hand; RTL describes function/behavior at a higher abstraction and leaves gate selection to the synthesis tool — This is the module's own stated distinction: gate-level modeling describes instantiated hardware primitives directly (naming every gate and every wire between them), while RTL (as in assign y = a & b; or an always_comb block) describes the desired behavior and lets the synthesis tool decide which actual gates implement it.
[interview] In the instantiation or g2(z, y, c);, which signal is the OR gate's output? A. z B. y C. c D. g2
A. z — Gate primitive port lists are always output-first: z is the gate's output, and y and c are its two inputs. g2 is not a signal at all — it's the instance name identifying this particular gate for waveform viewing, netlist tracing, and hierarchical reference.
Why must an internal wire connecting two gate instances (like y between g1 and g2) be explicitly declared with wire? A. Because any signal that is not a module port needs an explicit type declaration before it can be driven or read inside the module B. Because gate primitives cannot connect directly to module ports, only to declared internal wires C. Because wire declarations are required only for outputs, never for inputs D. Because Verilog automatically infers all internal signals as reg unless told otherwise
A. Because any signal that is not a module port needs an explicit type declaration before it can be driven or read inside the module — Module ports get their type from the input/output declaration in the port list, but any signal used purely internally — such as a wire carrying one gate's output into another gate's input — has no such declaration and must be given one explicitly with wire before it's referenced, or most tools will flag it as an undeclared identifier.
Using only primitive gate instantiations (no assign), build a module xnor_from_primitives with inputs a, b and output y implementing XNOR using only xor and not primitives (do not use the built-in xnor primitive).
module xnor_from_primitives ( input wire a, input wire b, output wire y ); wire xor_out; xor g1(xor_out, a, b); not g2(y, xor_out); endmodule
First compute the XOR of a and b into an internal wire, then invert that wire to get XNOR — you will need one internal wire declaration between the two gate instances.