Fixed-Point Numbers
This page explains fixed-point numbers, which let a CPU that can only handle integers work with fractional values.
<Note>
This page uses MathJax to display formulas. If your browser settings block script execution, the formulas on this page will not display correctly.
What is a fixed-point number?
The SNES CPU, the 65C816, has no instructions for handling fractional numbers. Instead, fractions are represented by regarding an integer value as “that value divided by some fixed number”. This is called a fixed-point number.
Definition (fixed-point number)
For an integer \(\ N\ \) and a predetermined non-negative integer \(\ f\ \), \(\ x = N / 2^f\ \) is called a fixed-point number with an \(\ f\ \)-bit fractional part.
Thinking in binary, this is the same as regarding “the binary point as lying between the \(\ f\ \)th and \(\ f+1\ \)th bits from the bottom”. Because the position of the point is fixed and does not move, it is called a “fixed”-point number.
For example, when \(\ f=8\ \), the 16-bit value $0180 represents the following number.
\[ \$0180 / 2^8 = 384 / 256 = 1.5 \]The high byte $01 is the integer part and the low byte $80 is the fractional part (\(\ \$80/256=0.5\ \)). If the fractional part is exactly one byte, the integer part and fractional part can be extracted byte by byte, which makes them very easy to handle on 8-bit and 16-bit CPUs.
Notation
The format of a fixed-point number is generally written by listing the number of bits in the integer part and in the fractional part. This site writes it as follows.
Notation rules
A fixed-point number with an \(\ m\ \)-bit integer part and an \(\ f\ \)-bit fractional part is written as \(\ m.f\ \) format. If signed, the sign bit is included in the integer part.
| Format | Total bits | Representable range | Size of one unit | Examples |
|---|---|---|---|---|
| Signed 8.8 | 16 | -128 to 127.996… | 1/256 | Mode 7 matrix (M7A-M7D) |
| Unsigned 2.12 | 14 | 0 to 3.9997… | 1/4096 | DSP pitch |
| Signed 16.8 | 24 | -32768 to 32767.996… | 1/256 | Subpixel coordinates |
| Signed 1.7 | 8 | -1 to 0.992… | 1/128 | FIR filter coefficients, volume |
In general, the range representable in signed \(\ m.f\ \) format is from \(\ -2^{m-1}\ \) to \(\ 2^{m-1}-2^{-f}\ \) inclusive, and for unsigned it is from \(\ 0\ \) to \(\ 2^m-2^{-f}\ \) inclusive. Negative numbers are represented by taking the two’s complement of the whole bit string. For example, \(\ -1.5\ \) in signed 8.8 format is $FE80, the two’s complement of \(\ 1.5=\$0180\ \).
DSP volumes and FIR filter coefficients act as “a signed 8-bit value divided by 128”, so they can be regarded as fixed-point numbers in 1.7 format. This format cannot represent exactly \(\ 1\ \); the maximum value $7F is a multiplier of \(\ 127/128 \fallingdotseq 0.99\ \).
Addition and subtraction
Fixed-point numbers with the same number of fractional bits can be added and subtracted directly as ordinary integers.
\[ \frac{A}{2^f} + \frac{B}{2^f} = \frac{A+B}{2^f} \]If the numbers of fractional bits differ, shift first to align them and then calculate. For example, to add an integer \(\ n\ \) to a value in 8.8 format, shift \(\ n\ \) left by 8 bits (multiply by 256) before adding.
Multiplication
When fixed-point numbers are multiplied together, the number of fractional bits becomes the sum of the two.
\[ \frac{A}{2^f} \times \frac{B}{2^g} = \frac{AB}{2^{f+g}} \]The product of two 8.8 values is in 16.16 format, so to return to the original 8.8 format, shift the product right by 8 bits. In other words, you take the middle two bytes of the 32-bit product.
Example
Compute \(\ 1.5 \times 2.25\ \) in 8.8 format.
\(\ \$0180 \times \$0240 = 384 \times 576 = 221184 = \$036000\ \)
Shifting right by 8 bits gives \(\ \$0360 = 864\ \), i.e. \(\ 864/256=3.375\ \).
When multiplying by a plain integer, the number of fractional bits in the result does not change, so no shift is needed. Also, if the multiplier is exactly \(\ 2^k\ \), the multiplication can be done with a \(\ k\ \)-bit left shift.
The SNES has an 8-bit x 8-bit unsigned multiplier (WRMPYA/WRMPYB) and a 16-bit x 8-bit signed multiplier for Mode 7 (M7A/M7B -> MPY). For how to use them, see Multiplication and division.
Division and reciprocals
When fixed-point numbers are divided, the number of fractional bits becomes the difference of the two.
\[ \frac{A/2^f}{B/2^g} = \frac{A}{B} \cdot 2^{g-f} \]Dividing values with the same number of fractional bits gives an integer quotient, and the fractional part is lost. To keep the fractional part, shift the dividend left first. To get an 8.8 quotient from two 8.8 values, shift \(\ A\ \) left by 8 bits and then divide by \(\ B\ \).
Division takes much longer than multiplication. When dividing by the same value many times, the standard technique is to compute the reciprocal \(\ 1/B\ \) first and replace the division with a multiplication.
Rounding
When the fractional part is discarded by a right shift, the result is always rounded down (truncation, toward the smaller value). Positive numbers move toward 0, and negative numbers move away from 0. For example, discarding the fractional part of \(\ -0.5\ \) ($FF80 in 8.8 format) gives \(\ \$FF=-1\ \).
To round to nearest instead, add exactly half of the discarded digits ($80 for an 8-bit right shift) before shifting.
\[ \mathrm{round}(x) = \left\lfloor x + \frac{1}{2} \right\rfloor \]Note that the 65C816 has no instruction that shifts right while preserving the sign (arithmetic shift right). To shift a signed value right, copy the most significant bit into the carry and then use the ROR instruction.
CMP #$8000 ; carry is set if the number is negative
ROR A ; shift right while putting the carry (sign) into the top bit
Error
Fixed-point numbers can only represent integer multiples of \(\ 1/2^f\ \). Other values are approximated by the nearest representable value, so there is an error of up to half a unit (\(\ 1/2^{f+1}\ \)).
This error accumulates as calculations are repeated. For example, in a routine that slows something down by multiplying its speed by a coefficient every frame, depending on the rounding direction the speed may never reach 0, or may reach 0 sooner than expected. Countermeasures are needed, such as increasing the number of fractional bits or handling values near 0 separately.
Difference from floating-point numbers
The floating-point numbers used in PCs and the like hold a value in the form “mantissa x 2 to the power of the exponent”, and move the position of the point via the exponent.
| Fixed-point numbers | Floating-point numbers | |
|---|---|---|
| Representable range | Narrow | Very wide |
| Precision | Constant regardless of magnitude (constant absolute error) | Proportional to magnitude (constant relative error) |
| Computation | Can be done with integer instructions alone; fast | Requires dedicated circuitry or complex processing |
On a CPU like the SNES’s that can only handle integers, floating-point calculations are very slow. For things whose value range is known in advance, such as coordinates, speeds and angles, fixed-point numbers are sufficient.