This article is part of the Embedded Software series: Ada for the Embedded C Developer
Many numerical applications typically use floating-point types to compute values. However, in some platforms, a floating-point unit may not be available. Other platforms may have a floating-point unit, but its use in certain numerical algorithms could be prohibitive in terms of performance. For those cases, fixed-point arithmetic can be a good alternative.
The difference between fixed-point and floating-point types might not be so obvious when looking at this code snippet:
-- fixed_definitions.ads package Fixed_Definitions is D : constant := 2.0 ** (-31); type Fixed is delta D range -1.0 .. 1.0 - D; end Fixed_Definitions; -- show_float_and_fixed_point.adb with Ada.Text_IO; use Ada.Text_IO; with Fixed_Definitions; use Fixed_Definitions; procedure Show_Float_And_Fixed_Point is Float_Value : Float := 0.25; Fixed_Value : Fixed := 0.25; begin Float_Value := Float_Value + 0.25; Fixed_Value := Fixed_Value + 0.25; Put_Line (“Float_Value = “ & Float’Image (Float_Value)); Put_Line (“Fixed_Value = “ & Fixed’Image (Fixed_Value)); end Show_Float_And_Fixed_Point;In this example, the application will show the value 0.5 for both Float_Value and Fixed_Value.
The major difference between floating-point and fixed-point types is in the way the values are stored. Values of ordinary fixed-point types are, in effect, scaled integers. The scaling used for ordinary fixed-point types is defined by the type's small (scale factor), which is derived from the specified delta and, by default, is a power of two.
Therefore, ordinary fixed-point types are sometimes called binary fixed-point types. In that sense, ordinary fixed-point types can be thought of being close to the actual representation on the machine. In fact, ordinary fixed-point types make use of the available integer shift instructions, for example.
Another difference between floating-point and fixed-point types is that Ada doesn't provide standard fixed-point types—except for the Duration type, which is used to represent an interval of time in seconds. While the Ada standard specifies floating-point types such as Float and Long_Float, we must declare our own fixed-point types. Note that, in the previous example, we’ve used a fixed-point type named “Fixed:”. This type isn't part of the standard, but it must be declared somewhere in the source code of our application.
The syntax for an ordinary fixed-point type is:
type (type_name) is delta (delta_value) range (lower_bound) .. (upper_bound);
By default, the compiler will choose a scale factor, or small, that’s a power of 2 no greater than
For example, we may define a normalized range between -1.0 and 1.0 as the following:
-- normalized_fixed_point_type.adb with Ada.Text_IO; use Ada.Text_IO; procedure Normalized_Fixed_Point_Type is D : constant := 2.0 ** (-31); type TQ31 is delta D range -1.0 .. 1.0 - D; begin Put_Line ("TQ31 requires " & Integer’Image (TQ31’Size & " bits"); Put_Line ("The delta value of TQ31 is " & TQ31'Image (TQ31'Delta)); Put_Line ("The minimum value of TQ31 is " & TQ31'Image (TQ31'First)); Put_Line ("The maximum value of TQ31 is " & TQ31'Image (TQ31'Last)); end Normalized_Fixed_Point_Type;
The output is:
TQ31 requires 32 bits The delta value of TQ31 is 0.0000000005 The minimum value of TQ31 is 0.0000000000 The maximum value of TQ31 is 0.9999999995In this example, we’re defining a 32-bit fixed-point data type for our normalized range. When running the application, we notice that the upper bound is close to one, but not exactly one. This is a typical effect of fixed-point data types—you can find more details in this discussion about the Q format.
In the case of C, since the language doesn't support fixed-point arithmetic, we need to emulate it using integer types and custom operations via functions. Let's look at this very rudimentary example:
-- main.c #include (stdio.h) #include (math.h) #define SHIFT_FACTOR 32 #define TO_FIXED(x) ((int) ((x) * pow (2.0, SHIFT_FACTOR - 1))) #define TO_FLOAT(x) ((float) ((double)(x) * (double)pow (2.0, -(SHIFT_FACTOR – 1)))) typedef int fixed; fixed add (fixed a, fixed b) { return a + b; } fixed mult (fixed a, fixed b) { return (fixed)(((long)a * (long)b) >> (SIFT_FACTOR – 1 )); } void display_fixed(fixed x) { printf("value (integer) = %d\n", x); printf("value (float) = %3.5f\n\n", TO_FLOAT(x)); } int main(int argc, const char * argv[]) { int fixed_value = TO_FIXED(0.25); printf("Original value\n"); display_fixed(fixed_value); printf("... + 0.25\n"); fixed_value = add(fixed_value, TO_FIXED(0.25)); display_fixed(fixed_value); printf("... + 0.5\n"); fixed_value = add(fixed_value, TO_FIXED(0.5)); display_fixed(fixed_value); return 0; }
Here, we declare the fixed-point type is fixed based on int and two operations for it: addition (via the add function) and multiplication (via the mult function). Note that while fixed-point addition is quite straightforward, multiplication requires right-shifting to match the correct internal representation. In Ada, since fixed-point operations are part of the language specification, they don't need to be emulated. Therefore, no extra effort is required from the programmer.
Also note that the example above is very rudimentary, so it doesn't take some of the side effects of fixed-point arithmetic into account. In C, you have to manually consider all side effects deriving from fixed-point arithmetic, while in Ada, the compiler takes care of selecting the right operations for you.
Read more from the Embedded Software series: Ada for the Embedded C Developer