Skip to content
VLSI Mentor

Tcl · Module 30

An SDC Generator

Three near-duplicate SDC files are three places for a rule to drift. One config and one generator is a rule that cannot.

Advanced22 min readtcledapart-iisdcconstraintscase-study

Part II · Module 30 · Chapter 30.3 · An SDC Generator

Three IP instances, three clocks, three sets of I/O budgets. Written by hand that is three SDC files that start identical and stop being identical the first time somebody changes a project rule in two of them.

The registry calls generation the foundation of constraint reuse, and the word that matters is foundation: the payoff is not that typing is saved. It is that a project rule exists in exactly one place and is therefore provably the same everywhere.

1. The configuration

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# ---- the configuration: one entry per instance -------------------------
set ::IP {
    {u_core  {core_clk 5.000} {{data_in 1.20} {cmd_in 0.90}} {{data_out 1.00}} {}}
    {u_mem   {mem_clk  3.333} {{rd_data 0.80}}               {{wr_data 0.75}} {async_refresh}}
    {u_io    {io_clk  10.000} {{rx 2.40}}                    {{tx 2.10}}      {async_phy}}
}

proc clock_uncertainty {period} {
    # 3% of the period, floored at 50 ps — a project rule, stated once.
    set u [expr {$period * 0.03}]
    return [expr {$u < 0.050 ? 0.050 : $u}]
}

Three lines, one per instance, each giving the clock and its period, the input budgets, the output budgets, and the asynchronous ports. Everything else about an SDC file is derivable from that, which is the claim this chapter has to make good on.

clock_uncertainty is the whole point in miniature. The project rule — 3% of the period, floored at 50 ps — is written once, as code, with its justification beside it. There is no copy of it in any generated file, which is what makes drift impossible rather than merely unlikely.

2. The generator

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc gen_sdc {inst spec} {
    lassign $spec clk inputs outputs exceptions
    lassign $clk clk_name period
    set unc [clock_uncertainty $period]
    set lines {}
    lappend lines "# generated for $inst — do not edit; edit the config"
    lappend lines "# generator: [file tail [info script]]   tcl [info patchlevel]"
    lappend lines ""
    lappend lines [format "create_clock -name %s -period %.3f \[get_ports %s\]" \
                       $clk_name $period $clk_name]
    lappend lines [format "set_clock_uncertainty %.3f \[get_clocks %s\]" $unc $clk_name]
    lappend lines ""
    foreach io $inputs {
        lassign $io port delay
        lappend lines [format "set_input_delay %.3f -clock %s \[get_ports %s\]" \
                           $delay $clk_name $port]
    }
    foreach io $outputs {
        lassign $io port delay
        lappend lines [format "set_output_delay %.3f -clock %s \[get_ports %s\]" \
                           $delay $clk_name $port]
    }
    if {[llength $exceptions]} {
        lappend lines ""
        foreach e $exceptions {
            # Every exception carries its reason, per 20.5.
            lappend lines "# $e: asynchronous by design; no synchroniser timing is claimed"
            lappend lines [format "set_false_path -from \[get_ports %s\]" $e]
        }
    }
    return $lines
}

It builds a list of lines and returns it; the caller writes them. That separation is worth a sentence: a proc that returns text can be tested, diffed and inspected without touching the filesystem, while a proc that writes a file can only be tested by writing a file. Chapter 27.7 made the same argument about helpers in general.

Two details inside it:

  • The header records what generated the file and with what. # generator: gen_sdc.tcl tcl 8.5.9, and do not edit; edit the config immediately above it. A generated file that does not say it is generated will be hand-edited within a month, and the edit will be lost on the next run with no message.
  • Every exception carries its reason. set_false_path is the most dangerous line in any SDC file, because it silently removes paths from analysis; Chapter 20.5 is explicit that the reason belongs next to it. Here the generator cannot emit one without the comment, because both come from the same foreach body.

3. Three files from three lines

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
=== generated from one config ===
  u_core   u_core.sdc                         9 line(s)
  u_mem    u_mem.sdc                          11 line(s)
  u_io     u_io.sdc                           11 line(s)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
=== how much is generated from how little ===
  3 config line(s) -> 3 file(s), 31 SDC line(s)
  and every one of the 3 files states the same uncertainty rule, once

And one of them in full:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
=== u_mem.sdc, in full ===
# generated for u_mem — do not edit; edit the config
# generator: gen_sdc.tcl   tcl 8.5.9

create_clock -name mem_clk -period 3.333 [get_ports mem_clk]
set_clock_uncertainty 0.100 [get_clocks mem_clk]

set_input_delay 0.800 -clock mem_clk [get_ports rd_data]
set_output_delay 0.750 -clock mem_clk [get_ports wr_data]

# async_refresh: asynchronous by design; no synchroniser timing is claimed
set_false_path -from [get_ports async_refresh]

Eleven lines, and the only one not directly present in the config is set_clock_uncertainty 0.100 — which is the one that mattered. 0.100 is 3% of 3.333, computed by the rule. Nobody typed it, so nobody can mistype it, and nobody can update the other two files and forget this one.

4. The rule, visible

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
=== the uncertainty rule, applied once and visible ===
  core_clk   period   5.000 -> uncertainty 0.150
  mem_clk    period   3.333 -> uncertainty 0.100
  io_clk     period  10.000 -> uncertainty 0.300

Printing the rule's input and output for every instance costs four lines and converts a buried computation into something a reviewer can check against the project's standard in one glance. A generator that prints only its output asks to be trusted.

Now the uncomfortable part. clock_uncertainty has two branches and only one of them has ever run: 3% of 5.000, 3.333 and 10.000 are 0.150, 0.100 and 0.300, all comfortably above the 50 ps floor. So the floor — the safety rail, the thing the rule exists to guarantee — was dead code in this project.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
=== and the floor, which none of the three above reaches ===
  period  1.000  3% = 0.0300  ->  0.050   FLOORED at 0.050
  period  1.667  3% = 0.0500  ->  0.050   3% applies
  period  2.000  3% = 0.0600  ->  0.060   3% applies
  period  5.000  3% = 0.1500  ->  0.150   3% applies

A 1 GHz clock reaches it: 3% of 1.000 ns is 30 ps, below the floor, so the floor applies and the answer is 50 ps. 1.667 is the exact crossover — 3% of it is 50 ps — and the row is included because a boundary you have not evaluated is a boundary you are guessing about.

This is the third time in three chapters that a branch had never fired: 30.1 §8's archive gate could not fire, 30.2 §6's histogram marker could not fire on realistic data, and this floor simply never had an input that reached it. Only the third is benign — and it is benign only because it was evaluated. Until the table above existed, nobody knew whether the floor worked.

5. Reading back what you generated

A generator you never read back is a generator you are trusting. The cheap check is to parse the file you just wrote and compare it to the configuration it came from:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# ---- the check that makes a generator trustworthy: read it back --------
proc verify_generated {path expect_clk expect_period} {
    set fh [open $path r] ; set found "" ; set per ""
    while {[gets $fh line] >= 0} {
        if {[regexp {^create_clock -name (\S+) -period (\S+)} $line -> c p]} {
            set found $c ; set per $p
        }
    }
    close $fh
    if {$found ne $expect_clk} { return "clock name '$found' != '$expect_clk'" }
    if {abs($per - $expect_period) > 1e-9} { return "period $per != $expect_period" }
    return ok
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
=== read back what was generated (the round trip) ===
  u_core   ok
  u_mem    ok
  u_io     ok

Three oks, which on their own are worth nothing — a check that always returns ok returns ok. So: corrupt the generated file three ways and see whether it notices.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
=== can verify_generated actually fail? three corruptions ===
  untouched                    ok
  clock name altered           clock name 'men_clk' != 'mem_clk'
  period altered by 3 ps       period 3.330 != 3.333
  create_clock line removed    clock name '' != 'mem_clk'
  restored                     ok

A one-character clock-name typo, a 3 picosecond period change, and a deleted create_clock line: all three caught, and the file verifies again once restored. The restored ok line is the other half of the test — a check that fires on everything is as useless as one that fires on nothing.

6. What generation does not fix

  • It does not make the configuration right. rd_data 0.80 being the correct input budget is a conversation with whoever owns the interface. The generator guarantees consistency, not correctness — and consistency is worth less when the shared value is wrong, because now it is wrong in three places at once with no chance of one of them being right by accident.
  • It does not survive a hand edit. do not edit in a comment is a request. Making the output read-only, or regenerating and diffing in the flow, is the enforcement; the comment is the courtesy.
  • It does not know what the tool accepted. This script wrote SDC text and parsed its own text back. Whether the tool read every line, and acted on it, is Chapter 29.1's question — the one answered by read_sdc returning a clock count in 30.1 §7, not by anything here.
  • The round trip only checks what it parses. verify_generated reads the clock name and period. It does not look at the input delays, the output delays or the exceptions, so a generator bug in any of those would pass all three oks. The check is real and partial, which is most checks.

Exercises

  1. Add a fourth instance with a 1.000 ns clock. Which line of the generated file differs in kind from the other three instances, and why?
  2. Change the project rule from 3% to 4%. How many files must you edit, and how many lines?
  3. Extend verify_generated to check the input delays as well. What does it have to know that it does not currently know?
  4. Make gen_sdc write the file itself instead of returning a list. Name a test that becomes impossible.
  5. Remove the # $e: asynchronous by design line from the exception loop. What has been lost, given that the set_false_path line is unchanged?
  6. The crossover period is 1.667 ns. Derive it, and say what you would do if the project rule changed to 5% with the same floor.

Answers

  1. set_clock_uncertainty 0.050 — and it differs because it is the floored value rather than 3% of the period. Every other instance's uncertainty is proportional to its clock; this one is not, so the one-line rule in section 1 is doing something qualitatively different, which is exactly why the floor exists and why section 4's table had to be printed.
  2. One file, one line — the 0.03 in clock_uncertainty. That is the entire argument for generation stated as a number. By hand it is three files, three lines, and a review that has to confirm all three were changed the same way.
  3. It has to know the expected delays, which means verify_generated needs the config entry rather than two scalars. That is the natural refactor and it has a consequence worth noticing: once the checker takes the config, it can verify everything the generator derived from the config, and the partial-check limit in section 6 goes away.
  4. Diffing the intended output against the file on disk, which is the test that detects a hand edit. More generally, any test that wants to see the text without a filesystem — in a loop over candidate configs, say — becomes a test that creates and deletes temporary files.
  5. The set_false_path still removes the path from analysis; what is lost is any record of why it was allowed to. Six months later that line is indistinguishable from a false path added to make a violation go away, and the only safe thing a reviewer can do with an unexplained one is question it. Chapter 20.5 treats the comment as part of the constraint, not decoration.
  6. The floor binds when period × 0.03 < 0.050, so the crossover is 0.050 / 0.03 = 1.667 ns. At 5% it becomes 0.050 / 0.05 = 1.000 ns — the floor would then bind only below 1 GHz, and the 1.000 ns row in section 4's table would land exactly on the boundary. Which is the reason to re-run that table after any change to the rule rather than reasoning about it: the row you kept as a demonstration may stop demonstrating anything.

Where this fits

Part of the Tcl curriculum.