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:
- Builds a window of addresses centred on the peak
- Reads each value from memory one by one
- Accumulates them in high-precision fixed-point arithmetic
- Divides the sum by the count of valid samples
- 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:
| Input | Action |
|---|---|
NaN or ±Infinity (exp == 11111) | Returns 0; caller sets nan_seen_r flag |
Denormal (exp == 00000) | Converts using f × 2^(FRAC_BITS−24) |
| Zero | Returns 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:
- Finding the highest set bit (MSB position =
lead) — this gives the FP16 exponent - Left-aligning the value so
leadsits at the top — the next 10 bits are the mantissa - 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:
| Condition | Output |
|---|---|
val == 0 | +0.0 |
exp_b >= 31 | +Infinity |
exp_b <= 0 | +0.0 (underflow flush) |
| Mantissa all-ones rounds up | Exponent 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
| Signal | When stable |
|---|---|
window_start_o, window_end_o | After ST_CALC_WIN — well before done_o |
window_avg_o | Registered in ST_CONVERT → valid on the first cycle of ST_DONE |
done_o, window_valid_o | Combinational decode of state_r == ST_DONE |
busy_o | Combinational decode; low in ST_IDLE and ST_DONE, high otherwise |
The Divider — Deep Dive
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.