Each recipe is a few commands on a trace from the repository, so you can follow along. Type them after : in the window. Decodes are kept in the trace's session, and :e runs them again on a new simulation, so you make them once. The first recipes use the demo trace, copied as in how it works:

cp docs/demo.vcd tour.vcd
wtf tour.vcd

Count the bus errors

An AXI4-Lite port answered something with an error, and you want to know which transfer. tour.vcd has one. Decode the port:

:decode bus axilite clock=top.aclk prefix=top.s_axi_

Space d d does the same from the list of found interfaces, and names the decode s_axi, after the prefix. The decode joins the handshakes into writes and reads, each with its address, data, response and delay, and checks the handshake rules as it goes: a dropped valid, data that changed while waiting, an error response.

:records bus

lists them all in one table, in time order. Type / and a filter, here resp != 0:

Name│Value│40ns60ns85ns100ns120ns140ns╭─ records · bus─────────────────────────────────────────────────────────────────── 1 of 4 ─╮│allwr rd errors wr.unmatched rd.unmatched aw w b ar r │󱑻│  whereresp!= 0│─│──────────────────────────────────────────────────────────────────────────────────────────────│▸ │ stream nstartlength delay addr data strb resp rule kind ││rd#1175ns30ns20ns0x400x00x2│▾│││ ││ │─│ │╰──────────────────── Enter jump / filter s S sort = group h l scroll Tab set Esc close ─╯─││NORMALsignals ▸top.aclk 85ns2ns/col  demo.vcd
The second read, of 0x40, was answered with 0x2, SLVERR. Enter puts the cursor on it.

Tab goes through the decode's tables one at a time: errors holds the rule broken at each cycle. To count each rule, press s until the rule column is the sorted one, then =.

Save the layout with :w, so the next recipes find the decode.

A protocol of your own

Most designs have handshakes no standard covers. Say a request is a cycle where req_valid is high, and its req_id says which one it is (tests/data/reqresp.vcd). Open it:

wtf tests/data/reqresp.vcd

Describe the request once, with roles in place of signal names:

:protocol hs roles=valid,id when=valid id=id

when is a search expression, sampled at the clock; every field after it is a value to keep. Then put it on each side:

:decode req hs clock=clk valid=req_valid id=req_id
:decode resp hs clock=clk valid=resp_valid id=resp_id
:add req resp
Name│Value│020ns40ns60ns80ns100ns││▾󰷏top││││┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌󱑻clk│0│──┘└─┘└─┘└─┘└─┘└─┘└─┘└─┘└─┘└─┘└─┘││┌─────────┐┌────┐req_valid│0│───────┘└────┘└─────────────────────────││───────🮦────🮦─────────🮦──────────────────────────────󰚈req_id│0x0│─0x0─🮧────🮧──0x2──🮧────────────0x3─────────────││┌────┐┌────┐┌────┐resp_valid│0│─────────────────┘└────┘└─────────┘└─────││─────────────────🮦─────────🮦──────────────🮦──────────󰚈resp_id│0x0│──────0x0──────🮧──0x2──🮧────0x1─────🮧──0x0───││🮣────🮦────🮢🮣────🮢▸󰙬req││🮡#0🮧#1🮠🮡#2🮠││🮣────🮢🮣────🮢🮣────🮢▸󰙬resp││🮡#0🮠🮡#1🮠🮡#2🮠││NORMALsignals ▸resp 02ns/col  reqresp.vcd[+]
Each request and response is a record, drawn as a row under the signals it came from.

A field can compute: next=pc + 4, imm=signed(insn[31:20]), and later fields can name earlier ones.

How long did each request take

Pair the requests with their responses. With id= a response answers the request with the same id; without it, the oldest one waiting:

:decode lat pair request=req response=resp id=id
:records lat lat.unmatched
Name│Value│020ns40ns60ns80ns100ns╭─ records · lat lat.unmatched────────────────────────────────────────────────── 4 records ─╮│alllat unmatched ││  where / filters on delay, req_id, resp_id, kind ││──────────────────────────────────────────────────────────────────────────────────────────────││ stream nstartlength delay req_id resp_id kind ││lat#015ns50ns40ns0x10x1││lat#125ns20ns10ns 0x2 0x2││unmatched#045ns10nsno response││unmatched#185ns10nsno request│╰──────────────────── Enter jump / filter s S sort = group h l scroll Tab set Esc close ─╯││NORMALsignals ▸top.resp_id 02ns/col  reqresp.vcd[+]
delay is the time from request to response. Request 3 was never answered, and the response with id 0 answers nothing.

s sorts by a column, S the other way: the slowest first. The fields are signals too, so /lat.delay > 20ns in the waveforms stops at each slow one.

The AXI decodes pair their own: bus.wr and bus.rd have a delay already.

Catch a dropped valid

A sender must hold valid until ready takes the data. A check decode turns a rule into a record at each cycle it holds, so a broken rule is something you can list and jump to:

:decode c check clock=clk dropped=fell(valid) && !past(ready)
:add c
Name│Value│020ns40ns60ns85ns100ns120ns140ns││▾󰷏top││││┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐󱑻clk│1│──┘└─┘└─┘└─┘└─┘└─┘└─┘└─┘└─┘└─┘└─┘└─┘└─┘└─┘└││┌──────────────┐┌─────────┐┌────┐valid│0│───────┘└─────────┘└─────────┘└─────────────││┌────┐┌────┐ready│0│─────────────────┘└─────────────────────────────┘└─────────────││───────🮦────────────────────────🮦────🮦──────────────🮦──────────────────󰚈data│0x23│─0x0─🮧─────────0x11─────────🮧────🮧────0x23────🮧──────0x33──────││🮣────🮢▸󰀩c│#0 dropped│🮡────🮠││││NORMALsignals ▸c #0 85ns–95ns rule=dropped 85ns2ns/col  handshake.vcd[+]
valid fell at 85ns while ready was low: the data 0x23 was never taken.

fell, rose, stable and past look at the cycle before. A check can hold several rules, each named: dropped=... changed=..., and the record says which one fired. The built-in protocols check theirs this way, in their errors table.

The other built-in decoders

axilite is one of five. Each takes the signals' common prefix, or the names of its roles, and a clock where the protocol has one. These lines run on the fixtures in tests/data/:

:decode s axis clock=top.clk prefix=top.            (axis.vcd: frames, with the data bytes)
:decode m axi4 clock=top.aclk prefix=top.m_axi_     (axi4.vcd: writes and reads with bursts)
:decode s spi sclk=top.spi_clk cs=top.spi_cs_n mosi=top.spi_mosi miso=top.spi_miso mode=0
:decode u uart line=top.uart_tx baud=115200         (uart.vcd: bytes and framing errors)

SPI and UART sample by time and take no clock. mode= is the SPI mode, 0 to 3, and baud= is the UART's baud rate; bits, order, parity and the other settings are in the reference. :records s lists the result as in the first recipe.

Name the states of a state machine

A state register is a number in the trace. If the simulator wrote an enum table, as Verilator does for a SystemVerilog enum, the row shows the names already. tests/data/enum.fst has one:

wtf tests/data/enum.fst

state shows IDLE, READ, WRITE and DONE, and /READ finds the cycles in that state. A value the table doesn't name stays hex. A trace without a table gets names from you:

:names count 0=START 1=RUN

The row switches to the enum radix and /RUN searches by name. Names are kept in the session. :names count off removes them.

Jump with the picker

Space f opens the picker on signals. Ctrl-s inside it goes to the next source: marks, records, commands, sessions, the trail and the interfaces. :pick marks opens it on the marks, and :pick records bus.rd on the reads; Enter puts the cursor there.

Two keys copy what you are looking at. yy copies the selected signal's value at the cursor, or the record on a stream's row, and yt copies the cursor's time. In a design with several state signals, :scope top.cpu makes names resolve under top.cpu first, so state means top.cpu.state. The scope is saved in the session.

Debug a failing test in CI

A test failed on the simulation server, and the trace is all you have of the run. Print the screen that shows the failure into the job's log, so you can read it there without copying the trace off the server. --snapshot draws one screen as text and exits, so it needs no terminal:

wtf tour.vcd --snapshot 100x12 --glyphs ascii --keys ':records bus<CR>'

--keys presses keys before the screen is drawn, in Vim's notation: <CR> is Enter. The session saved with :w in the first recipe has the bus decoded, so :records bus finds it. --glyphs ascii draws the lines with 7-bit characters, so the log has no box drawing that a plain terminal or a mail client would garble. The command prints:

Name            | Value      |      40ns      60ns        85ns    100ns     120ns     140ns
  +- records - bus ------------------------------------------------------------------ 4 records -+
  | all  wr  rd  errors  wr.unmatched  rd.unmatched  aw  w  b  ar  r                             | -
a | > where / filters on delay, addr, data, strb, resp, rule, kind                               |
  |----------------------------------------------------------------------------------------------|
> |   stream    n    start    length    delay    addr    data        strb    resp    rule    kin |
  |>  wr        #0   55ns     40ns      20ns     0x10    0xCAFEF00D  0xF     0x0                 |
v |   rd        #0   115ns    30ns      20ns     0x10    0xCAFEF00D          0x0                 |
  |   rd        #1   175ns    30ns      20ns     0x40    0x0                 0x2                 |
  +-------------------- Enter jump  / filter  s S sort  = group  h l scroll  Tab set  Esc close -+ -
                |            |
 NORMAL   signals >  top.aclk                                              85ns  2ns/col  tour.vcd

Leave out the height (--snapshot 100) to print as many rows as the screen needs.

To print a layout you saved somewhere else, load its session with -S. It replaces the <trace>.session.toml next to the trace:

wtf -S ci/bus.session.toml sim/run.fst --snapshot 100 --glyphs ascii

Commit the session next to the testbench. Each run in CI then prints the same rows, so the logs of two runs compare line by line.

Rerun and look again

You rerun the simulation, after a fix or with another seed, and want the rows and cursors you had on the new trace. Save the layout with :w first, then rerun. The window reloads the trace by itself when the file has stopped changing for a second, and :e reloads at once. Rows, cursor, zoom and decodes come back; signals the new run doesn't have are dropped and listed on the status line. How it works has the full account.

Reload clears undo, so the saved session is the way back to the layout you had.

Look inside a struct

A packed struct is one signal in the HDL, so the trace holds it as one row. Enter opens the row one level: the struct's fields, and each field that is a struct too. The fixture tests/data/types-verilator-structs.fst has pkt, a struct of a valid bit, a two-bit state and a pair of nibbles. Open it, add the signal and press Enter on its row:

wtf tests/data/types-verilator-structs.fst
:add types.pkt
Name│Value│010ns20ns││───────────────────🮦───────────────────────────────────────▾󰅩pkt│0x0│───────0x0───────🮧────────────────0x5A5────────────────││┌───────────────────────────────────────valid│0│───────────────────┘││───────────────────🮦───────────────────────────────────────󰓼state│IDLE│──────IDLE───────🮧────────────────BUSY─────────────────││───────────────────🮦───────────────────────────────────────▸󰅩pair│0x0│───────0x0───────🮧────────────────0xA5─────────────────││││NORMALsignals ▸types.pkt 0 types-verilator-structs.fst[+]
pkt opens into valid, state and pair. pair is a struct too: Enter on it opens hi and lo. The value 0x5A5 is the fields joined, first the most significant.

An array opens the same way. types.pkts opens into pkts[0] and pkts[1], and each of those into its fields. The row's value is the fields' values joined, so pkt is 0x5A5: valid 1, state BUSY and pair 0xA5.

What a row shows depends on what the simulator wrote:

Simulator and dumpWhat the row shows
Verilator FST with --trace-structsthe fields, named, as above
Verilator VCD with --trace-structsthe struct is a scope, so its fields show under their own names (hi), with no row for the struct
Verilator without --trace-structs, Icarusone vector with no field names. Enter opens it into bits. Name a field with a slice: :signal state = types.pkt[9:8]
nvcthe fields, named; the record's own value is text ('{valid:0, state:idle, ...}), and an enum is its name as text (idle)
GHDLits VCD leaves records and arrays out. Dump GHW instead (--wave=): records open into their fields. A GHW with a multidimensional array can't be read yet

The fixtures tests/data/types-* hold each simulator's dump of the same design. tests/data/types.sh makes them.

Share what you see

Send the session :w saved, <trace>.session.toml, with the trace, and whoever opens the trace gets your rows and cursors. A session under another name opens with -S:

wtf -S bus-debug.session.toml run.fst

For an issue, paste a snapshot from the CI recipe in a code block, printed in ASCII. --ansi keeps the colours as escape codes, for a terminal that shows them.

A session is plain TOML with signal names, times and the layout, and no waveform data. Open it in an editor and check it before you send it.