Skip to main content

Calculate Average Note

Here's a guided walkthrough of the entire module from top to bottom.


1. What Does It Do?​

Given a memory full of FP16 values and a "peak" address (max_addr_i), the module:

  1. Builds a window of addresses centred on the peak
  2. Reads each value from memory one by one
  3. Accumulates them in high-precision fixed-point arithmetic
  4. Divides the sum by the count of valid samples
  5. Converts the result back to FP16 and signals completion

2. The Big Picture: FSM​

The entire operation is controlled by a 9-state FSM:

start_i ↑
│
ST_IDLE
│
ST_CALC_WIN ← compute window boundaries (1 cycle)
│
┌─► ST_MEM_READ ← pulse mem_rd_en_o for 1 cycle
│ │
│ ST_MEM_WAIT ← stall until mem_rd_data_valid_i
│ │
│ ST_ACCUM ← convert FP16→fixed, accumulate
│ │
└────────┘ (loop back if more samples remain)
│ (exit when cur_addr_r == win_e_r)
│
ST_DIV_INIT ← latch dividend/divisor
│
ST_DIVIDING ← 48 cycles of binary long division
│
ST_CONVERT ← fixed-point quotient → FP16
│
ST_DONE ← hold until start_i goes low

3. Key Design Techniques​

A. FP16 → Fixed-Point Conversion (fp16_to_fix)​

Rather than doing floating-point addition (which requires aligning exponents each time), all values are first converted to a common Q24 fixed-point format — i.e., every value is multiplied by 2²⁴ and stored as an integer. This makes accumulation a simple integer add.

FP16 normal value = (1024 + mantissa) × 2^(exponent − 25)
Fixed-point repr = (1024 + mantissa) × 2^(exponent − 1)
↑
shift = exp−1, range [0,29]

Special cases handled:

InputAction
NaN or ±Infinity (exp == 11111)Returns 0; caller sets nan_seen_r flag
Denormal (exp == 00000)Converts using f × 2^(FRAC_BITS−24)
ZeroReturns 0 directly

B. The Accumulator​

accum_r is a 48-bit integer accumulating the fixed-point sum. It can safely hold up to 20 max-FP16 values:

max FP16 ≈ 65504
→ fixed-point ≈ 65504 × 2²⁴ ≈ 1.1 × 10¹²
→ 20 samples ≈ 2.2 × 10¹³ << 2⁴⁸ ≈ 2.8 × 10¹⁴ ✓

sample_cnt_r counts only valid (non-NaN/Inf) samples, since those are excluded.


C. Sequential Binary Divider (ST_DIV_INIT + ST_DIVIDING)​

A combinational divider for 48-bit operands would be enormous in hardware. Instead, a restoring binary long-division algorithm runs one bit per clock cycle, for exactly 48 cycles total.

The algorithm works like manual long division in binary:

For step = 47 downto 0:
rem_shifted = { remainder[46:0], dividend[step] } ← shift in next bit
if rem_shifted >= divisor:
quotient_bit = 1
remainder = rem_shifted − divisor
else:
quotient_bit = 0
remainder = rem_shifted
quotient = { quotient[46:0], quotient_bit } ← shift in new bit

After 48 cycles, dv_quot_r holds ⌊ accumulator / sample_count ⌋.

Division-by-zero guard: if sample_cnt_r == 0 (every sample was NaN/Inf), the divisor is set to 1 so the result is 0/1 = 0 → output is +0.0 in FP16.


D. Fixed-Point → FP16 (fix_to_fp16)​

The reverse conversion works by:

  1. Finding the highest set bit (MSB position = lead) — this gives the FP16 exponent
  2. Left-aligning the value so lead sits at the top — the next 10 bits are the mantissa
  3. Rounding: the first discarded bit (rb) triggers a round-up if set
FP16 exponent (biased) = lead − FRAC_BITS + 15
FP16 mantissa = bits [lead−1 : lead−10] (after alignment)

Edge cases handled:

ConditionOutput
val == 0+0.0
exp_b >= 31+Infinity
exp_b <= 0+0.0 (underflow flush)
Mantissa all-ones rounds upExponent incremented; re-checks for Inf

E. start_i Level Protocol​

start_i: _____|‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾|_____
↑ rising edge detected ↓ falling edge
│ → FSM starts │ → FSM leaves ST_DONE → ST_IDLE
done_o: ___________________________|‾‾‾‾‾|___
↑ ST_DONE entered

The flip-flop start_q delays start_i by one cycle. start_rise_w = start_i & ~start_q fires for exactly one cycle on the 0→1 transition, triggering the computation without a pulse generator in the upstream controller.


F. Window Boundary Clamping​

The window is placed symmetrically around max_addr:

calc_s = max(max_addr − ⌊window_size/2⌋, range_start)
calc_e = min(calc_s + window_size − 1, range_end)

This guarantees max_addr is always inside the window even when it's near the edge of the valid range.


4. Output Timing Guarantee​

SignalWhen stable
window_start_o, window_end_oAfter ST_CALC_WIN — well before done_o
window_avg_oRegistered in ST_CONVERT → valid on the first cycle of ST_DONE
done_o, window_valid_oCombinational decode of state_r == ST_DONE
busy_oCombinational decode; low in ST_IDLE and ST_DONE, high otherwise

The Divider — Deep Dive​

baseconvert

Dividend: base(10): 107 → base(2): 0110_1011
Quotient: base(2): 0001_0101 → base(10): 21
Remainder: base(2): 0000_0010 → base(10): 2

The Divider — How It Works​

The algorithm is exactly binary long division, the same procedure you were taught in school but in base 2. You process the dividend one bit at a time from MSB to LSB, building the quotient one bit at a time in the same order.

The core insight behind the rem_shifted construction:

rem_shifted = { rem[N−2:0], dividend[step] }

This is a left-shift of the partial remainder with the next dividend bit shifted in at the bottom — identical to how you "bring down" the next digit in pencil-and-paper division. Then a single comparison and conditional subtract answers the question: "does the divisor fit into this partial value?" Green rows in the table are the cycles where it does fit (quotient bit = 1, subtract happens). Gray rows are where it doesn't (quotient bit = 0, carry it forward).

After exactly N cycles (48 in the real module), every bit of dv_quot_r has been filled MSB-first and the result is the floor-division of the accumulator by the sample count.


FP16 Conversion — The Two Functions​

fp16_to_fix (FP16 → fixed-point)​

Normal​

The job is to multiply the FP16 value by 2²⁴ so all values share the same binary point.


For any normal half-precision floating-point number, the true value is:

bias = 2^(k-1) - 1 = 15
true_value = (-1)^sign * 2^(exp-15)* (1 + mantissa/1024)

For any positive normal number, the true value is:

2^(exp-15) * (1 + mantissa/1024)
= 2^(exp-15-10) * 2^10 * (1 + mantissa/1024)
= 2^(exp-25) * (1024 + mantissa)
= (1024 + mantissa) × 2^(exp − 25)

For a positive normal number the true value can be expressed as:

(1024 + mantissa) × 2^(exp − 25)

Multiplying by 2²⁴ gives:

(1024 + mantissa) × 2^(exp − 25 + 24) = (1024 + mantissa) << (exp − 1)

That shift is exp − 1(2^0 - 1 ~ 2^4 + 2^3 + 2^2 + 2^1 + 0* 2^0 - 1)(acturally 0~29), which for valid FP16 exponents lies in [0, 29] — so the hardware synthesises as a plain barrel shifter with no negative-shift logic needed. The implicit leading 1 (shown in amber in the widget) is the key: the stored mantissa only has 10 bits, but the full 11-bit value {1, mantissa} = 1024 + mantissa is what actually gets shifted.

Denormal​

For denormals, the exponent is zero and there is no implicit leading 1. The value is:

(-1)^sign × 2^(-14) × (0 + mantissa/1024)

For a positive denormal, the true value is:

= mantissa × 2^(-14-10) = mantissa × 2^(-24)

Multiplying by 2²⁴ gives:

That shift is (-24 + 24) = 0. So the hardware is just a passthrough for the mantissa bits in this case

fix_to_fp16 (fixed-point → FP16)​

This is the reverse. The fixed-point quotient is an integer representing result × 2²⁴. To find the FP16 exponent, you need to know where the most significant 1 bit sits:

lead = position of highest set bit
biased_exp = lead − 24 + 15

Subtracting 24 removes the fixed-point scale factor; adding 15 applies the FP16 bias. The mantissa is then just the 10 bits immediately below the leading 1 (extracted by left-aligning the value so the leading 1 sits at the MSB, then taking the next 10 bits). The round bit is the very next bit after those 10, and if it's set you add 1 to the mantissa — with a carry chain that can propagate into the exponent if the mantissa is all-ones. Try entering values like 1.5, 0.5, 100.0, or 0.001 in the widget to see how each field changes.