Back to EECS 31L Index

5. EECS 31L / 05 Structural Modeling

EECS 31L • Study Notes • Structural Modeling
Mahmoud Elfar • Spring 2026 • v0.1

v0.1: Initial version


Table of Contents


5.1. What is Structural Modeling?

Recall the three abstraction levels:

Level How you describe the circuit
Dataflow assign statements: describe what each signal equals
Behavioral Procedural blocks: describe what the circuit does, step by step
Structural Instantiate components and wire them together

In structural modeling, we describe a digital circuit by its topology:

Remember: Topology = Components + Interlinks

Blade Runner 2049 (2017) Source: Blade Runner 2049 (2017)

Most non-trivial circuits you will write or encounter in Verilog are:

There are three categories of components you can instantiate in a structural model:

In all cases, the mechanism is the same:

In the rest of this note we will cover:

↑ Back to top

5.2. Gate-Level Primitives

See: IEEE 1800-2017, 28.3 Gate and switch declaration syntax, pg. 876

Verilog provides a set of built-in gate primitives (or primitive gates, or primitives for short). Those primitives are modules already defined by the Verilog language. You can use them directly in your models

Primitive Function
and AND
or OR
xor XOR
not NOT (inverter)
nand NAND
nor NOR
xnor XNOR
buf Buffer

5.2.1. Syntax and Semantics

Syntax:

<primitive> <instance_name> (<output>, <input1>, <input2>, ...);

For not and buf, which take a single input:

not <instance_name> (<output>, <input>);
buf <instance_name> (<output>, <input>);

Semantics:

Example:
Here, we implement Y = AB + A'B'C using primitives.

module cl_example (input A, B, C, output Y);
  wire A_NOT, B_NOT, AND1, AND2, OR1;
  
  not g1 (A_NOT, A);          // A_NOT = A'
  not g2 (B_NOT, B);          // B_NOT = B'
  and g3 (AND1, A, B);       // AND1 = AB
  and g4 (AND2, A_NOT, B_NOT, C); // AND2 = A'B'C
  or  g5 (Y, AND1, AND2);    // Y = AND1 + AND2
endmodule

5.2.2. Example: Half Adder from Primitives

module half_adder (input a, input b, output s, output c);
  xor g_sum  (s, a, b);   // s = a ^ b
  and g_carry(c, a, b);   // c = a & b
endmodule

Compare this to the dataflow version from Note 2:

module half_adder (input a, input b, output s, output c);
  assign s = a ^ b;
  assign c = a & b;
endmodule

The logic is identical. The structural version makes the gate topology explicit – you can read off exactly which gates exist and how they are connected. The dataflow version expresses the same thing more compactly as equations. In practice, the dataflow version is easier to write and read. The structural version with primitives is closer to what a synthesis tool would produce after processing it.

↑ Back to top

5.3. Module Instantiation

See: IEEE 1800-2017, 23.3 Module instances, pg. 708

Note 1 introduced the basic syntax for module instantiation. This section covers the two port-connection styles in depth and the rules that govern them.

When you instantiate a module, you need to specify how to connect its ports to the signals in the parent module. There are two ways to do this.

5.3.1. Positional Mapping

Syntax:

<module_name> <instance_name> (<signal_1>, <signal_2>, ..., <signal_N>);

Semantics:

Example: given this module definition,

module fulladder (input x, input y, input cin, output sum, output cout);
  // ...
endmodule

a positional instantiation looks like:

fulladder fa0 (a, b, c_in, s, c_out);
//             ^  ^   ^     ^    ^
//             x  y  cin  sum  cout    (in definition order)

The risk: if the port order in the module definition ever changes, every positional instantiation silently rewires with no compile error and no warning. For small modules with two or three well-known ports, positional mapping is acceptable. For anything larger, prefer named mapping.

5.3.2. Named Mapping

Syntax:

<module_name> <instance_name> (
  .port_name_1 (signal_1),
  .port_name_2 (signal_2),
  ...
  .port_name_N (signal_N)
);

Semantics:

Example: the same fulladder with named mapping:

fulladder fa0 (
  .x   (a),
  .y   (b),
  .cin (c_in),
  .sum (s),
  .cout(c_out)
);

If the port order in the definition changes, this instantiation is unaffected. Named mapping is the safe default for any module with more than a handful of ports.

5.3.3. Rules and Restrictions

↑ Back to top

5.4. The generate Construct

See: IEEE 1800-2017, 27.1 Overview, pg. 751

Writing out 4 instantiations by hand is manageable. Writing out 32 is not. The generate construct lets you produce repetitive structure programmatically at elaboration time, before simulation begins.

Syntax:

genvar <i>;
generate
  for (<i> = <start>; <i> < <end>; <i> = <i> + 1) begin : <label>
    // instantiation or assign statements
  end
endgenerate

Semantics:

Example: generating a bitwise AND across two 8-bit vectors using assign.

input  [7:0] x, y;
output [7:0] z;

genvar i;
generate
  for (i = 0; i <= 7; i = i + 1) begin : and_bits
    assign z[i] = x[i] & y[i];
  end
endgenerate

This elaborates into exactly:

assign z[0] = x[0] & y[0];
assign z[1] = x[1] & y[1];
assign z[2] = x[2] & y[2];
// ... and so on through z[7]

Of course, for something this simple you would just write assign z = x & y. The generate construct pays off when the body contains module instantiations as demonstrated in the worked examples below.

↑ Back to top

5.5. Worked Examples

5.5.1. 4-bit Ripple-Carry Adder – Positional Mapping

Four fulladder instances chained together. The carry output of each stage feeds the carry input of the next.

module fa_4bit (result, carry, r1, r2, cin);
  input  [3:0] r1;
  input  [3:0] r2;
  input        cin;
  output [3:0] result;
  output       carry;

  wire c1, c2, c3;

  fulladder u0 (r1[0], r2[0], cin, result[0], c1);
  fulladder u1 (r1[1], r2[1], c1,  result[1], c2);
  fulladder u2 (r1[2], r2[2], c2,  result[2], c3);
  fulladder u3 (r1[3], r2[3], c3,  result[3], carry);
endmodule

The port order at each instantiation site must match the fulladder definition exactly: x, y, cin, sum, cout. Getting that order wrong produces a silently broken design – i.e., zero errors given, wrong behavior.

5.5.2. 4-bit Ripple-Carry Adder – Named Mapping

The same design expressed with named mapping. The synthesized hardware is identical.

module fa_4bit (result, carry, r1, r2, cin);
  input  [3:0] r1;
  input  [3:0] r2;
  input        cin;
  output [3:0] result;
  output       carry;

  wire c1, c2, c3;

  fulladder u0 (.x(r1[0]), .y(r2[0]), .cin(cin), .sum(result[0]), .cout(c1));
  fulladder u1 (.x(r1[1]), .y(r2[1]), .cin(c1),  .sum(result[1]), .cout(c2));
  fulladder u2 (.x(r1[2]), .y(r2[2]), .cin(c2),  .sum(result[2]), .cout(c3));
  fulladder u3 (.x(r1[3]), .y(r2[3]), .cin(c3),  .sum(result[3]), .cout(carry));
endmodule

More verbose, but immune to port reordering in the fulladder definition. For a four-bit adder the difference is minor. For a module with many ports, it is not.

For completeness, here is the same ripple-carry chain expressed with generate and a parameter to make the width configurable:

module fa_nbit #(parameter N = 4) (result, carry, r1, r2, cin);
  input  [N-1:0] r1;
  input  [N-1:0] r2;
  input          cin;
  output [N-1:0] result;
  output         carry;

  wire [N:0] c;       // c[0] = cin, c[N] = carry out

  assign c[0]  = cin;
  assign carry = c[N];

  genvar i;
  generate
    for (i = 0; i < N; i = i + 1) begin : adder_chain
      fulladder u (
        .x   (r1[i]),
        .y   (r2[i]),
        .cin (c[i]),
        .sum (result[i]),
        .cout(c[i+1])
      );
    end
  endgenerate
endmodule

To instantiate a 4-bit version (default):

fa_nbit fa4 (.r1(a), .r2(b), .cin(1'b0), .result(s), .carry(cout));

To instantiate a 16-bit version:

fa_nbit #(.N(16)) fa16 (.r1(a), .r2(b), .cin(1'b0), .result(s), .carry(cout));

The #(.N(16)) overrides the parameter at instantiation time. The generate loop then unrolls into 16 fulladder instances automatically with the same ripple-carry structure as 5.5.1 and 5.5.2, generalized to any width.

Addendum: EDA Playground Examples

↑ Back to top