Skip to main content

Side Quest: Floating Point Fixed

Using floating point #

In Chapter 3 – “Cylon” LED Bar I mentioned that floating-point support was broken for the Raspberry Pi Pico. This has now been fixed thanks to Jeremy Grosser. All you need to do is update the version of rp2040_hal in your alire.toml file:

[[depends-on]]
rp2040_hal = "^2.7.1"

That’s it — floating-point mathematics now works correctly.

Floating-point Cylon scanner #

Now let’s look at the actual floating-point version of the Cylon scanner:

pragma License (Modified_Gpl);
pragma Ada_2022;
pragma Extensions_Allowed (On);

with Pico;
with RP.GPIO;
with Ada.Real_Time;
with Ada.Numerics.Elementary_Functions;

---
--  Flowing LED sample from Chapter 3.1 of the Freenove C tutorial.
--  Cylon real-time edition using floating-point arithmetic.
--
procedure Cylon_Light_Float with
   No_Return
is
   package RT   renames Ada.Real_Time;
   package Num  renames Ada.Numerics;
   package Math renames Ada.Numerics.Elementary_Functions;

   use type Ada.Real_Time.Time_Span;
   use type Ada.Real_Time.Time;

   LED_Count   : constant := 10;         --  The LED bar has 10 LEDs
   Base_Delay  : constant := 0.06;       --  fastest speed (middle, in seconds)
   Amplitude   : constant := 0.12;       --  how much slower the LEDs are at the ends (in seconds)

   ---
   --  Range of LEDs connected to the bar
   --
   type LED_Number is range 1 .. LED_Count;

   ---
   --  Array to hold the pre-calculated delay times.
   --  This makes the flowing movement more organic (slower at the ends).
   --
   type Delay_Array_Type is array (LED_Number) of RT.Time_Span;

   ---
   --  Calculate the delay for a given LED so the scanner slows down at the ends.
   --
   --  @param LED  The LED for which the delay should be calculated
   --  @return     The delay as a Time_Span
   --
   function Delay_Time (LED : in LED_Number) return Ada.Real_Time.Time_Span with
      Inline,
      Pure_Function
   is
      Position : constant Float := Float (LED - LED_Number'First)
                                 / Float (LED_Number'Last - LED_Number'First);
      Phase    : constant Float := Position * Num.Pi;                -- 0 → π
      Factor   : constant Float := (1.0 - Math.Sin (Phase)) / 2.0;   -- 0..1..0
   begin
      return RT.To_Time_Span (Duration (Base_Delay + Amplitude * Factor));
   end Delay_Time;

   ---
   --  GPIO pins the LEDs are connected to
   --
   LEDs : constant array (LED_Number) of access RP.GPIO.GPIO_Point :=
      [Pico.GP16'Access,
       Pico.GP17'Access,
       Pico.GP18'Access,
       Pico.GP19'Access,
       Pico.GP20'Access,
       Pico.GP21'Access,
       Pico.GP22'Access,
       Pico.GP26'Access,
       Pico.GP27'Access,
       Pico.GP28'Access];

   ---
   --  Pre-calculated delay times (only computed once at elaboration)
   --
   Delay_Times : constant Delay_Array_Type := [for I in LED_Number => Delay_Time (I)];

   ---
   --  Time of the next LED change
   --
   Next : RT.Time := RT.Clock;

begin
   --  Configure all 10 GPIO pins as outputs
   --
   for I in LEDs'Range loop
      LEDs (I).Configure (RP.GPIO.Output);
   end loop;

   loop
      --  Sweep right to left
      --
      for I in LEDs'Range loop
         LEDs (I).Set;
         Next := @ + Delay_Times (I);
         delay until Next;
         LEDs (I).Clear;
      end loop;

      --  Sweep left to right
      --
      for I in reverse LEDs'Range loop
         LEDs (I).Set;
         Next := @ + Delay_Times (I);
         delay until Next;
         LEDs (I).Clear;
      end loop;
   end loop;
end Cylon_Light_Float;

To keep energy consumption low I pre-calculate the ten delay values once at program start. I declared the function with Inline and Pure_Function so the compiler has the best chance to optimise it (especially in release mode).

I also used the new Ada 2022 iterated component association to initialise the array:

   Delay_Times : constant Delay_Array_Type := [for I in LED_Number => Delay_Time (I)];

While this looks like syntactic sugar, it has a practical benefit: the array is now clearly constant. The optimiser can take advantage of that, and in theory a perfect compiler could even compute the values at compile time (though the perfect optimiser still doesn’t exist).

The little puzzle #

I ran into a confusing cascade of error messages while writing this. If you’re an experienced Ada programmer, can you spot the mistake in the following snippet (without looking at the corrected code above)?

   LED_Count  : constant := 10;
   type LED_Number is range 1 .. LED_Count;
   type Delay_Array_Type is array (1 .. LED_Count) of RT.Time_Span;
   function Delay_Time (LED : in LED_Number) return Ada.Real_Time.Time_Span;

   Delay_Times : Delay_Array_Type := [for I in LED_Number => Delay_Time (I)];

Answer:
Delay_Array_Type breaks the chain of strongly-typed declarations. By writing array (1 .. LED_Count) the index type becomes the anonymous universal integer instead of the named subtype LED_Number. Universal integer cannot be used as the index type of an array aggregate in this context, so the compiler complains at the aggregate even though the real problem is in the type declaration.

The error messages were misleading because they pointed at the aggregate rather than the faulty array type.

Advantages and disadvantages #

The floating-point version of the Cylon light is about 66 KB larger on the Pico and 11 KB larger on the Pico 2. Energy consumption is similar because the delays are calculated only once, but you can tweak parameters much faster — change a value, reset the board, make deploy and you’re testing again in about 10 seconds once muscle memory kicks in.

For the original Pico, which has no hardware floating-point unit, I will still avoid floating point in the main tutorials. An extra 66 KB is quite a lot on a small microcontroller. In the good old days the Atari 800 managed with just a 2 KB floating-point ROM!

Cylon Scanner with floating point