Skip to content
VLSI Mentor

Perl Part II · Module 23 · Static Timing Analysis Report Parsing

Clocks, Generated Clocks and Clock Groups

Every slack number in a timing report is measured against a clock, so a constraint or CDC review starts from the clock inventory rather than from the paths. Building that inventory means reading a constraint file whose statements are not lines, resolving periods that the file never states, and then comparing the result against what the tool thinks the clocks are — which is the only way to find a clock that is constrained and reaches nothing.

Advanced13 min readPerlPart IISTA Reports

Part II · Module 23 · Chapter 23.5 · Clocks, Generated Clocks and Clock Groups

1. Twenty-Seven Lines, Ten Statements

Azvya Education Pvt. Ltd.VLSI Mentor
clocks.pl
use strict;
use warnings;

# sdc/chip.sdc is a representative constraint file: every construct in it is
# present because it defeats a different naive parser. chip.clocks.rpt is a
# report_clocks-shaped summary. No STA tool exists here (see 23.1).

# ---- 1. SDC lines are CONTINUED with a trailing backslash. Join first.
sub logical_lines {
    my ($file) = @_;
    open my $fh, "<", $file or die "$file: $!";
    my (@out, $acc);
    while (my $l = <$fh>) {
        chomp $l;
        $l =~ s/^\s+//;
        next if $l =~ /^#/ || $l !~ /\S/;
        if ($l =~ s/\\\s*$//) { $acc .= "$l "; next }
        push @out, ($acc // "") . $l;
        $acc = undef;
    }
    push @out, $acc if defined $acc;
    close $fh;
    return @out;
}

my @phys = do { open my $f, "<", "sdc/chip.sdc" or die $!; my @l = <$f>; close $f; @l };
my @log  = logical_lines("sdc/chip.sdc");
printf "sdc/chip.sdc: %d physical lines -> %d logical statements\n", scalar @phys, scalar @log;

# ---- 2. the clock inventory
my (%clock, @groups, %unc);
for my $l (@log) {
    if ($l =~ /^create_clock\b/) {
        my ($n) = $l =~ /-name\s+(\S+)/;
        my ($p) = $l =~ /-period\s+([\d.]+)/;
        $clock{$n} = { period => $p, kind => "primary", source => undef } if defined $n;
        ($clock{$n}{source}) = $l =~ /\[get_ports\s+([^\]]+)\]/ if defined $n;
    }
    elsif ($l =~ /^create_generated_clock\b/) {
        my ($n) = $l =~ /-name\s+(\S+)/;
        my ($src) = $l =~ /-source\s+\[get_ports\s+([^\]]+)\]/;
        my ($div) = $l =~ /-divide_by\s+(\d+)/;
        $clock{$n} = { kind => "generated", source => $src, divide_by => $div } if defined $n;
    }
    elsif ($l =~ /^set_clock_groups\b/) {
        my $kind = ($l =~ /-(asynchronous|exclusive|logically_exclusive|physically_exclusive)/)
                     ? $1 : "unspecified";
        my @g;
        while ($l =~ /-group\s*\{([^}]*)\}/g) { push @g, [ split ' ', $1 ] }
        push @groups, { kind => $kind, groups => \@g };
    }
    elsif ($l =~ /^set_clock_uncertainty\s+([\d.]+)\s+-(setup|hold)\s+\[get_clocks\s+([^\]]+)\]/) {
        $unc{$3}{$2} = $1;
    }
}

# a generated clock's period is derived, not stated
for my $n (grep { $clock{$_}{kind} eq "generated" } keys %clock) {
    my $src = $clock{$n}{source};
    my ($parent) = grep { ($clock{$_}{source} // "") eq $src && $clock{$_}{kind} eq "primary" }
                   keys %clock;
    if (defined $parent && defined $clock{$parent}{period}) {
        $clock{$n}{period} = $clock{$parent}{period} * ($clock{$n}{divide_by} // 1);
        $clock{$n}{derived_from} = $parent;
    }
}

printf "\n%-12s %-10s %-9s %-14s %s\n", "CLOCK", "KIND", "PERIOD", "SOURCE", "note";
for my $n (sort keys %clock) {
    my $c = $clock{$n};
    printf "%-12s %-10s %-9s %-14s %s\n", $n, $c->{kind},
        (defined $c->{period} ? sprintf("%.3f", $c->{period}) : "-"),
        ($c->{source} // "-"),
        (defined $c->{derived_from}
            ? "period derived from $c->{derived_from} / $c->{divide_by}" : "");
}

print "\nclock groups:\n";
for my $g (@groups) {
    printf "  %-22s %s\n", $g->{kind},
        join("  |  ", map { join(",", @$_) } @{ $g->{groups} });
}

print "\nuncertainty:\n";
for my $c (sort keys %unc) {
    printf "  %-10s setup=%-7s hold=%s\n", $c,
        ($unc{$c}{setup} // "-"), ($unc{$c}{hold} // "-");
}

# ---- 3. cross-check the SDC against the tool's own clock report
print "\ncross-check against report_clocks:\n";
my %rpt;
open my $rf, "<", "reports/chip.clocks.rpt" or die $!;
while (my $l = <$rf>) {
    next unless $l =~ /^(\w+)\s+([\d.]+)\s+\{[^}]*\}\s+(\w+)\s+\{([^}]*)\}/;
    $rpt{$1} = { period => $2, attr => $3, src => $4 };
}
close $rf;
my %all = map { $_ => 1 } (keys %clock, keys %rpt);
printf "%-12s %-10s %-10s %s\n", "CLOCK", "SDC", "REPORT", "verdict";
for my $n (sort keys %all) {
    my $s = defined $clock{$n}{period} ? sprintf("%.3f", $clock{$n}{period}) : "-";
    my $r = exists $rpt{$n} ? sprintf("%.3f", $rpt{$n}{period}) : "-";
    my $v = !exists $rpt{$n}  ? "in SDC only -- constrained but unused?"
          : !exists $clock{$n} ? "in the REPORT only -- where did it come from?"
          : (abs($clock{$n}{period} - $rpt{$n}{period}) < 1e-9 ? "agree" : "PERIOD DIFFERS");
    printf "%-12s %-10s %-10s %s\n", $n, $s, $r, $v;
}
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
sdc/chip.sdc: 27 physical lines -> 10 logical statements

CLOCK        KIND       PERIOD    SOURCE         note
clk          primary    1.000     clk            
clk_div2     generated  2.000     clk            period derived from clk / 2
clk_ref      primary    4.000     ref_clk        
clk_slow     primary    8.000     slow_clk       
clk_unused   primary    2.000     spare_clk      
gclk         generated  1.000     clk            period derived from clk / 1

clock groups:
  asynchronous           clk,gclk,clk_div2  |  clk_ref
  exclusive              clk  |  clk_slow

uncertainty:
  clk        setup=0.030   hold=0.015

cross-check against report_clocks:
CLOCK        SDC        REPORT     verdict
clk          1.000      1.000      agree
clk_div2     2.000      2.000      agree
clk_ref      4.000      4.000      agree
clk_slow     8.000      8.000      agree
clk_unused   2.000      -          in SDC only -- constrained but unused?
gclk         1.000      1.000      agree
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
sdc/chip.sdc: 27 physical lines -> 10 logical statements

An SDC statement is not a line. It continues with a trailing backslash, and a real constraint file uses that constantly because the statements are long.

The joining has to happen before any matching:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
if ($l =~ s/\\\s*$//) { $acc .= "$l "; next }
push @out, ($acc // "") . $l;

The fixture contains a case chosen to make the cost concrete — clk_slow's -period is on a continued line:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
create_clock -name clk_slow \
    -period \
    8.000 [get_ports slow_clk]

A line-at-a-time parser matches create_clock -name clk_slow, finds no -period, and records the clock with an undefined period. It does not fail. It produces an inventory with a hole in it, and a hole in a clock inventory is a clock whose constraint nobody checks.

Three details in the joiner:

Strip comments and blanks before joining, or a comment between two continued fragments breaks the statement in half.

s/\\\s*$// tolerates trailing whitespace after the backslash — which is invisible in an editor and common in generated SDC.

Flush $acc after the loop. A file whose last line ends with a backslash would otherwise lose its final statement silently. Chapter 18.9's three-state thinking applied to a parser's own buffer.

2. A Generated Clock's Period Is Not In The File

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
clk_div2    generated  2.000  clk   period derived from clk / 2
gclk        generated  1.000  clk   period derived from clk / 1

Neither of those periods appears anywhere in the SDC. create_generated_clock states a source and a -divide_by; the period is the source clock's period multiplied by the divider.

So the inventory needs a second pass:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$clock{$n}{period} = $clock{$parent}{period} * ($clock{$n}{divide_by} // 1);
$clock{$n}{derived_from} = $parent;

Two things that matter about doing it this way:

It is a second pass, not inline. A generated clock can be defined before its source in the file, so the periods cannot be resolved while reading. Chapter 19.5's dependency ordering is the same shape — you cannot resolve a reference until everything is collected.

Record that it was derived. derived_from => "clk" in the output means a reviewer reading gclk 1.000 can see where the number came from, which matters because it is not in the file they are reviewing. Chapter 18.8's provenance label, and here it prevents a reasonable person concluding the parser invented a number.

A parser that skips this reports period: - for every generated clock, which looks like missing data and is actually a field that was never going to be there. Chapter 23.2's distinction between an absent value and an unparseable one, in a different file.

3. Clock Groups Carry A Kind

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
asynchronous  clk,gclk,clk_div2  |  clk_ref
exclusive     clk                |  clk_slow

set_clock_groups means different things depending on its switch, and the switch is as important as the groups:

-asynchronous — the clocks have no phase relationship, so paths between them are not timed and need synchronisers. This is the CDC question.

-exclusive (and -logically_exclusive / -physically_exclusive) — the clocks never run at the same time, usually a mux between modes. Paths between them are not timed because they cannot both exist, which is a completely different reason from the asynchronous case.

Reporting "clk and clk_slow are in different groups" without the kind tells a reviewer nothing: one case needs synchronisers and the other needs the mux checked. So the parser captures it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
my $kind = ($l =~ /-(asynchronous|exclusive|logically_exclusive|physically_exclusive)/) ? $1 : "unspecified";

and unspecified is a real outcome — a set_clock_groups with no kind switch is legal and means something tool-specific, which is worth flagging rather than defaulting.

The groups themselves come out with a global match (Chapter 11.16's while //g), because one statement carries several -group clauses:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
while ($l =~ /-group\s*\{([^}]*)\}/g) { push @g, [ split ' ', $1 ] }

4. The Cross-Check That Finds An Unused Clock

The last block compares the SDC against the tool's own clock report, and finds something neither file shows alone:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
clk_unused  SDC 2.000  REPORT -   in SDC only -- constrained but unused?

A clock defined in the constraints and absent from the tool's clock report. The tool did not report it because nothing in the design is clocked by it — a spare port, a removed block, a copy-pasted constraint.

That is precisely what a constraint review is looking for, and it is only visible as a difference between two sources. Chapter 21.7 made this argument for UVM's severity counts and Chapter 22.1 for three synthesis reports: the cross-check is free once both files are parsed, and it is the only check that is not verified against your own assumptions.

The three outcomes, as the script reports them:

In both, periods agree — the ordinary case, five of six here.

In the SDC only — constrained but unused. Usually harmless, occasionally a constraint that was meant to apply to something.

In the report only — the more alarming direction. A clock the tool knows about that your SDC does not define means it came from somewhere else: an included file you did not parse, a library-defined clock, or a tool default.

5. What To Remember

  • Every slack is measured against a clock, so a constraint review starts from the clock inventory.
  • Measured: 27 physical lines are 10 logical statements. An SDC statement is not a line.
  • Join continuations BEFORE matching — the fixture's clk_slow has its -period on a continued line, and a line-at-a-time parser records an undefined period without failing.
  • Strip comments and blanks before joining, or a comment splits a statement.
  • Tolerate whitespace after the backslash — invisible and common.
  • Flush the accumulator after the loop, or a file ending in a continuation loses its last statement (18.9).
  • A generated clock's period is NOT in the file — it is the source's period times -divide_by.
  • Resolve it in a second pass, because a generated clock can be defined before its source (19.5's shape).
  • Record derived_from so a reviewer can see the number was computed, not read (18.8).
  • period: - for a generated clock is a field that was never there, not missing data (23.2).
  • set_clock_groups carries a KIND, and it changes the meaning entirely: asynchronous needs synchronisers, exclusive needs the mux checked.
  • Treat a missing kind as unspecified and flag it rather than defaulting.
  • Use while //g for the several -group clauses in one statement (11.16).
  • Cross-check the SDC against report_clocks — it is free once both are parsed.
  • Measured: clk_unused is in the SDC and not in the report — constrained but reaching nothing, visible only as a difference.
  • "In the report only" is the alarming direction — an unparsed include, a library clock, or a tool default.
  • SDC is Tcl, so a pattern cannot see a computed period, a loop or a sourced file (20.2's boundary).
  • Use the SDC for intent and the report for reality, and treat the difference as the finding.

Chapter 23.6 returns to the path reports and the one tool-specific thing that defeats a generic parser: PrimeTime's column layout, and what happens to it when the lines are allowed to wrap.

Where this fits

Part of the Perl curriculum.