Draft preview. Lessons are being written and reviewed.

Digital Design / Module 8: Arithmetic and datapaths

46. Fixed-point arithmetic

Represent fractions without an FPU.

Lecture coming soon

See it move

The idea

Floating-point units are large and slow, so many chips handle fractions using fixed-point: ordinary integers with an agreed binary point. The hardware just adds and multiplies integers, and the designer keeps track of where the point lies.

Q format

Qm.n means m integer bits, including the sign, and n fractional bits. Q1.7 uses 8 bits, with a value of integer / 128, giving a range from -1 up to +0.992.

BitsAs integerValue in Q1.7
0100 0000640.5
1000 0000-128-1.0
0111 11111270.992

Addition and multiplication

Addition requires matching binary points, and it can overflow by one bit. Multiplication adds the formats: Q1.7 x Q1.7 gives Q2.14, using 16 bits. To return to Q1.7, shift right by 7 and keep the low 8 bits.

wire signed [15:0] wide = a * b;       // Q2.14
wire signed [15:0] shifted = wide >>> 7;

assign y = (shifted >  127) ?  8'sd127 :
           (shifted < -128) ? -8'sd128 :
           shifted[7:0];

Saturation

Only one case overflows here: -1 x -1 = +1, which Q1.7 can't represent. Without protection, the value wraps round to -1, which is the worst possible error. Saturation clamps to the largest value instead. This is a small error, and it's much less harmful in audio, video and neural-network computations.

Rounding

Plain shifting truncates, which adds a small negative bias over many operations. To round to nearest, add half of an output LSB (64 here) before shifting. It costs one small addition.

Common mistake

Using >> instead of >>> on a signed value, which fills in zeros and turns a negative value positive. Also check that both operands are declared signed, or the multiply itself is treated as unsigned.

Key ideas

  • Qm.n sets integer and fractional bits.
  • Products add fractional bits, so truncate deliberately.
  • Saturate instead of wrapping where overflow would be harmful.

Your exercise

Multiply two Q1.7 values and return Q1.7 with saturation.

Find the bug

This code has one deliberate bug. Read the symptom, then click the line you think is wrong.

What goes wrong: 0.5 x 0.5 is correct, but -0.5 x 0.5 comes out as a large positive number instead of -0.25.

Interview questions

coreQ1.7 x Q1.7 gives?

basicEncode 0.375 and -0.75 in Q1.7.

basicWhat range and resolution does a signed Q4.4 number have?

coreMultiply 0.5 by -0.75 in Q1.7 and return a Q1.7 result. Show the integers at each step.

corea and b are signed Q1.7 inputs, but the product wire was declared without signed. For 0.5 x -0.75, y comes out as 464 instead of -48. Why?

input  signed [7:0]  a, b;
wire          [15:0] wide = a * b;  // not signed
wire   signed [15:0] y = wide >>> 7;

coreIn a Q1.7 x Q1.7 multiply returned as Q1.7, which inputs overflow, and why saturate rather than wrap?

coreAdd 0.75 and 0.5 as Q1.7 values into an 8-bit signed result. What do you get, and how do you fix it?

advancedYour Q1.7 multiplier truncates with >>> 7. How do you round to nearest instead, and what does that fix?

wire signed [15:0] wide = a * b;
wire signed [15:0] rnd  = (wide + 16'sd64) >>> 7;

advancedwide is signed and holds -6144. Why does y hold 464, not -48?

wire signed [15:0] wide = a * b;   // -6144
wire signed [15:0] y = wide[15:7];

Practise these 9 questions with animations