Normalization Note
Here's a guided walkthrough with two interactive visuals — the FSM that drives everything, then a step-through of what happens to each individual sample.
1. What the module does in one sentence
For every one of the NUM_VALUES FP16 samples in memory: read it, divide it by avg_value_i, write the result (clamped to 1.0 if it would exceed 1) back to the same address. The whole operation is driven by an FSM that loops until all addresses are processed.
2. The FSM - the engine of the module
The loop-back dashed arrow from ST_MEM_WRITE back to ST_MEM_READ is the core of the design — the FSM runs that inner loop 2600 times, once per sample. Only after the last address does it fall through to ST_DONE.
3. Per-sample processing — the two paths
Every sample takes one of two routes through the FSM. The decision happens in ST_PROCESS based on what the sample and the average actually are:
4. The three core concepts explained
A. Why a bit-compare works for val ≥ avg
All values are guaranteed positive, so the sign bit is always 0. IEEE 754 is cleverly designed so that for positive numbers, the 15-bit field [14:0] is magnitude-ordered — a larger floating-point value always has a larger 15-bit pattern. That means val[14:0] >= avg[14:0] is a valid floating-point comparison with zero hardware: just a 15-bit unsigned comparator. No exponent alignment, no mantissa unpacking needed.
B. Why the dividend is {mA, 11 zeros} (22 bits)
To divide two FP16 mantissas and get a 10-bit mantissa result, you need the quotient mA/mB expressed with 10–11 bits of precision. The trick is to pre-scale mA upward by 2¹¹ before dividing. Then:
quotient = floor(mA × 2¹¹ / mB) ∈ [2¹⁰, 2¹²)
This gives at least 10 bits in the quotient, which is exactly what the FP16 mantissa field needs. The 22-cycle restoring divider produces this one bit per cycle, MSB-first.
C. Two normalization cases in ST_ROUND
The quotient always lands in [2¹⁰, 2¹²) — so only two cases exist based on bit 11:
quotient[11] | Where is the implicit leading 1? | Mantissa | Exponent adjustment |
|---|---|---|---|
1 | bit 11 | quotient[10:1] | val_exp − avg_exp + 15 |
0 | bit 10 | quotient[9:0] | val_exp − avg_exp + 14 |
After picking the mantissa, the round bit (first discarded quotient bit) is checked for round-to-nearest. A final signed 8-bit exponent bounds check flushes underflow to +0.0 and clamps overflow to +1.0.
Try entering values like 0.6 / 0.8, then 1.2 / 0.8 (triggers the clamp), then NaN inputs to see all paths of ST_PROCESS activate in the widget above.
ST_ROUND does one job: take the raw 22-bit integer quotient from the divider and produce a valid 16-bit FP16 number. It runs in a single clock cycle using pure combinational logic — no iteration.
The five division steps in detail
ST_ROUND does one job: take the raw 22-bit integer quotient from the divider and produce a valid 16-bit FP16 number. It runs in a single clock cycle using pure combinational logic — no iteration.
Step 1 — Receive the quotient
The 22-cycle divider produces quotient = ⌊ mA × 2¹¹ / mB ⌋. This is always in the range [2¹⁰, 2¹²) — that is, 11 or 12 significant bits — because both mA and mB are full 11-bit mantissas in [1024, 2047].
Step 2 — Normalise: find the implicit leading 1
This is the most important step. FP16 stores a mantissa where there is always an invisible leading 1 (for normal numbers). So you need to identify where that leading 1 is in the quotient:
quotient[11] = 1 → leading 1 is at bit 11
mantissa = quotient[10:1] (10 bits, shifted right)
round bit = quotient[0]
exponent = val_exp − avg_exp + 15
quotient[11] = 0 → leading 1 must be at bit 10 (since quotient ≥ 2¹⁰)
mantissa = quotient[9:0] (10 bits, no shift)
no round bit
exponent = val_exp − avg_exp + 14
The exponent formula works because dividing two FP16 numbers subtracts their exponents, just like subtracting logarithms. The +15 or +14 re-applies the FP16 bias (15) while accounting for which bit position the leading 1 was at.
Step 3 — Round
Only the quotient[11]=1 case produces a round bit (the first bit discarded after the 10-bit mantissa). If it's 1, add 1 to the mantissa. There's one special sub-case: if the mantissa is already all-ones (1111111111), adding 1 causes a carry that increments the exponent and resets the mantissa to zero — exactly the same as how 0.999... → 1.0 rounding works in decimal.
Step 4 — Bounds check the exponent
exp ≤ 0 → result is too small even for FP16 denormals → flush to +0.0
exp ≥ 31 → exponent field all-ones = Inf/NaN in FP16 → clamp to +1.0
otherwise → valid normal FP16
In practice, the clamp-to-+1.0 path here is unreachable — any sample that would produce a result ≥ 1.0 was already caught by the val ≥ avg bit-compare in ST_PROCESS and never reached the divider.
Step 5 — Assemble
Pack the three fields: {1'b0, exp[4:0], mant[9:0]}. The sign bit is always 0 because all inputs are guaranteed positive. The result register result_r holds this for one cycle before ST_MEM_WRITE puts it on the memory bus.
Try entering values where the sample is much smaller than the average (e.g. 0.1 / 0.9) to see the quotient[11]=0 path, or values close to equal (e.g. 0.79 / 0.8) to see rounding kick in.