Skip to content

analyzer: fix trigger reliability (spurious triggers, re-runs, wide probes) - #53

Merged
enjoy-digital merged 4 commits into
masterfrom
fix/wide-trigger-timing
Jul 14, 2026
Merged

analyzer: fix trigger reliability (spurious triggers, re-runs, wide probes)#53
enjoy-digital merged 4 commits into
masterfrom
fix/wide-trigger-timing

Conversation

@enjoy-digital

Copy link
Copy Markdown
Owner

Summary

Investigation of long-standing 'captures do not trigger correctly' reports, reproduced in simulation and on hardware. Four commits:

analyzer: fix trigger term flushing and register the comparator operands

  • Term flushing used a fixed 2*depth-cycle window that also ran from reset and kept draining after the term memory was already empty. Terms pushed within that window — right after reset, or right after disarming to re-arm — were silently consumed, so the capture armed with an empty term memory and triggered immediately on anything. Slow interactive/UART hosts rarely notice; fast CSR transports (PCIe/Ethernet) hit it easily. Flush now starts on the falling edge of enable and stops as soon as the memory is empty. (Reproduced in sim: a term pushed ~20 cycles after reset was eaten and the capture triggered instantly.)
  • The comparator was a fully-combinational data_width-wide compare fed by the unbuffered AsyncFIFO's block-RAM read port on one side and the raw probe mux on the other, feeding back into the consume handshake in a single scope cycle — a timing hazard that grows with probe width. The term memory is now a buffered FIFO and probe data/valid are registered, so the wide compare sits between registers. Captures shift by one scope cycle in absolute time; the trigger position within the capture is unchanged (locked by test).

analyzer: drain stale read-path samples during storage flush

Samples from a previous capture lingered in the CDC FIFO/read converter and shifted the next upload's head.

driver: fix re-running captures, resolve offsets per group

run() on a used driver instance armed with an empty (consumed) term memory and never re-armed storage (rising-edge enable). Terms are now recorded and reloaded on every run(), storage enable is cycled, and ordering guarantees no constant-hit level fires the re-armed storage. Trigger conditions also now resolve signal offsets in the currently selected group instead of always the last group.

test: exercise wide triggers through the CSR bus

The >32-bit trigger path had zero coverage: sims poked CSRStorage.write() atomically, bypassing the multi-word bus path hosts actually use. New test drives a 128-bit-probe analyzer exclusively through a csr_bus.CSRBank.

Validation

  • Test suite: 56 passed (updated legacy expectations documented per-test; the first analyzer test previously relied on the reset-window flush eating its term and never actually enabled the trigger).
  • Hardware (Kintex-7, 394-bit probe incl. a 128-bit deterministic pattern, UARTBone): 100/100 captures triggering on an exact 128-bit value (20 fresh-driver + 80 reused-driver), all with the trigger sample at the identical index and bit-exact consecutive capture contents; edge-trigger captures re-run 3/3 with the same terms and no reconfiguration — previously impossible on a reused driver.

Two reliability fixes in the trigger:

- Term flushing used a fixed 2*depth-cycle window that also ran from
  reset and kept draining after the memory was already empty. Terms
  pushed within that window - right after reset, or right after
  disarming to re-arm - were silently consumed, so the capture armed
  with an empty term memory and triggered immediately on anything.
  Slow interactive/UART hosts never noticed; fast CSR transports
  (PCIe, Ethernet) hit this easily, which shows up as captures that
  'do not trigger correctly'. Flush now starts on the falling edge of
  enable and stops as soon as the memory is empty.

- The comparator was a fully-combinational data_width-wide compare fed
  on one side by the unbuffered AsyncFIFO's block-RAM read port and on
  the other by the raw probe mux, feeding back into the consume
  handshake within a single scope cycle - a timing hazard that grows
  with probe width. The term memory is now a buffered FIFO and the
  probe data/valid are registered before the compare, so the wide
  comparator sits between registers.

The registered input delays data and hit together by one scope cycle:
captures shift by one cycle in absolute time, the trigger position
within the capture is unchanged.

The first analyzer test relied on the reset-window flush eating its
term (it never enabled the trigger): it now arms the trigger properly
on a reachable value and additionally checks the trigger sample's
position; the group-mux test now observes the match exactly at index 0
with offset=0.
Samples from a previous capture or idle drain can linger in the CDC
FIFO and read-width converter between the storage FIFO and the
mem_data CSR; the next upload then starts with stale words and every
following sample is shifted. Drain the read path during the storage
FLUSH state (no new data enters it there), so an upload always starts
with the new capture's first sample.
Three driver fixes so a capture can be re-run on the same driver
instance:

- The gateware's trigger memory consumes its terms on every capture,
  so re-running armed with an empty memory and triggered immediately
  on anything. add_trigger and the edge helpers now record the
  configured terms and run() disarms (letting the gateware flush
  leftovers), reloads all terms and rearms. As a consequence
  add_trigger no longer writes the hardware directly; the memory-full
  check happens at load time.

- Storage arms on the rising edge of its enable, but run() only ever
  wrote 1: a second run() on the same instance never re-armed storage
  and the previous capture's state was re-read as a new one. run()
  now clears and rises enable, after reloading the trigger terms: an
  armed trigger with a consumed term memory outputs a constant hit
  level that would otherwise fire the re-armed storage immediately.

- Signal offsets/masks for trigger conditions were flat attributes
  overwritten by each group, so a signal present in several groups
  always resolved to the last group's position regardless of the
  selected group. Conditions now resolve in the currently selected
  group; the flat attributes remain for compatibility and an
  ambiguity warning is printed when they differ across groups.
At data_width > 32 the trigger Mask/Value CSRs are compound (several
bus words), but all existing tests poked CSRStorage.write() which sets
the whole storage atomically - neither the word-by-word bus path that
hosts actually perform nor a comparator wider than 32 bits was ever
exercised.

Drive a 128-bit-probe analyzer exclusively through a csr_bus.CSRBank,
mirroring the host access pattern (MSB word first, ascending
addresses, separate arm strobe): check the capture is a consecutive
wide pattern, the trigger sample sits at its locked position within
the capture, and a second re-armed capture behaves identically.

Note: csr_bus.Interface.read samples dat_r one cycle too early for
the bank's registered read path; the test uses a local read helper
with the settle cycle.
@enjoy-digital
enjoy-digital merged commit e3127b4 into master Jul 14, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant