Skip to main content

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.

The FSM​

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?MantissaExponent adjustment
1bit 11quotient[10:1]val_exp − avg_exp + 15
0bit 10quotient[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.