Waveform Averager
Coherent time-domain averager for triggered waveforms: sums a programmable power-of-two number of waveforms sample by sample into a 36 bit accumulator RAM and streams the averaged waveform out as a burst. The number of averages and the waveform length are runtime inputs, the history can be cleared after every output or kept indefinitely, and a pile-up aware acquisition mode discards any waveform contaminated by a second trigger so that clean pulse templates can be built.
Introduction
Averaging $M$ repetitions of the same triggered waveform reduces the standard deviation of uncorrelated noise by $\sqrt{M}$ while leaving the coherent pulse shape untouched:
$$ \bar{x}[k] = \frac{1}{M}\sum_{m=0}^{M-1} x_m[k], \quad k = 0, 1, \ldots, L-1 $$
START opens an acquisition window and DV_IN (gated by ENABLE) marks
each valid input sample; sample $k$ of every waveform lands in cell $k$ of
the accumulator RAM. When $M$ waveforms have been accumulated, the averaged
waveform is streamed out one sample per clock: START_OUT pulses on the
first sample, DV_OUT is high for the whole burst, END_OUT pulses on the
last sample, and WAVE_COUNT carries $M$ for that burst.
$M$ is always a power of two, so the division is a shift and the average is
exact with no divider and no DSP. The exponent comes from the runtime
NEXP input (clamped to the compile-time Max averages exponent), and the
waveform length comes from the runtime LEN input — LEN does not have
to be a power of two, and leaving it unconnected uses the whole compiled
depth.
Pin Description
Properties
Maximum number of samples per waveform, power of two. Sizes the accumulator RAM (this depth x 36 bit) and the width of the LEN input. The waveform length actually accumulated is taken at run time from the LEN pin and does NOT have to be a power of two.
Maximum number of samples per waveform (64 .. 65536, power of 2). Sizes the accumulator RAM (this depth x 36 bit) and the width of the LEN input; in deferred mode it also sizes the scratch RAM. The length actually accumulated comes from the LEN pin at runtime and does not have to be a power of two. Keep this as small as your application needs.Default: 4096
Options: 64 128 256 512 1024 2048 4096 8192 16384 32768 65536
Depth of the internal pre-trigger buffer, in samples, power of two. 0 removes the buffer (and the PRETRIG pin) entirely: the waveform then starts exactly on the trigger. Anything else adds a circular memory of this depth x Input bits that continuously records the input, so the waveform can start up to this many samples BEFORE the trigger with no external delay line. The number of pre-trigger samples actually used is taken at run time from the PRETRIG pin.
Depth of the internal pre-trigger buffer in samples, power of two, or 0 to leave it out entirely (which also removes the PRETRIG pin). Costs one circular memory of this depth x InputBits. The number of pre-trigger samples actually used comes from the PRETRIG pin at run time. Keeping the buffer inside the block is what lets the pile-up veto cover the pre-trigger region – an external Delay block cannot.Default: 0
Options: 0 2 4 8 16 32 64 128 256 512 1024 2048 4096 8192 16384 32768
Width of the DIN samples, up to 24 bit. The accumulator is always 36 bit, so up to 2**(36-InputBits) full-scale waveforms can be summed before the cells overflow.
Width of DIN (1 .. 24). The accumulator is always 36 bit, so up to 2**(36-InputBits) full-scale waveforms can be summed before the cells can overflow.Default: 14
Range: 1 – 24
Interpretation of DIN. Drives the accumulator arithmetic, the overflow test, the rounding of the averaged output and the saturation of the raw output modes.
Signed or Unsigned interpretation of DIN. Drives the accumulator arithmetic, the overflow test, the rounding of the averaged output and the saturation of the raw output modes.Default: Signed
Options: Signed Unsigned
Largest exponent the runtime NEXP input can request (NEXP is clamped to it). Also sizes the WAVE_COUNT output, and in Keep-history mode it is the exponent at which the epoch is closed and the accumulator restarted.
Largest exponent the runtime NEXP input can request, 1 .. 31 (NEXP is clamped to it). Also sizes the WAVE_COUNT output, and in Keep-history mode it is the total count at which the epoch is closed and the accumulator restarted.Default: 20
Range: 1 – 31
Clear history after each output: after every output burst the accumulator is dropped, so each burst is the mean of its own 2NEXP waveforms. Keep history (cumulative): the accumulator is never dropped and a new burst is emitted every time the TOTAL waveform count reaches the next power of two (2NEXP, 2**(NEXP+1), …), so the divisor stays a shift and the mean stays exact. WAVE_COUNT always reports how many waveforms the burst being emitted contains.
- Clear history after each output: the accumulator is dropped after every burst; each burst is the mean of its own 2**NEXP waveforms.
- Keep history (cumulative): the accumulator is never dropped and a burst is emitted every time the total count reaches the next power of two, so the mean stays exact with no divider. WAVE_COUNT reports the running total.
Default: Clear history after each output
Options: Clear history after each output Keep history (cumulative)
Averaged, input width: DOUT is InputBits wide and carries the accumulator divided by WAVE_COUNT, rounded half-up. Accumulator raw, 32 bit (saturated): DOUT is 32 bit and carries the raw accumulator, saturated. Accumulator raw, 36 bit: DOUT is 36 bit and carries the raw accumulator, exact. In the two raw modes WAVE_COUNT is the divisor the downstream logic has to apply.
- Averaged, input width: DOUT is InputBits wide, the accumulator divided by WAVE_COUNT and rounded half-up.
- Accumulator raw, 32 bit (saturated): DOUT is 32 bit, raw sum.
- Accumulator raw, 36 bit: DOUT is the full accumulator, exact. In the raw modes WAVE_COUNT is the divisor to apply downstream.
Default: Averaged, input width
Options: Averaged input width Accumulator raw 32 bit (saturated) Accumulator raw 36 bit
Every accumulation is checked at 37 bit and the cell always SATURATES, never wraps. Stop averaging and flag: the block also halts as soon as a single cell overflows – no further waveform is accepted and no further burst is emitted until RESET or CLR. Saturate and flag, keep running: the OVERFLOW output is raised but averaging continues on saturated cells.
Cells always saturate rather than wrap, and OVERFLOW always latches. This property only decides whether the block additionally HALTS (accepting no further waveform until CLR / RESET) or keeps averaging on saturated cells.Default: Stop averaging and flag
Options: Stop averaging and flag Saturate and flag keep running
Direct (accumulate while acquiring): the trigger starts the recording and every sample is summed into the accumulator straight away. Deferred (accumulate at end, pile-up aware): the waveform is first captured into a scratch RAM (an extra MaxLength x InputBits memory) and it is added to the accumulator only at the END of the window; a second trigger inside the window marks it as piled up, and the DISCARD_PILEUP input then decides whether it is thrown away. Use this to build clean pulse templates. The commit pass costs one extra waveform length of dead time.
- Direct: the trigger starts the recording and samples are accumulated immediately.
- Deferred (pile-up aware): the waveform is captured into a scratch RAM and committed only at the end of the window; a second trigger inside the window marks it as piled up and DISCARD_PILEUP decides whether it is thrown away. Adds the DISCARD_PILEUP and PILEUP pins, one scratch RAM and one waveform length of dead time. Use it to build clean pulse templates.
Default: Direct (accumulate while acquiring)
Options: Direct (accumulate while acquiring) Deferred (accumulate at end pile-up aware)
Acquisition modes
Direct — the trigger starts the recording and every sample is summed
into the accumulator straight away. Lowest latency and lowest resource use.
A START arriving while a window is already open is ignored; the window
always runs for exactly LEN valid samples.
Deferred (pile-up aware) — the waveform is first captured into a scratch
RAM and is added to the accumulator only at the end of the window. If a
second trigger arrives inside the window the waveform is marked as piled up
and the DISCARD_PILEUP input decides its fate: 1 throws it away (it does
not count towards $M$), 0 accumulates it anyway. PILEUP pulses for one
clock at the end of every contaminated window either way, so pile-up can be
counted. This is the mode to use when building a clean pulse template.
It costs one extra scratch RAM (Max waveform depth × Input bits) and one
extra waveform length of dead time per acquisition.
Pre-trigger
Set Max pre-trigger to a non-zero depth and the block allocates a circular
buffer that continuously records the input, plus a PRETRIG input that picks
how many samples to actually use. The waveform then spans
x[T-PRETRIG .. T-PRETRIG+LEN), so the rising edge sits PRETRIG samples
into the burst and you get the baseline in front of it.
Do not build the pre-trigger with an external Delay block. It looks equivalent and it is not. Pile-up is vetoed by comparing trigger times, so if you delay
DINoutside the block the data window moves and the veto window does not: the two end up offset by the delay, the pre-trigger region becomes a blind spot, and a second pulse landing in it is averaged in — it shows up as a small extra pulse in front of the real one. With the internal buffer the block knowsPRETRIGand vetoes the whole captured span, which is why the buffer lives inside the IP.
The veto is exact on both edges:
- a trigger in the pre-trigger region [T-PRETRIG, T) vetoes the waveform, including a trigger the block itself had to refuse because it was busy committing or streaming out. That case is the common one: the deferred dead time is 2xLEN per accepted waveform, so every accepted waveform is by construction the first trigger after the block freed up, and an event refused a few clocks earlier is sitting right in its pre-trigger.
- a trigger past the end of the captured data, i.e. later than T+LEN-PRETRIG, does not veto: it never made it into the waveform, and throwing the waveform away for it would only cost efficiency.
PRETRIG is counted in valid samples, not clocks, so gaps on DV_IN
do not shift the alignment. It is clamped to the compiled buffer depth and to
LEN (a pre-trigger cannot be longer than the waveform), and it is latched
with LEN and NEXP at the start of an epoch. Leaving the pin unconnected
gives 0, i.e. the waveform starts on the trigger. The buffer needs PRETRIG
samples of history to be meaningful, so the first waveform after RESET
reads whatever the memory was initialised to if it arrives sooner than that.
History modes
Clear history after each output — the accumulator is dropped after every
burst, so each burst is the mean of its own $2^{NEXP}$ waveforms and
consecutive bursts are statistically independent. WAVE_COUNT is constant.
Keep history (cumulative) — the accumulator is never dropped and a new
burst is emitted every time the total count reaches the next power of two:
$2^{NEXP}$, $2^{NEXP+1}$, $2^{NEXP+2}$, … Each burst is the exact mean of
every waveform seen so far and WAVE_COUNT reports that total, so the
estimate keeps improving without the block ever needing a divider. Emissions
naturally become rarer as the average deepens. When the total reaches
$2^{MaxExp}$ the epoch is closed: the history is dropped and a new one
starts from $2^{NEXP}$ (this is also when LEN and NEXP are re-latched).
Clearing the history is free: the first waveform of an epoch is written
through (acc = x instead of acc = acc + x) rather than wiping the RAM.
A side effect worth knowing is that stale cells left over from a longer
previous LEN can never reach the output.
Output formats
With $M$ accumulated waveforms the accumulator holds $\sum x_m[k]$.
- Averaged, input width —
DOUTis Input bits wide and carries $\left\lfloor (\sum x_m + M/2) / M \right\rfloor$ (round half-up). No precision is lost that matters: the mean of $M$ values always fits the input range. - Accumulator raw, 32 bit (saturated) —
DOUTis 32 bit and carries the raw sum, saturated. - Accumulator raw, 36 bit —
DOUTis the full 36 bit accumulator, exact.
In the two raw modes WAVE_COUNT is the divisor the downstream logic has to
apply; they are the right choice when the normalization is done in software
or when the extra $\log_2 M$ bits of resolution are wanted.
Overflow
Every accumulation is checked at 37 bit and the cell always saturates,
never wraps, so a single overflowing sample cannot corrupt the whole
waveform. OVERFLOW latches high as soon as any one cell overflows, and the
On accumulator overflow property decides whether the block also halts:
- Stop averaging and flag — no further waveform is accepted and no
further burst is emitted until
RESETorCLR. Use this when a wrong result is worse than no result. - Saturate and flag, keep running — averaging continues on saturated
cells;
OVERFLOWtells you the result is no longer trustworthy.
With Input bits $b$ and a 36 bit accumulator, $2^{36-b}$ full-scale waveforms can be summed before overflow is even possible (e.g. 4096 with 14 bit input). Real signals rarely sit at full scale on every sample, so the practical limit is usually much higher.
Dead time
There is one accumulator RAM, no ping-pong. BUSY is high from the last
accumulated sample of a round through the last DV_OUT of its burst, and
triggers arriving while BUSY is high are ignored. The dead time is
therefore one waveform length every $2^{NEXP}$ waveforms in direct mode
(a $1/2^{NEXP}$ duty loss), plus one waveform length per acquisition in
deferred mode. Set Max waveform depth no larger than you need — it is what
sizes the RAM.
Status flags
ACQUIRING— a waveform is being captured (and, in deferred mode, also while it is being committed to the accumulator).INTEGRATING— a round is open: at least one waveform has been accumulated and the target count has not been reached yet. It stays high between waveforms, which is what distinguishes it fromACQUIRING.BUSY— the output burst owns the RAM and triggers are refused.
Timing contract
STARTis level sensitive for triggering: a highSTARTon a cycle where the block is idle opens a window, and the sample present on that same cycle (ifDV_INis high) is sample 0. This is what allows back-to-back waveforms on consecutive clocks; tyingSTARThigh makes the block free-run.- Pile-up detection is edge sensitive, so a
STARTheld high for the duration of a pulse does not flag its own window. - The block works at
DV_INinterval 1 (one sample per clock) and tolerates arbitraryDV_INgaps inside a window. LENandNEXPare latched at the start of the first (write-through) waveform of an epoch. Change them together with aCLRpulse.
Resources
One accumulator memory of Max waveform depth × 36 bit, plus a scratch memory of Max waveform depth × Input bits in deferred mode, plus a pre-trigger buffer of Max pre-trigger × Input bits. No DSP, no HLS, no divider and no multiplier: pure VHDL.
All three are written as the standard single-clock simple-dual-port template — synchronous read into a register, one read port, one write port — and none of them is ever reset, which is a hard requirement for block RAM (the contents of a BRAM cannot be cleared; that is also why the first waveform of an epoch is written through instead of wiping the memory). 36 bit is one native RAMB36 word in SDP mode, so the accumulator maps without padding.
Whether they actually land in block RAM is still the synthesiser’s
choice, not something this block forces: no ram_style attribute is
applied, and Vivado picks block vs distributed on a size heuristic. Small
configurations will legitimately become LUTRAM or plain registers, which is
the cheaper answer anyway. As an order of magnitude, depth 4096 × 36 bit is
147456 bit ≈ 4 RAMB36; treat that as arithmetic on the bit count, not as a
measured utilisation figure. If a specific implementation is required, force
it with a ram_style attribute and check the synthesis report.
Verified with a self-checking GHDL regression (15 configurations, 597
checks: both history modes, all three output formats, signed and unsigned,
gapless back-to-back rounds, LEN defaulting and clamping, LEN = 1,
NEXP = 0, half-up rounding on negative accumulations, overflow saturation
and halt, CLR, pile-up discard and override, and the
DV_OUT/START_OUT/END_OUT/BUSY/WAVE_COUNT framing). 21 deliberate
mutations of the core were all caught.