From: Tom Marshall
To: All
Subject: Fixed point math
I have been doing a fair amount of programming with fixed point math
recently. Fixed addition and subtraction are fairly obvious operations.
Fixed multiplication isn't too bad. But fixed division always makes me
stop and think for a minute or scribble on a piece of scratch paper.
So, I decided to make a few macros for doing fixed multiplication and
division. Maybe someone reading this will find it useful. Here's the
concept:
First off, this discussion will assume that we are using at least a 386
and all numbers are stored in 32-bit registers in "16.16" format. That
is, the high 16 bits represent the integer portion and the low 16 bits
represent the fractional portion. Here's a diagram:
EAX (hi) EAX (lo)
-----------------------------------
Bit : | 31 30 .. 17 16 | 15 14 .. 1 0 |
-----------------------------------
| | | | |
| | | | \-- 2^-2 (ie. quarters) place
| | | \----- 2^-1 (ie. halves) place
| | \---------- 2^0 (ie. ones) place
| \------------- 2^1 (ie. twos) place
\---------------------- 2^15 place or sign bit
If we call our number N, this representation can be thought of as
N*2^16. The "decimal point" is between bit 16 and 15. We can represent
numbers in the range of +/- 65536 for unsigned or +/- 32768 for signed.
Okay, now how do we manipulate these numbers?
Adding and subtracting is really simple. Just use the normal ADD and
SUB instructions.
When multiplying, you must remember that multiplying two 32- bit
registers leaves a 64-bit result. Using a little 8th grade math gives
us this:
(A*2^16) * (B*2^16) = A*B*2^32
The result is a 64-bit number with 32 bits in the integer portion and 32
bits in the fractional portion. Notice that we can't use those nifty
new forms of IMUL for doing this! The integer portion is in the high
register (EDX) and the fractional portion is in the low register (EAX).
To convert back to our original form, we shift EDX:EAX right by 16 bits
and disregard EDX. The result must be in the range of +/- 65536 or we
will lose integer precision converting it. Fraction precision will
always be lost b ecause we only have room for 16 fraction bits. But
that's the price for super fast fixed math.
Okay, now for the fun part: division. Using our mathematical
representation again, we get this:
(A*2^16) / (B*2^16) = A/B
That's no good because we're left with an integer! We need to multiply
the numerator by 2^16 before we divide to ensure that the answer has 16
bits of fractional precision. So let's try again:
(A*2^16*2^16) / (B*2^16) = A/B*2^16
Ok, that's better. The result is in exactly the form we want! Now what
about the remainder? It's not good for much at all because we're not
dividing integers. Don't try to use it to round your result .. it won't
work!
Now for the useful part... macros! These are all in TASM format, so
switch the macro declarations to use them under MASM (ie. MACRO foo =>
foo MACRO). That's it for now... someone please let me know if you
found this stuff useful. I can explain a sine table or whatever next.
MACRO FIXMUL ;EAX * EBX => EAX (EDX used)
xor edx,edx ; All numbers unsigned 16.16
mul ebx ; Overflow not checked
shrd eax,edx,16
ENDM
MACRO IFIXMUL ;EAX * EBX => EAX (EDX used)
cdq ; All numbers signed 16.16
imul ebx ; Overflow not checked
shrd eax,edx,16
ENDM
MACRO FIXDIV ;EAX / EBX => EAX (EDX = Rem)
xor edx,edx ; All numbers unsigned 16.16
ror eax,16 ; Overflow / fault not checked
xchg ax,dx
div ebx
ENDM
MACRO IFIXDIV ;EAX / EBX => EAX (EDX = Rem)
cdq ; All numbers signed 16.16
ror eax,16 ; Overflow / fault not checked
xchg ax,dx
idiv ebx
ENDM