Install wtf, then take a five-minute tour of it on a demo trace.

Install

wtf runs on Linux and macOS, on x86_64 and arm64. Windows is not supported.

Install the latest release with the script:

curl -fsSL https://raw.githubusercontent.com/xtrollyj00/wtf/main/install.sh | sh

The script downloads the release archive for your machine, checks it against the published SHA-256 checksum, and copies wtf to ~/.local/bin, or to $WTF_INSTALL_DIR if you set it. If that directory is not on your PATH, the script says so. Add it in your shell profile:

export PATH="$HOME/.local/bin:$PATH"

Check the install with wtf --version.

To build from source instead:

cargo install --locked --git https://github.com/xtrollyj00/wtf wtf-wave

The package is wtf-wave, and the command it installs is wtf, in ~/.cargo/bin.

Get the demo

The tour uses docs/demo.vcd, an AXI4-Lite bus with a write, a read of it back, and a read of an unmapped address that fails. The install does not ship it, so clone the repository:

git clone https://github.com/xtrollyj00/wtf
cd wtf

Take the tour

The demo comes with a saved session, which would skip the first-start screen, so work on a copy:

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

The first time, wtf asks which drawing your terminal shows cleanly and whether you like dark or light; troubleshooting says more.

Start from what wtf found

wtf opens to what to do first, each with its key. It has already looked through the trace for interfaces a built-in protocol decodes: 1 found is the demo's AXI4-Lite bus.

┃┃┃┃┃┃┃┃┃┃┃┃┃──────────────────🮦───────────────────🮦━━━━━━━━━━━━━━━────── 0x77 ──────🮧────── 0x74 ───────🮧━━━━ 0xXX ━━━━━~/tour.vcd19 signals · 0–240nsFind a signalSpace fBrowse the signalsaDecode an interface1 foundSpace d dEverything elseSpaceKeys?QuitqNORMALsignals ▸ 03ns/col  tour.vcd

Space opens a menu of everything else, and every entry shows its key, so you learn them as you go. Pause after any key and wtf shows what can follow.

Decode the bus

Space d d lists the interfaces found, with the signal each role takes. A role is one of the protocol's ports, such as valid, ready or addr:

╭─ signals marks records commands sessions trail interfaces─────────────────────────────────╮│1/1│axilite at top.s_axi_││────────────────────────────────────────────│decode s_axi axilite clock=top.aclk prefix=top.s_axi_││s_axi top.s_axi_axilite││││awvalid top.s_axi_awvalid│││awready top.s_axi_awready│││awaddrtop.s_axi_awaddr│││wvalid top.s_axi_wvalid│││wready top.s_axi_wready│││wdata top.s_axi_wdata│││bvalid top.s_axi_bvalid│││bready top.s_axi_bready│││arvalid top.s_axi_arvalid│││arready top.s_axi_arready│││araddr top.s_axi_araddr│││rvalid top.s_axi_rvalid│││rready top.s_axi_rready│││rdata top.s_axi_rdata│││wstrb top.s_axi_wstrb│││bresp top.s_axi_bresp│││rresp top.s_axi_rresp│││clock top.aclk│╰────────────────────────────────────────────┴───────────────── Enter decode C-s source Esc close ─╯PICKsignals ▸ 03ns/col  tour.vcd
Enter decodes it, named s_axi after its prefix.

The decode turns the handshakes into transactions: writes and reads with their address, data, response and delay, and the protocol's rules checked on every cycle. They come in as rows, in a group: s_axi.wr, s_axi.rd, s_axi.errors for the broken rules, and the unmatched streams (s_axi.wr.unmatched, s_axi.rd.unmatched) for responses with no request.

Find the failing read

:records s_axi lists every write, read and error in one table, in time order. Type / and a filter, resp != 0:

Name│Value│050ns100ns150ns200ns╭─ records · s_axi───────────────────────────────────────────────────────────────── 1 of 4 ─╮▾│allwr rd errorswr.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 ─╯▸󰀩s_axi.rd.unmatched││NORMALsignals ▸s_axi 04ns/col  tour.vcd[+]
One read failed: 0x40, answered with 0x2, SLVERR.

Enter puts the cursor on it and selects its row. Enter on the row unfolds the record into its fields, each a row of its own:

Name│Value│100ns150ns175ns200ns││▾󰷏s_axi││││──────🮢▸󰙬s_axi.wr││#0 …🮠││🮣──────🮢🮣──────🮢▾󰙬s_axi.rd│#1 20ns│🮡#0 …🮠🮡#1 …🮠││🮣──────🮢🮣──────🮢󰌖delay│20ns│🮡20ns🮠🮡20ns🮠││🮣──────🮢🮣──────🮢󰌖addr│0x40│🮡0x10🮠🮡0x40🮠││🮣──────🮢🮣──────🮢󰌖data│0x0│🮡0xC…🮠🮡0x0─🮠││🮣──────🮢🮣──────🮢󰌖resp│0x2│🮡0x0─🮠🮡0x2─🮠││🮣─🮢▸󰀩s_axi.errors││🮡─🮠││▸󰀩s_axi.wr.unmatched││││▸󰀩s_axi.rd.unmatched││││NORMALsignals ▸s_axi.rd #1 175ns–205ns delay=20ns addr=… 175ns4ns/col  tour.vcd[+]

e shows the whole value at the cursor in a box, and Esc closes it.

Add the signals around it

Space f finds a signal by name: type a few letters in order (s_axi_ar), and the preview shows the highlighted one's width, the decodes it is in and its value at the cursor. Tab marks and moves on:

Name │ Value │ 0 50ns 100ns175ns200ns╭─ signalsmarks records commands sessions trail interfaces ───────────────────────╮▾ 󰷏 │s_axi_ar5/48│top.s_axi_awready││────────────────────────────────────────│1 bit│▸ │+󰚈top.s_axi_araddr[7:0]│││+top.s_axi_arvalid│decodes│▸ │+top.s_axi_arready│s_axiaxiliteawready││top.s_axi_awready││▸ │󰚈top.s_axi_awaddr[7:0]│at 175ns 0││││▸ │││││────────────────────────────────────────│▸ ││83ns 270ns│╰─────────────────────────────────── 3 marked Enter add Tab mark C-s source Esc close ─╯│ │PICKsignals ▸s_axi.rd #1 175ns–205ns delay=20ns addr=… 175ns4ns/col  tour.vcd[+]
Enter adds the three marked, below the selected row. u takes it back.

a opens the signal panel instead, the trace as a tree of scopes, for when you'd rather browse.

Move, search, time it

KeysDoes
h lthe cursor one column left or right
w bthe selected signal's next or previous change
gg Gthe start or the end
:100nsgo to a time
z xzoom in or out around the cursor; :fit shows the whole trace
j kselect another row

A count repeats a key: 5l, 3w.

Searching takes an expression, written as in SystemVerilog. /0x40 finds a value on the selected row; with a clock first, it matches whole clock cycles, sampled at its edges. Put mark 2 at the read's address with m, then go to its data:

/@aclk s_axi_rvalid && s_axi_rready
Name│Value│100ns120ns140ns160ns180ns195ns││2─ 20ns ──1││▾󰷏s_axi││││▸󰙬s_axi.wr││││🮣──────────────🮢🮣──────────────🮢▸󰙬s_axi.rd│#1 20ns│🮡──#0 20ns───🮠🮡──#1 20ns───🮠││────────🮦────────────────────────🮦───────────────────────────󰚈s_axi_araddr│0x40│─0x0──🮧─────────0x10─────────🮧──────────0x40───────────││┌────┐┌─────────┐s_axi_arvalid│0│────────┘└───────────────────┘└─────────────────││┌────┐┌────┐s_axi_arready│0│────────┘└────────────────────────┘└─────────────────││🮣────🮢▸󰀩s_axi.errors│#0 r_resp│🮡────🮠││▸󰀩s_axi.wr.unmatched││││▸󰀩s_axi.rd.unmatched││││NORMALsignals ▸top.s_axi_arready 195ns2ns/col  tour.vcd[+]
The ruler shows the time between the mark and the cursor: 20 ns.

n / N go to the next or previous match, Tab to the next mark, '2 to mark 2, and '' back to where you were. The reference has every operator: rose, fell and past write protocol checks, as in the recipes.

Make it readable

r cycles the selected row's radix (hex, binary, decimal, ...), :radix q4.12 reads it as fixed point. V selects a range of rows and Space v g groups them; Enter folds a group, a stream or a bus. J and K move rows, dd removes one, and u undoes any of it.

The mouse does the common moves: a click selects a row or puts the cursor, Ctrl + wheel zooms, Shift + wheel scrolls in time, a drag across the waveforms zooms to that range, and a row dragged by its name moves. A right click opens a menu of what can be done there, each with its key.

Save, rerun, reload

:w writes tour.vcd.session.toml next to the trace, and the next wtf tour.vcd opens as you left it. How it works has the full account of sessions and reloading.

Your own trace

Give wtf a path to a VCD, FST or GHW file:

wtf trace.fst

Run wtf with no path to see the start screen. It lists the traces you opened recently: press the number beside one to open it, or o to type a path.

Space d d lists the interfaces wtf found in it. If the list is empty, the signal names did not match a built-in protocol. Space d D, or a :decode line, names the signal for every role:

:decode bus axilite clock=aclk awvalid=m_awv awready=m_awr awaddr=m_awa wvalid=m_wv wready=m_wr wdata=m_wd bvalid=m_bv bready=m_br bresp=m_bre arvalid=m_arv arready=m_arr araddr=m_ara rvalid=m_rv rready=m_rr rdata=m_rd rresp=m_rre

When the names share a prefix, prefix=m_ plus the role names is enough. Each protocol needs these roles (:help decode lists the optional ones):

protocolroles
axistvalid tready tdata
axiliteawvalid awready awaddr wvalid wready wdata bvalid bready bresp arvalid arready araddr rvalid rready rdata rresp
axi4the axilite roles, plus wlast and rlast
spisclk cs mosi
uartline

Next: How it works for the model, Recipes for errors, latency and your own handshakes. Stuck on a trace? Give it to an agent: it decodes and searches the same way, and shows you what it found.