Modules, Ports and Signals

A modern chip contains billions of transistors, and nobody — literally nobody — holds that in their head. What makes chips designable at all is the same trick that makes big software possible: boxes within boxes. A chip is a box containing a few dozen big boxes (CPU cluster, GPU, memory controller), each containing smaller boxes, down through perhaps a dozen levels until the boxes are as simple as an adder. In SystemVerilog every box is a module, its connection points are ports, and the wires between boxes are signals. This lesson is the grammar of boxes — brief to state, and used in every design you will ever write.

Anatomy of a module

Here is a half adder — it adds two bits, producing a sum bit and a carry bit:

module half_adder ( input logic a, input logic b, output logic sum, output logic carry ); assign sum = a ^ b; // XOR: 1 when the bits differ assign carry = a & b; // AND: 1 only for 1 + 1 endmodule

Signals wider than one bit are declared as vectors: logic [7:0] bus is eight bits, bus[7] the most significant (the left index) down to bus[0]. A vector [hi:lo] has hi - lo + 1 bits, and you can slice it: bus[3:0] is the low nibble. Widths are the units-of-measure of hardware design — tools warn loudly when the two sides of a connection disagree, and you should listen.

Building bigger boxes: instantiation

The payoff of a full adder — a 1-bit adder with carry-in, the cell that ripple-carry adders chain — is that it does not need to be designed from scratch. It is assembled from two half adders and an OR gate, by instantiating them:

module full_adder ( input logic a, input logic b, input logic cin, output logic sum, output logic cout ); logic s1, c1, c2; // internal signals: wires inside the box half_adder ha1 (.a(a), .b(b), .sum(s1), .carry(c1)); half_adder ha2 (.a(s1), .b(cin), .sum(sum), .carry(c2)); assign cout = c1 | c2; endmodule

Each half_adder ha1 (…) line places a copy of the half-adder hardware inside this module and gives it an instance name. The .port(signal) pairs are named connections — solder joints between the inner box's pins and this box's wires. (Positional connection half_adder ha1 (a, b, s1, c1); also exists; professionals avoid it for the same reason they avoid calling functions with eight anonymous booleans.) The three logic declarations are internal signals: wires that exist only inside this box, invisible through its walls.

The hierarchy, drawn

Follow a bit through: ha1 adds a and b; its sum meets cin in ha2; a carry from either stage means carry-out. And the crucial structural fact: from outside, full_adder is just five pins. Whoever instantiates it — a 32-bit adder chaining thirty-two of them, say — neither knows nor cares what is inside. That wall-by-wall ignorance is what lets a thousand engineers build one chip.

Simulate the assembly

Composition in the description mirrors composition in simulation. Model the half adder once, wire two of them exactly as the SystemVerilog does, and the full adder's truth table falls out:

// half_adder, as behaviour: {sum, carry} function halfAdder(a: number, b: number) { return { sum: a ^ b, carry: a & b }; } // full_adder = two half adders + OR, wired like the SystemVerilog. function fullAdder(a: number, b: number, cin: number) { const ha1 = halfAdder(a, b); // instance ha1 const ha2 = halfAdder(ha1.sum, cin); // instance ha2 return { sum: ha2.sum, cout: ha1.carry | ha2.carry }; } console.log("a b cin | sum cout"); console.log("--------+---------"); for (let a = 0; a <= 1; a++) for (let b = 0; b <= 1; b++) for (let cin = 0; cin <= 1; cin++) { const r = fullAdder(a, b, cin); console.log(`${a} ${b} ${cin} | ${r.sum} ${r.cout}`); }

Every row satisfies a + b + c_{in} = 2\,c_{out} + sum — the two output bits really are the binary count of how many input bits were 1.

On a real SoC the instance tree is a genuine wonder. At the top: a module with a name like soc_top, containing maybe thirty children — CPU cluster, GPU, memory controllers, peripherals. Each CPU core is thousands of modules; the whole tree runs ten to fifteen levels deep and totals millions of instances, many of them the same module stamped out over and over (one register-file cell, instantiated 4,096 times). This is also the economic unit of the industry: companies buy and sell IP blocks — pre-designed, pre-verified module subtrees. When a phone chip "contains Arm cores", what changed hands was, at bottom, a directory of SystemVerilog modules and the right to instantiate them.

half_adder ha1 (…) looks like calling half_adder() — it is nothing of the sort. A function call reuses one piece of code at different moments in time. An instantiation duplicates the hardware: ha1 and ha2 are two separate piles of gates, both physically present, both computing all the time, simultaneously. Ten thousand instances cost ten thousand copies' worth of silicon. This changes your design instincts: in software, calling a helper twice is free; in hardware, instantiating twice doubles the area — and conversely, hardware gets true parallelism without even trying, because every instance genuinely runs at once. Sharing one piece of hardware across time — the software instinct — must be built deliberately, with multiplexers and a schedule.