Change command¶
Use change when one timestamp is not enough and you need the moments when a small set of signals actually transitions.
change scans an inclusive time window, samples the signals from --signals, and prints a row only when at least one of those sampled values changed. By default, --on is *, which means "consider any change in the tracked signal set".
In practice, change is the command between value and property: it is more selective than sampling every cycle, but still shows raw signal snapshots instead of a derived pass/fail result.
--on is intentionally a SystemVerilog-style event-expression surface. Treat it as a practical CLI spelling of the same concepts you would use in @(...): named events, posedge/negedge/edge, * for any tracked change, unions with or or ,, and iff for gating. For the full shipped syntax and semantics, see reference/expression-language.
A rough mental model is this SystemVerilog-like pseudocode:
logic initialized = 1'b0;
sample_t prev;
always @(<event from --on, or @(*) when omitted>) begin
sample_t cur = sample(<signals from --signals>);
if (initialized && (cur != prev))
$display("@%0t ...", $time, cur);
prev = cur;
initialized = 1'b1;
end
That is only an intuition aid, not a normative definition: wavepeek runs over recorded dump timestamps, applies the selected inclusive --from/--to window, and initializes its baseline at --from.
For exact syntax and flags, run wavepeek help change.
Start with a short window and a focused signal list¶
This is the fastest way to answer "what changed here?":
$ wavepeek change --waves /opt/rtl-artifacts/picorv32_test_ez_vcd.fst \
--scope testbench.uut \
--signals cpu_state,mem_valid,mem_ready,trap \
--from 1010000ps --to 1040000ps --max 10
@1020000ps cpu_state=8'h40 mem_valid=1'h1 mem_ready=1'h0 trap=1'h0
@1030000ps cpu_state=8'h40 mem_valid=1'h1 mem_ready=1'h1 trap=1'h0
@1040000ps cpu_state=8'h40 mem_valid=1'h0 mem_ready=1'h0 trap=1'h0
Use this as the default pattern when you already know the scope and just need the transition points.
Trigger on one signal, but print several¶
--on decides when to sample. --signals decides what to print.
If you already know SystemVerilog event controls, read --on the same way: mem_valid means any change, posedge mem_valid means rising edges only, * means any change in the tracked set, and iff gates an event term without changing what gets printed.
If you care about every change of mem_valid, use the signal itself as the event:
$ wavepeek change --waves /opt/rtl-artifacts/picorv32_test_ez_vcd.fst \
--scope testbench.uut \
--signals cpu_state,mem_valid,mem_ready,trap \
--from 1000000ps --to 1040000ps \
--on mem_valid --max 10
@1020000ps cpu_state=8'h40 mem_valid=1'h1 mem_ready=1'h0 trap=1'h0
@1040000ps cpu_state=8'h40 mem_valid=1'h0 mem_ready=1'h0 trap=1'h0
Named event mem_valid means any change of that signal, not only the rising edge.
Keep only the edge you care about¶
If the deassert edge is noise, switch to an edge trigger:
$ wavepeek change --waves /opt/rtl-artifacts/picorv32_test_ez_vcd.fst \
--scope testbench.uut \
--signals cpu_state,mem_valid,mem_ready,trap \
--from 1000000ps --to 1040000ps \
--on "posedge mem_valid" --max 10
@1020000ps cpu_state=8'h40 mem_valid=1'h1 mem_ready=1'h0 trap=1'h0
This is usually the cleanest way to inspect request starts, handshake assertions, enables, and state-entry pulses.
Sample on clock edges only while a condition is true¶
When combinational chatter is irrelevant, gate sampling with iff:
$ wavepeek change --waves /opt/rtl-artifacts/picorv32_test_ez_vcd.fst \
--scope testbench.uut \
--signals cpu_state,mem_valid,mem_ready,trap \
--from 1010000ps --to 1040000ps \
--on "posedge clk iff mem_valid" --max 10
@1020000ps cpu_state=8'h40 mem_valid=1'h1 mem_ready=1'h0 trap=1'h0
@1030000ps cpu_state=8'h40 mem_valid=1'h1 mem_ready=1'h1 trap=1'h0
This means: sample on posedge clk, but only on cycles where mem_valid is true.
Use scope-relative names or full canonical paths¶
With --scope, short names stay readable. Without it, pass canonical paths directly:
$ wavepeek change --waves /opt/rtl-artifacts/picorv32_test_ez_vcd.fst \
--signals testbench.uut.cpu_state,testbench.uut.mem_valid,testbench.uut.mem_ready,testbench.uut.trap \
--from 0ps --to 20000ps --max 20
@10000ps testbench.uut.cpu_state=8'h40 testbench.uut.mem_valid=1'h0 testbench.uut.mem_ready=1'h0 testbench.uut.trap=1'h0
If you like scoped input but still want canonical names in human output, add --abs:
$ wavepeek change --waves /opt/rtl-artifacts/picorv32_test_ez_vcd.fst \
--scope testbench.uut \
--signals cpu_state,mem_valid,mem_ready,trap \
--from 0ps --to 20000ps --abs
@10000ps testbench.uut.cpu_state=8'h40 testbench.uut.mem_valid=1'h0 testbench.uut.mem_ready=1'h0 testbench.uut.trap=1'h0
Use JSON for scripts and agents¶
--json keeps canonical paths and moves warnings into the payload:
$ wavepeek change --waves /opt/rtl-artifacts/picorv32_test_ez_vcd.fst \
--scope testbench.uut \
--signals cpu_state,mem_valid,mem_ready,trap \
--from 1010000ps --to 1040000ps --json
{"$schema":"https://raw.githubusercontent.com/kleverhq/wavepeek/v0.5.0/schema/wavepeek.json","command":"change","data":[{"time":"1020000ps","signals":[{"path":"testbench.uut.cpu_state","value":"8'h40"},{"path":"testbench.uut.mem_valid","value":"1'h1"},{"path":"testbench.uut.mem_ready","value":"1'h0"},{"path":"testbench.uut.trap","value":"1'h0"}]},{"time":"1030000ps","signals":[{"path":"testbench.uut.cpu_state","value":"8'h40"},{"path":"testbench.uut.mem_valid","value":"1'h1"},{"path":"testbench.uut.mem_ready","value":"1'h1"},{"path":"testbench.uut.trap","value":"1'h0"}]},{"time":"1040000ps","signals":[{"path":"testbench.uut.cpu_state","value":"8'h40"},{"path":"testbench.uut.mem_valid","value":"1'h0"},{"path":"testbench.uut.mem_ready","value":"1'h0"},{"path":"testbench.uut.trap","value":"1'h0"}]}],"warnings":[]}
Watch for bounded-output warnings¶
If --max truncates the result, the command still succeeds and warns:
$ wavepeek change --waves /opt/rtl-artifacts/picorv32_test_ez_vcd.fst \
--scope testbench.uut \
--signals cpu_state,mem_valid,mem_ready,trap \
--from 1000000ps --to 11000000ps \
--on "posedge clk" --max 3
@1020000ps cpu_state=8'h40 mem_valid=1'h1 mem_ready=1'h0 trap=1'h0
@1030000ps cpu_state=8'h40 mem_valid=1'h1 mem_ready=1'h1 trap=1'h0
@1040000ps cpu_state=8'h40 mem_valid=1'h0 mem_ready=1'h0 trap=1'h0
warning: truncated output to 3 entries (use --max to increase limit)
If you disable the limit intentionally, that is also reported:
$ wavepeek change --waves /opt/rtl-artifacts/picorv32_test_ez_vcd.fst \
--scope testbench.uut \
--signals cpu_state,mem_valid,mem_ready,trap \
--from 1010000ps --to 1040000ps \
--on "posedge clk" --max unlimited
@1020000ps cpu_state=8'h40 mem_valid=1'h1 mem_ready=1'h0 trap=1'h0
@1030000ps cpu_state=8'h40 mem_valid=1'h1 mem_ready=1'h1 trap=1'h0
@1040000ps cpu_state=8'h40 mem_valid=1'h0 mem_ready=1'h0 trap=1'h0
warning: limit disabled: --max=unlimited
Non-obvious behavior¶
--fromis inclusive for selection, but it also initializes the baseline state.changedoes not emit a row exactly at--from; if you need the boundary value itself, usevalue.--ondoes not guarantee a row by itself. A trigger can fire, butchangestill suppresses the row if none of the requested--signalschanged.- In scoped mode, use scope-relative names in
--signalsand--on. Without--scope, use canonical full paths. - Empty output is valid. If the query is well-formed but nothing matched, the command succeeds and warns:
$ wavepeek change --waves /opt/rtl-artifacts/picorv32_test_ez_vcd.fst \
--scope testbench.uut \
--signals cpu_state,mem_valid,mem_ready,trap \
--from 0ps --to 20000ps \
--on "posedge mem_valid" --max 20
warning: no signal changes found in selected time range
When a query keeps coming back empty, widen one dimension at a time: start with the time window, then the trigger, then the signal list.