Topic 8 · Hierarchy & Reuse

Module Hierarchy

Video 1 of 4 · ~10 minutes

Dr. Mike Borowczak · Electrical & Computer Engineering · CECS · UCF

HierarchyParametersGenerateReuse

🌍 Where This Lives

Where it shows up

Linux is 30 million lines of code. The chip in your phone is hundreds of millions of gates. The CERN ATLAS detector handles a petabyte of raw data per second. None of these has ever been held in any one person's head, and every one of them works. The trick is the same one architects use for skyscrapers: nobody draws the whole building — they draw the rooms, and the rooms are made of walls, and the walls are made of bricks.

When it goes wrong

Boeing's 737 MAX MCAS was added without any one engineer tracing how it interacted with the rest of the autopilot. Ariane 5 reused a piece of Ariane 4 code that no longer made sense in its new context — but it was copied, not instantiated, so the assumption it carried was buried and forgotten. When the structure isn't clean, the failure modes get clever in ways no individual reviewer can anticipate.

⚠️ Named Ports Are Mandatory

❌ Wrong Model

counter c1 (clk, rst, en, count); — positional connection. Shorter, right?

✓ Right Model

counter u_cnt0 (.i_clk(clk), .i_reset(rst), .i_enable(en), .o_count(count));named port connection. If someone adds a port, swaps an order, or adds an input, positional connections silently break. Named connections fail at compile — fast, loud, fixable.

The receipt: Every industry Verilog style guide forbids positional connections above trivial cases. Lint tools flag them. Code reviews catch them. Today's lab forbids them. Every module instance in this course uses .port(signal).

👁️ I Do — Instantiate a Debouncer

// Top-level — wraps debounce as sub-module
module button_handler (
    input  wire i_clk, i_reset, i_btn_raw,
    output wire o_btn_clean
);
    // Instantiate by name
    debounce #(.CLKS_STABLE(500_000)) u_debounce_sw1 (
        .i_clk   (i_clk),
        .i_reset (i_reset),
        .i_noisy (i_btn_raw),
        .o_clean (o_btn_clean)
    );
endmodule
RTL block diagram of button_handler: top-level outer dashed box wraps a debounce instance u_debounce_sw1 with CLKS_STABLE=500_000. Color-coded signals i_clk (blue), i_reset (red), i_btn_raw (orange) enter the outer module and map to inner .i_clk, .i_reset, .i_noisy; inner .o_clean (green) exits as o_btn_clean.
My thinking: Three disciplines. (1) Descriptive instance u_debounce_sw1, not u1. (2) Parameter override #(.CLKS_STABLE(500_000)). (3) Named ports — .i_clk(i_clk). Each colored signal in the code maps to the same-colored arrow in the RTL.

🤝 We Do — Naming Conventions

ConceptPrefix / PatternExample
Module inputi_i_clk, i_data
Module outputo_o_sum, o_valid
Module inout (rare)io_io_bus
Internal registerr_r_state, r_counter
Internal wirew_w_sum, w_carry
Module instanceu_u_debounce_sw1
Compile-time constantALL_CAPSCLKS_STABLE, WIDTH
Together: Consistent prefixes turn a waveform into a readable document. When you see u_uart_rx.r_state, you instantly know: this is a register inside the uart_rx instance. The conventions aren't arbitrary — they're the lowest-cost way to make signals navigable.

🧪 You Do — Spot the Bad Hierarchy

module top (input wire clk,
            input wire data_in,
            output wire done);
    wire x, y, z;
    submod1 u1 (clk, data_in, x);
    submod2 u2 (x, y);
    submod3 u3 (clk, y, z);
    submod4 u4 (z, done);
endmodule
RTL block diagram of the bad-hierarchy top: four anonymous submod1..submod4 blocks named u1..u4 wired in a chain by single-letter wires x, y, z. Inputs clk (blue) and data_in (pink) enter; output done (green) exits. The grey x, y, z arrows and unnamed instance labels visually demonstrate that positional connections with meaningless names hide the design intent.

List the code-review issues.

Issues:
  1. Positional port connections everywhere — swap one and you'd never know.
  2. Meaningless instance names (u1, u2, ...) — unreadable in waveforms.
  3. Meaningless wire names (x, y, z) — can't tell what they carry.
  4. Module names say nothing (submod1) — file-naming bug.
Fix: named .ports, meaningful instances (u_decoder, u_filter), wires named for their purpose (w_decoded_byte).
▶ LIVE DEMO

Build a Hierarchical Design

~5 minutes — sync + debounce + edge = button_handler

▸ COMMANDS

cd lecture_examples/week2_day08/d08_s1_ex1/
ls *.v               # 4 module files + tb
cat day08_ex01_button_handler.v   # top-level composition
make sim
make stat            # total for full hierarchy
make prog

▸ EXPECTED STDOUT

sync_2ff.v
debounce.v
edge_detect.v
day08_ex01_button_handler.v
tb_button_handler.v

PASS: sync works
PASS: bounces filtered
PASS: 1-cycle release pulse
PASS: 1-cycle press pulse
=== 8 passed, 0 failed ===

Per-instance breakdown:
  u_sync:     2 SB_DFF
  u_debounce: ~30 cells
  u_edge:     1 SB_DFF + 1 LUT

🔧 Hierarchy Preserved or Flattened?

$ yosys -p "read_verilog *.v; hierarchy -top button_handler; synth_ice40; stat"

=== button_handler ===     ← top level
   Number of cells:       42
     $scopeinfo             3    ← hierarchy markers (retained)

=== debounce ===           ← sub-module, preserved
   Number of cells:       37
     SB_CARRY              18
     SB_DFFESR             20
     ...
What to notice: Yosys preserves hierarchy by default. The stat output shows each module's contribution separately. You can still inspect the 37-cell debouncer independently from the 3-cell sync and edge-detect. If you wanted to flatten everything into one pool for optimization, add flatten to the Yosys script — useful for critical-path analysis, costly to debug.
Pro tip: Keep hierarchy during simulation (navigability) and dev-stage synthesis. Flatten only when you need the absolute last 5% of area/timing from optimization passes.

🤖 Check the Machine

Ask AI: “I have sync_2ff.v, debounce.v, and edge_detect.v. Write a top-level button_handler module that instantiates all three in the correct order, using named port connections and descriptive instance names.”

TASK

AI composes 3 modules into a top.

BEFORE

Predict: wire chains from sync output → debounce input → edge input.

AFTER

Strong AI uses named ports + u_ instances. Weak AI uses positional.

TAKEAWAY

AI handles composition well if you give it the sub-module port lists.

Key Takeaways

 Hierarchy manages complexity. Real designs are 10+ levels deep.

 Always use named port connections (.port(signal)), never positional.

 Naming conventions: i_/o_/r_/w_/u_. Descriptive instance names.

 Yosys preserves hierarchy by default. Flatten only for optimization.

If a reviewer can't tell what a signal carries from its name, it's named wrong.

🔗 Transfer

Parameters & Parameterization

Video 2 of 4 · ~9 minutes

▸ WHY THIS MATTERS NEXT

You now instantiate modules. Next: how to make the same module serve different roles through parameters. One counter module that's 8-bit in one place, 16-bit in another, with any CLKS_STABLE value. This is IP reuse — and it's how industry scales.