Skip to main content

Real to Floating-point Conversion

// Special cases handled:
// +/-0 → fp16 ±0
// +/-Inf → fp16 ±Inf
// NaN → fp16 quiet NaN (0_11111_1000000000)
// overflow → fp16 ±Inf
// underflow → fp16 ±0 (flush-to-zero; subnormal path also included)
// subnormals → fp16 subnormal (when exp in range)

// ---------------------------------------------------------------------------
// Bit-level access to a 64-bit double via $realtobits / $bitstoreal
// ---------------------------------------------------------------------------
localparam int SIGN_BIT_F64 = 63;
localparam int EXP_HI_F64 = 62;
localparam int EXP_LO_F64 = 52;
localparam int EXP_BIAS_F64 = 1023;
localparam int EXP_BIAS_F16 = 15;
localparam int MANT_BITS_F64 = 52;
localparam int MANT_BITS_F16 = 10;

// fp16 exponent special values
localparam logic [4:0] EXP_INF_F16 = 5'b11111;
localparam logic [4:0] EXP_ZERO_F16 = 5'b00000;

// -------------------------------------------------------------------------
// Function: real_to_fp16
//
// Returns the 16-bit half-precision representation of 'r'.
// Rounding: round-to-nearest-even (ties round to even).
// -------------------------------------------------------------------------
function automatic logic [15:0] real_to_fp16(input real r);
// ---- unpack the double ----
logic [63:0] bits64;
logic sign;
logic [10:0] exp64; // raw biased exponent (11 bits)
logic [51:0] mant64; // significand fraction bits
int exp_unbiased; // true exponent (signed)
int exp16; // re-biased for fp16

// Working variables for rounding
logic [10:0] mant_shifted; // 10 mantissa bits + 1 guard bit
logic guard, round_bit, sticky;
logic [9:0] mant16;
logic [4:0] exp16_bits;
logic [15:0] result;

1. Bit extraction​

  • $realtobits() gives raw access to the IEEE 754 double's sign, 11-bit exponent, and 52-bit mantissa without any floating-point arithmetic.
// ------------------------------------------------------------------
// 1. Extract fields from the 64-bit representation
// ------------------------------------------------------------------
bits64 = $realtobits(r);
sign = bits64[SIGN_BIT_F64];
exp64 = bits64[EXP_HI_F64 : EXP_LO_F64];
mant64 = bits64[MANT_BITS_F64-1 : 0];

2. Special-value pass-through​

  • NaN, ±Inf, and ±0 are detected by their canonical bit patterns and mapped directly to the equivalent fp16 encodings.
  • A fp64 NaN always produces a quiet fp16 NaN.
// ------------------------------------------------------------------
// 2. Handle IEEE special values
// ------------------------------------------------------------------

// NaN: exponent all-ones AND non-zero mantissa
if (exp64 == 11'h7FF && mant64 != '0) begin
result = {sign, EXP_INF_F16, 10'b10_0000_0000}; // quiet NaN
return result;
end

// Infinity: exponent all-ones AND zero mantissa
if (exp64 == 11'h7FF && mant64 == '0) begin
result = {sign, EXP_INF_F16, 10'b00_0000_0000}; // ±Inf
return result;
end

// Zero (both +0 and -0)
if (exp64 == '0 && mant64 == '0) begin
result = {sign, 15'b0};
return result;
end

3. Exponent rebias​

  • the unbiased exponent (exp64 − 1023) is rebased to fp16's bias of 15.
  • Values above 30 overflow to ±Inf.
// ------------------------------------------------------------------
// 3. Compute unbiased exponent and re-bias for fp16
// fp64 bias = 1023, fp16 bias = 15
// exp_unbiased = exp64 - 1023
// exp16 = exp_unbiased + 15
// ------------------------------------------------------------------
if (exp64 == '0) begin
// fp64 subnormal input: treat as denormalized, exp = -1022
exp_unbiased = -1022;
end else begin
exp_unbiased = int'(exp64) - EXP_BIAS_F64;
end
exp16 = exp_unbiased + EXP_BIAS_F16;

// ------------------------------------------------------------------
// 4. Overflow → fp16 ±Inf
// fp16 max biased exp = 30 (11110); 31 is reserved for Inf/NaN
// ------------------------------------------------------------------
if (exp16 >= 31) begin
result = {sign, EXP_INF_F16, 10'b00_0000_0000};
return result;
end

4. Normal rounding (round-to-nearest-even)​

  • the top 10 bits of the 52-bit fp64 mantissa become the fp16 mantissa.
  • Bit 41 is the guard bit;
  • bits 40:0 are OR-reduced into a sticky bit.
  • The classic guard & (sticky | lsb) expression implements tie-to-even correctly.
  • A rounding carry that pushes the mantissa to 0x400 increments the exponent automatically.
// ------------------------------------------------------------------
// 5. Normal numbers: 1 <= exp16 <= 30
// Mantissa: take top 10 bits of mant64, apply round-to-nearest-even
//
// fp64 mantissa is 52 bits wide.
// fp16 mantissa is 10 bits wide.
// We need bits [51:42] for the result, bit [41] as guard,
// bits [40:0] ORed together as sticky.
// ------------------------------------------------------------------
if (exp16 >= 1) begin
mant16 = mant64[51:42]; // top 10 bits
guard = mant64[41]; // guard bit
sticky = |mant64[40:0]; // sticky = OR of remaining bits
round_bit = guard & (sticky | mant16[0]); // round-to-nearest-even

mant16 = mant16 + {9'b0, round_bit};

// Check if rounding caused mantissa overflow (0x3FF -> 0x400)
if (mant16 == 10'h3FF + 10'h001) begin
mant16 = '0;
exp16 = exp16 + 1;
// Re-check overflow after round-up
if (exp16 >= 31) begin
result = {sign, EXP_INF_F16, 10'b00_0000_0000};
return result;
end
end

exp16_bits = exp16[4:0];
result = {sign, exp16_bits, mant16};
return result;
end

5. Subnormal generation​

  • when the rebased exponent falls to 0 or below, the implicit leading 1 is reintroduced and the full 11-bit significand is right-shifted by 1 − exp16 positions.
  • Guard and sticky bits are recalculated for the shifted position and the same round-to-nearest-even rule is applied.

6. Flush to zero​

  • anything whose shift amount exceeds 11 bits is too small to represent even as a fp16 subnormal and is flushed to ±0.
// ------------------------------------------------------------------
// 6. Subnormal fp16 result: exp16 <= 0
// Value = (-1)^s * 2^(-14) * (0.mantissa)
// We need to right-shift the significand by (1 - exp16) positions
// to produce a subnormal with implicit leading 1 absorbed.
//
// shift_amount = 1 - exp16 (ranges 1..24 for representable subnormals;
// >= 25 flushes to zero)
// ------------------------------------------------------------------
begin
int shift_amount;
logic [10:0] full_mant; // 1 (implicit) + 10 bits
logic [10:0] shifted_mant;

shift_amount = 1 - exp16; // positive integer

if (shift_amount > 11) begin
// Too small to represent even as a subnormal → flush to ±0
result = {sign, 15'b0};
return result;
end

// Reconstruct full significand with implicit leading 1
// Use top 10 bits of fp64 mantissa
full_mant = {1'b1, mant64[51:42]}; // 11 bits

// Shift right; capture guard and sticky for rounding
shifted_mant = full_mant >> shift_amount;
guard = (shift_amount <= 11) ?
full_mant[shift_amount - 1] : 1'b0;
// Sticky: any bit below the guard position
sticky = 1'b0;
for (int i = 0; i < shift_amount - 1; i++) begin
sticky = sticky | full_mant[i];
end
sticky = sticky | (|mant64[41:0]); // include lower fp64 bits

round_bit = guard & (sticky | shifted_mant[0]);
shifted_mant = shifted_mant + {10'b0, round_bit};

mant16 = shifted_mant[9:0];
result = {sign, EXP_ZERO_F16, mant16};
return result;
end

endfunction : real_to_fp16

Summary of the conversion steps:​

  1. Bit extraction: Use $realtobits() to access the raw sign, exponent, and mantissa of the double-precision input.
  2. Special-value pass-through: Detect NaN, ±Inf, and ±0 and map them directly to the equivalent fp16 encodings.
  3. Exponent rebias: Rebase the unbiased exponent to fp16's bias of 15. Values above 30 overflow to ±Inf.
  4. Normal rounding: For normal numbers, round the mantissa to fit into 10 bits using round-to-nearest-even.
  5. Subnormal generation: If the rebased exponent is 0 or negative, shift the significand to create a subnormal number, applying rounding as needed.
  6. Flush to zero: If the value is too small to represent even as a subnormal, flush it to ±0.

details​

Round-to-nearest-even rule​

The expression encodes three distinct cases depending on how far the true value sits from the two representable neighbours.

First, name the three zones:

Call the value being rounded V. The two closest fp16 representable values are A (round down, keep mant16 as-is) and B = A + 1ulp (round up). The midpoint between them is exactly A + 0.5ulp.

  • V < midpoint → round down (toward A)
  • V > midpoint → round up (toward B)
  • V == midpoint → exact tie → round to whichever of A, B has an even LSB

The guard bit and sticky bit together tell you which zone you're in:

guardstickyPosition of VAction
0anyV < midpoint (closer to A)round down — round_bit = 0
11V > midpoint (closer to B)round up — round_bit = 1
10V == midpoint (exact tie)round to even

Now read the expression for each case:

round_bit = guard & (sticky | mant16[0])

Case 1 — guard = 0: The guard & short-circuits everything to 0. No increment. Correct, because V hasn't yet reached the midpoint.

Case 2 — guard = 1, sticky = 1: The truncated tail bits were nonzero, meaning V was strictly above the midpoint. sticky | mant16[0] is 1 regardless of mant16[0], so round_bit = 1. Round up unconditionally. Correct.

Case 3 — guard = 1, sticky = 0: V sits exactly at the midpoint — this is the tie. Now sticky | mant16[0] reduces to just mant16[0], the LSB of the result so far:

  • If mant16[0] = 1, the current result A is odd → round_bit = 1, increment to B (which is even). ✓
  • If mant16[0] = 0, the current result A is already even → round_bit = 0, keep A. ✓

In the tie case the expression is literally asking "is the current result odd?" and only rounding up when the answer is yes. That is exactly the round-to-nearest-even rule: always resolve a tie toward the value whose LSB is 0.

Why even and not odd?
With binary fractions, each ULP(unit of least precision) step flips the LSB. Consistently rounding ties toward the even neighbour means roughly half of all ties round up and half round down, which prevents the systematic upward bias you would get from always rounding ties up (round_bit = guard, ignoring sticky and mant16[0]).

Code​

// =============================================================================
// real_to_fp16.sv
//
// Non-synthesizable SystemVerilog function: converts a 'real' (64-bit IEEE 754
// double) to a 16-bit half-precision floating-point value (IEEE 754 binary16).
//
// FP16 format [15] [14:10] [9:0]
// sign exponent(bias=15) mantissa
//
// Special cases handled:
// +/-0 → fp16 ±0
// +/-Inf → fp16 ±Inf
// NaN → fp16 quiet NaN (0_11111_1000000000)
// overflow → fp16 ±Inf
// underflow → fp16 ±0 (flush-to-zero; subnormal path also included)
// subnormals → fp16 subnormal (when exp in range)
// =============================================================================

`timescale 1ns/1ps

package fp16_pkg;

// ---------------------------------------------------------------------------
// Bit-level access to a 64-bit double via $realtobits / $bitstoreal
// ---------------------------------------------------------------------------
localparam int SIGN_BIT_F64 = 63;
localparam int EXP_HI_F64 = 62;
localparam int EXP_LO_F64 = 52;
localparam int EXP_BIAS_F64 = 1023;
localparam int EXP_BIAS_F16 = 15;
localparam int MANT_BITS_F64 = 52;
localparam int MANT_BITS_F16 = 10;

// fp16 exponent special values
localparam logic [4:0] EXP_INF_F16 = 5'b11111;
localparam logic [4:0] EXP_ZERO_F16 = 5'b00000;

// -------------------------------------------------------------------------
// Function: real_to_fp16
//
// Returns the 16-bit half-precision representation of 'r'.
// Rounding: round-to-nearest-even (ties round to even).
// -------------------------------------------------------------------------
function automatic logic [15:0] real_to_fp16(input real r);
// ---- unpack the double ----
logic [63:0] bits64;
logic sign;
logic [10:0] exp64; // raw biased exponent (11 bits)
logic [51:0] mant64; // significand fraction bits
int exp_unbiased; // true exponent (signed)
int exp16; // re-biased for fp16

// Working variables for rounding
logic [10:0] mant_shifted; // 10 mantissa bits + 1 guard bit
logic guard, round_bit, sticky;
logic [9:0] mant16;
logic [4:0] exp16_bits;
logic [15:0] result;

// ------------------------------------------------------------------
// 1. Extract fields from the 64-bit representation
// ------------------------------------------------------------------
bits64 = $realtobits(r);
sign = bits64[SIGN_BIT_F64];
exp64 = bits64[EXP_HI_F64 : EXP_LO_F64];
mant64 = bits64[MANT_BITS_F64-1 : 0];

// ------------------------------------------------------------------
// 2. Handle IEEE special values
// ------------------------------------------------------------------

// NaN: exponent all-ones AND non-zero mantissa
if (exp64 == 11'h7FF && mant64 != '0) begin
result = {sign, EXP_INF_F16, 10'b10_0000_0000}; // quiet NaN
return result;
end

// Infinity: exponent all-ones AND zero mantissa
if (exp64 == 11'h7FF && mant64 == '0) begin
result = {sign, EXP_INF_F16, 10'b00_0000_0000}; // ±Inf
return result;
end

// Zero (both +0 and -0)
if (exp64 == '0 && mant64 == '0) begin
result = {sign, 15'b0};
return result;
end

// ------------------------------------------------------------------
// 3. Compute unbiased exponent and re-bias for fp16
// fp64 bias = 1023, fp16 bias = 15
// exp_unbiased = exp64 - 1023
// exp16 = exp_unbiased + 15
// ------------------------------------------------------------------
if (exp64 == '0) begin
// fp64 subnormal input: treat as denormalized, exp = -1022
exp_unbiased = -1022;
end else begin
exp_unbiased = int'(exp64) - EXP_BIAS_F64;
end
exp16 = exp_unbiased + EXP_BIAS_F16;

// ------------------------------------------------------------------
// 4. Overflow → fp16 ±Inf
// fp16 max biased exp = 30 (11110); 31 is reserved for Inf/NaN
// ------------------------------------------------------------------
if (exp16 >= 31) begin
result = {sign, EXP_INF_F16, 10'b00_0000_0000};
return result;
end

// ------------------------------------------------------------------
// 5. Normal numbers: 1 <= exp16 <= 30
// Mantissa: take top 10 bits of mant64, apply round-to-nearest-even
//
// fp64 mantissa is 52 bits wide.
// fp16 mantissa is 10 bits wide.
// We need bits [51:42] for the result, bit [41] as guard,
// bits [40:0] ORed together as sticky.
// ------------------------------------------------------------------
if (exp16 >= 1) begin
mant16 = mant64[51:42]; // top 10 bits
guard = mant64[41]; // guard bit
sticky = |mant64[40:0]; // sticky = OR of remaining bits
round_bit = guard & (sticky | mant16[0]); // round-to-nearest-even

mant16 = mant16 + {9'b0, round_bit};

// Check if rounding caused mantissa overflow (0x3FF -> 0x400)
if (mant16 == 10'h3FF + 10'h001) begin
mant16 = '0;
exp16 = exp16 + 1;
// Re-check overflow after round-up
if (exp16 >= 31) begin
result = {sign, EXP_INF_F16, 10'b00_0000_0000};
return result;
end
end

exp16_bits = exp16[4:0];
result = {sign, exp16_bits, mant16};
return result;
end

// ------------------------------------------------------------------
// 6. Subnormal fp16 result: exp16 <= 0
// Value = (-1)^s * 2^(-14) * (0.mantissa)
// We need to right-shift the significand by (1 - exp16) positions
// to produce a subnormal with implicit leading 1 absorbed.
//
// shift_amount = 1 - exp16 (ranges 1..24 for representable subnormals;
// >= 25 flushes to zero)
// ------------------------------------------------------------------
begin
int shift_amount;
logic [10:0] full_mant; // 1 (implicit) + 10 bits
logic [10:0] shifted_mant;

shift_amount = 1 - exp16; // positive integer

if (shift_amount > 11) begin
// Too small to represent even as a subnormal → flush to ±0
result = {sign, 15'b0};
return result;
end

// Reconstruct full significand with implicit leading 1
// Use top 10 bits of fp64 mantissa
full_mant = {1'b1, mant64[51:42]}; // 11 bits

// Shift right; capture guard and sticky for rounding
shifted_mant = full_mant >> shift_amount;
guard = (shift_amount <= 11) ?
full_mant[shift_amount - 1] : 1'b0;
// Sticky: any bit below the guard position
sticky = 1'b0;
for (int i = 0; i < shift_amount - 1; i++) begin
sticky = sticky | full_mant[i];
end
sticky = sticky | (|mant64[41:0]); // include lower fp64 bits

round_bit = guard & (sticky | shifted_mant[0]);
shifted_mant = shifted_mant + {10'b0, round_bit};

mant16 = shifted_mant[9:0];
result = {sign, EXP_ZERO_F16, mant16};
return result;
end

endfunction : real_to_fp16

endpackage : fp16_pkg