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
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;
}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 agreesdc/chip.sdc: 27 physical lines -> 10 logical statementsAn 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:
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:
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
clk_div2 generated 2.000 clk period derived from clk / 2
gclk generated 1.000 clk period derived from clk / 1Neither 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:
$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
asynchronous clk,gclk,clk_div2 | clk_ref
exclusive clk | clk_slowset_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:
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:
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:
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_slowhas its-periodon 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_fromso 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_groupscarries a KIND, and it changes the meaning entirely: asynchronous needs synchronisers, exclusive needs the mux checked.- Treat a missing kind as
unspecifiedand flag it rather than defaulting. - Use
while //gfor the several-groupclauses in one statement (11.16). - Cross-check the SDC against
report_clocks— it is free once both are parsed. - Measured:
clk_unusedis 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.
