Skip to content
VLSI Mentor

Perl Part II · Module 20 · Netlist Processing

Counting and Classifying Cells

A cell census is the most common thing anybody asks a netlist script for: how many flops, how much combinational logic, how many clock gates, and is there anything in here that should not be. Counting is a hash. The two things worth getting right are that a per-type count of the text is not a count of the design, and that a cell type the classifier does not recognise has to be reported as unrecognised rather than quietly dropped into a bucket called other.

Advanced14 min readPerlPart IINetlists

Part II · Module 20 · Chapter 20.5 · Counting and Classifying Cells

1. Two Censuses From One Netlist

Counting is Chapter 6.8's hash, once per cell type. The interesting part is that there are two legitimate counts, and the script produces both.

Azvya Education Pvt. Ltd.VLSI Mentor
census.pl
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 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 };
    }
    close $fh;
    return (\@mods, \@inst);
}

# ---- the family table. DATA, not logic (20.3's rule): retargeting to another
# library is an edit here. Order matters -- the first match wins.
my @FAMILY = (
  [ qr/^DF/,          "flop"          ],
  [ qr/^CKLN/,        "clock gate"    ],
  [ qr/^CK/,          "clock buffer"  ],
  [ qr/^(BUFF|INV)/,  "buffer/inv"    ],
  [ qr/^(AN|ND|OR|NR|XOR|XNR|MUX|AO|OA)/, "combinational" ],
  [ qr/^(FILL|TIEHI|TIELO|ANTENNA|DCAP)/, "physical-only" ],
);
sub family {
    my ($type) = @_;
    for my $f (@FAMILY) { return $f->[1] if $type =~ $f->[0] }
    return undef;                      # UNKNOWN -- never silently "other"
}

my ($mods, $inst) = read_netlist("netlist/counter_syn.v");
my %IS_MODULE = map { $_ => 1 } @$mods;

# ---- how many times is each module instantiated, for the elaborated count
my %children;
push @{ $children{ $_->{parent} } }, $_ for @$inst;
my %instantiated = map { $_->{type} => 1 } @$inst;
my ($top) = grep { !$instantiated{$_} } @$mods;

my %mult;                              # module -> how many elaborated copies
sub walk_mult {
    my ($mod, $times) = @_;
    $mult{$mod} += $times;
    for my $c (@{ $children{$mod} || [] }) {
        walk_mult($c->{type}, $times) if $IS_MODULE{ $c->{type} };
    }
}
walk_mult($top, 1);

# ---- two censuses: as WRITTEN, and as ELABORATED
my (%text, %elab);
for my $i (@$inst) {
    next if $IS_MODULE{ $i->{type} };
    $text{ $i->{type} }++;
    $elab{ $i->{type} } += $mult{ $i->{parent} } // 1;
}

printf "top module: %s\n", $top;
printf "module copies after elaboration: %s\n\n",
    join(", ", map { "$_ x$mult{$_}" } sort keys %mult);

printf "%-12s %-16s %-10s %-12s %s\n", "CELL TYPE", "FAMILY", "IN TEXT", "ELABORATED", "note";
my ($t_tot, $e_tot) = (0, 0);
for my $type (sort keys %text) {
    my $fam = family($type);
    $t_tot += $text{$type};
    $e_tot += $elab{$type};
    printf "%-12s %-16s %-10d %-12d %s\n", $type, ($fam // "*** UNKNOWN ***"),
        $text{$type}, $elab{$type},
        ($elab{$type} != $text{$type} ? "x" . ($elab{$type} / $text{$type}) : "");
}
printf "%-12s %-16s %-10d %-12d\n", "TOTAL", "", $t_tot, $e_tot;

print "\nper family (elaborated):\n";
my %fam_count;
for my $type (keys %elab) {
    my $f = family($type) // "*** UNKNOWN ***";
    $fam_count{$f} += $elab{$type};
}
printf "  %-16s %d\n", $_, $fam_count{$_} for sort keys %fam_count;

print "\nchecks worth running on a signoff netlist:\n";
my @physical = grep { (family($_) // "") eq "physical-only" } sort keys %elab;
printf "  physical-only cells present: %s\n",
    (@physical ? join(", ", map { "$_ x$elab{$_}" } @physical) : "none");
my @unknown = grep { !defined family($_) } sort keys %elab;
printf "  UNRECOGNISED cell types: %s\n",
    (@unknown ? join(", ", @unknown) : "none");
printf "  flop count (elaborated): %d\n", $fam_count{flop} // 0;
printf "  and in the TEXT it looks like: %d\n",
    (grep { (family($_) // "") eq "flop" } keys %text)
        ? do { my $n = 0; $n += $text{$_} for grep { (family($_)//"") eq "flop" } keys %text; $n }
        : 0;

# ---- is the UNKNOWN branch able to fire? A check that never fires on your
# input is not a check yet (the standing rule of this track).
print "\nproving the classifier can fail -- types it has never seen:\n";
my @PROBE = qw(DFCNQD1 INVD1 LVLHLD1 SDFFRPQ_X2 MY_CUSTOM_RAM retention_ff);
for my $t (@PROBE) {
    my $f = family($t);
    printf "  %-14s -> %s\n", $t, ($f // "*** UNKNOWN *** (would be reported)");
}
my $unknown_probe = grep { !defined family($_) } @PROBE;
printf "  %d of %d probe types are unrecognised, so the branch is live\n",
    $unknown_probe, scalar @PROBE;

print "\nand why 'other' would be the wrong bucket:\n";
print "  a retention flop classified as 'other' disappears from the flop\n";
print "  count, and a flop count that is quietly low is exactly the number\n";
print "  a reviewer trusts. UNKNOWN must be a named outcome, not a default.\n";
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
top module: counter_top
module copies after elaboration: counter_bit x4, counter_top x1

CELL TYPE    FAMILY           IN TEXT    ELABORATED   note
AN2D1        combinational    2          2            
BUFFD1       buffer/inv       1          1            
CKBD1        clock buffer     1          1            
CKLNQD1      clock gate       1          1            
DFCNQD1      flop             1          4            x4
FILL1        physical-only    1          1            
INVD1        buffer/inv       3          9            x3
TIELO        physical-only    1          1            
XOR2D1       combinational    3          3            
TOTAL                         14         23          

per family (elaborated):
  buffer/inv       10
  clock buffer     1
  clock gate       1
  combinational    5
  flop             4
  physical-only    2

checks worth running on a signoff netlist:
  physical-only cells present: FILL1 x1, TIELO x1
  UNRECOGNISED cell types: none
  flop count (elaborated): 4
  and in the TEXT it looks like: 1

proving the classifier can fail -- types it has never seen:
  DFCNQD1        -> flop
  INVD1          -> buffer/inv
  LVLHLD1        -> *** UNKNOWN *** (would be reported)
  SDFFRPQ_X2     -> *** UNKNOWN *** (would be reported)
  MY_CUSTOM_RAM  -> *** UNKNOWN *** (would be reported)
  retention_ff   -> *** UNKNOWN *** (would be reported)
  4 of 6 probe types are unrecognised, so the branch is live

and why 'other' would be the wrong bucket:
  a retention flop classified as 'other' disappears from the flop
  count, and a flop count that is quietly low is exactly the number
  a reviewer trusts. UNKNOWN must be a named outcome, not a default.

14 cell instantiations in the text; 23 cells in the elaborated design. The multiplier comes from Chapter 20.4's map: counter_bit is instantiated four times, so everything inside it counts four times.

The elaborated total of 23 is the same number Chapter 20.4 established three independent ways — a recursive expansion, an independent iterative one, and the simulator's own VCD scope list. Two chapters, two different methods, the same answer, which is the kind of agreement worth noticing when it happens.

The mechanism is one recursive walk that records how many copies of each module exist:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
sub walk_mult {
    my ($mod, $times) = @_;
    $mult{$mod} += $times;
    for my $c (@{ $children{$mod} || [] }) {
        walk_mult($c->{type}, $times) if $IS_MODULE{ $c->{type} };
    }
}
walk_mult($top, 1);

Then each cell's elaborated count is its text count times the multiplier of the module it sits in. Measured: counter_bit x4, counter_top x1.

2. The Flop Count

Look at the one row that matters most:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
DFCNQD1   flop   1 in text   4 elaborated   x4

The flop count is the single most-quoted number in any netlist census — it drives area estimates, power estimates, scan-chain planning and the "is this the netlist I think it is" sanity check. And a text census reports 1 where the design has 4.

The script says both, deliberately:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
flop count (elaborated): 4
and in the TEXT it looks like: 1

One level of hierarchy and four repeats is all it takes. A real design has ten levels and blocks repeated hundreds of times, so the gap is not 4× — it is whatever the hierarchy multiplies to, and it is always in the same direction: the text count is a floor.

3. The Family Table Is Data

Classification is a table of pattern to family, which is Chapter 20.3's rule — the varying part belongs in data, not in logic:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
my @FAMILY = (
  [ qr/^DF/,          "flop"          ],
  [ qr/^CKLN/,        "clock gate"    ],
  [ qr/^CK/,          "clock buffer"  ],
  [ qr/^(BUFF|INV)/,  "buffer/inv"    ],
  [ qr/^(AN|ND|OR|NR|XOR|XNR|MUX|AO|OA)/, "combinational" ],
  [ qr/^(FILL|TIEHI|TIELO|ANTENNA|DCAP)/, "physical-only" ],
);

Four properties that are easy to get wrong:

Order matters, first match wins. ^CKLN must come before ^CK, or every clock gate is counted as a clock buffer. The measurement confirms the order works: CKLNQD1 is a clock gate and CKBD1 a clock buffer.

Every pattern is anchored. ^DF and not DF, or MUXDFX1 becomes a flop. Chapter 11.8's anchors, and the same lesson Chapter 19.2 reached about filename filters.

Compiled patterns in a table, as Chapter 18.8 built for tool-version spellings and Chapter 7.11's dispatch tables do generally. Retargeting to a different library is an edit to this list.

It returns undef, not a default. That is section 4.

4. Unrecognised Is A Named Outcome

family() returns undef for a type it does not match, and the census prints *** UNKNOWN ***. On this netlist nothing is unrecognised — which means the branch is untested by the main measurement, so the script exercises it separately:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
LVLHLD1        -> *** UNKNOWN *** (would be reported)
SDFFRPQ_X2     -> *** UNKNOWN *** (would be reported)
MY_CUSTOM_RAM  -> *** UNKNOWN *** (would be reported)
retention_ff   -> *** UNKNOWN *** (would be reported)

4 of 6 probe types are unrecognised, and the other two still classify correctly. Both halves matter: a probe where everything comes back UNKNOWN would prove only that the function can return undef, not that it discriminates.

And look at what is in that list. SDFFRPQ_X2 is a scan flop — a perfectly ordinary name in another vendor's library — and ^DF does not match it. So the table is not merely incomplete in principle; it is demonstrably incomplete against a realistic name, which is the concrete form of "the family table is library-specific".

5. The Checks Worth Running

The census is the input to three checks that genuinely find things.

Physical-only cells in a functional netlist. Measured: FILL1 x1, TIELO x1. Fillers, tie cells, decaps and antenna diodes belong in a post-layout netlist and not in one you are about to simulate or hand to formal — they carry no function, and their presence usually means somebody passed the wrong file. This is the check that most often catches a real mistake, because the two netlists have similar names.

Unrecognised cell types. Covered above. In a real run this is the one that fires when a library is upgraded.

A count delta against the previous netlist. The census is a hash of type to count, so comparing two runs is a hash comparison — and this is Chapter 20.7's subject, because the useful form is not the delta itself but which deltas are worth reporting.

Two more that cost one line each, and are worth adding once the above exist:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# a netlist with no flops at all is almost always the wrong file
warn "no flops found -- is this a combinational block or the wrong netlist?\n"
    unless $fam_count{flop};

# a netlist that is ALL buffers and inverters is usually a clock-tree file
warn "buffer/inv is " . int(100 * $fam_count{"buffer/inv"} / $e_tot) . "% of cells\n"
    if $e_tot && $fam_count{"buffer/inv"} / $e_tot > 0.8;

Both are range checks in the sense Chapter 18.8 used: a number you cannot sanity-check is a number you should not publish, and the cheapest sanity check is whether the shape of the design matches what you expected to be handed.

6. What To Remember

  • Counting is a hash (6.8); the care goes into what is being counted.
  • There are two legitimate censuses: as written in the text, and as elaborated.
  • Measured: 14 cell instantiations in the text, 23 cells in the design.
  • The elaborated total matches 20.4's independently verified 23 — two chapters, two methods, one number.
  • One recursive walk gives each module's copy count; a cell's elaborated count is its text count times its module's multiplier.
  • Measured: the flop count is 1 in the text and 4 in the design. A 4x error from one level of hierarchy.
  • The text count is always a floor, never an estimate that might be high.
  • The wrong number is plausible — "1,200 flops" looks fine to everybody. Label it (elaborated) or (as written) every time.
  • If the hierarchy is unavailable, say so: "at least N", not N.
  • The family table is data, not logic — retargeting a library is an edit to a list.
  • Order matters and first match wins: ^CKLN before ^CK, or every clock gate becomes a buffer.
  • Anchor every family pattern, or MUXDFX1 becomes a flop (11.8).
  • family() returns undef, never a default bucket.
  • Proven able to fire: 4 of 6 probe types unrecognised, with 2 still classified correctly — a probe where everything is UNKNOWN proves nothing.
  • SDFFRPQ_X2, an ordinary scan-flop name, is missed by ^DF — the table is demonstrably library-specific.
  • A bucket called "other" converts an unknown into a known-looking zero (18.8).
  • Flag physical-only cells — fillers and tie cells in a functional netlist usually mean the wrong file was passed.
  • Flag no-flops and all-buffers as shape checks on the netlist you were handed.

Chapter 20.6 goes one level finer, into the connection lists themselves, and is careful about what it promises: reading what connects to a cell's pins at a single level of hierarchy is straightforward, and following a net across a boundary is not something a line scan can do.

Where this fits

Part of the Perl curriculum.