DSP - BLOCK ZERO CROSSINGS
How many times the signal CROSSES a runtime LEVEL during a block of N consecutive samples. N is a power of two chosen at RUN TIME on the EXP input pin (EXP = 10 means N = 1024). The cheapest frequency estimator there is: a clean sinusoid crosses exactly twice per period, so f = ZERO_CROSS * fs / (2*N). LEVEL is a PIN, latched per block, so with a slow baseline tracker on it the block becomes a discriminator-free pulse counter. Two conventions define the count: a sample sitting EXACTLY on the level is ignored - it is neither a crossing nor a break in the previous sign run - and the SIGN MEMORY CARRIES OVER across block boundaries, so no crossing is lost at the seams. The result is an integer count, MaxBlockExponent + 1 bits wide, which cannot overflow; there is no output Q format. One comparator, one counter, two flip-flops - no multiplier, no divider, no square root, and the result of a block is presented exactly 2 clocks after that block’s last sample. IN_DV is the only qualifier and there is deliberately no CE pin. Optional BUSY / INTEGRATING / SAMPLE_COUNT status outputs. Blocks of up to 2^20 samples out of the box, 2^31 if you ask for it.
Introduction
The Block Zero Crossings block chops the input stream into consecutive blocks of N samples and, at the end of each block, publishes how many times the signal crossed a reference level $\ell$ during that block. With $d_i = x_i - \ell$:
$$ \mathrm{ZERO_CROSS} = #{, i : \operatorname{sign}(d_i) \ne \operatorname{sign}(d_{i-1}) ,} $$
counted over the block, where samples with $d_i = 0$ take no part at all (see The two conventions below - they are the whole semantics of this block).
$\ell$ is driven on the LEVEL pin and defaults to 0 when the pin is left
unconnected, in which case the count degenerates into the usual zero
crossings.
N is a runtime input, not a property. You drive the exponent on the
EXP pin and the block size is $N = 2^{\mathrm{EXP}}$:
| EXP | N | EXP | N |
|---|---|---|---|
| 4 | 16 | 12 | 4096 |
| 6 | 64 | 16 | 65536 |
| 8 | 256 | 20 | 1048576 |
Because N is a power of two, every division by N in this family is an exact arithmetic shift - which is why the block size can be changed while the design is running, for free. This particular block does not even need one: it reports a raw count. If you want the crossing rate rather than the count, divide by N yourself downstream, where the shift is again free.
What it is FOR
-
The cheapest frequency estimator there is. For a clean sinusoid there are exactly two crossings per period, so
$$ f ;\approx; \frac{\mathrm{ZERO_CROSS} \cdot f_s}{2 N} $$
one comparator and one counter, no transform, no multiplier, no resonator. For a signal that is not a clean sinusoid the same number is still a perfectly good dominant-frequency / activity indicator.
-
A voicing / noisiness feature. The zero crossing rate is the classic cheap discriminator between tonal and noisy content, and between a signal and its absence.
-
A discriminator-free pulse counter. Drive
LEVELfrom a slow baseline tracker and each pulse that leaves and returns to the baseline contributes two crossings - a hit counter with no hysteresis logic and no dead time, whose threshold can be moved at run time.
Cost
One subtractor for $d = x - \ell$, one comparator, one counter of Max Block Exponent + 1 bits, a hold register for it and two flip-flops of sign memory. No multiplier, no divider, no square root, and no serial arithmetic: the tail is a constant 2 clocks.
When to use this instead of Block Statistics
The all-in-one Block Statistics block is not deprecated and computes this same crossing count among twenty other statistics. The rule is simple:
- you want several statistics of the SAME block - the crossing count and the RMS and the min/max of the same N samples - use Block Statistics. They share one accumulator and one serial tail, so the second and third statistic are nearly free.
- you want exactly one number - use this block. Then you synthesise only that number: the pin list, the logic and the tail are all that the crossing count needs, and nothing else reaches the synthesiser.
Two Block Statistics blocks side by side would duplicate the accumulators; two per-operator blocks side by side duplicate them too. One Block Statistics block never does.
Pin Description
IN_DV is high.
'1'. (There is deliberately no CE
pin - to stall the block, gate this.)
LEVEL during the block. Unsigned
integer, no Q format: its width is Max Block Exponent + 1 bits, which
cannot overflow - a block of N samples has at most N - 1 crossings.
Samples exactly ON the level are ignored (they neither count nor break the
run), and the sign memory carries over from the previous block. Updated on
the OUT_DV clock and on no other; it holds the previous block’s result
until then.
ZERO_CROSS is updated on this clock and on no other.
BUSY is still high here and falls on the next clock.
OUT_DV pulse, and it falls on the clock after. On a continuous stream it
simply stays high. Present on the symbol only when Enable BUSY = YES.
How many samples have been accumulated so far in the current block: 1 after the first, N after the N-th. It is NOT cleared at the end of a block
- it HOLDS the final count through the tail and past
OUT_DV, until the first sample of the next block takes it back to 1, so on theOUT_DVclock it reads the length of the block being presented - the N you need to turn the count into a rate. OnlyRESETclears it to 0. Fixed 32 bits. Present on the symbol only when Enable SAMPLE_COUNT = YES.
Properties
Number of INTEGER bits of the input sample (the sign, when present, uses one of them).
Integer bits of the input sample (the sign, when present, uses one of them). 1..64. Default 16. The LEVEL pin shares this format.Default: 16
Options: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
Number of FRACTIONAL bits of the input sample, i.e. the bits to the right of the binary point. Total width = integer + fractional bits, and must not exceed 64.
Fractional bits of the input sample. 0..64. Total input width must be 2..64 bits. Default 0. The LEVEL pin shares this format.Default: 0
Options: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
Select whether the input sample is signed (two’s complement) or unsigned.
SIGNED (two’s complement) or UNSIGNED input. Default SIGNED. An UNSIGNED input costs one extra bit internally, because a sample has to be promoted to signed before the deviation from the level can be formed.Default: SIGNED
Options: UNSIGNED SIGNED
Largest block-size exponent the accumulators are sized for: the block can be up to 2^MaxBlockExponent samples long. The EXP input is clamped to this value at run time. Raising it widens the internal accumulators, and ON THE BLOCKS WHOSE SERIAL ENGINES ARE SIZED FROM THOSE ACCUMULATORS (Coefficient of Variation, SNR, Skewness, Kurtosis, Correlation, Autocorrelation, Linear Regression) it also LENGTHENS THE SERIAL TAIL – even when the runtime EXP is small. Keep it at the largest block you actually use. The default of 20 covers blocks of up to 1048576 samples.
Largest block-size exponent the block counter is sized for: the block can be up to $2^{\text{MaxBlockExponent}}$ samples long, and theEXP input is
clamped to this value at run time. It also SIZES THE OUTPUT here: the
ZERO_CROSS pin is Max Block Exponent + 1 bits wide. It does NOT
lengthen the latency of this block, which is a constant 2 clocks. Keep it
at the largest block you actually use. 1..31, default 20, i.e. blocks
of up to 1048576 samples out of the box.
Default: 20
Options: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
YES: the BUSY (high from the first sample of a block until its result is out – it COVERS THE SERIAL TAIL, and its last high clock IS the OUT_DV pulse) pin is present. NO: the pin AND all of its logic are removed BEFORE synthesis, so nothing is paid for it.
YES: theBUSY pin exists. It is high from the first sample of a block
until its result is out, tail included, and its last high clock is the
OUT_DV pulse. NO: the pin and its register are removed before synthesis.
Default NO.
Default: NO
Options: NO YES
YES: the INTEGRATING (high only while the block is ACCUMULATING; it drops as soon as the N-th sample has been taken and the tail starts, so BUSY-and-not-INTEGRATING means ‘computing’) pin is present. NO: the pin AND all of its logic are removed BEFORE synthesis, so nothing is paid for it.
YES: theINTEGRATING pin exists. It is high only while the block is
accumulating, so BUSY high with INTEGRATING low means “the samples are
all in, I am computing”. NO: the pin and its register are removed.
Default NO.
Default: NO
Options: NO YES
YES: the SAMPLE_COUNT (32 bit, how many samples have been accumulated so far in the current block: 1 after the first, N after the N-th. It is NOT cleared at the block end – it holds N until the NEXT block’s first accepted sample takes it back to 1. On a CONTINUOUS stream that happens DURING the serial tail, so at OUT_DV it reads how far into the next block the input has already got, NOT N. To capture the length of the block being presented, latch SAMPLE_COUNT on the clock INTEGRATING falls – that one always reads N) pin is present. NO: the pin AND all of its logic are removed BEFORE synthesis, so nothing is paid for it.
YES: theSAMPLE_COUNT pin exists - a fixed 32 bit count of the samples
accumulated so far in the current block, holding the final count through
the tail and past OUT_DV. NO: the pin and its counter are removed.
Default NO.
Default: NO
Options: NO YES
ROUND: round to nearest when a result has to be requantised into a coarser output format. TRUNCATE: drop the bits (cheaper, adds a negative bias).
ROUND / TRUNCATE. No effect on this block. The result is an integer count presented at its natural width and nothing is ever requantised, so there is nothing to round. The property is emitted for uniformity across the Block statistic family, so that one property page fits every member. Default ROUND.Default: ROUND
Options: TRUNCATE ROUND
YES: clip to the largest representable value of each output format (symmetric for signed formats). NO: wrap around.
YES / NO. No effect on this block. The count cannot overflow its Max Block Exponent + 1 bit output - a block of N samples has at most N - 1 crossings - and nothing is ever requantised, so there is nothing to clip or wrap. The property is emitted for uniformity across the family. Default YES.Default: YES
Options: NO YES
The two conventions
These are not implementation details. They are the semantics of this block, and unlike on the area blocks they are directly observable, because they change a count.
1. A sample exactly ON the level is IGNORED
A sample with $d = 0$ is not a crossing, and it does not break the run of the previous sign. It falls through the detector without touching either the counter or the memory. Consequences:
- a flat signal sitting exactly on the level reports 0 crossings, not one per sample;
- a signal that touches the level and comes back on the same side reports none - it never got to the other side;
- a signal that touches the level and continues through it reports one crossing, at the first sample that is genuinely on the far side.
This is the convention that makes the count usable on quantised data, where “exactly on the threshold” is not a corner case but a common event. The naive alternative - treating $d = 0$ as a side of its own - would turn a signal resting on the level into a crossing generator.
2. The sign memory CARRIES OVER across blocks
Only the COUNTER is cleared at a block boundary. The predecessor of the first sample of a block is the last sample of the previous block whose deviation was not zero. The stream is continuous, and a per-block reset of the memory would systematically lose one crossing at every boundary - exactly the crossing that straddles the seam.
Two consequences, both deliberate:
- the memory is cleared only by
RESET, so the very first sample after a reset can never produce a crossing (there is nothing to compare it with); - if
LEVELchanges between two blocks, the carried sign was measured against the OLD level, so exactly one comparison at the start of the new block is still referred to the previous level. From the second sample on, everything refers to the new one.
Accumulation and IN_DV
IN_DV is the only qualifier. It says “this clock carries a sample”: a
sample is examined, updates the sign memory, and counts towards N, exactly on
the clocks where IN_DV is high. Clocks with IN_DV low are ignored
completely - whatever sits on IN during them cannot corrupt the block, and
in particular cannot fake a crossing - while the tail keeps running, which is
what you want: the tail has nothing to do with the input stream.
Unconnected, IN_DV ties to '1', EXP ties to 10 (N = 1024) and LEVEL
ties to all zeros, so the block free-runs with nothing wired except IN
and counts true zero crossings.
There is deliberately no CE pin. On the all-in-one Block Statistics block an earlier revision had one, and it did not survive synthesis: with nothing but internal state gated by it, Vitis could reason the frozen path away and delete the port from the generated entity while SciCompiler’s wrapper still wired it, which failed a real Vivado build with [VRFC 10-718] formal port <ce> does not exist in entity. The whole per-operator family was built without one. To stall this block, gate its
IN_DV- a block that only looks at samples onIN_DVhas no need to be frozen.
The LEVEL pin
LEVEL is an input pin in the input Q format, not a property, so it can
be swept, servo’d or driven from a slow baseline tracker while the design
runs. Like EXP, it is latched on the first accepted sample of a block
and held for that whole block, so a mid-block change takes effect on the
NEXT block and a count is never a mixture of two levels - with the single
documented exception of the carried sign described above.
When EXP changes
EXP is clamped to Max Block Exponent and then latched on the first
accepted sample of a block, and held for that whole block. A change
therefore takes effect on the NEXT block: a block in progress always
finishes against the N it was started with, and a block is never emitted
against a different N than the one it was counted over. You can sweep EXP
from a register interface while the stream runs and every result stays
self-consistent.
Timing: the latency contract
OUT_DV pulses for one clock, L clocks after the clock on which the N-th
sample of the block was accepted - not when that sample arrives.
ZERO_CROSS is updated on that same clock and on no other. For this block
$$ L = 2 $$
and it is a constant: there is no serial arithmetic here at all, so L does
not depend on the input width, on Max Block Exponent or on EXP. The two
clocks are one to enter the final state and one to present the registered
count.
The rule that governs the whole family is that the tail of one block must finish before the next block completes, i.e.
$$ 2^{\mathrm{EXP}} \ge L $$
If a block completes while the previous tail is still running, that block’s
result is DROPPED: no OUT_DV for it, the counter and the sign memory are
unaffected and later blocks come out correctly, but a result is silently
skipped. There is no error pin for it.
With $L = 2$ that condition is $2^{\mathrm{EXP}} \ge 2$, i.e. EXP $\ge$ 1, so it cannot bite here: the only value that violates it is EXP = 0, a block of a single sample - which could not contain a crossing anyway. The blocks where this rule really matters are the ones with a serial tail - Block RMS, Block Variance, Block Std Dev and Block Crest Factor, whose L runs to tens of clocks and whose minimum usable exponent the compiler prints in the compilation log.
Knowing where the block is: BUSY, INTEGRATING and SAMPLE_COUNT
Three optional status outputs, all defaulting to NO. They answer different questions:
INTEGRATING |
BUSY |
|
|---|---|---|
| counting the block | 1 | 1 |
| tail computing | 0 | 1 |
| idle | 0 | 0 |
Every output of this block is a register, so each status bit is observed on the clock after the event that sets it:
INTEGRATINGrises on the clock after the FIRST sample of a block is accepted and falls on the clock after the N-th - it is high exactly while the block is ACCUMULATING.BUSYcovers the accumulation and the tail. It rises withINTEGRATING, stays high across the tail, and its LAST HIGH CLOCK IS THEOUT_DVPULSE; it falls on the clock after.- On a continuous stream the next block starts before the previous tail
ends, so
BUSYnever drops andINTEGRATINGdips for exactly one clock per block boundary - which makes it a free block marker. SAMPLE_COUNTis a fixed 32 bits and reads 1 after the first accepted sample, N after the N-th. It is NOT cleared at the block end: it HOLDS N through the tail and pastOUT_DV, until the first sample of the next block takes it back to 1. So on theOUT_DVclock it reads the length of the block being presented - which is the useful thing to latch alongside the result, and exactly what you need to turn the count into a rate. OnlyRESETclears it to 0.
Q formats, and why the output has none
IN and LEVEL share one fixed point format (integer bits, fractional bits,
sign), the same convention as the Fixed P. family - they are compared
against each other, so a separate format for the level would only be a way to
get it wrong.
The output has no Q format at all. ZERO_CROSS is an integer count,
so its width is not a user choice: it is Max Block Exponent + 1 bits, which
cannot overflow - a block of N samples has at most N - 1 crossings, and
$N - 1 < 2^{\text{MaxBlockExponent}+1}$ always.
As a consequence this block never requantises anything. The Rounding and Saturation properties exist for uniformity across the family - one property page fits every Block statistic - but they have no effect whatsoever on this block.
Accuracy
There is none to discuss: the result is an exact integer produced by a counter. The host regression demands tolerance ZERO.
Verification
The core is regression tested by a host-side csim harness
(tb/block-ops/run_tb.ps1) that runs one simulated clock at a time and
follows OUT_DV. The expected values come from tb/block-ops/gen_golden.py,
which evaluates the definition above in exact integer arithmetic and shares no
algorithm with the core; the tolerance is 0. Crossing coverage includes
pseudo-random and sinusoidal inputs at two exponents, a signal with samples
sitting exactly on the level (which must report the runs unbroken), a
constant signal parked on a non-zero level (which must report 0), a LEVEL
pin that changes between blocks - pinning the one comparison that is still
referred to the old level - and maximum positive / maximum negative inputs.
The status outputs are checked clock by clock against the contract above.
A cross-check compiles this core and the all-in-one block_stats.cpp into the
same binary, drives them with identical stimulus, and compares the two clock
by clock, which is what pins the carried sign memory across block boundaries.