Skip to content

GLS · Chapter 4 · SDF Annotation

Debugging Failed & Partial SDF Annotation

SDF annotation can go wrong in several ways, and the worst failure is the silent one. A missing or unreadable file errors loudly and is easy to catch. The quiet failures are the danger: a wrong scope, or a mismatch in the divider, instance path, or cell name, leaves instances unmatched. The most insidious case is partial annotation, where some instances match while the rest are silently left at zero delay. The run then looks timed but is only partly timed, so behaviour in the un-annotated portion is never exercised with real delays. This lesson catalogs the failure modes, shows how the annotation log and coverage report expose them, and argues for making missing or partial annotation fail loud instead of passing quietly.

Foundation12 min readGLSSDFDebuggingAnnotation CoveragePartial Annotation

Chapter 4 · Section 4.5 · SDF Annotation

Project thread — before trusting the counter's timed run (4.6), you must confirm every counter instance annotated. This lesson is how you verify that — and catch the subtree that didn't.

1. Why Should I Learn This?

A partly-annotated run is more dangerous than an un-annotated one — because it looks timed.

  • Loud failures (missing file) are easy; silent ones (wrong scope, path/name mismatch, partial match) are the risk.
  • Partial annotation leaves part of the design zero-delay with no error.
  • The annotation log / coverage report is the only reliable detector.

This is the safety net for the whole chapter (4.1–4.4) and gates the counter capstone (4.6).

2. Real Silicon Story — the 90%-timed regression

A post-layout timing regression passed, and the annotation log showed thousands of matched entries — clearly a timed run. But a control subblock had a handful of unmatched instances (a name change after an ECO) that no one read past the big matched count.

Those unmatched instances ran zero-delay. A race in that subblock — real at post-layout delays — never appeared, because that corner of the design was never timed. The regression was ~90% annotated and 100% trusted.

Lesson: matched counts are not enough — read the unmatched list too. Partial annotation silently leaves part of the design untimed.

3. Concept — the failure modes and how each shows up

Loud failures (easy):

  • File not found / unreadable — the annotator errors immediately.
  • Malformed SDF — parse error.

Quiet failures (dangerous):

  • Wrong scope in $sdf_annotate (4.3) — SDF paths resolve against the wrong instance → nothing (or the wrong things) match.
  • DIVIDER mismatch (/ vs ., 4.2) — instance paths don't resolve.
  • Instance-path / cell-name mismatch — netlist renamed (ECO, different elaboration) vs SDF → unmatched.
  • Partial annotationsome instances match, others are silently left at zero delay.

The detector — the annotation log / coverage report:

  • Matched counts (IOPATH, INTERCONNECT, TIMINGCHECK).
  • Unmatched list — the instances not annotated (the part that matters most).
  • Read both. A big matched count can still hide a small, critical unmatched set.

Policy: make missing/partial annotation fail the run, not pass it — §5 below writes that gate as a script. Scope reminder: even a fully annotated run is dynamic — STA is signoff (0.3), and the delays being annotated come from the corner you selected (timing corners).

Annotation outcomes: missing file fails loud; wrong scope or mismatch leaves unmatched; partial annotation silently leaves instances zero-delay; coverage log detects itno filescope/paths OKwrong scope /mismatchdetect viaenforce$sdf_annotaterunsMissing/malformed file →LOUD errorAll match →FULLY timedPARTIAL: somezero-delay, silentCoverage log:matched + UNMATCHEDlistMakepartial/missingFAIL LOUD
Figure 1 — SDF annotation outcomes (representative). A missing or malformed file fails LOUD (easy to catch). A wrong scope, divider mismatch, or instance/cell-name mismatch leaves instances UNMATCHED. The worst outcome is PARTIAL annotation: some instances timed, others silently zero-delay, so the run looks timed but is not fully. The annotation log / coverage report (matched counts AND the unmatched list) is the detector — read both, and make partial/missing annotation fail loud.

4. Mental Model — check the receipt, not just the total

Annotation is like a bulk delivery: the coverage log is the receipt.

  • A big matched total is like "1,842 items delivered" — reassuring, but not the whole story.
  • The unmatched list is the "items missing" line — small, easy to skip, and the one that bites.
  • Read the missing line. A few undelivered (unmatched) instances can be exactly the ones you needed timed.

Trusting the total without the missing line is how a 90%-timed run gets called timed.

5. Working Example — a coverage log with unmatched instances

A representative annotation log showing partial coverage:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# Annotation coverage log — REPRESENTATIVE, tool-neutral
SDF: reading sdf/counter4_tt.sdf  (scope: tb.u_dut)
SDF: annotated  40 IOPATH, 14 INTERCONNECT, 8 TIMINGCHECK
SDF: WARNING - 3 unmatched instances:
       u_dut/u_ctrl/u_q0   (no matching instance - renamed after ECO?)
       u_dut/u_ctrl/u_q1
       u_dut/u_ctrl/u_x2
# 3 instances LEFT AT ZERO DELAY. Big matched count, small critical miss. READ the warnings.

Practical context (representative, tool-neutral):

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# Turn silent partial annotation into a loud failure:
#   1) capture the annotation log every run
#   2) grep for 'unmatched' / 'could not annotate' / 'WARNING'
#   3) fail the run (nonzero exit) if unmatched > 0   <-- policy, not luck
# Common root causes to check: scope arg (4.3), DIVIDER (4.2), post-ECO name changes.

Partial annotation — one bit timed, one zero — as a real waveform:

Partial annotation: one counter bit timed, another (unmatched) still zero-delay

8 cycles
One flop shows a clk-to-Q delay while an unmatched flop updates instantly, revealing partial annotationQ0 delayedQ0 delayedQ1 zero-delay (unmatched)Q1 zero-delay (unmatched)CKQ0 (annotated)Q1 (unmatched)t0t1t2t3t4t5t6t7
Representative. Q0 matched its SDF entry and updates a real clk-to-Q delay after CK. Q1 was in the unmatched list (renamed after an ECO), so it stayed zero-delay and updates instantly. The mix — some delayed, some not — is the visible signature of partial annotation; the coverage log names which.

Run it — annotate the same netlist twice, once with the wrong scope

The failure this page is about is invisible by construction, so the way to internalise it is to cause it. Everything below is a complete, runnable experiment: a two-flop netlist, a small SDF, and two $sdf_annotate calls that differ only in their scope argument.

sdf_scope_demo.v — the netlist and the two annotation attempts
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ─────────────────────────────────────────────────────────────────────────────
//  iverilog -o sdfdemo sdf_scope_demo.v && ./sdfdemo
//  vcs -sverilog +neg_tchk sdf_scope_demo.v && ./simv
//
//  Two identical flops. The SDF names ONE of them. Run A annotates with the
//  correct scope; run B uses a scope one level off - the classic ECO/refactor
//  mistake - and every instance silently stays at zero delay.
// ─────────────────────────────────────────────────────────────────────────────
`timescale 1ns/1ps
 
module dff (output reg q, input d, ck);
  always @(posedge ck) q <= d;
  specify
    (ck => q) = 0.10;          // placeholder - SDF overwrites this
  endspecify
endmodule
 
module ctrl (output q0, q1, input d, ck);
  dff u_q0 (.q(q0), .d(d),  .ck(ck));
  dff u_q1 (.q(q1), .d(q0), .ck(ck));
endmodule
 
module tb;
  reg d = 0, ck = 0;
  wire q0, q1;
  ctrl u_dut (.q0(q0), .q1(q1), .d(d), .ck(ck));
 
  always #5 ck = ~ck;
 
  initial begin
    // ── Change ONE character to switch experiments ──────────────────────
    //  Correct scope - resolves tb.u_dut.u_q0 :
    $sdf_annotate("ctrl.sdf", u_dut);
    //  Wrong scope - paths resolve against tb, so nothing matches:
    // $sdf_annotate("ctrl.sdf", tb);
 
    d = 1;
    repeat (4) @(posedge ck);
    $finish;
  end
 
  // The observable. A matched flop shows a real clk-to-Q; an unmatched one
  // updates in the same delta as the edge.
  time edge_t, q0_t, q1_t;
  always @(posedge ck) edge_t = $time;
  always @(q0) begin q0_t = $time; $display("  q0 changed %0t after the edge", q0_t - edge_t); end
  always @(q1) begin q1_t = $time; $display("  q1 changed %0t after the edge", q1_t - edge_t); end
endmodule
ctrl.sdf — note what it names, and what it does not
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
(DELAYFILE
  (SDFVERSION "3.0")
  (DESIGN "ctrl")
  (DIVIDER /)                          // '/' vs '.' - a mismatch here alone
  (TIMESCALE 1ns)                      // resolves NOTHING (see 4.2)
  (CELL
    (CELLTYPE "dff")
    (INSTANCE u_q0)                    // ← only u_q0 is named
    (DELAY (ABSOLUTE (IOPATH ck q (0.750) (0.750))))
  )
)

What each run prints.

run A — correct scope
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  q0 changed 750ps after the edge     <- annotated: real clk-to-Q from the SDF
  q1 changed 100ps after the edge     <- NOT in the SDF: kept its placeholder
run B — scope one level too high
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  q0 changed 100ps after the edge     <- unmatched
  q1 changed 100ps after the edge     <- unmatched

Three things this makes concrete that prose cannot.

Run A is partial annotation as a normal outcome. u_q0 is timed and u_q1 is not, because the SDF simply never mentioned u_q1 — and nothing about that is an error. This is the state the §2 story shipped in, and the only difference between it and a fully-timed run is a line in a log nobody read.

Run B is the silent failure. Every instance is un-annotated, the simulation runs happily to completion, and the timing is uniformly wrong. There is no error, no X, no mismatch — just a design running at placeholder delays. A regression in this state passes exactly as fast and exactly as green as a correct one.

And the DIVIDER line is not decoration. Change it to . while the tool expects / and every instance path fails to resolve — producing output identical to run B, from a one-character difference in a file most engineers never open.

The gate — turn the log into a pass/fail

§5's practical context describes a policy in three comment lines. It is worth writing, because a policy that lives in a comment is a policy nobody runs:

check_sdf_annotation.sh — fail the run, do not warn about it
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#!/usr/bin/env bash
# Wrap the simulation so partial or missing annotation is a NONZERO EXIT.
# Tool-neutral: adjust the patterns to your simulator's annotator wording.
set -uo pipefail
 
LOG="${1:?usage: check_sdf_annotation.sh <sim.log>}"
 
# 1. Was the annotator invoked at all? A run with no SDF is not a timed run,
#    and this is the case a matched-count check cannot detect - there is no
#    count to be small.
if ! grep -qiE 'sdf|annotat' "$LOG"; then
  echo "FAIL: no annotation activity in $LOG - was \$sdf_annotate called?"
  exit 1
fi
 
# 2. Any unmatched instance leaves part of the design at zero delay.
UNMATCHED=$(grep -icE 'unmatched|could not annotate|no matching instance' "$LOG" || true)
if [ "$UNMATCHED" -gt 0 ]; then
  echo "FAIL: $UNMATCHED unmatched-instance report(s) - design is only PARTLY timed"
  grep -iE 'unmatched|could not annotate|no matching instance' "$LOG" | head -20
  exit 1
fi
 
# 3. A plausible matched count. Zero matches with no warning usually means the
#    scope resolved to something empty - run B above, which reports nothing.
MATCHED=$(grep -oiE 'annotated[[:space:]]+([0-9]+)' "$LOG" | grep -oE '[0-9]+' | head -1)
if [ -z "${MATCHED:-}" ] || [ "$MATCHED" -eq 0 ]; then
  echo "FAIL: zero IOPATH annotations - scope argument almost certainly wrong"
  exit 1
fi
 
echo "PASS: annotation clean ($MATCHED matched, 0 unmatched)"

Why check 1 and check 3 both exist. They catch the two failures a naive "grep for unmatched" misses entirely. If $sdf_annotate was never called — a build-flow bug, an ifdef that excluded it — there are no unmatched warnings because there was no annotation attempt; the log is clean and the run is untimed. And run B's wrong-scope case produces zero matches and zero warnings, because from the annotator's point of view it was asked about a scope containing nothing to annotate. A gate that only looks for the word "unmatched" passes both.

6. Debugging Session — a passing regression that was only partly timed

1

A timing regression passes with a large matched count, but a handful of instances were silently left at zero delay after a post-ECO rename — the un-annotated subblock's timing behaviour was never exercised, hidden by trusting the total instead of reading the unmatched list

PARTIAL ANNOTATION HIDES BEHIND A BIG MATCHED COUNT
Symptom

A post-layout timing regression passes; the annotation log shows a large matched count. A subblock's timing-dependent behaviour (a race) never appears in GLS, though it is real at post-layout delays.

Root Cause

Partial annotation. A few instances in that subblock did not match the SDF — commonly a name change after an ECO, a wrong scope (4.3), or a DIVIDER/path mismatch (4.2). Unmatched instances are silently left at zero delay: no error, just untimed. Because the matched count was large, no one read the small unmatched list, so the un-annotated subblock ran zero-delay and its timing behaviour was never exercised. The run was ~90% timed and 100% trusted — a false-confidence failure, not a tool bug.

Fix

Read the coverage report — both the matched counts and the unmatched list — and treat any unmatched instance as a failure to investigate. Fix the root cause (correct the scope, the DIVIDER, or the renamed instance paths so the SDF matches the current netlist), then re-run and confirm 0 unmatched. Make this a policy: fail the run (nonzero exit) when unmatched > 0, so partial annotation becomes loud. The lesson: the dangerous SDF failure is silent partial annotation — some instances timed, others left zero-delay — and the coverage report (matched and unmatched) is the only reliable detector; make it fail loud. (Even at 0 unmatched, the run is dynamic — STA is signoff, 0.3.)

7. Common Mistakes

  • Reading only the matched count. The unmatched list is where the risk is.
  • Trusting a passing run without the log. Partial annotation is silent.
  • Ignoring post-ECO renames. Netlist name changes unmatch SDF paths.
  • Not enforcing a policy. Unmatched > 0 should fail the run.
  • Assuming full annotation = timing closure. STA (all corners) is signoff (0.3).

8. Industry Best Practices

  • Always capture and read the annotation coverage log — matched and unmatched.
  • Fail the run on any unmatched instance (nonzero exit) — make partial loud.
  • Re-verify annotation after ECOs / re-elaboration (names can change).
  • Check scope + divider first when instances don't match (4.2/4.3).
  • Confirm 0 unmatched before trusting a timed run — then defer closure to STA.

Senior Engineer Thinking

  • Beginner: "It annotated thousands of entries — clearly a timed run."
  • Senior: "How many unmatched? Even three silently-zero instances can hide a real race. I read the unmatched list, fix the mismatch, and fail the run if anything is unannotated."

The senior treats the unmatched list as the real signal and enforces zero-unmatched as policy.

Silicon Impact

Silent partial annotation is one of the most dangerous GLS traps: a run that is believed fully timed but leaves a subblock zero-delay never exercises that block's timing behaviour with real delays — so a real race, glitch, or check firing there can reach silicon as an intermittent failure (0.3), and the false confidence discourages further checking. Because the failure produces no error, only reading the coverage report and enforcing zero-unmatched defends against it. This discipline — receipt over total — is what makes a "timing GLS" trustworthy end-to-end.

Engineering Checklist

  • Captured the annotation coverage log every run.
  • Read matched counts and the unmatched list.
  • Failed the run on any unmatched instance (policy, nonzero exit).
  • Re-verified annotation after ECOs / re-elaboration.
  • Confirmed 0 unmatched before trusting timing; deferred closure to STA.

Try Yourself

  1. Annotate an SDF onto a small netlist, then rename one instance in the netlist (simulating an ECO) and re-run.
  2. Observe: the annotation log now lists that instance as unmatched, and it runs zero-delay while others are timed.
  3. Change: fix the instance name (or the SDF path) so it matches again.
  4. Expect: the log reports 0 unmatched and the instance is timed. Then script a check that fails the run when unmatched > 0.

Any free Verilog simulator prints an SDF annotation/coverage log. No paid tool required.

Interview Perspective

  • Weak: "If the SDF is wrong, the simulator errors."
  • Good: "A missing file errors, but wrong scope or path mismatches leave instances unmatched — silently zero-delay."
  • Senior: "The dangerous failure is partial annotation: some instances timed, others silently zero-delay. A big matched count can hide a few critical misses, so I read the unmatched list, fix the scope/divider/renamed paths, and enforce zero-unmatched as a run-failing policy. Even then it's dynamic — STA signs off."

9. Interview / Review Questions

Short answer: unknown from that number alone. A large matched count says most of the design was annotated; it says nothing about the instances that were not.

Why it matters: partial annotation leaves the unmatched instances at zero delay, so a timing-dependent bug in that corner cannot appear. The run is green because it never simulated the timing you care about.

Small example: a control subblock renamed by an ECO produces three unmatched instances under a 1,800-entry matched count — 99.8% annotated and 100% trusted.

Remember: read the unmatched list, not the total. The total is the reassuring number; the unmatched list is the actionable one.

Where This Is Specified

The SDF format itself — DELAYFILE, DIVIDER, TIMESCALE, CELL/CELLTYPE/INSTANCE, and the IOPATH / INTERCONNECT / TIMINGCHECK constructs used above — is standardised as IEEE Std 1497, Standard Delay Format. The $sdf_annotate system task that consumes it, together with its optional scope, configuration-file, log-file, and MTM arguments, is defined in IEEE Std 1364 (Verilog) and carried into IEEE Std 1800 (SystemVerilog); both are listed by the IEEE Standards Association.

The part worth reading in the standard rather than assuming is what $sdf_annotate promises when a path fails to resolve: it is not required to be an error. Unmatched instances are a normal, reportable outcome, because an SDF legitimately need not name every cell in a design. That is the root of everything on this page — the silence is specified behaviour, so the loudness has to be added by you.

Note also that the exact wording of the annotation log is tool-specific. The standard defines the format and the task, not the report, which is why §5's gate matches on several phrasings and why it should be tuned to the simulator your regression actually runs.

Related lessons. The format and flow are what is SDF, SDF file anatomy, and the annotation flow. What the annotated numbers mean is delay types in gate simulation and interconnect delay; which numbers to annotate is timing corners and min/typ/max selection. Once a timed run does fail, the triage chain is first-mismatch triage, real vs false violations, and timing checks and notifiers.

10. Key Takeaways

  • SDF annotation fails loudly (missing/malformed file) or, dangerously, quietlywrong scope (4.3), DIVIDER/path/cell-name mismatch (4.2), or partial annotation.
  • Partial annotation matches some instances and silently leaves others at zero delay, so the run looks timed but is only partly timed.
  • The annotation log / coverage report is the detector — read the matched counts and the unmatched list (a big total can hide a small critical miss).
  • Make missing/partial annotation fail loud (fail the run on any unmatched instance); re-verify after ECOs / re-elaboration.
  • Even a fully annotated run is dynamicSTA (all corners) is the timing signoff (0.3). Next: 4.6 — annotating the counter with SDF, end to end.

Quick Revision

Annotation fails loud (missing file) or SILENT (wrong scope, DIVIDER/path/name mismatch, PARTIAL match → zero-delay). Partial = looks timed, isn't. Detector = coverage log: matched counts + UNMATCHED list — read both. Fail the run on unmatched > 0. Re-check after ECOs. Full annotation still dynamic; STA signs off. Next: 4.6 — annotating the counter.