RTL & Logic Design · lesson 12

Floating Point and a Datapath Project

By Tabish Iqbal · Last updated:

The floating-point add algorithm (align, add, normalize, round) worked by hand, then a capstone datapath and FSM controller taken from spec to verified RTL.

12 min read · Free, no login

This path's last lesson has two jobs. The first is the floating-point add algorithm โ€” the sequence of steps every FP adder performs, regardless of which exact format it implements. The second is the capstone this whole path has been building toward: taking a specification through to verified RTL, with a real datapath (registers and functional units wired together) driven by a real controller (a synchronous FSM), checked end to end against a reference model rather than asserted to work.

Reading time: about 12 minutes.

12.1 IEEE 754, briefly recapped

The Fixed-Point and Floating-Point Representation lesson in the Digital Electronics path already derived the IEEE 754 single-precision field layout in full โ€” sign, an 8-bit exponent biased by 127, and a 23-bit mantissa with an implied leading 1 โ€” and worked a complete encoding example. That derivation is not repeated here; what matters for this lesson is only the shape of the result: a normalized value is stored as 1.mantissa × 2exponent−127, and two floating-point numbers being added will, in general, have different stored exponents. That one fact is the entire reason floating-point addition needs more than a single adder.

12.2 The floating-point add algorithm

Adding two floating-point numbers is not "add the bit patterns" the way two's-complement addition is. The two operands' binary points sit in different places until something lines them up, and the result of adding two aligned significands is not guaranteed to still be a normalized 1.xxxx value. Four steps, always in this order, handle both problems.

  1. Align the exponents by shifting the smaller-exponent operand's significand right, one position per unit of exponent difference, until both operands share the larger exponent. Shifting right is what keeps the value the same while reducing the exponent's effective weight โ€” the standard-format lesson's own worked example already showed a stored exponent as a power of two multiplying a 1.xxxx significand, and shifting that significand right by k is exactly dividing by 2k, which cancels against raising the exponent by k.
  2. Add (or, if the operands' effective signs differ, subtract) the two now-aligned significands, using an ordinary binary adder. Nothing about this step is specific to floating point โ€” it is lesson 9's adder, applied to the significand fields only.
  3. Normalize the sum back into 1.xxxx form. Adding two numbers in [1.0, 2.0) can produce a sum in [2.0, 4.0), which needs a one-position right shift and an exponent increment to restore the 1.xxxx shape; subtracting two close-in-magnitude numbers can instead produce a result with leading zeros, which needs a left shift and an exponent decrement instead. Addition of two same-signed operands can only ever trigger the first case, never the second โ€” a point the capstone project below relies on directly.
  4. Round the normalized significand back down to the format's actual mantissa width, since the align step may have shifted bits past the far end of the significand. Rounding up can itself carry out (1.111 rounding up becomes 10.000), which forces a second normalization: shift right once more and add 1 to the exponent.
Worked example: 12.0 + 3.25

Let us add 12.0 and 3.25 and track each step, writing each significand in normalized binary form.

Starting values. 12.0 = 1100.02 = 1.100000 × 23 (exponent 3, stored biased exponent 130). 3.25 = 11.012 = 1.101000 × 21 (exponent 1, stored biased exponent 128).

Align. The exponents differ by 3−1 = 2, so 3.25's significand shifts right by 2 positions, and its effective exponent becomes 3 to match: 1.101000 → 0.011010, now scaled by 23 like the other operand. As a check, 0.011010 × 8 = 0.40625 × 8 = 3.25 โ€” the value has not changed, only which power of two it is being multiplied by.

Add. 1.100000 + 0.011010 = 1.111010, still scaled by 23. Decoding this directly: 1.1110102 = 1.90625, and 1.90625 × 8 = 15.25 โ€” which is exactly 12.0 + 3.25, confirming the addition before normalization or rounding are even considered.

Normalize. 1.111010 already has its leading 1 in the units position โ€” no bit pattern here reached 2.0 or higher โ€” so no shift or exponent adjustment is needed; the result keeps exponent 3 (stored 130).

Round. The sum already terminates in a finite number of bits with no bits shifted past the format's mantissa width in this particular example, so rounding changes nothing here; the final mantissa field is simply 1110100...0, padded to the format's width.

Thus, 12.0 + 3.25 = 15.25, reached through exactly the four steps above, each one individually checkable against the plain decimal arithmetic.

12.3 Capstone: a floating-point adder, spec to verified RTL

Building a full IEEE 754 single-precision adder โ€” with denormals, infinities, NaNs and correct round-to-nearest-even โ€” is realistically a multi-week project, not a one-lesson capstone. This project instead applies the exact four-step algorithm above to a deliberately small, custom 8-bit format, so the whole spec-to-RTL path โ€” datapath, controller, and verification against a reference model โ€” fits in one place and can be checked by hand. The format: 1 sign bit (fixed at 0 here โ€” the project is scoped to adding two positive, normalized operands, so subtraction's leading-zero normalization case and all of IEEE 754's special values are deliberately out of scope), a 4-bit exponent biased by 7, and a 3-bit mantissa with an implied leading 1. The final mantissa is truncated rather than rounded โ€” a second deliberate simplification, since production FP hardware would round. A third is exponent overflow: when the larger exponent is already 15 and the significand sum carries out, exp_big + 1 wraps to 0 instead of saturating or flagging overflow, which affects 208 of the 16,384 possible positive operand pairs.

The datapath carries each significand in 6 bits internally โ€” the implied 1, the 3 stored fraction bits, and 2 guard bits below them. Under truncation those guard bits never change the result: a bit-exact Python model of this datapath with 0 guard bits matches the 2-guard-bit version on all 16,384 operand pairs, because truncating early or late discards the same value. They are carried here for illustration, and become necessary, together with a sticky bit, once round-to-nearest is added. The controller is a synchronous FSM with one state per algorithm step, matching the four-step structure in section 12.2 almost one for one: S_COMPARE (decide which operand has the larger exponent, computing the gap), S_ALIGN, S_ADD, and S_NORM (which, as section 12.2 notes, only ever needs the right-shift-and-increment case for this restricted, positive-operand-only addition).

controller states (top), and the datapath registers each state loads (bottom) IDLE exp_a, exp_bfrac_a, frac_b COMPARE exp_big, diffmant_big/small ALIGN mant_small>> diff ADD mant_sum7 bits NORM resultexp, frac DONE done= 1 NORM: if mant_sum carries out, result = {0, exp_big + 1, mant_sum[5:3]}, else {0, exp_big, mant_sum[4:2]}
Each controller state advances one algorithm step and updates exactly the datapath registers that step owns; the carry out of mant_sum is the only signal S_NORM needs to decide between the two result-forming paths.
The controller's state transitions and the datapath's core add/normalize logic
// Controller next-state logic -- one state per algorithm step
always @* begin
    case (state)
        S_IDLE:    next_state = start ? S_COMPARE : S_IDLE;
        S_COMPARE: next_state = S_ALIGN;
        S_ALIGN:   next_state = S_ADD;
        S_ADD:     next_state = S_NORM;
        S_NORM:    next_state = S_DONE;
        S_DONE:    next_state = S_IDLE;
        default:   next_state = S_IDLE;
    endcase
end

// S_ADD: add the two already-aligned 6-bit significands into a 7-bit sum
mant_sum <= {1'b0, mant_big} + {1'b0, mant_small};

// S_NORM: the sum can only overflow into [2.0,4.0) for two positive
// normalized operands -- never underflow below 1.0 -- so only the
// carry-out case below is reachable here.
if (mant_sum[6])
    result <= {1'b0, exp_big + 1'b1, mant_sum[5:3]};
else
    result <= {1'b0, exp_big,        mant_sum[4:2]};

Verifying this design needed a different approach from every earlier lesson's testbench, because the custom format's bit patterns are not easy to eyeball the way a plain integer sum is. The testbench instead carries its own small reference model, written using Verilog's real type purely for simulation-side checking: a decode function converts a format bit pattern into an ordinary floating-point value ((1.0 + frac/8.0) * 2.0**(exp-7)), and every test case decodes both operands, adds them in real arithmetic, decodes the hardware's result, and compares the two, allowing a tolerance of one LSB of the result's own exponent scale, the most that truncation can lose. That is a simple but weak check: an off-by-one-LSB bug would pass it too. The design is fully deterministic, so a stronger reference is a bit-accurate model that truncates exactly as the hardware does and must match it bit for bit โ€” the Python model mentioned above is one, and it matches the RTL on all 16,384 positive operand pairs.

PASS: 1.000000 + 1.000000 = 2.000000 (hardware: 2.000000, truncation error 0.000000 <= lsb 0.250000)
PASS: 1.500000 + 0.500000 = 2.000000 (hardware: 2.000000, truncation error 0.000000 <= lsb 0.250000)
PASS: 1.250000 + 1.250000 = 2.500000 (hardware: 2.500000, truncation error 0.000000 <= lsb 0.250000)
PASS: 4.000000 + 0.250000 = 4.250000 (hardware: 4.000000, truncation error 0.250000 <= lsb 0.500000)
PASS: 1.125000 + 1.000000 = 2.125000 (hardware: 2.000000, truncation error 0.125000 <= lsb 0.250000)
ALL FP8_ADD TESTS PASSED

Each line exercises a different part of the datapath. The first three, and the fifth, carry out of the significand add and take the renormalize-right-and-increment path in S_NORM; 1.5 + 0.5 additionally needs a one-position alignment shift. 4.0 + 0.25 forces a four-position alignment shift, the largest in this suite, but does not carry out. Two lines show truncation at work. 4.0 + 0.25 returns 4.0, where the result's LSB is 0.5, so the error of 0.25 is half an LSB; 4.25 lies exactly halfway between 4.0 and 4.5, so IEEE round-to-nearest-even would also return 4.0, and only round-half-up would give 4.5. Likewise 1.125 + 1.0 = 2.125 is an exact tie between 2.0 and 2.25 (error 0.125, LSB 0.25), and round-to-nearest-even also gives 2.0. Rounding would change results only where truncation discards more than half an LSB, which none of these five cases do. Yosys's synth and check on the full controller-plus-datapath design report every register as a real flip-flop cell and zero inferred latches โ€” confirmed directly from the cell list, not merely from a summary line, which matters most of all in a design with this many conditional branches across several always blocks.

Thus, this capstone closes the loop the whole path opened in lesson 1: a specification (align, add, normalize, round, restricted and documented where it was deliberately simplified) became a datapath and a controller, and that RTL was checked โ€” not asserted โ€” against an independent reference model, the same discipline every verified module in lessons 9 through 12 has followed.

12.4 Interview questions

Why does floating-point addition need an alignment step that plain integer addition never needs? Two floating-point operands generally have different stored exponents, meaning their binary points sit at different positions. Shifting the smaller-exponent operand's significand right until both share the larger exponent is what makes a plain bit-for-bit addition of the two significands meaningful.

Why can normalizing the sum of two positive, already-normalized operands only ever need a right shift, never a left shift? After alignment, the larger-exponent operand's significand is still at least 1.0, and the shifted one is at least 0 and below 2.0, so their sum lies in [1.0, 4.0) โ€” never below 1.0, which is the only condition that would call for a left-shift (leading-zero) renormalization instead.

What would change in this design if it needed to support subtraction (differing signs) as well as addition? Quite a lot. The datapath needs a magnitude compare and swap, so the larger magnitude is always the minuend (when exponents are equal, that means comparing the significands), plus result-sign logic. Normalization needs a leading-zero counter driving a variable left shift and exponent decrement, because subtracting close values can leave many leading zeros. Exact cancellation needs a true zero result, which this format cannot represent. And if rounding is added, a sticky bit is needed to record whether any shifted-out bit was 1.

Why does the testbench decode the hardware's result back to a real number instead of comparing raw bit patterns? Because it is simple: the design truncates, so its result differs from the exact real sum by up to one LSB, and a real-valued model with a one-LSB tolerance accepts that without modelling truncation. It is the weaker choice, though. The design is deterministic, so a bit-accurate reference model that truncates identically gives an exact, non-flaky match and would catch an off-by-one-LSB bug that the tolerance check hides.

12.5 Further reading

  • Harris, David Money, and Sarah L. Harris. Digital Design and Computer Architecture. Morgan Kaufmann.
  • Mano, M. Morris, and Michael D. Ciletti. Digital Design. Pearson.
  • Cavanagh, Joseph. Verilog HDL Design Examples. CRC Press.
  • Ciletti, Michael D. Advanced Digital Design with the Verilog HDL. Pearson.
  • Dally, William J., and R. Curtis Harting. Digital Design: A Systems Approach. Cambridge University Press.
  • Cummings, Clifford E. Papers presented at SNUG (Synopsys Users Group).
  • HDLBits โ€” interactive Verilog exercises.

Key takeaways

  • Floating-point addition is always align, add, normalize, round, in that order, with a second normalization if rounding carries out โ€” a sequence this lesson's worked example (12.0 + 3.25 = 15.25) checks step by step against plain decimal arithmetic.
  • Adding two positive, normalized operands can only ever overflow past 2.0 during normalization, never underflow below 1.0 โ€” a fact the capstone's controller relies on directly to need only one renormalization case.
  • The capstone project deliberately restricts scope โ€” positive operands only, truncation instead of rounding, exponent overflow that wraps, a custom 8-bit format โ€” and states each simplification explicitly rather than hiding it, exactly the discipline a real project plan should bring to any corner it decides to cut.
  • Verifying a design whose outputs are not easy to eyeball calls for a reference model; a bit-accurate model that truncates exactly as the hardware does gives an exact match, while a real-valued model with a one-LSB tolerance, as used here, is simpler but can hide an off-by-one-LSB bug.
  • This capstone's controller-plus-datapath structure, with one FSM state per algorithm step, is the same shape every multi-cycle design in this path (lesson 10's division datapath included) has used โ€” a pattern that scales to far larger specifications than this lesson's deliberately small one.