Perl Part II · Module 20 · Netlist Processing
Port Connections Within One Level
With instances and their connection lists parsed, inverting the data gives a net-to-pins map: for any net, every instance pin that touches it. That answers a great many real questions at one level of hierarchy, and it stops abruptly at two others. This chapter builds the map, uses it to find unconnected pins, and then measures the two things the netlist text genuinely does not contain — which is worth knowing precisely, because both gaps are easy to paper over with a number that looks like an answer.
Advanced14 min readPerlPart IINetlists
Part II · Module 20 · Chapter 20.6 · Port Connections Within One Level
1. Net To Connections
The instance data is keyed by instance. The useful inversion is keyed by net — Chapter 7.7's hash of arrays again, built in four lines:
for my $i (grep { $_->{parent} eq $LEVEL } @$inst) {
for my $p (ports_of($i->{conns})) {
my ($port, $net) = @$p;
push @{ $net_conns{$net} }, "$i->{name}.$port";
}
}Note the grep: one level only. Instances inside counter_bit are excluded, because their nets live in a different namespace — which is section 4's subject.
use strict;
use warnings;
my @KEYWORDS = qw(module endmodule input output inout wire reg assign
parameter localparam always initial generate endgenerate
genvar begin end supply0 supply1);
my %IS_KW = map { $_ => 1 } @KEYWORDS;
sub ports_of { # depth-aware split (20.3)
my ($conns) = @_;
my @frag;
my $cur = "";
my $depth = 0;
for my $ch (split //, $conns) {
if ($ch =~ /[({\[]/) { $depth++ }
elsif ($ch =~ /[)}\]]/) { $depth-- }
if ($ch eq "," && $depth == 0) { push @frag, $cur; $cur = ""; next }
$cur .= $ch;
}
push @frag, $cur if $cur =~ /\S/;
my @p;
for my $f (@frag) {
push @p, [$1, defined $2 ? $2 : ""] if $f =~ /\.(\w+)\s*\(\s*(.*?)\s*\)\s*$/s;
}
return @p;
}
sub read_netlist {
my ($file) = @_;
open my $fh, "<", $file or die "$file: $!";
my (@mods, @inst, $cur, $buf, $in_block);
while (my $raw = <$fh>) {
my $l = $raw;
if ($in_block) { if ($l =~ s{.*?\*/}{}) { $in_block = 0 } else { next } }
$l =~ s{/\*.*?\*/}{}g;
if ($l =~ s{/\*.*$}{}) { $in_block = 1 }
$l =~ s{//.*$}{};
next unless $l =~ /\S/;
if ($l =~ /^\s*module\s+(\w+)/) { push @mods, $1; $cur = $1; $buf = ""; next }
if ($l =~ /^\s*endmodule\b/) { undef $cur; $buf = ""; next }
next unless defined $cur;
$buf .= " $l";
next unless $buf =~ /;/;
my $stmt = $buf; $buf = "";
$stmt =~ s/;.*$//; $stmt =~ s/\s+/ /g; $stmt =~ s/^ | $//g;
next unless $stmt =~ /^(\w+)\s+(\w+)\s*\((.*)\)$/;
next if $IS_KW{$1};
push @inst, { type => $1, name => $2, parent => $cur, conns => $3 };
}
close $fh;
return (\@mods, \@inst);
}
my ($mods, $inst) = read_netlist("netlist/counter_syn.v");
# ---- net -> every (instance, port) that touches it, WITHIN ONE MODULE
my $LEVEL = "counter_top";
my (%net_conns, @unconnected);
for my $i (grep { $_->{parent} eq $LEVEL } @$inst) {
for my $p (ports_of($i->{conns})) {
my ($port, $net) = @$p;
if ($net eq "") { push @unconnected, "$i->{name}.$port ($i->{type})"; next }
push @{ $net_conns{$net} }, "$i->{name}.$port";
}
}
printf "connections inside %s only:\n\n", $LEVEL;
printf "%-12s %-7s %s\n", "NET", "TOUCHES", "instance.port";
for my $net (sort keys %net_conns) {
printf "%-12s %-7d %s\n", $net, scalar @{ $net_conns{$net} },
join(" ", @{ $net_conns{$net} });
}
print "\npins with an EMPTY connection:\n";
printf " %s\n", $_ for @unconnected;
printf " (%d found)\n", scalar @unconnected;
print "\nthe four most-connected nets:\n";
my @by_fanout = sort { @{ $net_conns{$b} } <=> @{ $net_conns{$a} } || $a cmp $b }
keys %net_conns;
for my $net (@by_fanout[0 .. 3]) {
printf " %-12s %d\n", $net, scalar @{ $net_conns{$net} };
}
# a CONSTANT is not a net, and it sits in the map looking like one
my @consts = grep { /^\d+'[bdho]/ } keys %net_conns;
printf "\nconstants that look like nets in the map: %s\n",
(@consts ? join(", ", sort @consts) : "none");
# ---- THE LIMIT: which connection DRIVES the net?
print "\nwhich connection DRIVES net n10?\n";
printf " touching n10: %s\n", join(" ", @{ $net_conns{n10} || [] });
print " the netlist text does not say which of these is an output.\n";
print " 'Z' is an output and 'CP' an input -- but that is a fact about the\n";
print " CELL LIBRARY, not about this file. Without it, a driver cannot be\n";
print " identified, so fanout above is a count of CONNECTIONS, not loads.\n";
# ---- THE OTHER LIMIT: the same physical net has two names across a boundary
print "\ncrossing a hierarchy boundary:\n";
my ($bit0) = grep { $_->{name} eq "bit0" } @$inst;
my %b0 = map { $_->[0] => $_->[1] } ports_of($bit0->{conns});
printf " in %s: bit0.Q is connected to net '%s'\n", $LEVEL, $b0{Q};
my ($sreg) = grep { $_->{parent} eq "counter_bit" && $_->{name} eq "state_reg" } @$inst;
my %sr = map { $_->[0] => $_->[1] } ports_of($sreg->{conns});
printf " in counter_bit: state_reg.Q is connected to net '%s'\n", $sr{Q};
printf " same physical net, two names ('%s' and '%s').\n", $b0{Q}, $sr{Q};
print " Joining them needs the port MAP of counter_bit, which means parsing\n";
print " its header -- and doing it for every level is writing an elaborator.\n";connections inside counter_top only:
NET TOUCHES instance.port
1'b0 1 clk_gate.TE
clk 1 clk_gate.CP
count[0] 4 U20.A bit0.Q U21.A2 U22.A1
count[1] 3 U21.A1 bit1.Q U22.A2
count[2] 3 U23.A1 bit2.Q U24.A2
count[3] 2 U25.A1 bit3.Q
enable 1 clk_gate.E
gclk 2 clk_gate.ECK clk_buf.I
n10 5 clk_buf.Z bit0.CP bit1.CP bit2.CP bit3.CP
n11 2 U20.ZN bit0.D
n12 2 U21.Z bit1.D
n13 3 U22.Z U23.A2 U24.A1
n14 2 U23.Z bit2.D
n15 2 U24.Z U25.A2
n16 2 U25.Z bit3.D
rst_n 5 bit0.CDN bit1.CDN bit2.CDN bit3.CDN U26.A
pins with an EMPTY connection:
U26.Z (BUFFD1)
U27.ZN (TIELO)
(2 found)
the four most-connected nets:
n10 5
rst_n 5
count[0] 4
count[1] 3
constants that look like nets in the map: 1'b0
which connection DRIVES net n10?
touching n10: clk_buf.Z bit0.CP bit1.CP bit2.CP bit3.CP
the netlist text does not say which of these is an output.
'Z' is an output and 'CP' an input -- but that is a fact about the
CELL LIBRARY, not about this file. Without it, a driver cannot be
identified, so fanout above is a count of CONNECTIONS, not loads.
crossing a hierarchy boundary:
in counter_top: bit0.Q is connected to net 'count[0]'
in counter_bit: state_reg.Q is connected to net 'Q'
same physical net, two names ('count[0]' and 'Q').
Joining them needs the port MAP of counter_bit, which means parsing
its header -- and doing it for every level is writing an elaborator.Sixteen nets, each with the pins that touch it. That table answers the questions people actually bring to a netlist: what is on this net, which net feeds that pin, how many things touch the clock.
One detail in the output worth noticing: 1'b0 is in the map, looking like a net. It is a constant — the clock gate's test-enable tied low. A constant is not a net and should not be in a connectivity analysis, which is why the script reports which keys are constants:
my @consts = grep { /^\d+'[bdho]/ } keys %net_conns;Left unfiltered, a design with two hundred tie-lows gets a "net" called 1'b0 with a fanout of two hundred, and it will be the top entry in every fanout report you produce.
2. Unconnected Pins
Measured: two of them.
U26.Z (BUFFD1)
U27.ZN (TIELO)An empty connection — .Z() — is legal and means exactly what it says: nothing is attached. Finding them is a one-line test during the inversion, and the result is one of the genuinely useful QC checks Chapter 20.7 automates.
What an unconnected pin means depends on which pin it is, and the netlist text alone cannot tell you:
An unconnected output is usually harmless and sometimes informative — a buffer driving nothing is dead logic that synthesis should have removed, so its presence can indicate the netlist was written before a final optimisation pass.
An unconnected input is a real problem. It floats, simulates as x, and may synthesise to something unintended.
An unconnected tie cell output, like U27.ZN above, is pointless but harmless.
Telling these apart needs pin directions — which is the next section, and the reason this check reports what it found rather than how bad it is.
3. The Driver Limit
The map says five pins touch n10:
clk_buf.Z bit0.CP bit1.CP bit2.CP bit3.CPWhich one drives it? Anybody who has seen a cell library knows: Z is an output and CP is a clock input, so clk_buf drives four flop clocks. But read where that knowledge came from — the library, not this file.
The netlist records which net is attached to which pin and nothing about the pin. So the honest statement of what the map gives you is:
- it can tell you every pin on a net, and how many there are
- it cannot tell you which is the driver, which are loads, or whether the net has a driver at all
That has a direct consequence for the most common thing people compute from this map. The number 5 against n10 is a count of connections, not a fanout. Fanout is loads, which is connections minus the driver — and knowing that requires knowing which one is the driver.
# honest
printf "net %s: %d connections\n", $net, scalar @{ $net_conns{$net} };
# dishonest without the library
printf "net %s: fanout %d\n", $net, scalar @{ $net_conns{$net} };Two ways to get the missing information, both of which mean leaving the text:
Read the library. A Liberty file, or the cell models' own module headers, give every pin's direction. If the cell models are available — as lib/cells.v is here — their input/output declarations are themselves scannable, and that turns connections into directed connectivity.
Ask the tool. A connectivity or fanout report from the synthesis tool is authoritative and already knows.
4. The Boundary Limit
The second limit is structural, and the measurement makes it concrete:
in counter_top: bit0.Q is connected to net 'count[0]'
in counter_bit: state_reg.Q is connected to net 'Q'
same physical net, two names ('count[0]' and 'Q').One physical net, two names. Inside counter_bit the flop's output is called Q, because that is the module's port name. In counter_top the same signal is count[0]. A text scan sees two unrelated strings in two unrelated namespaces.
Joining them requires the port map of counter_bit: which port name corresponds to which position, and what each is connected to at the instantiation. That is parseable — the module header and the input/output lines are right there — and doing it at every level, for every instance, with buses split and reordered, is the point where you have written an elaborator.
So the ceiling is explicit, and it is why the registry's own description of this chapter says within one level:
Inside one module, the map is complete and reliable. Every pin, every net, exact.
Across a boundary, it is two maps that cannot be joined without reading port maps — and across several boundaries, with buses and generate blocks, you are back at Chapter 20.2's bottom row, where the answer is the tool's output.
5. What To Remember
- Invert the instance data to get a net-to-pins map — a hash of arrays (7.7), four lines.
- Filter to one module with
grepbefore inverting; other levels are a different namespace. - Measured: 16 nets inside one module, each with its full pin list.
- A constant is not a net.
1'b0sits in the map looking like one, and in a real design it becomes the highest-fanout "net" in every report. Filter/^\d+'[bdho]/. - Measured: 2 unconnected pins found —
U26.ZandU27.ZN. - An empty connection is legal and means nothing is attached.
- What an unconnected pin means depends on its direction: an unconnected output is usually dead logic, an unconnected input floats and is a real problem.
- Measured: 5 pins touch
n10, and the text does not say which drives it. - Pin direction is a fact about the library, not about the netlist.
- So the count is connections, not fanout — fanout is loads, and loads are connections minus the driver.
- Say
connections, notfanout, unless you have read the library. - Get directions from the cell models' own
input/outputlines when they are available, or from the tool. - Guessing direction from pin names (
Z,Q,ZNout;A,D,CPin) works often and fails silently — if you use it, label the output as inferred (18.8). - Measured: one physical net has two names across a boundary —
count[0]above,Qinside. - Joining them needs port maps at every level, which is writing an elaborator.
- Inside one module the map is exact; across a boundary it is two maps that cannot be joined.
- Know which questions the ceiling excludes — "where does this signal come from" is a tool question, and saying so is more useful than answering it wrongly.
Chapter 20.7 closes the module by turning these pieces into the small set of checks worth running automatically — the ones that catch real problems, reported in a form a reviewer can act on rather than a wall of output nobody reads.
Where this fits
Part of the Perl curriculum.
