📜
Module 1: The Tcl Evaluation Model & Fundamentals (The Dodekalogue)
A Tcl script is a UTF-8 text file consisting of commands separated by newlines or semicolons (;). The standard portable shebang line invokes tclsh via /usr/bin/env. Tcl does not have a distinct compilation step; code is evaluated directly or dynamically compiled into bytecode on the fly.
#!/usr/bin/env tclsh
# ==============================================================================
# Script: hello_world.tcl
# Description: Canonical Tcl 8.6+ executable script structure
# ==============================================================================
package require Tcl 8.6
puts "Hello, world! Welcome to Sovereign Tcl Scripting."
puts "Tcl Interpreter Version: [info patchlevel]"
# Semicolons allow multiple commands on a single line:
set project "Apollo"; set revision 4; puts "Project: $project Rev: $revision"
Never use hardcoded paths like #!/usr/bin/tclsh in enterprise EDA environments. EDA vendor tools (Synopsys, Cadence, Mentor) ship their own internal tclsh builds located in proprietary trees. Always use /usr/bin/env tclsh or invoke via the tool shell (e.g. dc_shell -f script.tcl or vivado -mode batch -source script.tcl).
Unlike Python, C++, or Verilog, everything in Tcl is fundamentally a command. There are no reserved syntax keywords (even if, for, proc, and set are standard commands). Evaluation operates in two strict, decoupled phases:
1. Grouping & Substitution Phase: The interpreter parses words separated by whitespace and performs substitutions ($, [], \) from left to right in a single pass.
2. Command Execution Phase: The first word is looked up as the command name, and the remaining words are passed as argument strings without further interpretation.
# In Tcl: Command word0 word1 word2 ...
# All words are strings until interpreted by the specific command!
set cmd puts
$cmd "Dynamically dispatched puts!" ;# Rule: variable substitution happens BEFORE execution
# 'if' is just a command that accepts condition and script arguments:
set condition 1
if $condition {
puts "Evaluated as condition!"
}
Because Tcl evaluates in a single pass left-to-right, substitutions inside words occur before the command receives them. If you don't use braces {} to defer substitution, nested logic can evaluate prematurely or suffer code-injection vulnerabilities.
In Tcl, # is not a comment token anywhere on a line; it is only a comment if it appears where a command name is expected (at the start of a line or immediately after a semicolon ;). Tcl does not support C-style multi-line comments (/* ... */).
# Valid comment: starts at the command position of the line
set clk_period 10.0
# INCORRECT / FATAL SYNTAX ERROR:
# set clk_period 10.0 # Define clock in nanoseconds
# -> Tcl interprets '#' and subsequent text as extra arguments to 'set'!
# CORRECT INLINE COMMENT: Must terminate command with semicolon ';' first:
set clk_period 10.0 ;# Clock period in ns (semicolon starts new command position)
# Multi-line comment idiom:
# Block line 1
# Block line 2
# Block line 3
Adding a comment after a command without a preceding semicolon (e.g. puts "run" # comment) will cause Tcl to throw wrong # args: should be "puts ?-nonewline? ?channelId? string" because # is passed as a literal string argument to puts!
Variables are created or mutated using set varName ?value?. If a value argument is provided, set assigns it and returns the value. If only the variable name is passed, set reads and returns the variable's current value. Variables can be destroyed using unset.
# Assignment:
set design_name "alu_core"
set num_stages 4
set target_freq 1.25e9
# Reading via 'set' without $ (useful when constructing variable names):
set current_design [set design_name]
# Chained assignment:
set a [set b [set c 0]]
# Unsetting variables:
unset target_freq
# Safe unsetting without error if variable doesn't exist (Tcl 8.4+):
unset -nocomplain target_freq uninitialized_var
When assigning, do not prefix the variable name with $! Writing set $var 10 evaluates $var first and assigns to whatever variable name was held inside $var (indirect reference), which is almost always a bug.
The $ character triggers variable substitution. The interpreter matches letters, digits, and underscores following $ as the identifier. When variable names contain special characters (hyphens, dots, colons, spaces) or are concatenated directly against alphanumeric suffixes, enclose the identifier in braces: ${var_name}.
set prefix "cpu"
set suffix "_inst"
# Ambiguous concatenation trap:
# puts "$prefix_inst" ;# ERROR: looks for variable named 'prefix_inst'!
# Disambiguated concatenation using ${...}:
puts "${prefix}_inst" ;# Outputs: cpu_inst
puts "${prefix}${suffix}" ;# Outputs: cpu_inst
# Variable names containing hyphens or punctuation:
set {max-fanout} 32
puts "${max-fanout}" ;# Outputs: 32
In EDA scripts, netlist hierarchical names frequently use punctuation (e.g. u_core/u_alu/clk). Trying to read $u_core/u_alu/clk stops at the slash. Always wrap hierarchical or punctuated paths in ${...} or pass variable names to get_cells directly.
Square brackets [...] invoke command substitution. The script within brackets is recursively evaluated as a complete Tcl script, and the standard output/return value of the last command replaces the bracketed text inside the parent command word.
# Execute 'expr' and substitute result:
set area [expr {1024 * 768}]
# Deep nesting:
set log_file [file join [pwd] "reports" [format "run_%04d.log" 42]]
puts "Writing to: $log_file"
# Multiple command substitutions in a single string:
puts "Active design: [get_current_design] contains [llength [get_cells]] instances."
In EDA shells (PrimeTime, Innovus, Vivado), square brackets [get_cells ...] are used for command substitution, but Verilog bus indices also use square brackets (e.g. data[7:0])! If unescaped, Tcl attempts to execute a command named 7:0! Always escape Verilog bus brackets in strings (e.g. data\[7:0\] or {data[7:0]}).
Backslashes prevent interpretation of special characters (\$, \[, \], \{, \}, \") or insert non-printing control characters (\n newline, \t tab, \r carriage return, \xHH hex, \uHHHH unicode). A backslash at the immediate end of a line acts as line continuation, turning the newline and leading whitespace of the next line into a single space.
# Escaping special characters:
puts "The price is \$100.00 and bracket literal is \[test\]"
# Clean multi-line command formatting via backslash continuation:
create_clock -name "SYS_CLK" \
-period 5.0 \
-waveform {0.0 2.5} \
[get_ports "clk_in"]
# Line continuation inside long lists:
set eda_tools [list \
"dc_shell" \
"icc2_shell" \
"pt_shell" \
"innovus" \
]
Trailing whitespace after a backslash (\ ) is a silent, fatal bug! The backslash will escape the trailing space instead of the newline, causing Tcl to terminate the command prematurely and treat the next line as an unknown command.
This is the single most critical concept in Tcl:
• Double Quotes ("...") group words together while enabling variable ($), command ([]), and backslash (\) substitution.
• Braces ({...}) group words together while completely suppressing all substitutions. Everything inside braces is treated as pure literal text.
set tool "PrimeTime"
# Double Quotes: Substituted immediately
puts "Welcome to $tool (PID: [pid])"
# Output: Welcome to PrimeTime (PID: 14820)
# Braces: Raw literals, zero substitution
puts {Welcome to $tool (PID: [pid])}
# Output: Welcome to $tool (PID: [pid])
# Why this matters for 'expr' and 'proc':
# Code blocks for procs, loops and if-statements MUST be enclosed in braces
# so they are passed intact to the command rather than evaluated upfront!
If you write if "$x > 10" { ... }, Tcl performs substitution during initial parsing. If $x contains special characters or spaces, it breaks the grammar. Always use braces: if {$x > 10} { ... }.
Introduced in Tcl 8.5, the expansion prefix {*} (the "splat" operator) unpacks a list into individual argument words before executing the command. It eliminates the need for unsafe, injection-prone eval strings.
set options [list -exact -nocase]
# Unpacks the 2 elements of $options as separate arguments:
set match [string match {*}$options "FOO*" "foobar"]
# Clean tool command dispatch:
set cmd [list exec gcc -O3 -Wall main.c -o firmware.bin]
{*}$cmd ;# Executes gcc with exact arguments preserved
# Comparison with dangerous legacy eval:
# eval $cmd ;# DANGEROUS: Forces second round of evaluation, breaks if filenames contain spaces!
Never use eval "cmd $args" in modern Tcl. eval concatenates arguments into a string and re-parses it, meaning filenames with spaces, dollar signs, or brackets will trigger unwanted substitutions or syntax crashes. Always use {*}$args.
The subst command performs substitutions on a string without executing the resulting string as a command. Flags allow selective disabling of command substitution (-nocommands), variable substitution (-novariables), or backslashes (-nobackslashes). It is the canonical tool for template interpolation and code generation.
set module "spi_master"
set width 32
# Template with selective variable substitution:
set template {
module $module #(
parameter DATA_WIDTH = $width
) (
input wire clk,
input wire [width-1:0] data_in
);
// Generated automatically on [clock format [clock seconds]]
endmodule
}
# Only substitute $vars, do NOT execute commands inside brackets:
set generated_hdl [subst -nocommands $template]
puts $generated_hdl
If you call subst $template without -nocommands on user-supplied or external files, any square brackets (e.g. [exec rm -rf ...] or Verilog bus indices [31:0]) will attempt execution! Always pass -nocommands when rendering HDL templates.
2
Variables, Scopes & Arrays
In Tcl, reading an uninitialized variable raises a fatal runtime error (can't read "var": no such variable). info exists varName checks whether a variable is defined in the current scope without throwing errors, returning 1 if defined, or 0 otherwise.
# Safe initialization pattern
if {![info exists clk_freq_mhz]} {
set clk_freq_mhz 100.0 ;# Default fallback
puts "INFO: clk_freq_mhz unset, using default: $clk_freq_mhz"
}
# Works for both scalars and array elements:
if {[info exists setup_slack(clk_core)]} {
puts "Slack recorded: $setup_slack(clk_core) ns"
}
Do NOT pass a dollar sign to info exists! Write info exists my_var, NOT info exists $my_var. Passing $my_var evaluates the variable content first and checks whether a variable with THAT name exists (indirect lookup).
Procedures in Tcl execute in an isolated local stack frame. To read or write a top-level global variable from inside a proc, you must explicitly bind it using global varName or access it directly using the fully-qualified root namespace prefix $::varName.
set DESIGN_NAME "soc_top"
set VIOLATION_COUNT 0
proc check_status {} {
global DESIGN_NAME VIOLATION_COUNT
puts "Auditing design: $DESIGN_NAME"
incr VIOLATION_COUNT
}
# Alternative: Direct namespace resolution (idiomatic in EDA scripts)
proc reset_counter {} {
set ::VIOLATION_COUNT 0
puts "Direct global access: $::DESIGN_NAME counter reset to $::VIOLATION_COUNT"
}
check_status
reset_counter
Overusing global makes EDA scripts brittle and difficult to debug across sourced sub-flows. Prefer passing arguments or encapsulating state in dedicated namespaces or dictionaries.
Tcl does not have raw memory pointers. Instead, upvar ?level? otherVar myVar binds a local variable name directly to a variable in a parent call stack frame. This enables true call-by-reference, custom control structures, and in-place mutation of arrays or large data structures.
# In-place increment of an external variable (level 1 = direct caller)
proc double_in_place {var_name} {
upvar 1 $var_name local_ref
set local_ref [expr {$local_ref * 2}]
}
set max_fanout 16
double_in_place max_fanout
puts "Doubled max_fanout: $max_fanout" ;# Outputs: 32
# Passing arrays by reference:
proc print_timing_summary {array_name} {
upvar 1 $array_name stats
foreach corner [array names stats] {
puts "Corner [format %-10s $corner]: $stats($corner) ns"
}
}
When passing the variable name to a proc that uses upvar, pass the plain name without $ (e.g. double_in_place max_fanout). Passing $max_fanout binds to whatever string value is stored inside max_fanout.
The variable command declares or links a variable belonging to the enclosing namespace. Inside procs within that namespace, calling variable varName provides local access to the namespace-scoped variable without polluting the global scope.
namespace eval ::sta_auditor {
# Declare namespace variables with default initial values
variable wns_threshold 0.0
variable tns_total 0.0
variable audited_clocks [list]
proc configure {threshold} {
variable wns_threshold
set wns_threshold $threshold
}
proc report {} {
variable wns_threshold
variable tns_total
puts "Auditor configured for threshold: $wns_threshold ns (TNS: $tns_total ns)"
}
}
::sta_auditor::configure -0.150
::sta_auditor::report
Operating system environment variables are exposed directly through the built-in associative array ::env. Modifying ::env(KEY) automatically updates the environment for all subsequent child processes spawned by exec.
# Querying environment variables safely
if {[info exists ::env(PDK_ROOT)]} {
set pdk_path $::env(PDK_ROOT)
} else {
set pdk_path "/opt/pdks/sky130A"
puts "WARNING: PDK_ROOT unset, defaulting to $pdk_path"
}
# Exporting a new variable to child processes (e.g. for synthesis tool)
set ::env(OPENLANE_VERBOSE) "1"
set ::env(NUM_THREADS) "8"
puts "Target PDK: $pdk_path"
puts "System User: [expr {[info exists ::env(USER)] ? $::env(USER) : $::env(USERNAME)}]"
Accessing $::env(NON_EXISTENT) throws a runtime error. Always guard lookups with if {[info exists ::env(VAR)]} before reading.
Tcl arrays are true hash tables (string keys mapped to string values). They are NOT ordered lists. Arrays cannot be passed directly by value to procedures—they must be accessed using upvar or serialized with array get / array set.
# Element assignment
set corner_freq(ss_125c) 800
set corner_freq(tt_25c) 1200
set corner_freq(ff_m40c) 1600
# Bulk population from key-value pair list
array set buffer_strengths {
BUF_X1 1.0
BUF_X2 2.1
BUF_X4 4.4
BUF_X8 8.9
}
# Array operations
puts "Total buffer variants: [array size buffer_strengths]"
puts "Available corners: [array names corner_freq]"
# Iterating over array keys
foreach corner [lsort [array names corner_freq]] {
puts "Corner $corner => $corner_freq($corner) MHz"
}
# Serializing array to flat list:
set flat_list [array get buffer_strengths]
Writing puts $corner_freq throws can't read "corner_freq": variable is array. In Tcl, an array name alone has no scalar value. You must specify a key: $corner_freq($key), or serialize with [array get corner_freq].
Tcl does not have native multi-dimensional array structures. Because array keys can be any arbitrary string, multiple dimensions are conventionally emulated by joining indices with a comma (e.g. $arr($row,$col)). The comma is simply a character in the key string.
# 2D Grid floorplan density mapping (die coordinate x, y)
for {set x 0} {$x < 4} {incr x} {
for {set y 0} {$y < 4} {incr y} {
set grid_density($x,$y) [expr {rand() * 100.0}]
}
}
# Accessing a coordinate
puts "Tile (2,3) Density: [format %.1f $grid_density(2,3)]%"
# Pattern matching over multi-dimensional keys
puts "All tiles at Row 2:"
foreach key [array names grid_density "2,*"] {
puts " Key $key = [format %.1f $grid_density($key)]%"
}
Do NOT insert spaces after the comma! $grid_density($x,$y) and $grid_density($x, $y) are two completely different hash keys because the space is preserved literally.
Variable traces allow you to register callback procedures that trigger automatically whenever a variable is read, written, or unset. This is invaluable in EDA frameworks for logging design changes, enforcing read-only parameters, or tracking state mutations during simulation.
# Callback proc signature: name1 (array/scalar), name2 (elem/empty), op (read/write/unset)
proc audit_write {name1 name2 op} {
upvar 1 $name1 var
if {$name2 ne ""} {
puts "AUDIT: $name1($name2) modified -> $var($name2)"
} else {
puts "AUDIT: $name1 modified -> $var"
}
}
set current_stage "synthesis"
# Attach write watcher:
trace add variable current_stage write audit_write
set current_stage "floorplan" ;# Triggers: AUDIT: current_stage modified -> floorplan
set current_stage "cts" ;# Triggers: AUDIT: current_stage modified -> cts
# Remove trace when done:
trace remove variable current_stage write audit_write
Modifying the traced variable inside its own write-trace callback without temporarily disabling the trace triggers an infinite recursion loop resulting in stack overflow.
3
Expressions, Operators & Arithmetic
Always enclose expressions passed to expr in curly braces: expr {$a + $b}. Unbraced expressions expr $a + $b suffer from double evaluation: the Tcl parser substitutes variables first, then expr evaluates the resulting string. This destroys bytecode compilation speed (up to 30x slower) and introduces critical code-injection vulnerabilities.
# ❌ DANGEROUS & SLOW: Double evaluation vulnerability
set user_input "100; exec rm -rf /"
# expr $user_input + 5 <-- EXECUTES MALICIOUS COMMAND!
# ✅ SAFE & JIT-COMPILED: Always double-brace
set a 10
set b 20
set sum [expr {$a + $b}]
set hypot [expr {hypot($a, $b)}]
set is_valid [expr {$sum > 0 && $a != $b}]
# Benchmarking: Braced vs Unbraced in loops
# expr {$i + 1} executes via direct bytecode instructions.
# expr $i + 1 forces re-tokenization on every iteration.
Rule of thumb for all Tcl production code: NEVER write an unbraced expr. Even while {$i < 10} and if {$x > 0} rely on expr internally; bracing them ensures one-time compilation.
Tcl expr supports standard C-style operators with standard operator precedence: addition (+), subtraction (-), multiplication (*), division (/), remainder/modulus (%), exponentiation (**), and comparisons (<, <=, >, >=).
set clk_period_ns 2.5
set setup_slack -0.125
# Basic arithmetic
set total_latency [expr {$clk_period_ns * 4 + 0.35}]
set power_of_two [expr {2 ** 10}] ;# 1024 (exponentiation)
set mod_remainder [expr {1024 % 32}] ;# 0
# Comparisons return 1 (true) or 0 (false)
set is_violating [expr {$setup_slack < 0.0}]
set in_range [expr {$total_latency >= 10.0 && $total_latency <= 20.0}]
puts "Violating: $is_violating, Latency: $total_latency ns"
Integer division truncates! In Tcl, expr {5 / 2} yields 2, not 2.5. If you need fractional division, ensure at least one operand is floating-point: expr {5.0 / 2} or expr {double($a) / $b}.
Always use eq and ne for string comparisons inside expr. The traditional operators == and != attempt to coerce both operands to numbers first. If two strings resemble numeric notations (e.g. hex 0x10 vs 16, or leading zeros 08), == causes unexpected equality or syntax errors.
set str1 "0x10"
set str2 "16"
# ❌ TRAP: Coerces to integers! 0x10 == 16 evaluates to true (1)
puts [expr {$str1 == $str2}] ;# Prints: 1
# ✅ CORRECT: Lexicographical string comparison
puts [expr {$str1 eq $str2}] ;# Prints: 0
set tool "innovus"
# Always use eq/ne for EDA tool/state checks:
if {$tool eq "innovus"} {
puts "Configuring Cadence Innovus flow..."
} elseif {$tool eq "pt_shell"} {
puts "Configuring Synopsys PrimeTime STA..."
}
Tcl supports low-level bitwise operations identical to C and Verilog: bitwise AND (&), OR (|), XOR (^), unary bitwise NOT (~), left shift (<<), and right shift (>>). Essential for bitfield masking, register modeling, and hardware address decoding.
set csr_reg 0x000000A5 ;# 10100101b
set ENABLE_BIT [expr {1 << 0}] ;# Bit 0
set IRQ_MASK [expr {0xF << 4}] ;# Bits 7:4
# Bit testing:
set is_enabled [expr {($csr_reg & $ENABLE_BIT) != 0}]
# Bit setting (OR):
set csr_reg [expr {$csr_reg | (1 << 2)}] ;# Set bit 2
# Bit clearing (AND with NOT):
set csr_reg [expr {$csr_reg & ~(1 << 0)}] ;# Clear bit 0
# Extracting field:
set irq_field [expr {($csr_reg & $IRQ_MASK) >> 4}]
puts "Updated CSR: [format 0x%08X $csr_reg], IRQ field: $irq_field"
The ternary operator evaluated inside expr provides compact inline branching. Only the selected branch expression is evaluated (short-circuit execution), preventing division by zero or unwarranted side effects in unchosen paths.
set slack -0.045
set status [expr {$slack >= 0.0 ? "MET" : "VIOLATED"}]
set count 0
# Safe division using ternary short-circuiting:
set avg [expr {$count > 0 ? ($total / $count) : 0.0}]
puts "Timing Status: $status (Slack: ${slack}ns)"
Tcl expr includes standard C standard math library functions directly: sqrt(), hypot(x,y), ceil(), floor(), round(), abs(), log(), log10(), exp(), sin(), cos(), tan(), and rand().
# Euclidean wirelength between two cell coordinates (x1, y1) and (x2, y2)
set x1 120.5; set y1 340.0
set x2 250.0; set y2 510.5
set dx [expr {$x2 - $x1}]
set dy [expr {$y2 - $y1}]
set wirelength [expr {hypot($dx, $dy)}] ;# sqrt(dx^2 + dy^2)
# Rounding to grid steps
set grid_pitch 0.190
set snapped_x [expr {round($x1 / $grid_pitch) * $grid_pitch}]
puts "Wirelength: [format %.3f $wirelength] um, Snapped X: [format %.3f $snapped_x] um"
While Tcl strings are polymorphic, expr supports explicit type casting functions: double() forces 64-bit IEEE floating-point conversion, int() truncates to integer, and wide() casts to a 64-bit signed integer.
set clock_count "12500000"
set sim_time_ps "450239485739"
# Force floating-point precision for frequency calculation
set freq_ghz [expr {double($clock_count) / (double($sim_time_ps) / 1000.0)}]
# Truncate floating number to whole cycles
set whole_cycles [expr {int(floor($freq_ghz * 100))}]
puts "Simulated freq: [format %.4f $freq_ghz] GHz (Whole cycles: $whole_cycles)"
Starting with Tcl 8.5, Tcl incorporates LibTomMath for seamless arbitrary-precision arithmetic (bignums). Integer calculations never silently overflow 32-bit or 64-bit boundaries; they transparently scale up to consume available system memory.
# Computing 2^128 (128-bit hardware address space) without overflow:
set addr_space [expr {2 ** 128}]
puts "128-bit Address Max: $addr_space"
# Factorial of 50
proc bignum_factorial {n} {
set res 1
for {set i 2} {$i <= $n} {incr i} {
set res [expr {$res * $i}]
}
return $res
}
puts "50! = [bignum_factorial 50]"
In legacy Tcl, developers used set tcl_precision 17 to control float formatting. This variable is officially deprecated in modern Tcl and causes thread contention and precision bugs. Use format "%.Nf" $val to format floating-point numbers explicitly.
# ❌ AVOID: Obsolete global float precision setting
# set ::tcl_precision 12
# ✅ RECOMMENDED: Format output explicitly via format
set capacitance_pf [expr {1.0 / 3.0}]
puts "Raw IEEE float: $capacitance_pf"
puts "EDA clean print: [format "%.4f" $capacitance_pf] pF"
puts "Scientific notation: [format "%.3e" $capacitance_pf] F"
Comparing floating-point values directly with == is notoriously dangerous due to IEEE 754 precision artifacts (e.g. 0.1 + 0.2 != 0.3). Always test differences against an epsilon: expr {abs($a - $b) < 1e-9}.
4
Control Flow, Branching & Loops
The if command evaluates a test expression and executes the corresponding body script. In Tcl, curly braces for condition tests and action bodies are command arguments—meaning the opening brace must stay on the same line as if, elseif, or else.
set setup_slack -0.045
set drc_violations 0
if {$setup_slack >= 0.0 && $drc_violations == 0} {
puts "STAGE PASSED: Timing and DRC clean."
} elseif {$setup_slack < 0.0 && $setup_slack > -0.100} {
puts "STAGE WARNING: Minor timing violation ($setup_slack ns). Running ECO optimizer..."
} else {
puts "STAGE FAILED: Major timing closure failure ($setup_slack ns)."
}
Placing the opening brace on a new line (e.g. if {$test}\n{) causes an error because Tcl sees the newline as the command terminator. Always place { on the same line as if / else, or use a backslash \.
The switch command branches based on pattern matching against a target string. Matching modes include -exact (default), -glob (wildcards *, ?), and -regexp (full regular expressions). Use -- before the string to prevent flags in input strings from masquerading as switch options.
set cell_ref "DFF_X2_A9TR"
# Glob matching pattern:
switch -glob -- $cell_ref {
"BUF_*" { puts "Type: Buffer ($cell_ref)" }
"INV_*" { puts "Type: Inverter ($cell_ref)" }
"DFF_*" { puts "Type: Sequential Flip-Flop ($cell_ref)" }
default { puts "Type: Combinational Gate ($cell_ref)" }
}
# Regular Expression pattern with captures:
set pin_name "clk_core_buf_i"
switch -regexp -- $pin_name {
{^clk_.*_i$} { puts "Input Clock Pin: $pin_name" }
{^rst_.*_n$} { puts "Active-low Reset Pin: $pin_name" }
default { puts "Standard Data Pin" }
}
By default, Tcl switch bodies do not fall through to subsequent cases. To share a single action body across multiple pattern arms, specify a single hyphen - as the branch script.
set file_ext ".vh"
switch -exact -- $file_ext {
".v" -
".sv" -
".vh" {
set lang "verilog"
puts "Parsed as Verilog/SystemVerilog source."
}
".vhd" -
".vhdl" {
set lang "vhdl"
puts "Parsed as VHDL source."
}
default {
error "Unsupported source file format: $file_ext"
}
}
The for loop accepts four arguments: initialization script, test expression, next-step script, and body. Always brace both the test expression and next step to ensure JIT bytecode optimization and correct variable evaluation on each iteration.
# Standard counting loop with incr
for {set i 0} {$i < 8} {incr i} {
puts "Configuring pipeline stage $i (reg_stage_${i}_q)"
}
# Stepping through memory bus width in powers of 2
for {set width 8} {$width <= 128} {set width [expr {$width * 2}]} {
puts "Supported AXI bus width: $width bits"
}
A while loop repeatedly executes its body as long as the condition expression evaluates to non-zero. You MUST brace the condition! If the condition is unbraced, Tcl substitutes the variable value once before the loop begins, creating a catastrophic infinite loop.
# ❌ FATAL TRAP: Unbraced condition evaluates ONCE to "1" => INFINITE LOOP!
# set count 3
# while $count > 0 { puts $count; incr count -1 }
# ✅ CORRECT: Bracing allows re-evaluation of $count on every iteration
set count 3
while {$count > 0} {
puts "Hold-time fix iteration: $count"
incr count -1
}
# Polling simulation status with timeout
set timeout_cycles 100
set sim_running 1
while {$sim_running && $timeout_cycles > 0} {
incr timeout_cycles -1
# Check simulation status...
if {$timeout_cycles == 95} { set sim_running 0 }
}
foreach is the standard idiom for iterating through elements of a Tcl list. It is fast, clean, handles whitespace properly, and avoids index arithmetic.
set clock_domains [list "clk_sys" "clk_ddr" "clk_pcie" "clk_dsp"]
foreach clk $clock_domains {
puts "Constraining clock domain: $clk"
# SDC constraint example:
# create_clock -name $clk -period 2.5 [get_ports $clk]
}
Tcl foreach has built-in superpowers: it can take multiple variables from a single list (e.g. key-value pairs) OR iterate across multiple independent lists in parallel (similar to Python zip()).
# 1. Pairwise iteration over key-value flat list:
set pad_assignments [list "PAD_A0" 3.3 "PAD_A1" 3.3 "PAD_HS" 1.8]
foreach {pad voltage} $pad_assignments {
puts "Pin $pad set to IO voltage ${voltage}V"
}
# 2. Parallel iteration across two separate lists (Parallel Zip):
set clocks [list "clk_core" "clk_mem" "clk_axi"]
set periods [list 1.0 2.5 4.0]
foreach clk $clocks period $periods {
puts "Domain [format %-10s $clk] -> Period: ${period}ns"
}
If the lists passed to parallel foreach have uneven lengths, the shorter lists supply empty string values "" for the remaining iterations. Always verify list lengths with llength if strict synchronization is required.
Introduced in Tcl 8.6, lmap operates like foreach but collects the return value of each iteration into a new list (list comprehension / map). Calling continue skips collecting that item, turning lmap into an elegant filter.
# Transform (Map): Convert list of MHz frequencies to period in ns
set freqs_mhz [list 100 200 400 800]
set periods_ns [lmap f $freqs_mhz {
expr {1000.0 / $f}
}]
puts "Periods: $periods_ns" ;# {10.0 5.0 2.5 1.25}
# Filter: Retain only violating timing paths (slack < 0.0)
set slacks [list 0.12 -0.04 0.35 -0.15 0.00]
set violations [lmap s $slacks {
if {$s >= 0.0} continue
set s
}]
puts "Violating slacks: $violations" ;# {-0.04 -0.15}
break immediately terminates the innermost enclosing loop. continue skips the remainder of the current loop iteration and advances immediately to the next evaluation cycle.
set net_list [list "net_clk" "net_vss" "net_data0" "net_vdd" "net_data1"]
foreach net $net_list {
# Skip power rails using continue:
if {$net eq "net_vdd" || $net eq "net_vss"} {
continue
}
# Stop parsing if critical clock net inspected:
if {$net eq "net_clk"} {
puts "Found critical root clock: $net"
}
}
5
Procedures, Arguments & Call Frames
Procedures are defined with proc name argList body. If no explicit return statement is executed, the procedure automatically returns the value produced by the last executed command in the body.
# Standard procedure definition:
proc calc_clock_period_ns {freq_mhz} {
if {$freq_mhz <= 0} {
error "Invalid frequency: $freq_mhz (must be > 0)"
}
return [expr {1000.0 / $freq_mhz}]
}
set p [calc_clock_period_ns 500]
puts "Period for 500 MHz: ${p}ns"
To specify default argument values, format the parameter in the argument list as a two-element sub-list: {argName defaultValue}. All parameters with default values must appear at the end of the argument list.
proc create_sdc_clock {clk_port {period_ns 10.0} {waveform {0.0 5.0}}} {
puts "SDC: create_clock -name $clk_port -period $period_ns -waveform {$waveform} \[get_ports $clk_port\]"
}
# Invocation using defaults:
create_sdc_clock "clk_slow"
# Overriding period:
create_sdc_clock "clk_fast" 2.5
# Overriding both period and duty cycle waveform:
create_sdc_clock "clk_asym" 4.0 {0.0 1.0}
If the last formal argument in a procedure's parameter list is named args, Tcl bundles all remaining arguments passed by the caller into a single list assigned to $args. This powers variable-argument functions and command wrappers.
proc report_critical_pins {net_name args} {
puts "Net: $net_name (Pins connected: [llength $args])"
foreach pin $args {
puts " -> $pin"
}
}
report_critical_pins "net_reset" "u_cpu/rst_n" "u_mem/rst_n" "u_dma/rst_n"
The return command supports advanced control codes: -code ok (0), -code error (1), -code return (2), -code break (3), or -code continue (4). Using return -code error -errorcode ... "msg" raises structured exceptions catchable by callers.
proc validate_voltage {corner volt} {
if {$volt < 0.70 || $volt > 1.35} {
return -code error -errorcode [list EDA VOLTAGE_OUT_OF_SPEC $corner] \
"Voltage ${volt}V at corner $corner exceeds operating range \[0.70V .. 1.35V\]"
}
return 1
}
# Catching the structured error:
if {[catch {validate_voltage "worst_case" 0.65} errMsg options]} {
puts "Caught Error: $errMsg"
puts "Error Code: [dict get $options -errorcode]"
}
tailcall cmd ?args...? replaces the current procedure's execution stack frame with the execution of the target command. This eliminates call stack frame buildup, enabling infinite recursive state machines and deep tree traversals with zero stack-overflow risk.
# Tail-recursive accumulator for traversing deep hierarchical design nets
proc count_down_tail {n acc} {
if {$n <= 0} { return $acc }
# tailcall replaces current frame: constant memory O(1) stack depth!
tailcall count_down_tail [expr {$n - 1}] [expr {$acc + $n}]
}
puts "Sum 1..100000 = [count_down_tail 100000 0]"
apply {args body ?namespace?} ?values...? evaluates an anonymous lambda procedure without registering a permanent command name in the interpreter symbol table. Ideal for throwaway callbacks and custom comparator predicates.
# Direct inline lambda execution:
set square [apply {{x} {expr {$x * $x}}} 12]
puts "12 squared = $square" ;# 144
# Lambda stored in variable:
set to_kelvin {{celsius} {expr {$celsius + 273.15}}}
puts "125 C in Kelvin: [apply $to_kelvin 125.0] K"
# Used with custom lsort:
set cells [list "BUF_X4" "BUF_X1" "BUF_X16" "BUF_X2"]
# Custom sort by size extracted after 'X':
set sorted [lsort -command [list apply {{a b} {
scan $a "BUF_X%d" sa
scan $b "BUF_X%d" sb
expr {$sa - $sb}
}}] $cells]
puts "Sorted buffers: $sorted" ;# BUF_X1 BUF_X2 BUF_X4 BUF_X16
Tcl features dynamic runtime introspection. You can inspect currently registered procedures, discover their argument signatures, query default parameter values, and extract procedure bodies as raw strings for dynamic wrapping or logging.
proc report_timing {clock_name {max_paths 10} {detail "summary"}} {
puts "Reporting up to $max_paths for $clock_name"
}
# Dynamic procedure signature inspector:
proc inspect_proc {pname} {
puts "Procedure: $pname"
foreach arg [info args $pname] {
if {[info default $pname $arg defVal]} {
puts " Param: $arg (default: '$defVal')"
} else {
puts " Param: $arg (REQUIRED)"
}
}
}
inspect_proc report_timing
info level returns the current procedure call stack depth or the exact command and arguments of any stack level. info frame provides source file names and precise line numbers, enabling custom debuggers and stack tracers.
# EDA Log tracer printing caller file and line
proc log_checkpoint {msg} {
# Frame -1 gives caller information
set frame_info [info frame -1]
set file [expr {[dict exists $frame_info file] ? [file tail [dict get $frame_info file]] : "interactive"}]
set line [expr {[dict exists $frame_info line] ? [dict get $frame_info line] : "?"}]
puts "\[$file:$line\] CHECKPOINT: $msg"
}
log_checkpoint "Beginning clock tree synthesis insertion"
6
Lists & List Operations
Always use the list command to construct lists containing variables with spaces or special characters. Creating lists via string concatenation (e.g. set l "$a $b") corrupts nested spacing and causes element boundary bugs.
# ❌ FRAGILE: String concatenation with whitespace issues
set pin1 "u_cpu/clk in"
set pin2 "u_mem/clk"
set bad_list "$pin1 $pin2"
puts "Length bad_list: [llength $bad_list]" ;# Prints 3! "in" became element 2
# ✅ ROBUST: list command preserves element boundaries
set good_list [list $pin1 $pin2]
puts "Length good_list: [llength $good_list]" ;# Prints 2!
puts "Element 0: [lindex $good_list 0]" ;# "u_cpu/clk in"
lindex list ?index...? extracts elements by zero-based index. Special index keywords include end (last element) and end-N (relative from end). Passing multiple index arguments extracts directly from nested lists.
set stages [list "synth" "floorplan" "placement" "cts" "route" "sta"]
puts "First stage: [lindex $stages 0]" ;# synth
puts "Last stage: [lindex $stages end]" ;# sta
puts "Pre-route: [lindex $stages end-2]" ;# cts
# Multi-dimensional indexing into nested list:
set matrix [list [list 1 2 3] [list 4 5 6] [list 7 8 9]]
puts "Row 1, Col 2: [lindex $matrix 1 2]" ;# 6
llength list counts the number of top-level elements in a list. In Tcl 8.5+, lists maintain internal object representations with cached lengths, making llength an instant O(1) operation.
set cell_instances [list "u0" "u1" "u2" "u3" "u4"]
set total [llength $cell_instances]
if {$total == 0} {
puts "WARNING: Module contains no sub-instances."
} else {
puts "Audit: $total cell instances detected."
}
lappend varName ?val...? appends items directly to a variable in-place (O(1) amortized). lset varName ?index...? newVal modifies an element in-place. linsert and lreplace return new modified copies.
# Fast in-place appending (note: pass variable NAME, not $var)
set drc_errors [list]
lappend drc_errors "METAL1_MIN_WIDTH_01"
lappend drc_errors "VIA2_SPACING_04" "OVERLAP_CHECK_09"
puts "Errors: $drc_errors"
# In-place element modification:
set clocks [list "clk_100" "clk_200" "clk_400"]
lset clocks 1 "clk_250"
puts "Updated clocks: $clocks" ;# clk_100 clk_250 clk_400
# Inserting at index 1 (returns new list):
set stages [list "synth" "route"]
set stages [linsert $stages 1 "floorplan" "placement"]
# Replacing elements from index 1 to 2:
set stages [lreplace $stages 1 2 "opt_placement"]
Writing lappend $my_list $item is wrong! lappend expects the variable NAME lappend my_list $item. Adding $ attempts to append into a variable named after the contents of my_list.
lrange list first last extracts a contiguous sub-list from index first through last (inclusive). Out-of-bound indices are automatically clamped to valid bounds.
set path_delays [list 0.12 0.45 0.78 1.02 1.34 1.89 2.10]
# Top 3 slowest paths:
set top3 [lrange $path_delays end-2 end]
puts "Top 3 slowest delays: $top3"
# All elements except first and last:
set middle [lrange $path_delays 1 end-1]
lsort ?options? list sorts list items. Key options: -ascii (default), -integer, -real, -increasing, -decreasing, -unique (deduplicates), and -index n (sorts multi-column sublists by column n).
# 1. Numeric sorting (ascending)
set slacks [list 0.25 -0.05 -0.89 0.12 -0.01]
set worst_first [lsort -real -increasing $slacks]
puts "Worst slacks: $worst_first" ;# -0.89 -0.05 -0.01 0.12 0.25
# 2. Deduplication using -unique
set visited_cells [list "u_cpu" "u_mem" "u_cpu" "u_dma" "u_mem"]
set unique_cells [lsort -unique $visited_cells]
puts "Unique cells: $unique_cells" ;# u_cpu u_dma u_mem
# 3. Multi-column table sorting by column index 1 (Slack column):
set timing_table [list \
[list "path_01" -0.150 "clk_core"] \
[list "path_02" 0.050 "clk_core"] \
[list "path_03" -0.420 "clk_mem"]]
set sorted_table [lsort -real -index 1 $timing_table]
foreach row $sorted_table {
puts "Path [lindex $row 0]: [lindex $row 1] ns ([lindex $row 2])"
}
Never use plain lsort on numbers! By default, lsort uses ASCII lexicographical ordering, meaning 100 sorts before 20! Always pass -integer or -real.
lsearch ?options? list pattern searches a list and returns the index of the first match (or -1 if not found). Flags include -exact, -glob (default), -regexp, -inline (returns matching value instead of index), and -all (returns all matches).
set netlist_pins [list "u_cpu/clk" "u_cpu/data_in\[0\]" "u_cpu/data_in\[1\]" "u_cpu/rst_n"]
# Check existence (-exact):
set idx [lsearch -exact $netlist_pins "u_cpu/rst_n"]
if {$idx != -1} {
puts "Found reset pin at index $idx"
}
# Filter all matching bus pins (-all -inline):
set bus_pins [lsearch -all -inline -glob $netlist_pins "u_cpu/data_in*"]
puts "Data bus pins: $bus_pins" ;# {u_cpu/data_in[0] u_cpu/data_in[1]}
lreverse list reverses element order in O(n) time. lassign list varName ?varName...? unpacks elements into distinct variables in a single command, returning any unassigned surplus elements.
# Unpacking coordinate tuple (x1, y1, x2, y2):
set bbox [list 10.0 25.0 110.0 225.0]
lassign $bbox x1 y1 x2 y2
puts "BBox: ($x1, $y1) to ($x2, $y2)"
# Capturing head and remainder:
set remaining_stages [lassign [list "synth" "floorplan" "cts" "route"] current_stage next_stage]
puts "Now: $current_stage, Next: $next_stage, Queued: $remaining_stages"
# Reversing pipeline stage execution:
set reversed_flow [lreverse [list "stage1" "stage2" "stage3"]]
puts "Rollback order: $reversed_flow" ;# stage3 stage2 stage1
split string ?splitChars? breaks a string into a list on delimiter boundaries (e.g. parsing CSV lines or hierarchy paths). join list ?joinString? collapses list items into a single delimited string.
# 1. Parsing hierarchical EDA cell instance path
set full_path "top/u_core/u_alu/adder_32/cout"
set path_tokens [split $full_path "/"]
puts "Hierarchy depth: [llength $path_tokens]"
puts "Leaf instance: [lindex $path_tokens end]"
puts "Parent module: [join [lrange $path_tokens 0 end-1] "/"]"
# 2. Parsing CSV report line:
set csv_line "BUF_X4,120.4,350.2,MET"
lassign [split $csv_line ","] cell x y status
puts "Cell $cell at ($x, $y) -> $status"
Consecutive split delimiters produce empty elements! E.g. split "a//b" "/" produces {a {} b}. Use [lsearch -all -inline -not [split $str "/"] ""] if empty tokens should be discarded.
7
Dictionaries (Tcl 8.5+)
Unlike legacy arrays, Tcl dict objects are true first-class values. They can be stored in scalar variables, passed by value into procedures, nested arbitrarily, and returned directly. Accessing non-existent keys throws an error unless guarded.
# Creation using dict create or literal key-value list:
set chip_cfg [dict create \
design_name "crypto_core" \
metal_layers 6 \
target_clock_mhz 800.0]
# Reading a key:
set design [dict get $chip_cfg design_name]
puts "Design: $design"
# Updating/setting a key (in-place on variable):
dict set chip_cfg metal_layers 7
dict set chip_cfg process_node "16nm"
# Removing a key:
dict unset chip_cfg process_node
puts "Updated configuration dict: $chip_cfg"
dict exists $dict key ?key...? checks for the presence of a key without raising an exception. dict keys and dict values extract keys and values matching optional glob patterns. dict size returns the number of key-value pairs.
set timing_metrics [dict create wns -0.05 tns -1.25 num_viols 18]
# Safe access with dict exists:
if {[dict exists $timing_metrics wns]} {
set wns [dict get $timing_metrics wns]
puts "WNS: ${wns}ns"
}
puts "Metric count: [dict size $timing_metrics]"
puts "Available keys: [dict keys $timing_metrics]"
puts "Pattern keys: [dict keys $timing_metrics *ns]" ;# wns tns
dict incr varName key ?increment? increments an integer counter directly inside a dictionary variable without reading and re-setting. dict lappend varName key ?val...? appends items directly to a list stored inside a dictionary value.
set drc_summary [dict create]
# Increment counters atomically (creates key if non-existent!):
dict incr drc_summary "MIN_SPACING"
dict incr drc_summary "MIN_SPACING"
dict incr drc_summary "MIN_AREA" 5
puts "DRC Counts: $drc_summary" ;# MIN_SPACING 2 MIN_AREA 5
# Appending to lists inside dict:
set pin_map [dict create]
dict lappend pin_map "clk_core" "u0/clk" "u1/clk"
dict lappend pin_map "clk_core" "u2/clk"
puts "Clock fanout: [dict get $pin_map clk_core]"
dict for {keyVar valVar} $dict body iterates efficiently over all key-value entries in insertion order. In Tcl, dictionaries preserve deterministic key insertion order (unlike arrays which are pseudo-random hashes).
set clock_targets [dict create \
clk_cpu 1200.0 \
clk_pcie 500.0 \
clk_uart 40.0]
dict for {clk freq} $clock_targets {
set period_ns [expr {1000.0 / $freq}]
puts "Clock [format %-10s $clk]: [format %7.1f $freq] MHz (T = [format %.3f $period_ns] ns)"
}
dict update dictVar key localName body unpacks specific keys into local variables and writes modifications back to the dictionary upon body completion. dict with dictVar body automatically maps ALL dictionary keys to local variables.
set cell_info [dict create ref "INV_X2" drive 2.0 area 0.450]
# dict update: Map 'area' to local 'a', modify in-place:
dict update cell_info area a {
set a [expr {$a * 1.15}] ;# 15% cell padding
}
puts "Updated cell info: $cell_info"
# dict with: Read/write all keys directly
dict with cell_info {
puts "Current cell $ref has drive strength $drive and area $area"
set drive [expr {$drive * 2}]
}
puts "Post-upsize: $cell_info"
Dictionaries support multi-level nested keys naturally. Passing multiple key arguments to dict get, dict set, or dict exists traverses down nested hierarchy levels without intermediate parsing.
set corners [dict create]
# Deep nested assignment: dict set var k1 k2 k3 value
dict set corners "ss_125c" "setup" "wns" -0.120
dict set corners "ss_125c" "setup" "tns" -4.500
dict set corners "ff_m40c" "hold" "wns" -0.015
# Querying deep nested values:
set ss_wns [dict get $corners "ss_125c" "setup" "wns"]
puts "Slow-Slow 125C Setup WNS: ${ss_wns}ns"
# Deep existence check:
if {[dict exists $corners "ss_125c" "hold" "wns"]} {
puts "Found hold metrics for ss_125c"
} else {
puts "Notice: ss_125c hold not analyzed."
}
dict filter $dict creates a new dictionary by filtering entries matching pattern criteria: -key pattern, -value pattern, or -script {kVar vVar} {expr} (custom boolean expression evaluated per entry).
set slacks [dict create \
path_01 0.25 \
path_02 -0.15 \
path_03 -0.42 \
path_04 0.08]
# Filter only violating paths with -script:
set violations [dict filter $slacks script {path slack} {
expr {$slack < 0.0}
}]
puts "Violations dict: $violations" ;# path_02 -0.15 path_03 -0.42
# Filter keys by glob pattern:
set filtered_keys [dict filter $slacks key "*01" "*04"]
puts "Endpoints 01 & 04: $filtered_keys"
Understanding when to use dict vs array is critical in production Tcl architecture. Modern EDA frameworks strongly prefer dict for state encapsulation and API returns.
# Dicts can be returned directly from procs:
proc get_power_breakdown {} {
return [dict create dynamic 45.2 leakage 12.8 total 58.0]
}
# Arrays REQUIRE upvar or global to return:
proc get_power_breakdown_arr {out_arr_name} {
upvar 1 $out_arr_name arr
set arr(dynamic) 45.2
set arr(leakage) 12.8
}
# Dicts can be easily nested, converted to JSON, or serialized:
set p [get_power_breakdown]
puts "Total Power: [dict get $p total] mW"
Arrays support trace add variable; dictionaries do NOT support traces on individual keys. If you need fine-grained write traces per key, use an array; for all data passing and API returns, use dictionaries.
8
Strings & Formatting
string length str returns the number of UTF-8/Unicode characters. string range str first last extracts a substring from index first through last (supports end and end-N).
set cell_name "u_cpu_alu_add32_inst"
puts "Name length: [string length $cell_name]"
# Extract prefix (0..4):
puts "Prefix: [string range $cell_name 0 4]" ;# u_cpu
# Extract suffix (strip 'u_'):
puts "Stripped: [string range $cell_name 2 end]" ;# cpu_alu_add32_inst
# Last 4 characters:
puts "Tail: [string range $cell_name end-3 end]" ;# inst
string first needle haystack ?startIndex? finds the first occurrence index (-1 if absent). string last finds the final occurrence. string match ?-nocase? pattern str performs fast wildcard glob matching (*, ?, [abc]).
set hier_path "top/core/dcache/ram_array\[128\]"
# Finding delimiters
set slash_pos [string first "/" $hier_path] ;# 3
set last_slash [string last "/" $hier_path] ;# 14
# Glob matching:
if {[string match -nocase "*dcache*" $hier_path]} {
puts "Hierarchy belongs to data cache domain"
}
if {[string match "*\\\[*\\\]" $hier_path]} {
puts "Path points to a bus/array index element"
}
string equal ?-nocase? ?-length n? str1 str2 returns 1 if strings match exactly. string compare returns -1, 0, or 1 for lexicographical ordering.
set netA "CLK_NET"
set netB "clk_net"
# Case-insensitive equality:
if {[string equal -nocase $netA $netB]} {
puts "Nets match ignoring case sensitivity."
}
# Prefix check up to 4 characters:
if {[string equal -length 4 "DFF_X1" "DFF_X4"]} {
puts "Both cells share 'DFF_' flip-flop prefix."
}
Transforms character casing across strings: string tolower, string toupper, and string totitle (capitalizes first letter, lowercases remainder).
set raw_corner "sS_125C_wOrSt"
set normalized [string tolower $raw_corner]
puts "Normalized: $normalized" ;# ss_125c_worst
set macro_name [string toupper "sram_256x32"]
puts "Macro: $macro_name" ;# SRAM_256X32
string trim str ?chars? strips characters from both ends. Defaults to stripping whitespace (spaces, tabs, newlines, carriage returns). trimleft and trimright strip from their respective sides only.
set dirty_line " slack = -0.045 ns \r\n"
set clean_line [string trim $dirty_line]
puts "Clean: '$clean_line'"
# Stripping specific characters (e.g. leading slashes or braces)
set pin_expr "{u_cpu/clk_i}"
set raw_pin [string trim $pin_expr "{}"]
puts "Raw pin: $raw_pin" ;# u_cpu/clk_i
string map ?options? mappingDict str performs simultaneous multiple substring replacements in a single linear pass (no recursive replacement conflicts). string replace str first last ?newStr? replaces a slice by index.
# Multi-token template interpolation with string map:
set tpl "create_clock -name %CLK% -period %PERIOD% \[get_ports %PORT%\]"
set rendered [string map [list \
"%CLK%" "clk_sys" \
"%PERIOD%" "2.5" \
"%PORT%" "core_clk_p"] $tpl]
puts "SDC: $rendered"
# SDC: create_clock -name clk_sys -period 2.5 [get_ports core_clk_p]
# Sanitizing Verilog escaped identifiers:
set v_net "\net_bus[0] "
set sanitized [string map [list "\\" "" " " ""] $v_net]
string is class ?-strict? str tests whether a string conforms to a data type without throwing syntax errors. Supported classes include integer, wideinteger, double, boolean, alnum, alpha, ascii, digit, space, and print.
proc set_clock_period {val} {
# -strict ensures empty string "" is NOT counted as valid number
if {![string is double -strict $val]} {
error "Invalid clock period '$val': must be a floating point number!"
}
puts "Clock period accepted: ${val}ns"
}
set_clock_period "2.5" ;# OK
# set_clock_period "two_ns" ;# Throws error safely
Without -strict, string is integer "" returns 1 (true)! Always pass -strict when validating user arguments or parsed file inputs.
The format formatString ?arg...? command generates strings formatted with standard ANSI C sprintf specifiers. Essential for building clean, aligned ASCII summary tables in EDA reports.
# Pretty-printed EDA timing report table:
puts [string repeat "=" 60]
puts [format "%-20s | %12s | %10s | %6s" "Clock Domain" "Freq (MHz)" "Slack (ns)" "Status"]
puts [string repeat "-" 60]
set rows [list \
[list "clk_core" 1200.0 0.045 "MET"] \
[list "clk_ddr" 800.0 -0.120 "VIOL"] \
[list "clk_pcie" 250.0 0.890 "MET"]]
foreach row $rows {
lassign $row clk freq slack status
puts [format "%-20s | %12.2f | %10.3f | %6s" $clk $freq $slack $status]
}
puts [string repeat "=" 60]
scan string format ?varName...? parses data from strings according to C sscanf conversion specifiers. Returns the count of successfully matched conversions.
# Extracting physical DEF coordinates: ( 12500 34200 )
set def_token "( 12500 34200 )"
if {[scan $def_token "( %d %d )" x y] == 2} {
puts "Parsed Cell Placement: X=$x, Y=$y"
}
# Parsing library cell name and drive strength:
set cell_type "AND2_X4_HVT"
if {[scan $cell_type "%\[^_\]_X%d_%s" gate drive vt] == 3} {
puts "Gate: $gate, Drive: X$drive, VT: $vt"
}
9
Regular Expressions & Pattern Matching
regexp ?switches? exp string ?matchVar? ?subMatchVar...? matches a regular expression against a string. If match variables are supplied, matchVar receives the full match, and subsequent variables receive parenthesized capture groups.
set log_line "VIOLATION: Setup slack on endpoint 'u_core/reg_q[3]' is -0.145 ns"
# Extract endpoint and slack value using regex captures:
set pattern {slack on endpoint '([^']+)' is ([-+]?[0-9]*\.?[0-9]+)}
if {[regexp $pattern $log_line full_match endpoint slack_val]} {
puts "Captured Endpoint: $endpoint"
puts "Captured Slack: $slack_val ns"
}
Always enclose the regex pattern in curly braces {...}! If you use double quotes, Tcl evaluates backslashes and square brackets before the regexp engine sees them, corrupting character classes like \d and bus indices like \[0\].
Key regexp switches: -nocase (case insensitive), -line (enables multiline anchoring where ^/$ match line breaks), -indices (stores start and end character offsets instead of matched substrings), and -- (ends switch processing).
set report "Error: Setup timing failed.\nWARNING: Max slew violation."
# Multiline match with -line and -nocase
if {[regexp -line -nocase {^error:\s*(.*)$} $report -> err_msg]} {
puts "Found multiline error: $err_msg"
}
# Finding character indices offset:
if {[regexp -indices {timing failed} $report idx]} {
puts "Match found from character [lindex $idx 0] to [lindex $idx 1]"
}
Passing -all -inline turns regexp into a global extractor, returning all matches and capture groups across the entire string as a flat list. This is the fastest way to tokenize log files or extract netlist pins.
set verilog_line "wire [31:0] data_bus, addr_bus, ctrl_bus;"
# Extract all bus identifier names:
set bus_names [regexp -all -inline {\w+_bus} $verilog_line]
puts "Extracted buses: $bus_names" ;# data_bus addr_bus ctrl_bus
# Extracting key=value pairs:
set text "WNS=-0.045 TNS=-1.200 VIOLS=15"
set pairs [regexp -all -inline {(\w+)=([^\s]+)} $text]
foreach {full key val} $pairs {
puts " Key: $key => Value: $val"
}
Tcl regex engines feature fine-grained boundary anchors: ^ (start of string/line), $ (end of string/line), \m (start of word), \M (end of word), and \y (any word boundary).
set net_name "clk_core"
# Exact whole-string anchor:
if {[regexp {^clk_[a-z0-9_]+$} $net_name]} {
puts "Valid clock naming format: $net_name"
}
# Word boundary matching:
set line "set clk [get_ports clk_i]"
# Match isolated word 'clk' without matching 'clk_i':
regexp -all -inline {\mclk\M} $line
By default, quantifiers (*, +, {n,m}) are greedy—they consume as many characters as possible. Adding a ? (e.g. *?, +?) makes them non-greedy (lazy), stopping at the earliest possible match.
set code "/* comment 1 */ data = 1; /* comment 2 */"
# ❌ GREEDY: Consumes from first /* to last */
regexp {/\*.*\*/} $code bad_match
puts "Greedy match: $bad_match"
# ✅ NON-GREEDY: Stops at first closing */
regexp -all -inline {/\*.*?\*/} $code
regsub ?switches? exp string subSpec ?varName? matches a regex against string and replaces matches with subSpec. Passing -all replaces every occurrence across the string.
# Replacing all buffer cells in a netlist line:
set netlist_line "BUF_X1 u1 (.A(n1), .Y(n2)); BUF_X1 u2 (.A(n2), .Y(n3));"
set updated_line [regsub -all {BUF_X1} $netlist_line "BUF_X4"]
puts "Upsized netlist: $updated_line"
# Stripping out Verilog bus subscripts [31:0] -> _31_0_
set pin "data_out\[31\]"
set safe_name [regsub -all {\[([0-9]+)\]} $pin {_\1}]
puts "Safe symbol name: $safe_name" ;# data_out_31
Inside the replacement string subSpec of regsub, \0 references the entire matched text, \1 references the first parenthesized group, \2 references the second, and so on.
# Reformatting timing path slack report line:
set line "Endpoint: reg_stage_q Slack: -0.150 ns"
set reformatted [regsub {Endpoint:\s*(\S+)\s*Slack:\s*([-\d.]+)\s*ns} $line \
{PATH_METRIC|cell=\1|slack=\2ns}]
puts "$reformatted"
# PATH_METRIC|cell=reg_stage_q|slack=-0.150ns
Tcl supports standard regex shorthand characters: \d (digit), \D (non-digit), \s (whitespace), \S (non-whitespace), \w (word char [a-zA-Z0-9_]), \W (non-word), and POSIX bracket classes [[:xdigit:]], [[:space:]].
# Validating hexadecimal address in memory map (e.g. 0x00FF8000)
set addr "0x00FF8000"
if {[regexp {^0x[[:xdigit:]]{8}$} $addr]} {
puts "Valid 32-bit hex address format: $addr"
}
# Extracting all words separated by arbitrary whitespace:
set input "clk_core clk_mem \t clk_axi\nclk_uart"
set words [regexp -all -inline {\S+} $input]
puts "Words: $words"
A complete, reusable procedure to parse Synopsys PrimeTime or Cadence Tempus timing reports, capturing Startpoint, Endpoint, Path Group, and Slack into structured dictionaries.
proc parse_timing_report_chunk {report_text} {
set violations [list]
# Split text into timing path blocks separated by 'Startpoint:'
set paths [regexp -all -inline -line {(?n)^ Startpoint: .*?(?=^ Startpoint:|\Z)} $report_text]
foreach path_block $paths {
set path_data [dict create]
if {[regexp -line {^\s*Startpoint:\s*(\S+)} $path_block -> start]} {
dict set path_data startpoint $start
}
if {[regexp -line {^\s*Endpoint:\s*(\S+)} $path_block -> end]} {
dict set path_data endpoint $end
}
if {[regexp -line {^\s*Path Group:\s*(\S+)} $path_block -> grp]} {
dict set path_data path_group $grp
}
if {[regexp -line {^\s*slack\s*\((VIOLATED|MET)\)\s*([-\d.]+)} $path_block -> status slack]} {
dict set path_data status $status
dict set path_data slack [expr {double($slack)}]
lappend violations $path_data
}
}
return $violations
}
10
File I/O, File System & Channels
open fileName ?access? ?permissions? opens a file or pipeline and returns an opaque channel identifier (e.g. rc0, file4). Standard modes: r (read, default), w (truncate/write), a (append), r+ (read/write existing), w+ (truncate/read/write).
# Open file for writing reports
set fh [open "run_sta.log" w]
puts $fh "Timestamp: [clock format [clock seconds] -format {%Y-%m-%d %H:%M:%S}]"
puts $fh "Tool: OpenROAD Timing Engine"
close $fh
# Open file for appending execution records
set log_fh [open "regression.summary" a]
puts $log_fh "RUN_042: PASSED (Setup Slack: +0.025ns)"
close $log_fh
Always close channels with close $fh inside a try ... finally block! Failing to close channels leaks OS file descriptors and causes data corruption due to unwritten OS write buffers.
read $fh ?numChars? slurps the entire file (or N characters) into memory at once. gets $fh ?varName? reads a single line, stripping the newline character. Line-by-line reading with gets maintains constant low memory when processing gigabyte-scale netlists or GDS/DEF stream files.
# Approach 1: Slurp entire file into a single string (small SDC / config files)
set fh [open "constraints.sdc" r]
set sdc_content [read $fh]
close $fh
# Approach 2: Memory-efficient line-by-line streaming (multi-GB synthesis logs/DEF)
set fh [open "innovus.log" r]
set viol_count 0
while {[gets $fh line] >= 0} {
if {[string match "*VIOLATION*" $line]} {
incr viol_count
}
}
close $fh
puts "Total violations detected: $viol_count"
puts ?-nonewline? ?channelId? string writes data to an open channel or stdout. flush channelId forces all queued user-space buffer data out to the operating system immediately.
# Writing without newline for progress meters:
puts -nonewline stdout "Running placement optimization... "
flush stdout ;# Force output to console immediately before heavy task
# Heavy computation loop...
after 1000
puts stdout "DONE [format %.2f 1.25]s"
# Writing formatted ECO commands to script file:
set eco_fh [open "fix_hold.eco.tcl" w]
puts $eco_fh "# Automated ECO Hold buffer insertion"
puts $eco_fh "insert_buffer -net {u_cpu/data_reg_q} -cell {CLKBUF_X2} -new_net {net_eco_001}"
flush $eco_fh
close $eco_fh
fconfigure channelId ?name? ?value...? controls I/O channel behavior. Key properties: -buffering (full, line, none), -translation (auto, binary, crlf, lf), -encoding (utf-8, iso8859-1, binary), and -blocking (0 or 1).
# 1. Binary mode for parsing OASIS/GDSII layout stream files:
set gds_fh [open "chip_layout.gds" r]
fconfigure $gds_fh -translation binary -encoding binary
set gds_header [read $gds_fh 4]
close $gds_fh
# 2. Line-buffered real-time log stream:
set log_fh [open "live_synthesis.log" w]
fconfigure $log_fh -buffering line -encoding utf-8 -translation lf
puts $log_fh "INIT: Synthesis engine spawned" ;# Flushed immediately
Tcl pre-defines three special channel handles: stdin (standard input), stdout (standard output), and stderr (standard error). You can write diagnostic errors directly to stderr without terminating execution.
# Writing diagnostic warning to standard error:
puts stderr "ERROR-TIM-01: Critical setup violation detected in clk_fast domain!"
# Reading interactive input from terminal:
puts -nonewline stdout "Proceed with ECO buffer insertion? \[y/N\]: "
flush stdout
gets stdin user_response
if {[string tolower $user_response] eq "y"} {
puts stdout "Applying ECO..."
} else {
puts stdout "Aborted by user."
}
Never concatenate filesystem paths using manual slashes or plus signs. Always use file join and file split to guarantee cross-platform portability between Linux server farms and Windows developer workstations.
set root_dir "/opt/designs"
set sub_dir "soc_top/runs/run_01"
set filename "final_timing.rpt"
# 1. Robust path joining:
set full_path [file join $root_dir $sub_dir $filename]
puts "Full Path: $full_path"
# 2. Path component deconstruction:
puts "Dirname: [file dirname $full_path]" ;# /opt/designs/soc_top/runs/run_01
puts "Tail: [file tail $full_path]" ;# final_timing.rpt
puts "Extension: [file extension $full_path]" ;# .rpt
puts "Rootname: [file rootname $full_path]" ;# .../final_timing
file normalize path resolves all relative dots (., ..), symbolic links, and duplicate slashes, returning an absolute, canonical path string. pwd returns the current working directory.
# Current directory
set cwd [pwd]
puts "Working directory: $cwd"
# Resolving messy relative paths:
set rel_path "../../tech/sky130/libs/../lef/sky130_fd_sc_hd.lef"
set abs_clean [file normalize $rel_path]
puts "Normalized: $abs_clean"
# Native OS formatting (e.g. backslashes on Windows for external tools)
puts "OS Native: [file nativename $abs_clean]"
Tcl provides built-in filesystem inspection queries: file exists, file isfile, file isdirectory, file size (bytes), file mtime (POSIX modification epoch), file readable, and file writable.
set netlist "syn/outputs/soc_top.v"
if {[file exists $netlist]} {
set bytes [file size $netlist]
set mtime [file mtime $netlist]
set date_str [clock format $mtime -format {%Y-%m-%d %H:%M:%S}]
puts "Netlist: $netlist ($bytes bytes, modified $date_str)"
} else {
error "Target netlist $netlist does not exist!"
}
Manage files and directories without spawning external shell commands: file mkdir ?dir...? (creates intermediate parents automatically), file copy ?-force? src dst, file rename ?-force? src dst, and file delete ?-force? ?path...?.
set out_dir "build/stage_sta"
# Ensure output directory exists (recursive mkdir -p):
file mkdir $out_dir
# Archive previous timing reports:
if {[file exists "$out_dir/timing.rpt"]} {
file copy -force "$out_dir/timing.rpt" "$out_dir/timing.rpt.bak"
}
# Clean temporary files safely:
file delete -force "temp_eco.tcl"
glob ?-nocomplain? ?-directory dir? ?pattern...? finds files matching wildcard patterns. Combine with recursion to traverse entire directory trees looking for HDL sources or reports.
# Recursive directory tree walker finding all Verilog & SystemVerilog files:
proc find_hdl_files {root_dir} {
set results [list]
# Find matching files in current dir (-nocomplain suppresses error if empty):
foreach f [glob -nocomplain -directory $root_dir -types f *.v *.sv] {
lappend results $f
}
# Recursively traverse child sub-directories:
foreach d [glob -nocomplain -directory $root_dir -types d *] {
set child_name [file tail $d]
if {$child_name eq "." || $child_name eq ".." || $child_name eq ".git"} continue
set results [concat $results [find_hdl_files $d]]
}
return $results
}
set verilog_files [find_hdl_files "rtl"]
puts "Discovered [llength $verilog_files] HDL sources."
11
Subprocesses, Errors & Namespaces
exec ?switches? arg ?arg...? executes external OS binaries or command pipelines (e.g. invoking Yosys, Verilator, or Git). Tcl connects commands with pipes (|) just like a Unix shell and captures output.
# Running git to query current commit hash:
set git_rev [string trim [exec git rev-parse --short HEAD]]
puts "Design git commit: $git_rev"
# Pipeline execution: count total cell instances in Verilog netlist:
set cell_count [exec grep -c "^ \\w\\+ u_" "synth/output.v" | tr -d " "]
puts "Synthesized cell instances: $cell_count"
By default, if an external program writes anything to standard error (even innocent banner warnings), exec treats it as a failure and throws an exception. Redirect stderr to stdout using 2>@1 or >& to merge streams cleanly.
# Redirect standard error into standard output stream:
set tool_output [exec yosys -V 2>@1]
puts "Yosys Version: $tool_output"
# Redirect combined stdout & stderr directly into a disk log file:
exec openlane --design soc_top >& "openlane_run.log"
When a spawned child process exits with a non-zero exit code (e.g. exit 1 or grep finding no matches), exec immediately raises a fatal Tcl error. Always wrap exec with catch or try and inspect $::errorCode for the real exit status.
# Safe subprocess invocation pattern:
set status [catch {exec grep "NO_SUCH_STRING" "design.v"} output]
if {$status != 0} {
# Check if failure was just a non-zero exit code:
if {[lindex $::errorCode 0] eq "CHILDSTATUS"} {
set exit_code [lindex $::errorCode 2]
puts "Process exited with code $exit_code (grep found no match)."
} else {
puts "System invocation error: $output"
}
} else {
puts "Found match: $output"
}
Appending an ampersand & to the end of an exec command line spawns the external process in the background without blocking Tcl execution, returning a list of operating system Process IDs (PIDs).
# Launch simulation in background
set pids [exec verilator --trace-fst -cc dut.v &]
puts "Spawned background simulation process: PID [lindex $pids 0]"
# Current Tcl interpreter PID:
puts "Orchestrator PID: [pid]"
Tcl 8.6 introduced modern structured exception handling with try body ?handler...? ?finally script?. The finally block is guaranteed to execute regardless of normal exit, error exceptions, return, or break.
set fh [open "sta_run.log" w]
try {
puts $fh "Running critical static timing analysis..."
# Perform operations that might raise errors:
if {[expr {rand() < 0.1}]} {
throw {EDA TIMING TIMEOUT} "Timing engine exceeded max runtime"
}
puts "STA completed cleanly."
} trap {EDA TIMING} {msg opt} {
puts stderr "Caught EDA timing error: $msg"
} on error {msg opt} {
puts stderr "Unexpected general failure: $msg"
} finally {
# Guaranteed cleanup of channel descriptor:
close $fh
puts "Log channel closed safely."
}
catch script ?resultVar? ?optionsVar? intercepts any abnormal completion code (1=error, 2=return, 3=break, 4=continue) and returns 0 on success or non-zero on exception. optionsVar receives error metadata.
if {[catch {
set file_size [file size "non_existent.def"]
} errMsg errOpts]} {
puts "Handled error gracefully: $errMsg"
puts "Stack trace:\n[dict get $errOpts -errorinfo]"
}
error message ?errorInfo? ?errorCode? throws an error string. In Tcl 8.6+, throw type message provides clean, structured exceptions where type is a standardized hierarchy list.
proc enforce_timing_closure {wns} {
if {$wns < 0.0} {
# Structured exception with taxonomy list
throw [list EDA STA VIOLATION WNS] \
"Design failed timing closure: worst negative slack is ${wns}ns"
}
}
# Legacy syntax:
# error "Fatal setup timing failure: ${wns}ns" "" [list EDA STA VIOLATION]
Namespaces provide distinct symbol scopes for commands and variables, preventing collisions in large multi-engineer EDA script codebases. Procs can be exported with namespace export and imported with namespace import.
namespace eval ::sta_toolkit {
namespace export audit_setup audit_hold
variable target_library "sky130_fd_sc_hd"
proc audit_setup {corner} {
variable target_library
puts "Auditing setup timing against $target_library at corner $corner"
}
proc audit_hold {corner} {
puts "Auditing hold timing at corner $corner"
}
}
# Qualified invocation:
::sta_toolkit::audit_setup "ss_125c"
# Selective command importing:
namespace import ::sta_toolkit::audit_setup
audit_setup "tt_25c"
Tcl organizes shared libraries using the package management system. Files register their identity with package provide name version, and consuming scripts declare dependencies using package require name ?version?.
# Inside library file: eda_utils.tcl
package provide eda_utils 1.4
namespace eval ::eda_utils {
proc banner {title} {
puts [string repeat "=" 50]
puts " $title"
puts [string repeat "=" 50]
}
}
# Inside top-level flow runner:
# package require Tcl 8.6
# package require eda_utils 1.4
# ::eda_utils::banner "STARTING PHYSICAL SYNTHESIS"
12
EDA & VLSI Semiconductor Automation
Every Tier-1 electronic design automation (EDA) software suite—including Synopsys (Design Compiler, PrimeTime, ICC2), Cadence (Genus, Innovus, Tempus), AMD/Xilinx (Vivado), and open-source flows (OpenROAD, Yosys)—embeds a native C/C++ Tcl interpreter. Tcl serves as the universal interactive shell, command-line interface, constraints format (SDC), and flow automation engine across the global semiconductor industry.
# Universal EDA Shell Session Header Pattern:
puts "INFO: Starting EDA Semiconductor Scripting Flow..."
puts "Host Machine: $::tcl_platform(os) ($::tcl_platform(machine))"
# Tool-specific environment variable inspection:
if {[info exists ::synopsys_program_name]} {
puts "Environment: Synopsys $::synopsys_program_name ($::sh_product_version)"
} elseif {[info exists ::env(CDS_LOAD_ENV)] || [info exists ::innovus_version]} {
puts "Environment: Cadence Digital Implementation Suite"
} elseif {[info exists ::env(VIVADO_VERSION)]} {
puts "Environment: AMD / Xilinx Vivado Design Suite"
} elseif {[info exists ::openroad_version]} {
puts "Environment: OpenROAD Silicon Compiler"
}
In modern chips containing tens of millions of gates, representing netlist objects as native Tcl string lists would consume tens of gigabytes of RAM. EDA tools invent Collections: lightweight C++ pointers/handles to internal C++ netlist database objects. Standard Tcl list commands like llength and lindex do not work on collections—you must use sizeof_collection and collection iterators.
# Query cells from database (returns an opaque collection handle):
set all_invs [get_cells -hierarchical -filter "ref_name =~ INV_*"]
# ❌ WRONG: llength returns 1 because the collection handle is a single token "_sel42"!
puts "Bogus list length: [llength $all_invs]"
# ✅ CORRECT: sizeof_collection queries internal C++ netlist database:
set inv_count [sizeof_collection $all_invs]
puts "Verified Inverter Count: $inv_count"
# Converting collection to native Tcl string list of names (when needed):
set inv_names [get_object_name $all_invs]
puts "First 3 names: [lrange $inv_names 0 2]"
Converting a 5-million cell collection into a Tcl string list via get_object_name can exhaust interpreter heap memory. Keep objects in collections whenever possible!
foreach_in_collection varName $collection body is the mandatory idiom for iterating over database collection elements. On each cycle, varName is bound to a single-element collection handle pointing to the underlying C++ database record.
# Iterate over all sequential flip-flop cells in the design:
set dff_cells [get_cells -hierarchical -filter "is_sequential == true"]
puts "Auditing [sizeof_collection $dff_cells] sequential elements..."
set async_rst_count 0
foreach_in_collection cell $dff_cells {
set c_name [get_object_name $cell]
# Check if cell has asynchronous clear pin:
set rst_pin [get_pins -quiet -of_objects $cell -filter "name == CDN || name == RN"]
if {[sizeof_collection $rst_pin] > 0} {
incr async_rst_count
}
}
puts "Flip-flops with async resets: $async_rst_count"
EDA provides high-performance C++ set operations on collections without writing nested loops: filter_collection filters by boolean attribute expressions, add_to_collection computes unions, and remove_from_collection computes set differences.
set all_bufs [get_cells -hierarchical -filter "is_buffer == true"]
# Filter out low-drive buffers (drive strength X1):
set high_drive_bufs [filter_collection $all_bufs "ref_name !~ *X1*"]
# Set Union: Combine clock buffers and high-drive buffers
set clk_bufs [get_cells -hierarchical -filter "ref_name =~ CLKBUF_*"]
set target_pool [add_to_collection $high_drive_bufs $clk_bufs]
# Set Difference: Exclude "dont_touch" cells
set dont_touch_cells [get_cells -hierarchical -filter "dont_touch == true"]
set eligible_cells [remove_from_collection $target_pool $dont_touch_cells]
puts "Optimization candidates: [sizeof_collection $eligible_cells]"
The five cornerstone accessors in semiconductor database query scripts: get_cells (instances/gates), get_nets (interconnect wires), get_pins (hierarchical/leaf cell terminals), get_ports (chip/macro top boundary IO pads), and get_clocks (SDC timing clock domains).
# 1. Top-level boundary ports:
set reset_ports [get_ports -filter "direction == in && name =~ *rst*"]
# 2. Cell instance pins connected to clock:
set clk_pins [get_pins -hierarchical -filter "name == CLK || name == CK"]
# 3. Nets with high capacitance load:
set heavy_nets [get_nets -hierarchical -filter "capacitance > 0.050"]
# 4. SDC Clocks defined on design:
set all_sdc_clocks [get_clocks]
puts "Active Clocks: [get_object_name $all_sdc_clocks]"
get_ports queries chip boundaries; get_pins queries cell instances. Querying get_pins [get_ports clk] fails because top IO ports are not cell pins!
all_fanin and all_fanout perform graph traversal across combinational logic cones. Flags include -to/-from (start/endpoint), -flat, -startpoints_only (finds driving flip-flops/ports), and -endpoints_only.
# Identify all registers driving into ALU critical comparator pin:
set target_pin [get_pins "u_core/u_alu/cmp_gt_i1/B"]
set driving_regs [all_fanin -to $target_pin -startpoints_only -flat]
puts "Driving Startpoints: [sizeof_collection $driving_regs]"
# Trace all load sinks driven by master interrupt line:
set irq_net [get_nets "master_irq_n"]
set irq_sinks [all_fanout -from $irq_net -endpoints_only -flat]
puts "Interrupt fanout sinks: [sizeof_collection $irq_sinks]"
get_attribute $object attrName extracts exact physical, electrical, and timing properties from netlist objects (e.g. area, slack, capacitance, is_hierarchical, origin, bbox). list_attributes -class ... lists all supported attributes.
set cell [get_cells "u_cpu/u_alu/mul_32_inst"]
# Inspect electrical and physical attributes:
set ref_lib_cell [get_attribute $cell ref_name]
set cell_area [get_attribute $cell area]
set is_seq [get_attribute $cell is_sequential]
puts "Cell $cell:"
puts " Library Ref: $ref_lib_cell"
puts " Silicon Area: $cell_area um^2"
puts " Sequential: $is_seq"
# Querying net capacitance and fanout load:
set net [get_nets "clk_core_tree_net"]
set net_cap [get_attribute $net total_capacitance]
puts "Clock Net Capacitance: ${net_cap}pF"
The Synopsys Design Constraints (SDC) specification is written purely in Tcl syntax. Core timing rules include defining real and virtual clocks, IO boundary budgets, clock uncertainty/jitter, and timing exceptions (false paths & multicycle paths).
# 1. Primary Clock Definition (500 MHz -> Period 2.0ns, 50% duty cycle)
create_clock -name "clk_sys" -period 2.000 [get_ports "sys_clk_i"]
# 2. Generated Clock (Divide-by-2 derived clock from PLL)
create_generated_clock -name "clk_div2" -source [get_ports "sys_clk_i"] \
-divide_by 2 [get_pins "u_pll/clk_out_q"]
# 3. Clock Uncertainty (Jitter + Margin)
set_clock_uncertainty -setup 0.080 [get_clocks "clk_sys"]
set_clock_uncertainty -hold 0.030 [get_clocks "clk_sys"]
# 4. Input & Output Delay Constraints (60% board budget external)
set_input_delay -clock "clk_sys" -max 0.600 [get_ports "data_in[*]"]
set_output_delay -clock "clk_sys" -max 0.500 [get_ports "data_out[*]"]
# 5. Timing Exceptions: False Paths (Asynchronous reset & CDC)
set_false_path -from [get_ports "rst_n_i"]
set_clock_groups -asynchronous -group [get_clocks "clk_sys"] -group [get_clocks "clk_div2"]
A production-grade timing path auditor proc for PrimeTime / Innovus / OpenROAD that iterates over worst timing paths, calculates Total Negative Slack (TNS), Worst Negative Slack (WNS), and generates a structured closure summary.
proc audit_timing_closure {clock_domain {max_paths 100}} {
puts "=========================================================="
puts " AUDITING TIMING CLOSURE: Domain '$clock_domain'"
puts "=========================================================="
# Query timing paths collection
set paths [get_timing_paths -max_paths $max_paths -slack_lesser_than 0.0]
set path_count [sizeof_collection $paths]
if {$path_count == 0} {
puts "✅ TIMING CLEAN: Zero setup violations in domain '$clock_domain'."
return [dict create status "CLEAN" wns 0.0 tns 0.0 count 0]
}
set wns 0.0
set tns 0.0
set idx 0
foreach_in_collection path $paths {
set slack [get_attribute $path slack]
set start [get_object_name [get_attribute $path startpoint]]
set end [get_object_name [get_attribute $path endpoint]]
if {$slack < $wns} { set wns $slack }
set tns [expr {$tns + $slack}]
if {$idx < 5} {
puts [format "Viol #%02d | Slack: %8.3f ns | From: %-30s -> To: %s" \
[expr {$idx + 1}] $slack [file tail $start] [file tail $end]]
}
incr idx
}
puts "----------------------------------------------------------"
puts [format "SUMMARY: WNS = %8.3f ns | TNS = %8.3f ns | Viols = %d" $wns $tns $path_count]
puts "=========================================================="
return [dict create status "VIOLATED" wns $wns tns $tns count $path_count]
}
Excessive fanout causes large transition slews, electromigration risks, and setup timing degradation. This production procedure audits all non-clock nets in the design against a maximum fanout threshold.
proc audit_high_fanout_nets {{fanout_threshold 32}} {
puts "Scanning netlist for nets with fanout > $fanout_threshold..."
# Query all design nets, excluding power, ground, and clock nets
set candidate_nets [get_nets -hierarchical -filter "is_power == false && is_ground == false"]
set violating_nets [list]
foreach_in_collection net $candidate_nets {
# Check if net is part of a clock tree:
if {[get_attribute -quiet $net is_clock_network] eq "true"} continue
# Query all load pins connected to this net
set load_pins [get_pins -quiet -leaf -of_objects $net -filter "direction == in"]
set fanout_count [sizeof_collection $load_pins]
if {$fanout_count > $fanout_threshold} {
set net_name [get_object_name $net]
lappend violating_nets [list $net_name $fanout_count]
}
}
# Sort descending by fanout count:
set sorted_viols [lsort -integer -decreasing -index 1 $violating_nets]
puts "Found [llength $sorted_viols] High-Fanout Nets (HFN):"
foreach item [lrange $sorted_viols 0 9] {
lassign $item nName fCount
puts [format " Net: %-45s | Fanout: %5d loads" $nName $fCount]
}
return $sorted_viols
}
An automated Engineering Change Order (ECO) generator script that extracts hold-time violating endpoints and synthesizes tool-specific buffer insertion commands written to an executable ECO Tcl script.
proc generate_hold_eco_script {out_eco_filename {delay_cell "DLY2_X1"} {target_slack 0.050}} {
set eco_fh [open $out_eco_filename "w"]
puts $eco_fh "# ========================================================"
puts $eco_fh "# Automated Post-Route Hold Timing ECO Fix Script"
puts $eco_fh "# Target Delay Cell: $delay_cell"
puts $eco_fh "# Generated: [clock format [clock seconds]]"
puts $eco_fh "# ========================================================\n"
# Query all hold violations (min timing paths)
set hold_paths [get_timing_paths -delay_type min -max_paths 500 -slack_lesser_than 0.0]
set eco_count 0
foreach_in_collection path $hold_paths {
set slack [get_attribute $path slack]
set endpoint_pin [get_attribute $path endpoint]
set pin_name [get_object_name $endpoint_pin]
# Get the input net connected to this violating D-pin
set net [get_nets -of_objects $endpoint_pin]
set net_name [get_object_name $net]
set eco_inst "u_eco_hold_${eco_count}"
set eco_net "net_eco_hold_${eco_count}"
# Write tool-agnostic physical ECO commands:
puts $eco_fh "# Hold Violation: $slack ns on $pin_name"
puts $eco_fh "ecoAddRepeater -term $pin_name -cell $delay_cell -name $eco_inst -newNetName $eco_net"
incr eco_count
}
close $eco_fh
puts "SUCCESS: Generated $eco_count hold-fixing ECO operations in '$out_eco_filename'."
return $eco_count
}
A complete regression orchestrator script that executes multi-corner PnR or STA jobs in parallel subprocesses, normalizes heterogeneous log outputs into standard JSON/CSV summary reports, and verifies tape-out readiness.
proc run_batch_corner_regression {corners_list run_dir} {
file mkdir $run_dir
set report_summary [dict create]
puts "=========================================================="
puts " LAUNCHING MULTI-CORNER REGRESSION: [llength $corners_list] Corners"
puts "=========================================================="
foreach corner $corners_list {
set corner_log [file join $run_dir "sta_${corner}.log"]
puts "Executing STA for corner '$corner' -> $corner_log"
# Spawn batch STA job (e.g. OpenROAD or PrimeTime batch)
set cmd [list openroad -exit -metrics [file join $run_dir "${corner}.json"] \
"scripts/sta_${corner}.tcl"]
set start_time [clock seconds]
set exit_status [catch {exec {*}$cmd >& $corner_log} exec_err]
set elapsed [expr {[clock seconds] - $start_time}]
# Audit log for critical metrics:
set wns 0.0
set log_fh [open $corner_log r]
while {[gets $log_fh line] >= 0} {
if {[regexp {worst slack\s*([-\d.]+)} $line -> captured_slack]} {
set wns [expr {double($captured_slack)}]
}
}
close $log_fh
set status_label [expr {$wns >= 0.0 ? "PASS" : "FAIL"}]
dict set report_summary $corner [dict create \
status $status_label wns $wns runtime $elapsed exit_code $exit_status]
puts [format "Corner %-12s | Status: %-4s | WNS: %7.3f ns | Runtime: %3ds" \
$corner $status_label $wns $elapsed]
}
# Export normalized CSV report:
set csv_fh [open [file join $run_dir "regression_summary.csv"] w]
puts $csv_fh "Corner,Status,WNS_ns,Runtime_s"
dict for {c info} $report_summary {
dict with info {
puts $csv_fh "$c,$status,$wns,$runtime"
}
}
close $csv_fh
puts "=========================================================="
puts " REGRESSION FINISHED: Summary written to $run_dir/regression_summary.csv"
puts "=========================================================="
return $report_summary
}