Skip to content

gen/fhdl/verilog: Hierarchical Verilog fixes (dropped logic, undriven clocks, renamed domains). - #2765

Merged
enjoy-digital merged 5 commits into
masterfrom
gen/hierarchical-fixes
Nov 9, 2026
Merged

enjoy-digital merged 5 commits into
masterfrom
gen/hierarchical-fixes

Conversation

@enjoy-digital

Copy link
Copy Markdown
Owner

Fixes to the hierarchical Verilog generation (--hierarchical-verilog, Builder(hierarchical=True)), found while using it to break down the utilization of a large design: LitePCIe's soft PCIe PHY at 8 GT/s x4 and x8 (~60-190k LUTs) on a Kintex UltraScale+. Before these fixes the hierarchical netlist failed in Vivado (undriven clocks: no clock signal specified in event control). Once that was fixed, synthesis silently lost logic: 45k LUTs / 24k FFs against 62k / 39k for the flat netlist of the same design.

One commit per issue, each with a test that fails without it:

  1. Logic of an inlined module that is also a shared submodule elsewhere was dropped. LitePCIeEndpoint also registers its PHY as a submodule. The shared alias carried the owner's statement ids, so the alias' hierarchical ancestors counted them as emitted. When the owner was inlined into a common ancestor, that ancestor dropped them and they were emitted nowhere, starting with the PHY's clock assignments. The ids emitted by a subtree now leave out those an alias takes from an owner outside the subtree.
  2. A child was inlined (with its whole subtree) for a signal of its own only driven by its parent. Such a signal is an input port of the child. add_pcie drives the PHY's MSI stream from the SoC, so the whole PHY (~185k LUTs at x8) ended up in the top module and the hierarchical utilization report couldn't break it down. A child is still inlined when it also drives the signal.
  3. A ClockDomain whose clk is another domain's ClockSignal (LiteICLink's shared transmit clock, cd.clk = ClockSignal(...)) crashed the hierarchical lowering (unhashable type: 'ClockSignal'). It's now resolved to the real clock, as the namer does when printing. The namer looks the domains up by name in a dict or a clock domain list, follows chained aliases, and names the domain in its reset-less error (it read sig.cd after sig became None).
  4. Sync logic of a module below a ClockDomainsRenamer was in the old domain when the module was kept hierarchical: sys_clk instead of the renamed one. Each module's sync statements are now grouped by their domain in the design's final fragment.
  5. A signal nobody drives between two modules was declared as an undriven wire in the parent, instead of a reg with its reset value as in the flat netlist. A decoder's ce = Signal(reset=1), never assigned, became 0 and its logic was trimmed. Only the signals a child drives are forced to wires now.

Tested:

  • python3 -m pytest test/hdl/: 63 passed. Full pytest test: the same 15 failures as on master in this environment (test_integration.py CPU boot tests and test_serialboot_abort_recovers_and_loads, missing toolchains), no new one.
  • LitePCIe soft PHY SoC on xcku3p (Vivado 2024.1 synthesis, hierarchical against flat), 8 GT/s x4 endpoint: 67.5k LUTs / 39.2k FFs against 62.4k / 38.8k flat (a bit more from no cross-boundary optimization), no undriven net, 544 modules. The hardware builds stay flat; the hierarchical netlist was used for synthesis reports only.

Written by an AI agent (Claude Code), supervised by Florent Kermarrec.

🤖 Generated with Claude Code

enjoy-digital and others added 5 commits November 9, 2026 13:33
…shared submodule elsewhere.

A module registered as a submodule of two parents is emitted by its first parent (owner), the other one gets
a shared alias, not emitted. The alias carries the owner's statement ids so that the statements Migen merged
into its parent aren't emitted twice, but these ids also counted as emitted by the alias' hierarchical
ancestors: when the owner was inlined into a common ancestor, that ancestor dropped them and they were
emitted nowhere. Seen with LitePCIe's soft PHY (LitePCIeEndpoint also registers its PHY): the PHY's clock
assignments were missing, Vivado: no clock signal specified in event control.

The ids emitted by a subtree now leave out those an alias takes from an owner outside the subtree. Test added.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…riven by its parent.

Such a signal is an input port of the child. Inlining it took the whole subtree into the parent: LiteX's
add_pcie drives the PHY's MSI stream from the SoC, so LitePCIe's soft PHY (~185k LUTs at x8) was emitted in
the top module and the hierarchical utilization report couldn't break it down. A child is still inlined when
it also drives the signal. The test forcing an inlined child now has the child drive the signal too, test
added for the input port.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ClockSignal (hierarchical).

LiteICLink's transceivers set cd.clk = ClockSignal(...) for a shared transmit clock: the flat converter
handles it, the hierarchical one put the ClockSignal in its signal sets (TypeError: unhashable type:
'ClockSignal'). The real clock is now resolved (as the namer does when printing) and is a port of the module.
The namer looks the clock domains up by name in a dict or a clock domain list (the hierarchical one passed a
list: no attribute 'get'), follows chained aliases, and names the domain in its reset-less error. Test added.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ncestor (hierarchical).

A ClockDomainsRenamer renames the domains of the merged fragment of the module it wraps, the fragments of the
modules below keep the names they were built with. A descendant kept as a module (not inlined) had its sync
logic in the old domain (sys_clk instead of the renamed one). Each module's sync statements are now grouped by
their domain in the design's final fragment. Test added (renamed module with a ResetInserter child).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…een modules (hierarchical).

The signals between children were all declared as wires in the parent: one never assigned (as a decoder's
Signal(reset=1) clock enable) was an undriven wire instead of a constant, and the logic it enables was
trimmed. Only the signals a child drives are wires now, the others regs with their reset value as in the flat
netlist. Test added.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@enjoy-digital
enjoy-digital merged commit 0f008ae into master Nov 9, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant