From a5abef767288c84804ec83dd8e8f24aadd9a4f31 Mon Sep 17 00:00:00 2001 From: Abraxas3d Date: Sat, 16 May 2026 18:16:50 -0700 Subject: [PATCH] Cast PROTECTION FROM OVERFLOW: saturate EMA sum to PROD_W range The combinational sum assignment summed two shift_left'd signed terms without saturation, which could wrap past +/- 2^(PROD_W-1) when sustained high-amplitude inputs drove the accumulator past the signed PROD_W-bit range. Once wrapped, the cascade output flipped negative and the EMA settled into a stable wrong-sign equilibrium with no recovery. Symptom in our use case (polyphase channelizer power detector, AMSAT-UK FunCube+ MDT-SIC): channel-0 EMA-2 sum oscillated between near +2^42 and near -2^42 every ~280us under sustained DC input, ending tests in a negative-pinned state that contaminated subsequent measurements. Fix: compute the sum in EXTRA_W=4 wider intermediate (sum_wide), then clamp to +/- (2^(PROD_W-1) - 1) before assigning to sum. Preserves bit-exact behavior for in-range values; clamps gracefully at the limits. Verified: full 1ms simulation across 64 EMA cascades shows MSB stays 0 throughout, no wrap events, all 9 testbench assertions pass. --- src/lowpass_ema.vhd | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/lowpass_ema.vhd b/src/lowpass_ema.vhd index 9f1aad5..b669693 100644 --- a/src/lowpass_ema.vhd +++ b/src/lowpass_ema.vhd @@ -126,6 +126,13 @@ ARCHITECTURE rtl OF lowpass_ema IS SIGNAL avg_rms : REAL := 0.0; SIGNAL avg_real : REAL := 0.0; + CONSTANT EXTRA_W : NATURAL := 4; -- guard bits: max shift_left + 1 for the add + CONSTANT SAT_W : NATURAL := PROD_W + EXTRA_W; + CONSTANT SAT_MAX_V : signed(SAT_W -1 DOWNTO 0) := to_signed(2**(PROD_W-1) - 1, SAT_W); + CONSTANT SAT_MIN_V : signed(SAT_W -1 DOWNTO 0) := to_signed(-(2**(PROD_W-1)), SAT_W); + + SIGNAL sum_wide : signed(SAT_W -1 DOWNTO 0); + BEGIN @@ -144,8 +151,18 @@ ASSERT False REPORT "FULL_SCALE: " & real'image(FULL_SCALE) SEVERITY NOTE; -- pragma translate_on -sum <= shift_left(resize(mult_data, PROD_W), MULT_DATA_SHIFT) + - shift_left(resize(mult_sum, PROD_W), MULT_SUM_SHIFT); +-- ============================================================================ +-- BEFORE: +-- sum <= shift_left(resize(mult_data, PROD_W), MULT_DATA_SHIFT) + +-- shift_left(resize(mult_sum, PROD_W), MULT_SUM_SHIFT); +-- ============================================================================ + +sum_wide <= shift_left(resize(mult_data, SAT_W), MULT_DATA_SHIFT) + + shift_left(resize(mult_sum, SAT_W), MULT_SUM_SHIFT); + +sum <= to_signed(2**(PROD_W-1) - 1, PROD_W) WHEN sum_wide > SAT_MAX_V ELSE + to_signed(-(2**(PROD_W-1)), PROD_W) WHEN sum_wide < SAT_MIN_V ELSE + resize(sum_wide, PROD_W); sum_shift <= resize(shift_right(sum, SUM_SHIFT_W), PROD_W - SUM_SHIFT_W);