See the transactions on your bus.

wtf is a waveform viewer for the terminal. It opens VCD, FST and GHW traces where your simulations run, moves like Vim, and decodes AXI, SPI and UART into transactions you can filter, count and measure.

$curl -fsSL https://raw.githubusercontent.com/xtrollyj00/wtf/main/install.sh | sh
To try it, in a clone of the repository: cp docs/demo.vcd tour.vcd && wtf tour.vcd
Name│Value│40ns60ns85ns100ns120ns140ns160ns││2─ 20ns ──3─ 20ns ──1││┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐󱑻aclk│1│└─┘└─┘└─┘└─┘└─┘└─┘└─┘└─┘└─┘└─┘└─┘└─┘└─┘└─┘└─┘└─││🮣───────────────────🮢▸󰙬bus.wr│#0 20ns│🮡─────#0 20ns─────🮠││▾󰷏write││││┌──────────────┐s_axi_awvalid│0│───────┘└──────────────────────────────────────────────────────││┌────┐s_axi_awready│0│─────────────────┘└──────────────────────────────────────────────────────││───────🮦─────────────────────────────────────────────────────────────────────󰚈s_axi_awaddr│0x10│─0x0─🮧───────────────────────────────0x10────────────────────────────────││┌─────────┐s_axi_wvalid│0│───────┘└───────────────────────────────────────────────────────────││┌────┐s_axi_wready│0│────────────┘└───────────────────────────────────────────────────────────││───────🮦─────────────────────────────────────────────────────────────────────󰚈s_axi_wdata│0xCAFEF00D│─0x0─🮧────────────────────────────0xCAFEF00D─────────────────────────────││┌────┐s_axi_bvalid│1│───────────────────────────┘└────────────────────────────────────────────││─────────────────────────────────────────────────────────────────────────────󰚈s_axi_bresp│0x0│────────────────────────────────────0x0────────────────────────────────────││🮣──────────────🮢🮣────▸󰙬bus.rd││🮡──#0 20ns───🮠🮡────││▸󰀩bus.errors││││NORMALsignals ▸top.aclk 85ns2ns/col  demo.vcd
An AXI4-Lite write in docs/demo.vcd: the decoded write on top, the handshake under it, marks 2 and 3 timing the address phase. This is wtf's own output, drawn as text with its Nerd Font icons.
0ns

Open a trace

wtf tour.vcd, or a trace of your own. With no session saved yet, wtf shows where to start. It has already looked for interfaces a built-in protocol can decode.

Next time it opens to the rows, marks and decodes you left.

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

Decode the bus

Space d d picks the interface and decodes it. The writes, the reads and the protocol errors become rows, and :records s_axi lists them in one table, in time order.

Type a filter like resp != 0 to keep the bad ones, = to count each value, Enter to jump there.

Name│Value│050ns100ns150ns200ns╭─ records · s_axi────────────────────────────────────────────────────────────────────── 4 records ─╮▾│allwr rd errorswr.unmatched rd.unmatched aw w b ar r ││  where / filters on delay, addr, data, strb, resp, rule, kind ││──────────────────────────────────────────────────────────────────────────────────────────────────────││ stream nstartlength delay addr data strb resp rule kind ││wr#055ns40ns20ns0x100xCAFEF00D0xF0x0││rd#0115ns30ns20ns 0x10 0xCAFEF00D 0x0││rd#1175ns30ns20ns 0x40 0x0 0x2││errors#0195ns10nsr_resp│╰──────────────────────────── Enter jump / filter s S sort = group h l scroll Tab set Esc close ─╯││NORMALsignals ▸s_axi 04ns/col  tour.vcd[+]
40ns

Find the cycle

Search with an expression, sampled at a clock: /@aclk s_axi_awvalid && !s_axi_awready stops at each cycle where the address waits. n goes to the next one.

rose, fell and past write protocol checks the same way.

Name│Value│020ns45ns60ns80ns100ns120ns140ns││┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐󱑻aclk│1│──┘└─┘└─┘└─┘└─┘└─┘└─┘└─┘└─┘└─┘└─┘└─┘└─┘└─┘└─┘└─││🮣───────────────────🮢▸󰙬bus.wr││🮡─────#0 20ns─────🮠││▾󰷏write││││┌──────────────┐s_axi_awvalid│1│──────────────────────┘└───────────────────────────────────────││┌────┐s_axi_awready│0│────────────────────────────────┘└───────────────────────────────────────││──────────────────────🮦──────────────────────────────────────────────────────󰚈s_axi_awaddr│0x10│────────0x0─────────🮧────────────────────────0x10────────────────────────││┌─────────┐s_axi_wvalid│1│──────────────────────┘└────────────────────────────────────────────││┌────┐s_axi_wready│0│───────────────────────────┘└────────────────────────────────────────────││──────────────────────🮦──────────────────────────────────────────────────────󰚈s_axi_wdata│0xCAFEF00D│────────0x0─────────🮧─────────────────────0xCAFEF00D─────────────────────││┌────┐s_axi_bvalid│0│──────────────────────────────────────────┘└─────────────────────────────││─────────────────────────────────────────────────────────────────────────────󰚈s_axi_bresp│0x0│────────────────────────────────────0x0────────────────────────────────────││🮣──────────────🮢▸󰙬bus.rd││🮡──#0 20ns───🮠SEARCH/@aclk s_axi_awvalid && !s_axi_awready
60ns

Keys you already know

Time runs along the line, so Vim's motions move the cursor. Space opens a menu of the rest, and every entry shows its key.

  • w bnext and previous change
  • gg Gstart and end
  • :2usgo to a time
  • m2 '2set and jump to a mark
  • uundo any change to the rows
Name│Value│40ns60ns85ns100ns120ns140ns160ns││2─ 20ns ──3─ 20ns ──1││┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐󱑻aclk│1│└─┘└─┘└─┘└─┘└─┘└─┘└─┘└─┘└─┘└─┘└─┘└─┘└─┘└─┘└─┘└─││🮣───────────────────🮢▸󰙬bus.wr│#0 20ns│🮡─────#0 20ns─────🮠││▾󰷏write││││┌──────────────┐s_axi_awvalid│0│───────┘└──────────────────────────────────────────────────────││┌────┐s_axi_awready│0│─────────────────┘└──────────────────────────────────────────────────────││───────🮦─────────────────────────────────────────────────────────────────────󰚈s_axi_awaddr│0x10│─0x0─🮧───────────────────────────────0x10────────────────────────────────││┌─────────┐╭─Space────────────────────────────────────────────────────────────────────────────────────────────────────╮│ ││v→+viewm→marks:pick marks ││t→+timer→records:pick records ││d→+decode:→commands:pick commands ││a→+agentn→messages:messages ││q→+session?→keys:help ││f→find a signal:pick signals ││ │╰──────────────────────────────────────────────────────────────────────────────────────────────── Esc close ─╯NORMALSpacesignals ▸top.aclk 85ns2ns/col  demo.vcd
80ns

Your own protocol

Most handshakes in a design have no standard. Describe one in a line, sampled at its clock: :protocol hs roles=valid,id when=valid id=id, then decode each side with it.

Its records get the same table, search and rows as AXI's, and pair matches requests with responses to time each one.

Name│Value│022ns40ns60ns80ns100ns││┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌󱑻clk│0│──┘└─┘└─┘└─┘└─┘└─┘└─┘└─┘└─┘└─┘└─┘││┌─────────┐┌────┐req_valid│1│───────┘└────┘└─────────────────────────││───────🮦────🮦─────────🮦──────────────────────────────󰚈req_id│0x1│─0x0─🮧────🮧──0x2──🮧────────────0x3─────────────││┌────┐┌────┐┌────┐resp_valid│0│─────────────────┘└────┘└─────────┘└─────││─────────────────🮦─────────🮦──────────────🮦──────────󰚈resp_id│0x0│──────0x0──────🮧──0x2──🮧────0x1─────🮧──0x0───││🮣────🮦────🮢🮣────🮢▸󰙬req│#0│🮡#0🮧#1🮠🮡#2🮠││🮣────🮢🮣────🮢🮣────🮢▸󰙬resp││🮡#0🮠🮡#1🮠🮡#2🮠││🮣────🮦─────────🮦─────────🮢▸󰙬lat│#0 40ns│🮡────🮧#1 10ns🮧#0 40ns🮠││NORMALsignals ▸lat #0 15ns–65ns delay=40ns req_id=0x1… 22ns2ns/col  reqresp.vcd[+]

Stays on your machine

wtf reads your traces where they are and opens no network connections: nothing is uploaded, nothing is reported. Its sessions are TOML files next to the trace.

If you use an AI coding agent, wtf has an optional server that gives it the same decoders and records, and shows you what it found. It does nothing unless you set it up: for agents.