Skip to content
VLSI Mentor

Tcl · Module 30

A Waveform Setup Script

The last script of the track, and the smallest. A wave window is a budget of attention, and this one spends it deliberately.

Intermediate18 min readtcledapart-iisimulationdebugcase-study

Part II · Module 30 · Chapter 30.6 · A Waveform Setup Script

A failing regression test is most useful in the first minute. If the waveform opens empty, that minute goes on adding signals; if it opens with every signal in the design, that minute goes on scrolling. Either way it is spent before any thinking happens.

This is the smallest script in the module and the one most likely to be used daily. Its argument is not about correctness — it is that a wave window is a budget of attention, and a script can spend it better than a human in a hurry.

1. Read the hierarchy, do not maintain a list

The signal list comes from the design:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# ---- the signal list, read from the design's own hierarchy -------------
proc read_hierarchy {path} {
    set fh [open $path r] ; set scope {} ; set sigs {}
    while {[gets $fh line] >= 0} {
        set t [string trim $line]
        if {[regexp {^scope\s+(\S+)$} $t -> n]} { lappend scope $n ; continue }
        if {$t eq "upscope"} { set scope [lrange $scope 0 end-1] ; continue }
        if {[regexp {^signal\s+(\S+)\s+(\S+)\s+(\d+)$} $t -> n kind w]} {
            lappend sigs [list "/[join $scope /]/$n" $kind $w]
        }
    }
    close $fh
    return $sigs
}

A hand-written wave.do listing forty signal paths works until somebody renames a module, and then it fails silently — the simulator adds the signals it recognises and says nothing about the ones it does not. You discover it when a trace you needed is missing from a window you are already debugging in.

Reading the hierarchy instead means a renamed module changes the paths and the grouping automatically, and a deleted signal disappears rather than becoming an invisible omission. This is the same preference Chapter 30.3 made for generated SDC over three maintained files, in a place where the stakes are lower and the frequency much higher.

Here is the hierarchy this chapter uses — a small FIFO testbench, written as a scope/signal listing of the kind a simulator can produce:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
=== the hierarchy as read, all of it ===
  /fifo_tb/clk           reg          1 bits
  /fifo_tb/rst_n         reg          1 bits
  /fifo_tb/din           reg          8 bits
  /fifo_tb/dout          wire         8 bits
  /fifo_tb/full          wire         1 bits
  /fifo_tb/testname      reg        256 bits
  /fifo_tb/errors        integer     32 bits
  /fifo_tb/i             integer     32 bits
  /fifo_tb/dut/clk       wire         1 bits
  /fifo_tb/dut/rst_n     wire         1 bits
  /fifo_tb/dut/din       wire         8 bits
  /fifo_tb/dut/dout      reg          8 bits
  /fifo_tb/dut/count     reg          3 bits
  /fifo_tb/dut/wptr      reg          2 bits
  /fifo_tb/dut/DEPTH     parameter   32 bits
  /fifo_tb/dut/WIDTH     parameter   32 bits

scope and upscope nest, so the parser keeps a stack and builds each full path by joining it. Sixteen signals, two scopes.

2. The wave window is a mock

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# ---- the mock wave window ----------------------------------------------
namespace eval ::w { variable items {} ; variable groups {} }
proc add_wave {args} {
    set group "" ; set radix "" ; set paths {}
    for {set i 0} {$i < [llength $args]} {incr i} {
        switch -glob -- [lindex $args $i] {
            -group { incr i ; set group [lindex $args $i] }
            -radix { incr i ; set radix [lindex $args $i] }
            -*     { error "add_wave: unknown option '[lindex $args $i]'" {} {WAVE BADOPT} }
            default { lappend paths [lindex $args $i] }
        }
    }
    if {[llength $paths] == 0} { error "add_wave: no signal given" {} {WAVE NOPATH} }
    foreach p $paths {
        if {$group ne "" && [lsearch -exact $::w::groups $group] < 0} {
            lappend ::w::groups $group
        }
        lappend ::w::items [list $group $p $radix]
    }
    return [llength $paths]
}
proc save_layout {path} {
    file mkdir [file dirname $path]
    set fh [open $path w]
    puts $fh "# wave layout — generated, [llength $::w::items] signal(s)"
    foreach g $::w::groups {
        puts $fh "add wave -group $g \\"
        set first 1
        foreach it $::w::items {
            if {[lindex $it 0] ne $g} { continue }
            set r [lindex $it 2]
            puts $fh [format "    %s%s \\" [lindex $it 1] [expr {$r eq "" ? "" : " -radix $r"}]]
        }
        puts $fh ""
    }
    close $fh
    return [llength $::w::items]
}

add_wave and save_layout are thirty lines written for this module — the same teaching-mock arrangement as the rest of Module 30. They accept the options a real wave command accepts, reject an unknown one with an errorCode, and write the layout in the form a simulator would read. No simulator read it here, and the chapter does not claim one did; what is being taught is how to decide what goes in the window, which is script logic and not tool behaviour.

Note add_wave returns the number of signals it added, and refuses a call with no signal at all. A setup script that adds nothing and reports nothing is the failure this whole chapter is trying to avoid.

3. The exclusions are data, with a reason each

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# ---- the selection rules, as data with a reason each (25.3 section 4) --
set ::RULES {
    {parameter   {a parameter never changes}}
    {wide        {too wide to read as a waveform}}
    {bookkeeping {testbench bookkeeping, not design state}}
}
proc rule_hit {rule sig} {
    lassign $sig path kind width
    switch -- $rule {
        parameter   { return [expr {$kind eq "parameter"}] }
        wide        { return [expr {$width > 64}] }
        bookkeeping { return [expr {$kind eq "integer"}] }
    }
    return 0
}

Three rules, each with the sentence that justifies it. A parameter never changes, so a waveform of it is a flat line consuming a row. A 256-bit string is unreadable as a waveform. A testbench loop counter is bookkeeping, not design state.

None of those is universally true — and that is exactly why the reason is stored next to the rule. Someone debugging a parameterisation bug wants the parameters; someone chasing a testbench bug wants errors and i. The script's job is to make the default good and the reason legible, not to be right for everyone. Same shape as Chapter 30.2 §1's pruned scenarios and Chapter 30.5 §2's plan: the decision recorded where it is used.

4. Group by module, clock and reset first

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# ---- group by MODULE, which is what the hierarchy already tells us ----
proc module_of {path} {
    set parts [split [string trimleft $path /] /]
    if {[llength $parts] <= 1} { return top }
    return [join [lrange $parts 0 end-1] /]
}

proc setup_waves {hier layout} {
    set sigs [read_hierarchy $hier]
    array set fired {}
    foreach r $::RULES { set fired([lindex $r 0]) 0 }
    set kept {}
    foreach s $sigs {
        set dropped ""
        foreach r $::RULES {
            if {[rule_hit [lindex $r 0] $s]} { set dropped [lindex $r 0] ; break }
        }
        if {$dropped eq ""} { lappend kept $s } else { incr fired($dropped) }
    }
    puts "  === selection, every rule with what it caught ==="
    foreach r $::RULES {
        puts [format "    %-12s %2d  %s" [lindex $r 0] $fired([lindex $r 0]) [lindex $r 1]]
    }
    puts [format "    %-12s %2d  of %d in the hierarchy" kept [llength $kept] [llength $sigs]]

    # group by module, clock and reset first within each
    set by [dict create]
    foreach s $kept { dict lappend by [module_of [lindex $s 0]] $s }
    puts ""
    puts "  === grouped by module ==="
    dict for {mod ss} $by {
        set clocks {} ; set rest {}
        foreach s $ss {
            if {[regexp {/(clk|rst_n|reset)$} [lindex $s 0]]} { lappend clocks $s } else { lappend rest $s }
        }
        foreach s [concat $clocks $rest] {
            set radix [expr {[lindex $s 2] > 1 ? "hex" : ""}]
            if {$radix eq ""} { add_wave -group $mod [lindex $s 0] } \
            else              { add_wave -group $mod -radix $radix [lindex $s 0] }
        }
        puts [format "    %-14s %d signal(s)%s" $mod [llength $ss] \
                  [expr {[llength $clocks] ? ", clock/reset first" : ""}]]
    }
    set n [save_layout $layout]
    puts ""
    puts "  === saved ==="
    puts "    [llength $::w::groups] group(s), $n signal(s) -> [file tail $layout]"
    return $n
}

The grouping needs no configuration, because the hierarchy already contains it — module_of is the path minus its last element. Within each group, clock and reset are hoisted to the top, because they are what you read every other signal against.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  === selection, every rule with what it caught ===
    parameter     2  a parameter never changes
    wide          1  too wide to read as a waveform
    bookkeeping   2  testbench bookkeeping, not design state
    kept         11  of 16 in the hierarchy

  === grouped by module ===
    fifo_tb        5 signal(s), clock/reset first
    fifo_tb/dut    6 signal(s), clock/reset first

  === saved ===

Every rule reports what it caught, next to why it exists. A selection script that prints only its result asks to be trusted; one that prints parameter 2 lets you notice immediately when a rule starts eating something you wanted.

And the layout it writes:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  === saved ===
    2 group(s), 11 signal(s) -> wave.do

  === the layout, first 12 lines ===
    # wave layout — generated, 11 signal(s)
    add wave -group fifo_tb \
        /fifo_tb/clk \
        /fifo_tb/rst_n \
        /fifo_tb/din -radix hex \
        /fifo_tb/dout -radix hex \
        /fifo_tb/full \

    add wave -group fifo_tb/dut \
        /fifo_tb/dut/clk \
        /fifo_tb/dut/rst_n \

Multi-bit signals get -radix hex and single-bit ones do not, which is a one-line rule that removes a whole category of small annoyance. The file says it is generated and how many signals it holds, for Chapter 30.3 §2's reason.

5. What each rule is actually buying

A rule without a number attached is an opinion. Removing each one in turn:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
=== each rule removed in turn: what it was buying ===
  without parameter    13 signals in the window (instead of 11)
  without wide         12 signals in the window (instead of 11)
  without bookkeeping  13 signals in the window (instead of 11)
  with every rule          11
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
=== and with no rules at all ===
  16 signals, ungrouped: the window you would have scrolled

Now the honest reading of those numbers: on a sixteen-signal testbench they are small, and 11 versus 16 is not the difference between debuggable and not. What scales is the ratio and the kind of signal removed. A real block has hundreds of parameters, and every one of them is a flat line; the string and the loop counter generalise to every piece of testbench scaffolding in a UVM environment. The technique is worth having at this size because it is free, and worth having at real size because by then nobody will add it.

The measurement is still worth running, for the reason Chapter 30.2 §6 and Chapter 30.4 §5 both arrived at: a rule you have never seen remove anything may not work. All three remove something here.

6. Honest limits

  • It cannot know which signals this failure needs. It produces a good default window, which is a different thing from the right one. The signal you actually need is often one the rules removed, and the fix is to read the rules, not to delete them.
  • It cannot see what the hierarchy does not list. Signals inside a generate block, inside a package, or optimised away by the simulator are not in the listing and so cannot be added.
  • -radix hex is a guess. It is right for data and addresses and wrong for a signed counter or a one-hot state register, which read better as decimal and binary respectively.
  • A saved layout is a fourth thing to keep in step with the design, alongside the filelist, the constraints and the testbench. Generating it on every run rather than committing it is what keeps that from mattering.

Exercises

  1. Add a rule excluding signals whose name ends in _unused. What has to change, and what must the rule carry with it?
  2. module_of returns everything but the last path element. What happens to a signal at the top level of the testbench, and is that right?
  3. Make the radix rule give -radix unsigned to anything wider than 16 bits. Which signal in this hierarchy changes?
  4. The wide rule drops a 256-bit string that is genuinely useful — it holds the test name. Fix that without weakening the rule.
  5. Remove the clock/reset hoisting. What is lost, given that all the same signals are still in the window?
  6. The layout is regenerated every run. Name a case where committing it instead would be correct.

Answers

  1. One entry in ::RULES and one switch arm in rule_hit — and the entry must carry its reason, because that is what makes the rule reviewable six months later. The cost of a rule is not the code; it is that somebody will eventually wonder why a signal is missing, and the reason is the only thing that answers them.
  2. module_of returns top for a one-element path, so such a signal lands in a group called top. Correct here: it keeps the grouping total, so no signal can be added without a group, and add_wave with an empty group name would otherwise put it nowhere. A grouping that silently drops its exceptions is the shape of bug this module has been about throughout.
  3. testname, at 256 bits — except it never reaches the radix rule, because wide removed it first. Which is a small lesson in ordering: a selection stage in front of a formatting stage means the formatting stage never sees the cases the selection stage removed, and a test of the formatting rule has to bypass the selection.
  4. Make the rule width > 64 && kind ne "reg" and you have weakened it; make it a named exception — testname listed as always-keep, checked before the rules — and the rule stays intact with the exception visible. The second is better because the exception is one line someone can read, rather than a condition that silently changes what wide means.
  5. The signals are all present and the window is harder to read, because every value has to be interpreted against a clock edge you have to go looking for. Ordering is not decoration in a waveform; it is the difference between reading a value and locating it first. Hoisting costs one regexp per signal.
  6. When the layout has been hand-tuned for a specific recurring failure — a particular bus decoded a particular way, signals ordered to show a specific handshake. Then it is not a default any more, it is a debugging artefact worth keeping, and it belongs in version control with a name that says which failure it is for.

Where this fits

Part of the Tcl curriculum.