AFix
Description
Auto-ranging Fixed-Point, AFix
, is a fixed-point class which tracks the representable range of values while preforming fixed-point operations.
Warning: Much of this code is still under development. API and function calls may change.
User feedback is appreciated!
Declaration
AFix can be created using bit sizes or exponents:
AFix.U(12 bits) // U12.0
AFix.UQ(8 bits, 4 bits) // U8.4
AFix.U(8 exp, 12 bits) // U8.4
AFix.U(8 exp, -4 exp) // U8.4
AFix.U(8 exp, 4 exp) // U8.-4
AFix.S(12 bits) // S11 + sign
AFix.SQ(8 bits, 4 bits) // S8.4 + sign
AFix.S(8 exp, 12 bits) // S8.3 + sign
AFix.S(8 exp, -4 exp) // S8.4 + sign
These will have representable ranges for all bits.
For example:
AFix.U(12 bits)
will have a range of 0 to 4095.
AFix.SQ(8 bits, 4 bits)
will have a range of -4096 (-256) to 4095 (255.9375)
AFix.U(8 exp, 4 exp)
will have a range of 0 to 256
Custom range AFix
values can be created be directly instantiating the class.
class AFix(val maxValue: BigInt, val minValue: BigInt, val exp: ExpNumber)
new AFix(4096, 0, 0 exp) // [0 to 4096, 2^0]
new AFix(256, -256, -2 exp) // [-256 to 256, 2^-2]
new AFix(16, 8, 2 exp) // [8 to 16, 2^2]
The maxValue
and minValue
stores what backing integer values are representable.
These values represent the true fixed-point value after multiplying by 2^exp
.
AFix.U(2 exp, -1 exp)
can represent:
0, 0.5, 1.0, 1.5, 2, 2.5, 3, 3.5
AFix.S(2 exp, -2 exp)
can represent:
-2.0, -1.75, -1.5, -1.25, -1, -0.75, -0.5, -0.25, 0, 0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75
Exponent values greater 0 are allowed and represent values which are larger than 1.
AFix.S(2 exp, 1 exp)
can represent:
-4, 2, 0, 2
AFix(8, 16, 2 exp)
can represent:
32, 36, 40, 44, 48, 52, 56, 60, 64
Note: AFix
will use 5 bits to save this type as that can store 16
, its maxValue
.
Mathematical Operations
AFix
supports Addition (+
), Subtraction (-
), and Multiplication (*
) at the hardware level.
Division (\
) and Modulo (%
) operators are provided but are not recommended for hardware elaboration.
Operations are preformed as if the AFix
value is a regular Int
number.
Signed and unsigned numbers are interoperable. There are no type differences between signed or unsigned values.
// Integer and fractional expansion
val a = AFix.U(4 bits) // [ 0 ( 0.) to 15 (15. )] 4 bits, 2^0
val b = AFix.UQ(2 bits, 2 bits) // [ 0 ( 0.) to 15 ( 3.75)] 4 bits, 2^-2
val c = a + b // [ 0 ( 0.) to 77 (19.25)] 7 bits, 2^-2
val d = new AFix(-4, 8, -2 exp) // [- 4 (- 1.25) to 8 ( 2.00)] 5 bits, 2^-2
val e = c * d // [-308 (-19.3125) to 616 (38.50)] 11 bits, 2^-4
// Integer without expansion
val aa = new AFix(8, 16, -4 exp) // [8 to 16] 5 bits, 2^-4
val bb = new AFix(1, 15, -4 exp) // [1 to 15] 4 bits, 2^-4
val cc = aa + bb // [9 to 31] 5 bits, 2^-4
AFix
supports operations without without range expansion.
It does this by selecting the aligned maximum and minimum ranges from each of the inputs.
+|
Add without expansion.
-|
Subtract without expansion.
Inequality Operations
AFix
supports standard inequality operations.
A === B
A =\= B
A < B
A <= B
A > B
A >= B
Warning: Operations which are out of range at compile time will be optimized out!
Bitshifting
AFix
supports decimal and bit shifting
<<
Shifts the decimal to the left. Adds to the exponent.
>>
Shifts the decimal to the right. Subtracts from the exponent.
<<|
Shifts the bits to the left. Adds fractional zeros.
>>|
Shifts the bits to the right. Removes fractional bits.
Saturation and Rounding
AFix
implements saturation and all common rounding methods.
Saturation works by saturating the backing value range of an AFix
value. There are multiple helper functions which
consider the exponent.
val a = new AFix(63, 0, -2 exp) // [0 to 63, 2^-2]
a.sat(63, 0) // [0 to 63, 2^-2]
a.sat(63, 0, -3 exp) // [0 to 31, 2^-2]
a.sat(new AFix(31, 0, -1 exp)) // [0 to 31, 2^-2]
AFix
rounding modes:
// The following require exp < 0
.floor() or .truncate()
.ceil()
.floorToZero()
.ceilToInf()
// The following require exp < -1
.roundHalfUp()
.roundHalfDown()
.roundHalfToZero()
.roundHalfToInf()
.roundHalfToEven()
.roundHalfToOdd()
An mathematical example of these rounding modes is better explained here: Rounding - Wikipedia
All of these modes will result in an AFix
value with 0 exponent. If rounding to a different exponent is required
consider shifting or use an assignment with the truncated
tag.
Assignment
AFix
will automatically check and expand range and precision during assignment. By default, it is an error to assign
an AFix
value to another AFix
value with smaller range or precision.
The .truncated
function is used to control how assignments to smaller types.
def truncated(saturation: Boolean = false,
overflow : Boolean = true,
rounding : RoundType = RoundType.FLOOR)
def saturated(): AFix = this.truncated(saturation = true, overflow = false)
RoundType
:
RoundType.FLOOR
RoundType.CEIL
RoundType.FLOORTOZERO
RoundType.CEILTOINF
RoundType.ROUNDUP
RoundType.ROUNDDOWN
RoundType.ROUNDTOZERO
RoundType.ROUNDTOINF
RoundType.ROUNDTOEVEN
RoundType.ROUNDTOODD
The saturation
flag will add logic to saturate to the assigned datatype range.
The overflow
flag will allow assignment directly after rounding without range checking.
Rounding is always required when assigning a value with more precision to one with lower precision.