Skip to content

GLS · Chapter 5 · GLS Testbench Adaptation

Hierarchical References & Bind After Synthesis

The internal signal you probed by name in RTL may simply not exist after synthesis. Optimization renames, merges, and encodes internal signals, so a state enum becomes a handful of renamed flip-flop outputs and intermediate nets dissolve into cells. As a result, a hierarchical reference into a submodule state variable resolves to nothing or reads X on the netlist. There are three durable ways to keep checkers and monitors working across synthesis. Reference ports, since module boundaries survive. Preserve specific signals with a keep or dont touch pragma so a name stays for debug, at a possible area cost. Or use bind to attach a monitor to an instance by port connection rather than a fragile textual path. This lesson shows when each applies, and notes that a resolved reference still does not make GLS a timing signoff.

Foundation12 min readGLSHierarchical ReferencebindSynthesisCheckers

Chapter 5 · Section 5.2 · GLS Testbench Adaptation

Project thread — the FSM's state is exactly the kind of internal signal synthesis encodes into flops. This lesson is how a checker keeps watching it; 5.5 binds a monitor to the FSM.

1. Why Should I Learn This?

The fastest way to break a testbench on the netlist is a reference to an internal signal that no longer exists.

  • Synthesis renames/encodes internals — stateq_reg[*].
  • Ports survive; internal names usually don't.
  • bind and preserved signals are how you keep visibility deliberately.

This is the concrete fix for assumption (1) of 5.1, and it underlies the FSM checker in 5.5.

2. Real Silicon Story — the monitor that watched a signal that no longer existed

A verification monitor keyed off dut.u_ctrl.state to check FSM legality. On RTL it worked; on the netlist it either failed to elaborate or read constant X.

The FSM's state had been encoded into flip-flops and renamed by synthesis — there was no net called state anymore. The monitor was watching a ghost. Rebinding it via bind to the DUT instance and checking the outputs (or a preserved state signal) restored real coverage.

Lesson: internal RTL names are not guaranteed to survive synthesis. Use ports, bind, or explicitly preserved signals for gate-level visibility.

3. Concept — why internals break, and three durable fixes

Why hierarchical references break:

  • Renaming — internal nets get tool-generated names (n123, q_reg[2]).
  • Encoding — a state enum becomes several flop outputs (one-hot/binary).
  • Merging/dissolving — combinational nets are absorbed into cells; some disappear.
  • Result: tb.dut.u_ctrl.state fails to resolve or reads X.

Fix (1) — reference ports.

  • Module ports persist across synthesis (they are the boundary contract).
  • Prefer port-based checking wherever possible.

Fix (2) — preserve specific signals.

  • A preserve / keep / dont_touch pragma tells synthesis to not optimise away a named signal.
  • Keeps a name alive for debug — at a possible area/optimization cost; use sparingly.

Fix (3) — bind.

  • bind attaches a checker/monitor module to a DUT instance by port connection.
  • It binds to the instance (which persists), not a fragile textual path — so the same bound checker works on RTL and netlist.
  • Caveat: if the checker needs an internal signal, that signal must still exist (preserve it).

Scope: these restore visibility; they don't change behaviour, and a resolved reference doesn't make GLS a timing proof (STA signs off, 0.3).

RTL hierarchy exposes named state; synthesized hierarchy renames it to flop outputs; ports persist, bind attaches a monitor to the instanceports (clk, rst_n, out)ports (clk, rst_n, out)SURVIVE synthesis — check hereSURVIVE synthesis — check herestate (RTL) -> q_reg[*]state (RTL) -> q_reg[*]RENAMED/encoded — ref breaksRENAMED/encoded — ref breaksu_ctrlu_ctrlsub-blocksub-blockbind checkerbind checkerattached to instance by portattached to instance by portu_dutu_dutDUT (ports persist)DUT (ports persist)tb_duttb_duttestbenchtestbench
Figure 1 — why hierarchical references break, and how to keep visibility (representative). In RTL the hierarchy exposes named internals (u_ctrl.state). Synthesis encodes state into renamed flops (q_reg[*]) and dissolves internal nets, so tb.dut.u_ctrl.state no longer resolves. Durable fixes: reference PORTS (they persist), PRESERVE a named signal for debug (keep/dont_touch), or BIND a monitor module to the DUT instance by port connection (binds to the instance, not a textual path).

4. Mental Model — ports are the street address; internals are the furniture

Think of the DUT as a house.

  • Ports are the street address — stable; the mail (your checker) always reaches it.
  • Internal signals are furniture inside — synthesis rearranges and relabels it freely.
  • A hierarchical reference to an inner signal is mail addressed to a specific chair — it fails when the chair is moved or removed.
  • bind delivers to the house (instance) and brings its own furniture (checker logic); preserving a signal is nailing one chair in place for debug.

Address the house (ports/instance), not the furniture (fragile internal names).

5. Working Example — port-based check and a bound monitor

Fragile internal reference vs durable alternatives:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// FRAGILE — internal reference that breaks after synthesis
if (tb.u_dut.u_ctrl.state != S_RUN) $error("bad state");   // 'state' renamed/encoded -> X or unresolved
 
// DURABLE (1) — check a PORT (survives synthesis)
if (tb.u_dut.out != golden) $error("output mismatch");     // ports persist
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// DURABLE (3) — bind a checker module to the DUT instance (by PORT connection)
module fsm_checker (input logic clk, rst_n, input logic [3:0] out);
  // ... assertions/coverage on the ports (and any PRESERVED internals) ...
endmodule
 
// bind attaches the checker to every dut instance, on RTL AND netlist:
bind dut fsm_checker u_chk (.clk(clk), .rst_n(rst_n), .out(out));

Practical context (representative, tool-neutral):

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# Keeping gate-level visibility deliberately:
#   ports          -> always available (boundary contract)
#   bind checker   -> attaches to the instance on RTL AND netlist (reuse one checker)
#   preserve 'state' (keep / dont_touch pragma) -> a named internal survives for debug
# Cost note: preserving signals can block optimization (area/timing) -> use sparingly.

Port reference survives, internal reference reads X — as a real waveform:

Post-synthesis: a port reference tracks the real value; a renamed internal reference reads X

8 cycles
An output port shows the real value while a hierarchical reference to a renamed internal signal reads X on the netlistport OK; internal ref = Xport OK; internal ref …clkout (port)state (internal ref)XXXXXXXXt0t1t2t3t4t5t6t7
Representative. On the netlist, the output PORT (out) carries the real value, so a port-based checker works. A reference to the renamed internal 'state' resolves to a signal that no longer exists as such and reads X — a false failure. Reference ports (or bind/preserve), not fragile internal names.

6. Debugging Session — a monitor watching a renamed signal

1

A gate-level monitor fails to elaborate or reads constant X because it references an internal signal that synthesis renamed and encoded into flops — the signal no longer exists under that name; fix with port-based checks, bind, or a preserved signal

INTERNAL NAMES DON'T SURVIVE SYNTHESIS
Symptom

A monitor keyed on dut.u_ctrl.state works on RTL but, on the netlist, either fails to elaborate (unresolved path) or reads constant X.

Root Cause

Synthesis renamed and encoded state — the enum became several renamed flip-flop outputs (q_reg[*]), and intermediate nets were dissolved into cells. There is no net called state on the netlist, so the hierarchical path resolves to nothing (elaboration error) or to a signal that reads X. The monitor is watching a ghost — a signal that existed in RTL but not, by that name, in the gate-level design. It is not a netlist bug; it is a reference to an optimised-away/renamed internal.

Fix

Restore visibility durably: check ports where the information is observable at the boundary; bind a checker module to the DUT instance (port-connected, so it works on RTL and netlist); and where an internal signal is genuinely needed, preserve it explicitly (a keep/dont_touch pragma) so its name survives — accepting the possible area/optimization cost. The lesson: internal RTL names are not guaranteed to survive synthesis (renaming/encoding/merging); write gate-level checks against ports, via bind, or against explicitly preserved signals — not fragile hierarchical paths. (Restoring a reference doesn't change behaviour or close timing — STA signs off, 0.3.)

7. Common Mistakes

  • Referencing internal signals by RTL name on the netlist. They're renamed/encoded/removed.
  • Assuming state variables persist. An enum becomes flop outputs (q_reg[*]).
  • Over-preserving signals. Blocks optimization (area/timing) — preserve sparingly.
  • Binding to an internal that was still optimised away. bind needs the signal to exist (preserve it).
  • Thinking a resolved reference means timing is proven. GLS stays dynamic (0.3).

8. Industry Best Practices

  • Prefer port-based checking — ports are the stable contract.
  • Use bind to attach reusable checkers to instances (RTL + netlist).
  • Preserve only the few signals you truly need for gate-level debug.
  • Plan visibility before synthesis — decide what to keep.
  • Keep GLS/STA distinct — visibility fixes don't close timing.

Senior Engineer Thinking

  • Beginner: "dut.u_ctrl.state reads X — the netlist lost my state."
  • Senior: "Synthesis encoded state into renamed flops — there's no state net. I'll check ports, bind a monitor to the instance, and preserve the one internal I truly need."

The senior designs for gate-level visibility with ports/bind/preserve, expecting internal names to change.

Silicon Impact

Gate-level visibility is what lets GLS catch real problems — uninitialized state, illegal transitions, timing-dependent behaviour. If your monitors silently watch ghost signals (renamed-away internals reading X), you either get false failures (noise that invites waivers) or, worse, false passes where a check never really evaluates — letting a genuine bug slip toward silicon (0.3). Building checks against ports, bind, and deliberately preserved signals keeps gate-level coverage real, so the GLS run you trust is actually observing the design.

Engineering Checklist

  • Referenced ports for gate-level checks where possible.
  • Used bind to attach reusable checkers to DUT instances.
  • Preserved only the specific internal signals needed for debug.
  • Confirmed bind-referenced internals still exist on the netlist.
  • Verified checks actually evaluate on the netlist (no ghost/X references).

Try Yourself

  1. Reference an internal signal (dut.u_ctrl.state) from a testbench on the RTL — it works.
  2. Observe: run the same reference on the netlist — it fails to resolve or reads X.
  3. Change: rewrite the check against an output port, and add a bind-attached checker module.
  4. Expect: the port/bind checks work on both RTL and netlist. Then add a preserve pragma to keep one internal and confirm its name survives.

Any free Verilog simulator supports hierarchical references and bind. Preserve pragmas are synthesis-tool features, but the concept is universal. No paid tool required.

Interview Perspective

  • Weak: "Hierarchical references work the same on RTL and netlist."
  • Good: "Synthesis renames and encodes internals, so internal references break; reference ports instead."
  • Senior: "State becomes renamed flop outputs and nets dissolve, so dut.x.state won't resolve. I write gate-level checks against ports, bind reusable checkers to instances so one checker works on RTL and netlist, and preserve the few internals I truly need — accepting the optimization cost. Visibility fixes don't close timing; STA does."

9. Interview / Review Questions

10. Key Takeaways

  • Synthesis renames, encodes, and merges internal signals, so hierarchical references like dut.u_ctrl.state break or read X on the netlist (a state enum becomes renamed flop outputs q_reg[*]).
  • Ports survive synthesis — prefer port-based checking for gate-level runs.
  • bind attaches a checker/monitor to a DUT instance by port connection, so one checker works on RTL and netlist (bind to the instance, not a textual path).
  • Preserve (via keep/dont_touch) only the specific internal signals you truly need for debug — at a possible area/optimization cost.
  • These restore visibility, not timing — a resolved reference doesn't make GLS a signoff (STA does, 0.3). Next: 5.3 — timing-aware stimulus & clock generation.

Quick Revision

Internal refs break after synthesis (rename/encode/merge → state becomes q_reg[*], reads X). Ports survive — check them. bind a checker to the DUT instance by port → works on RTL + netlist. Preserve (keep/dont_touch) only internals you must reach (area cost). Visibility ≠ timing closure (STA signs off). Next: 5.3 — timing-aware stimulus.