Makefile · Part II · Module 7 · EDA Tool Integration
Synthesis and STA Automation
Synthesis and timing analysis are driven by Tcl, not by command-line options, and they answer in a report file rather than an exit status. That changes what the Makefile is responsible for: not just composing a command, but reading the answer. This chapter builds the flow — and includes the moment my own slack gate printed a timing violation and let the build pass.
Advanced28 min readMakefileGNU MakePart IISynthesisSTA
Up to now every tool in this track has been a simulator, invoked with options and asked a yes-or-no question. Synthesis and static timing analysis work differently in two ways that both matter to a Makefile.
They are driven by Tcl. dc_shell -f synth.tcl hands the tool a program, not a list of flags, so most of the configuration lives in a script the tool interprets — which is why Chapter 7.3 came first.
And they answer in a report file. A simulation's testbench at least prints UVM_ERROR : 0; a synthesis run prints pages of statistics and leaves the number you care about — the worst slack — in a timing report. Nothing in the exit status tells you whether the design made timing.
1. A Tcl Script the Tool Runs
Here is a synthesis script. It is a real Tcl program: the top third is ordinary Tcl, and only the lower part calls tool commands.
# Parameters come from the environment, so the Makefile owns them.
set design [expr {[info exists ::env(DESIGN)] ? $::env(DESIGN) : "axi_lite_slave"}]
set period [expr {[info exists ::env(PERIOD)] ? $::env(PERIOD) : 10.0}]
set rundir [expr {[info exists ::env(RUNDIR)] ? $::env(RUNDIR) : "build/syn"}]
set filelist [expr {[info exists ::env(FILELIST)]? $::env(FILELIST): "filelists/all.f"}]
file mkdir $rundir
puts " \[synth\] design=$design period=${period}ns rundir=$rundir"
# Read the SAME filelist the simulation uses - one list, many readers (5.2).
set srcs {}
set fh [open $filelist r]
foreach line [split [read $fh] \n] {
set line [string trim $line]
if {$line eq "" || [string match "//*" $line] || [string match "+*" $line]} { continue }
lappend srcs $line
}
close $fh
puts " \[synth\] [llength $srcs] source file(s) from $filelist"
read_verilog $srcs
current_design $design
link
create_clock clk -period $period
set_input_delay 1.0 -clock clk [list rst_n wr_en rd_en]
set_output_delay 1.0 -clock clk [list rd_data err]
compile_ultra -no_autoungroup
report_timing -file [file join $rundir timing.rpt]
report_area -file [file join $rundir area.rpt]
write -format verilog -hierarchy -output [file join $rundir ${design}_netlist.v]
exit 0Two design decisions in there are worth defending.
Parameters arrive through the environment, not as hardcoded values. The Makefile sets DESIGN, PERIOD, RUNDIR and FILELIST, and the script falls back to sensible defaults so it can also be run by hand. That is Chapter 7.3 §2's environment channel, chosen because paths contain characters you do not want re-split.
The script reads the same filelist as the simulation. A synthesis flow with its own source list is a flow that will eventually synthesise a different design from the one you verified. One list, many readers — Chapter 5.2's argument, now paying off across tools.
[synth] design=axi_lite_slave period=10.0ns rundir=build/syn
[synth] 2 source file(s) from filelists/all.f
dc> read_verilog (2 file(s))
dc> current_design axi_lite_slave
dc> link
dc> create_clock clk -period 10.0
dc> set_input_delay 1.0 -clock clk {rst_n wr_en rd_en}
dc> set_output_delay 1.0 -clock clk {rd_data err}
dc> compile_ultra -no_autoungroup
dc> report_timing -> build/syn/timing.rpt (slack 2.600)
dc> report_area -> build/syn/area.rpt
dc> write -> build/syn/axi_lite_slave_netlist.v
[synth] doneThe [synth] lines are the script talking; the dc> lines are the stubs. Note that the filelist parsing found 2 source files — that is real Tcl reading the real file.
2. Driving It From Make
The Makefile's job is to set the parameters, put the outputs somewhere findable, and react to the answer.
DESIGN ?= axi_lite_slave
PERIOD ?= 10.0
DC_SHELL ?= dc_shell
SYNDIR = $(BUILD)/syn/$(DESIGN)/$(PERIOD)
export DESIGN PERIOD FILELIST
.PHONY: synth
synth: $(SYNDIR)/timing.rpt
$(SYNDIR)/timing.rpt: $(FILELIST) syn/synth.tcl
@mkdir -p $(@D)
@RUNDIR=$(SYNDIR) $(DC_SHELL) -f syn/synth.tcl > $(SYNDIR)/synth.log 2>&1 \
|| { echo " [synth] TOOL FAILED - see $(SYNDIR)/synth.log"; exit 1; }
@echo " [synth] $(DESIGN) @ $(PERIOD)ns -> $(SYNDIR)"Three things carried over from earlier chapters. The tool is named by a variable, never found on PATH (Chapter 7.1 §6). The run directory carries everything that distinguishes one run from another — here the design and the clock period, because synthesising the same design at 8 ns and 10 ns gives two different results that must not overwrite each other (Chapter 5.3 §6). And the whole tool log is captured to a file rather than left on the terminal, because a synthesis log is thousands of lines.
The prerequisites are the filelist and the script. Edit either and synthesis re-runs; edit neither and it does not. For a real flow you would add the constraints file and, as Chapter 5.2 §7 showed, the sources the filelist names.
3. The Tool Exits 0 on a Timing Violation
Now the behaviour that decides the shape of everything else. Ask for a clock the design cannot make — 7 ns instead of 10:
Startpoint: clk
Endpoint: count_reg
data arrival time 7.400
data required time 7.000
slack (VIOLATED) -0.400 dc_shell exit status = 0Negative slack, the word VIOLATED in the report, and an exit status of zero.
This is the same asymmetry Chapter 5.1 §4 and Chapter 5.4 §5 measured for the simulator, and for the same reason: from the tool's point of view nothing went wrong. It was asked to synthesise a design against a constraint, and it did. "The design did not make timing" is a result, not a tool error — and a tool that exited non-zero every time a design missed timing would be unusable, because missing timing is the normal state of affairs for most of a project.
So the Makefile has to read the report. There is no alternative.
4. A Slack Gate
The gate is a small Tcl script, which keeps it usable both inside the tool and outside it:
# Usage: slack.tcl <timing.rpt> [threshold]
# Exits 0 if the worst slack is >= threshold, 1 if not, 2 if unreadable.
if {[llength $argv] < 1} { puts stderr "usage: slack.tcl <timing.rpt> \[threshold\]"; exit 2 }
set rpt [lindex $argv 0]
set thr [expr {[llength $argv] > 1 ? [lindex $argv 1] : 0.0}]
if {![file exists $rpt]} { puts stderr "slack.tcl: no such report: $rpt"; exit 2 }
set fh [open $rpt r]; set txt [read $fh]; close $fh
set worst ""
foreach line [split $txt \n] {
if {[regexp {slack\s+\((MET|VIOLATED)\)\s+(-?[0-9.]+)} $line -> verdict value]} {
if {$worst eq "" || $value < $worst} { set worst $value; set wverdict $verdict }
}
}
if {$worst eq ""} { puts stderr "slack.tcl: no slack line found in $rpt"; exit 2 }
puts [format "worst slack %s ns (%s), threshold %s" $worst $wverdict $thr]
if {$worst < $thr} { exit 1 } else { exit 0 }It walks the file line by line and matches each line, rather than reading the whole file and anchoring with ^ — which is Chapter 7.3 §4's lesson applied rather than re-learned. And it tracks the worst slack rather than the first, because a real report lists many paths.
Four outcomes, all measured:
--- the 10 ns run (slack +2.600) ---
rc=0 :: worst slack 2.600 ns (MET), threshold 0.0
--- the 7 ns run (slack -0.400) ---
rc=1 :: worst slack -0.400 ns (VIOLATED), threshold 0.0
--- a report with no slack line ---
rc=2 :: slack.tcl: no slack line found in /tmp/empty.rpt
--- a missing report ---
rc=2 :: slack.tcl: no such report: /tmp/nosuch.rptThe two rc=2 cases are the ones people leave out, and they are the ones that matter most on a bad day. A truncated report because the disk filled, or a missing report because the tool died in elaboration, must not be reported as "timing met". An unreadable answer is not a passing answer — the same principle as Chapter 6.4 §3's INCOMPLETE state.
A SLACK_MIN threshold above zero is worth having, too: make synth SLACK_MIN=0.2 demands 200 ps of margin, which is how you keep a design off the edge of its constraint while the back end still has work to do.
5. My Own Gate Printed the Violation and Passed
Here is the gate wired into the recipe, as I first wrote it:
@tclsh scripts/slack.tcl $@ $(SLACK_MIN) | sed 's/^/ /' \
|| { echo " [synth] TIMING NOT MET at $(PERIOD)ns"; exit 1; } [synth] axi_lite_slave @ 7.0ns -> build/syn/axi_lite_slave/7.0
worst slack -0.400 ns (VIOLATED), threshold 0.0
make rc=0Read that carefully. The gate ran. The gate printed VIOLATED. The gate's message is right there on the screen. And make exited 0.
The || was testing the exit status of the pipeline, and the last command in that pipeline is sed, which succeeded at indenting the text. This is exactly Chapter 4.3 §4's hazard and exactly what Chapter 7.3 §5 measured — and I wrote it anyway, one chapter later, because the | sed was there to make the output pretty and pretty-printing does not feel like a logic change.
Three variants, measured:
--- piped (the bug) ---
worst slack -0.400 ns (VIOLATED), threshold 0.0
(recipe continued - gate did NOT fire)
make rc=0
--- with pipefail ---
worst slack -0.400 ns (VIOLATED), threshold 0.0
GATE FIRED
make: *** [pipefail] Error 1
make rc=2
--- no pipe at all ---
worst slack -0.400 ns (VIOLATED), threshold 0.0
GATE FIRED
make: *** [nopipe] Error 1
make rc=2The fix used here keeps the indentation and loses the pipe, by capturing the status before printing anything:
@tclsh scripts/slack.tcl $@ $(SLACK_MIN) > $(@D)/slack.txt; st=$$?; \
sed 's/^/ /' $(@D)/slack.txt; \
[ $$st -eq 0 ] || { echo " [synth] TIMING NOT MET at $(PERIOD)ns (gate rc=$$st)"; exit 1; }$ make synth
[synth] axi_lite_slave @ 10.0ns -> build/syn/axi_lite_slave/10.0
worst slack 2.600 ns (MET), threshold 0.0
make rc=0
$ make synth PERIOD=7.0
[synth] axi_lite_slave @ 7.0ns -> build/syn/axi_lite_slave/7.0
worst slack -0.400 ns (VIOLATED), threshold 0.0
[synth] TIMING NOT MET at 7.0ns (gate rc=1)
make: *** [build/syn/axi_lite_slave/7.0/timing.rpt] Error 1
make rc=2It also writes the verdict to slack.txt, which turns out to be useful on its own: a one-line file per run that a report can collect without re-parsing the full timing report.
6. Never Make the Report the Target
The fixed gate exits non-zero — and the flow still has a hole. Ask for the same failing run again:
make: Nothing to be done for `synth'.
make rc=0Exit 0, nothing run, and the timing violation has been forgotten.
This is Chapter 5.4 §6's bug arriving in a new place, for the same reason: timing.rpt is the target, the gate's failure does not remove it, and a target that exists and is newer than its prerequisites is up to date by definition. The second invocation never ran the gate at all.
The fix is 5.4's fix. Make the verdict the target and let the report be a product of getting there:
synth: $(SYNDIR)/timing.ok
$(SYNDIR)/timing.ok: $(FILELIST) syn/synth.tcl
@mkdir -p $(@D)
@RUNDIR=$(SYNDIR) $(DC_SHELL) -f syn/synth.tcl > $(SYNDIR)/synth.log 2>&1 \
|| { echo " [synth] TOOL FAILED - see $(SYNDIR)/synth.log"; exit 1; }
@tclsh scripts/slack.tcl $(SYNDIR)/timing.rpt $(SLACK_MIN) > $(@D)/slack.txt; st=$$?; \
sed 's/^/ /' $(@D)/slack.txt; \
[ $$st -eq 0 ] || { echo " [synth] TIMING NOT MET at $(PERIOD)ns"; exit 1; }
@touch $@
@echo " [synth] $(DESIGN) @ $(PERIOD)ns MET -> $(SYNDIR)"Measured, in sequence — and the third invocation is the one that matters:
$ make synth
worst slack 2.600 ns (MET), threshold 0.0
[synth] axi_lite_slave @ 10.0ns MET -> build/syn/axi_lite_slave/10.0
rc=0
$ make synth PERIOD=7.0
worst slack -0.400 ns (VIOLATED), threshold 0.0
[synth] TIMING NOT MET at 7.0ns
make: *** [build/syn/axi_lite_slave/7.0/timing.ok] Error 1
rc=2
$ make synth PERIOD=7.0
worst slack -0.400 ns (VIOLATED), threshold 0.0
[synth] TIMING NOT MET at 7.0ns
make: *** [build/syn/axi_lite_slave/7.0/timing.ok] Error 1
rc=2build/syn/axi_lite_slave/10.0/timing.oktiming.ok exists only if the gate passed, touch $@ is the last line so a failure cannot record itself as a success, and the timing report and the full tool log both survive for whoever has to look at them. The failing run asked for a second time re-runs and fails again, which is the whole point.
7. From Synthesis to STA
Timing analysis runs on the netlist synthesis produced, which makes it a genuine dependency rather than a convention:
PT_SHELL ?= pt_shell
STADIR = $(BUILD)/sta/$(DESIGN)/$(PERIOD)
NETLIST = $(SYNDIR)/$(DESIGN)_netlist.v
.PHONY: sta
sta: $(STADIR)/timing.rpt
$(STADIR)/timing.rpt: $(SYNDIR)/timing.rpt sta/sta.tcl
@mkdir -p $(@D)
@RUNDIR=$(STADIR) NETLIST=$(NETLIST) \
$(PT_SHELL) -f sta/sta.tcl > $(STADIR)/sta.log 2>&1 \
|| { echo " [sta] TOOL FAILED - see $(STADIR)/sta.log"; exit 1; }
@tclsh scripts/slack.tcl $@ $(SLACK_MIN) > $(@D)/slack.txt; st=$$?; \
sed 's/^/ /' $(@D)/slack.txt; \
[ $$st -eq 0 ] || { echo " [sta] TIMING NOT MET at $(PERIOD)ns"; exit 1; }The STA script checks that the netlist it was told to read actually exists, and fails loudly if not — because "I analysed nothing and found no violations" is the worst possible outcome:
if {$netlist ne "" && ![file exists $netlist]} {
puts stderr " \[sta\] ERROR: netlist not found: $netlist"
exit 1
}One command, both stages:
[synth] axi_lite_slave @ 10.0ns -> build/syn/axi_lite_slave/10.0
worst slack 2.600 ns (MET), threshold 0.0
[sta] axi_lite_slave @ 10.0ns -> build/sta/axi_lite_slave/10.0
worst slack 2.250 ns (MET), threshold 0.0
make rc=0build/sta/axi_lite_slave/10.0/pt_shell.argv
build/sta/axi_lite_slave/10.0/slack.txt
build/sta/axi_lite_slave/10.0/sta.log
build/sta/axi_lite_slave/10.0/timing.rpt
build/syn/axi_lite_slave/10.0/area.rpt
build/syn/axi_lite_slave/10.0/axi_lite_slave_netlist.v
build/syn/axi_lite_slave/10.0/dc_shell.argv
build/syn/axi_lite_slave/10.0/slack.txt
build/syn/axi_lite_slave/10.0/synth.log
build/syn/axi_lite_slave/10.0/timing.rpt(The two .argv files are the mocks recording what they were handed, as in Chapter 7.1 §7; a real tool leaves no such file.)
Asking for sta ran synth first, because the netlist is a real prerequisite. Nobody had to remember the order — which has been the point since Chapter 1.2.
8. Sweeping the Clock Period
With both stages gated, "what is the fastest clock this design makes?" becomes a loop — the same idea as Chapter 6.3's seed sweep, over a different variable:
PERIODS ?= 7.0 7.5 7.6 8.0 9.0 10.0
.PHONY: sweep
sweep:
@printf ' %-8s %-26s %s\n' period synthesis sta
@for p in $(PERIODS); do \
s=$$($(MAKE) -s -f $(firstword $(MAKEFILE_LIST)) synth PERIOD=$$p > /dev/null 2>&1 \
&& echo MET || echo "NOT MET"); \
t=$$($(MAKE) -s -f $(firstword $(MAKEFILE_LIST)) sta PERIOD=$$p > /dev/null 2>&1 \
&& echo MET || echo "NOT MET"); \
printf ' %-8s %-26s %s\n' "$$p" "$$s" "$$t"; \
doneNote -f $(firstword $(MAKEFILE_LIST)) on the recursive calls. A sub-make does not inherit -f, which Chapter 6.4 §7 measured the hard way, and $(MAKEFILE_LIST) carries a leading space so the $(firstword ...) is not optional.
period synthesis sta
7.0 NOT MET NOT MET
7.5 MET NOT MET
7.6 MET NOT MET
8.0 MET MET
9.0 MET MET
10.0 MET MET 7.0 synth: slack (VIOLATED) -0.400 sta: slack (VIOLATED) -0.750
7.5 synth: slack (MET) 0.100 sta: slack (VIOLATED) -0.250
7.6 synth: slack (MET) 0.200 sta: slack (VIOLATED) -0.150
8.0 synth: slack (MET) 0.600 sta: slack (MET) 0.250
9.0 synth: slack (MET) 1.600 sta: slack (MET) 1.250
10.0 synth: slack (MET) 2.600 sta: slack (MET) 2.250Look at 7.5 and 7.6: synthesis says MET and STA says VIOLATED. A flow that gated only on the synthesis report would have passed both of those and handed the design on.
That two-stage disagreement is the structural point, and it is why the two gates in §6 and §7 are separate rather than one check at the end. The size of the gap here is a constant I chose in the mock — 0.35 ns — and says nothing about any real technology. The direction is the part worth carrying away: each stage sees more of reality than the one before it, so each needs its own gate, and the later one is the one that gets to be right.
9. Common Mistakes
Trusting the tool's exit status. Measured: slack -0.400, the word VIOLATED in the report, and dc_shell exiting 0 (§3).
Putting a pipe between the gate and the status. Measured in my own Makefile: the gate printed VIOLATED and make exited 0, because || tested sed (§5).
Testing a gate by reading its message. Test it on a known failure and check $? — the message was correct in the broken version (§5).
Making the report the target. A failed gate leaves the report on disk, so the next invocation reports nothing to do and exits 0 — measured (§6).
touching the stamp before the gate. Then a violation records itself as a pass; the touch goes last (§6).
Treating an unreadable report as a pass. A missing or truncated report must be its own outcome, not "timing met" (§4).
Leaving the clock period out of the run path. Two periods are two different results and will overwrite each other (§2).
Giving synthesis its own source list. It will eventually synthesise a design you never verified; read the simulation's filelist (§1).
Gating only on synthesis. Measured: 7.5 ns and 7.6 ns passed synthesis and failed STA (§8).
A bare $(MAKE) in the sweep. -f is not inherited, so the sub-make reads the wrong file (Chapter 6.4 §7, §8 here).
Parallelising a synthesis sweep like a simulation sweep. Licenses are counted; simulations mostly are not (§8).
10. Check Your Understanding
Question 1. A synthesis flow runs nightly and has never failed. The timing reports contain negative slack. How is that possible, and what single change fixes it?
Question 2. A slack gate prints worst slack -0.400 ns (VIOLATED) and the build passes. The gate script is correct and exits 1 when run by hand. Where is the bug?
Question 3. After the gate is fixed, the first make synth at an impossible period fails correctly, and the second one exits 0 without running anything. Why, and what is the fix?
Question 4. Why should a missing timing report be a different outcome from a report showing negative slack, when both mean "not shippable"?
Question 5. A design meets timing in synthesis at 7.5 ns and fails STA at the same period. Which gate should the flow believe, and what does that imply about where the gates go?
11. Exercise
The reports below come from the mock shells described in the opening callout; the Tcl, the Makefile and every exit status are real. With real tools, the same steps apply unchanged.
Step 1. Synthesise at the default period and read the gate's verdict:
make synth [synth] axi_lite_slave @ 10.0ns -> build/syn/axi_lite_slave/10.0
worst slack 2.600 ns (MET), threshold 0.0Step 2. Confirm the tool itself does not report a violation. Run it directly and check the status:
PERIOD=7.0 RUNDIR=build/syn7 ./tools/mock/dc_shell -f syn/synth.tcl > /dev/null
echo "dc_shell rc=$?"
grep slack build/syn7/timing.rptdc_shell rc=0
slack (VIOLATED) -0.400Step 3. Now let the gate do its job, and check $? rather than the message:
make synth PERIOD=7.0; echo "make rc=$?" [synth] axi_lite_slave @ 7.0ns -> build/syn/axi_lite_slave/7.0
worst slack -0.400 ns (VIOLATED), threshold 0.0
[synth] TIMING NOT MET at 7.0ns (gate rc=1)
make: *** [build/syn/axi_lite_slave/7.0/timing.rpt] Error 1
make rc=2Step 4. Ask for the same failing run again:
make synth PERIOD=7.0; echo "make rc=$?"make: Nothing to be done for `synth'.
make rc=0That is §6's bug. Switch the target to timing.ok as shown there and repeat — the second invocation must fail again.
Step 5. Run the full chain and the sweep:
make clean && make sta
make sweep period synthesis sta
7.0 NOT MET NOT MET
7.5 MET NOT MET
7.6 MET NOT MET
8.0 MET MET
9.0 MET MET
10.0 MET METStep 6. Prove the gate's two unreadable outcomes, which are the ones most flows omit:
echo "nothing here" > /tmp/empty.rpt
tclsh scripts/slack.tcl /tmp/empty.rpt 0.0 ; echo "rc=$?"
tclsh scripts/slack.tcl /tmp/nosuch.rpt 0.0 ; echo "rc=$?"slack.tcl: no slack line found in /tmp/empty.rpt
rc=2
slack.tcl: no such report: /tmp/nosuch.rpt
rc=2Then break the gate the way I did, and check the status rather than the screen. Change the gate line back to tclsh scripts/slack.tcl $@ $(SLACK_MIN) | sed 's/^/ /' || { ...; exit 1; } and run make synth PERIOD=7.0; echo $?. The violation prints, and the status is 0. Add set -o pipefail; to the front of that line and run it again — status 2. That one-character-wide difference between a gate that works and a gate that only looks like it works is the most useful thing in this chapter.
12. Remember
- Synthesis and STA are driven by Tcl, not options. The tool interprets a program, so most configuration lives in a script — and the script should take its parameters from the environment.
- Read the same filelist as the simulation. A separate source list eventually synthesises a design nobody verified.
- The tool exits 0 on a timing violation. Measured: slack
-0.400, VIOLATED in the report, exit status 0 — the same asymmetry as the simulator, for the same reason. - So the Makefile must parse the report. There is no other channel.
- A gate needs four outcomes: met, violated, no slack line, and no report. An unreadable answer is not a passing answer.
- Never pipe a gate's output before testing its status. Measured in my own Makefile:
VIOLATEDprinted andmakeexited 0 because||testedsed. - Test a gate on a known failure and check
$?, not the message. The broken gate's message was perfectly correct. - Never make the report the target. Measured: after a failed gate, the next invocation said nothing to be done and exited 0, forgetting the violation.
- Make the verdict the target, with
touch $@last, and keep the report and the full tool log as products. - Put the clock period in the run path, because two periods are two different results.
- The netlist makes STA depend on synthesis, so one command runs both in the right order.
- Gate each stage separately. Measured: 7.5 ns and 7.6 ns met timing in synthesis and violated it in STA, so a synthesis-only gate would have passed them.
- Use
-f $(firstword $(MAKEFILE_LIST))for recursive calls in a sweep, since-fis not inherited. - Do not parallelise a synthesis sweep like a simulation sweep. Licenses are counted and expensive.
Module 7 is complete. The flow now drives any of three simulators by a single variable, hands structured work to Bash, Tcl and Python without losing values or failures, and runs Tcl-driven synthesis and timing analysis with a gate on each stage's report.
What it is not yet is a flow somebody else can own. Everything so far has lived in one Makefile per subject, written by the person who runs it, on a tree small enough to hold in your head. A real project has several blocks, several flows, shared rules nobody wants to copy, a compute farm with limits, and new engineers who need it to work on the first try. Module 8 is about that: organising a large Makefile without it becoming unreadable, making parallel builds reliable rather than merely fast, and the practices that keep a flow trustworthy when it is no longer yours alone.
Where this fits
Part of the Makefile curriculum.
