Skip to main content

Chapter 4 – Analog & PWM

Link Purpose
C Tutorial Chapter 4 – Analog & PWM Freenove’s official C version – Chapter 4 – Analog & PWM
Chapter 4 Analog & PWM - Starter Kit for Raspberry Pi Pico Freenove’s official video description
Alire crate Alire crate containing the Ada code in this chapter
GNATdoc documentation for this chapter Automatically generated HTML documentation for the Ada code in this chapter

A short recap of PWM #

Pulse Width Modulation (PWM) lets us create analogue-like outputs on digital GPIO pins by varying the duty cycle of a high-frequency square wave. The Raspberry Pi Pico’s RP2040 has excellent hardware PWM support, but the Ada HAL requires a little care with frequency, reload values and duty-cycle calculations. That is exactly what we explore in this chapter.

I also used my Voltcraft MSO-5102B mixed-signal oscilloscope throughout. As a teenager in the early 1980s I could only dream of owning a storage oscilloscope; today I capture perfect screenshots with a USB stick and watch every detail of my Ada code in real time.

sketch_04_1_breathing_light.adb #

Developing Pico.Analog / Write_Analog #

The original Freenove C example used a simple analogWrite function. Neither the RP2040 HAL nor the board support package provided one, so I decided to write a reusable Pico.Analog package that we can also use in Chapter 5.

At first everything looked tidy in the specification, but getting Write_Analog to behave correctly was surprisingly difficult.

I started with a high frequency of 1 MHz and a reload of 1000, copied from an example I found. When I ran the breathing loop the LED looked fine but not perfectly smooth even to the naked eye. The MSO-5102B told a surprising story: it measured only 999 Hz instead of 1 MHz. I thought this might be the problem so I tried various user frequencies. None improved the situation and some didn’t work at all.

Oscilloscope showing 999Hz
Oscilloscope showing 999Hz

Eventually I figured out that what I saw on the Oscilloscope the base frequency was divided by the reload period and approximate 1kHz is enough for an LED.

Still, no matter what I did the duty cycle did not move linearly. In the upper quarter (75 %–100 %) the brightness jumped in big steps. On the scope the pulse width changed in uneven increments. I had written an operator * for Percentage that divided first and then multiplied — a classic fixed-point mistake. Because Percentage is a delta 0.1 fixed-point type and Period is an integer, the division lost precision near 100 %.

The fix was simple once I saw it (pair-programming with myself, or rather with Grok, helped enormously): multiply first, then divide. I also expanded the calculation to Integer to avoid overflow as Period is only UInt16 and added special cases for exactly 0 % and 100 %.

Here is the final, smooth version (now written as a concise function expression with Inline and Pure_Function):

   function "*"
      (Left  : in RP.PWM.Period;
       Right : in Percentage)
       return RP.PWM.Period is
     (if Right = 0.0 then 0
      elsif Right = 100.0 then Left
      else RP.PWM.Period (Integer (Left) * Integer (Right) / 100))
   with Inline, Pure_Function;

With the new operator the duty cycle moved perfectly linearly on the scope. I could watch the bright bar sweep smoothly from left to right and back again. When I pressed Auto-Set, Channel 2 (which I was not using) automatically hid itself — a nice little convenience of the MSO-5102B.

The final specification and body of Pico.Analog are shown below. The package also includes convenient Map overloads and the safe To_PWM_LED helper we will need for multiple LEDs.

Specification of Pico.Analog #

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

with Pico.Utils;
with RP.GPIO;
with RP.PWM;
with HAL;

---
--   Simplified analogue GPIO using Pulse Width Modulation (PWM).
--
--   Pulse Width Modulation (PWM) allows us to generate analogue-like outputs
--   on digital GPIO pins by varying the duty cycle of a high-frequency square
--   wave. This package provides a simple, reusable interface for PWM-based
--   analogue output on the Raspberry Pi Pico. It is designed to be a universal
--   library component, saving us from repetitive code and helping the community.
--
package Pico.Analog is
   use type RP.PWM.Period;
   use type HAL.UInt8;

   type PWM_Point is private;

   ---
   --  Default values suitable for dimming an LED.
   --
   Default_Frequency : constant RP.Hertz       := 1_000_000;
   Default_Reload    : constant RP.PWM.Period  := 1_000;
   Default_Divider : constant RP.PWM.Divider := RP.PWM.Divider (RP.Clock.Frequency (RP.Clock.SYS) / Default_Frequency);

   ---
   --  Analogue value as fixed point percentage.
   --
   type Percentage is delta 0.1 digits 4 range 0.0 .. 100.0;

   ---
   --  Analogue value as 8-bit value (for compatibility with C code).
   --
   subtype Analog_Level is HAL.UInt8;

   ---
   --  Create a new PWM point from a GPIO point. This configures the PWM hardware for the given GPIO pin with the
   --  specified base frequency and reload period. Note that the final output frequency will be approximately
   --  Frequency divided by Reload, adjusted to the nearest divider of the Pico's base frequency.
   --
   --: @param Point     GPIO point to use for the PWM output
   --: @param Frequency Base frequency in Hertz
   --: @param Reload    Reload period (ticks per cycle)
   --: @return          New PWM point configured and ready for use
   function To_PWM
      (Point     : in out RP.GPIO.GPIO_Point;
       Frequency : in     RP.Hertz      := Default_Frequency;
       Reload    : in     RP.PWM.Period := Default_Reload)
       return PWM_Point;

   ---
   --  Set the PWM output level using a percentage duty cycle (0.0 to 100.0).
   --
   procedure Write_Analog (Point : in PWM_Point; Level : in Percentage) with
      Inline;

   ---
   --  Set the PWM output level using an 8-bit value (0 to 255).
   --
   procedure Write_Analog (Point : in PWM_Point; Level : in Analog_Level) with
      Inline;

   ---
   --  Get the underlying PWM point to use with the RP.PWM package if more advanced control is required.
   --
   function Get_Base (Point : in PWM_Point) return RP.PWM.PWM_Point with
      Inline, Pure_Function;

   ---
   --  Get the frequency set for this PWM point.
   --
   function Get_Frequency (Point : in PWM_Point) return RP.Hertz with
      Inline, Pure_Function;

   ---
   --  Get the reload counter set for this PWM point.
   --
   function Get_Reload (Point : in PWM_Point) return RP.PWM.Period with
      Inline, Pure_Function;

   ---
   --  Calculate the actual reload counter needed for a given analogue output expressed as a percentage.
   --
   --: @param Left   Base reload counter
   --: @param Right  Desired analogue output as a percentage (0.0 .. 100.0)
   --: @return       Scaled reload counter corresponding to the duty cycle
   function "*"
      (Left  : in RP.PWM.Period;
       Right : in Percentage)
       return RP.PWM.Period is
      (if Right = 0.0 then 0
       elsif Right = 100.0 then Left
       else RP.PWM.Period (Integer (Left) * Integer (Right) / 100)) with
      Inline, Pure_Function;

   ---
   --  Calculate the actual reload counter needed for a given analogue output expressed as an 8-bit level.
   --
   --: @param Left   Base reload counter
   --: @param Right  Desired analogue output as an 8-bit value (0 .. 255)
   --: @return       Scaled reload counter corresponding to the duty cycle
   function "*"
      (Left  : in RP.PWM.Period;
       Right : in Analog_Level)
       return RP.PWM.Period is
      (if Right = 0 then 0
       elsif Right = 255 then Left
       else RP.PWM.Period (Integer (Left) * Integer (Right) / 255)) with
      Inline, Pure_Function;

   ---
   --  Map an input value from a custom range directly to a Percentage (0.0 .. 100.0).
   --
   --  This is a convenient shortcut that scales any integer input (for example from an ADC, a sensor, or a counter)
   --  into the full percentage range used by ``Write_Analog``. It gives you 1000 distinct levels (0.0, 0.1, 0.2 …
   --  99.9, 100.0) which provides finer control than the 256 levels of ``Analog_Level``.
   --
   --  Internally it first calls ``Pico.Utils.Map`` to get a value in the range 0..999, then converts that to
   --  ``Percentage``. The division by 10 is done by the fixed-point type itself — you do **not** need to split
   --  the integer manually.
   --
   --  Example:
   --    --  Convert a potentiometer reading (0..1023) to a PWM percentage
   --    Brightness := Map (Pot_Value, 0, 1023);
   --    Write_Analog (LED_Point, Brightness);
   --
   --: @param In_Value  Value to be mapped
   --: @param In_Min    Lower bound of the input range
   --: @param In_Max    Upper bound of the input range
   --: @return          Value scaled to the range 0.0 .. 100.0 as Percentage
   function Map_Percentage
      (In_Value : in Integer;
       In_Min   : in Integer;
       In_Max   : in Integer)
       return Percentage is (Percentage (Pico.Utils.Map (In_Value, In_Min, In_Max, 0, 999)) / 10) with
      Inline, Pure_Function;

   ---
   --  Map an input value from a custom range directly to an Analog_Level (0..255).
   --
   --  This is a convenient shortcut for the common case where you want to convert a sensor reading or any integer
   --  value into an 8-bit analogue output suitable for PWM. It internally calls ``Pico.Utils.Map`` and scales the
   --  result to the full range of ``Analog_Level``.
   --
   --  Example:
   --    --  Convert a potentiometer reading (0..1023) to PWM level
   --    LED_Level := Map (Pot_Value, 0, 1023);
   --
   --: @param In_Value  Value to be mapped
   --: @param In_Min    Lower bound of the input range
   --: @param In_Max    Upper bound of the input range
   --: @return          Value scaled to the range 0..255 as Analog_Level
   function Map_Analog_Level
      (In_Value : in Integer;
       In_Min   : in Integer;
       In_Max   : in Integer)
       return Analog_Level is
      (Analog_Level
          (Pico.Utils.Map
              (In_Value, In_Min, In_Max, Integer (Analog_Level'First), Integer (Analog_Level'Last - 1)))) with
      Inline, Pure_Function;

private

   type PWM_Point is record
      Base      : RP.PWM.PWM_Point;
      Frequency : RP.Hertz;
      Reload    : RP.PWM.Period;
   end record;

   function Get_Base (Point : in PWM_Point) return RP.PWM.PWM_Point is (Point.Base);
   function Get_Frequency (Point : in PWM_Point) return RP.Hertz is (Point.Frequency);
   function Get_Reload (Point : in PWM_Point) return RP.PWM.Period is (Point.Reload);

end Pico.Analog;

Body of Pico.Analog #

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

package body Pico.Analog is

   function To_PWM
      (Point     : in out RP.GPIO.GPIO_Point;
       Frequency : in     RP.Hertz      := Default_Frequency;
       Reload    : in     RP.PWM.Period := Default_Reload)
       return PWM_Point
   is
      Retval : constant PWM_Point :=
         (RP.PWM.To_PWM (Point),
          Frequency,
          Reload);
   begin
      RP.PWM.Set_Frequency (Retval.Base.Slice, Frequency);
      RP.PWM.Set_Interval (Retval.Base.Slice, Reload);
      RP.PWM.Enable (Retval.Base.Slice);
      Point.Configure (RP.GPIO.Output, RP.GPIO.Floating, RP.GPIO.PWM);

      return Retval;
   end To_PWM;

   procedure Write_Analog (Point : in PWM_Point; Level : in Percentage) is
      Reload     : constant RP.PWM.Period := Get_Reload (Point);
      Duty_Cycle : constant RP.PWM.Period := Reload * Level;
   begin
      RP.PWM.Set_Duty_Cycle (Point.Base.Slice, Point.Base.Channel, Duty_Cycle);
      return;
   end Write_Analog;

   procedure Write_Analog (Point : in PWM_Point; Level : in Analog_Level) is
      Reload     : constant RP.PWM.Period := Get_Reload (Point);
      Duty_Cycle : constant RP.PWM.Period := Reload * Level;
   begin
      RP.PWM.Set_Duty_Cycle (Point.Base.Slice, Point.Base.Channel, Duty_Cycle);
      return;
   end Write_Analog;

begin
   RP.PWM.Initialize;
end Pico.Analog;

The actual sketch. #

The actual program us now just a simple as the C version. As will any future sketch using PWM Output.

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

with Pico;
with Ada.Real_Time;
with Pico.Analog;

---
--  Breathing LED sample from Chapter 4.1 from the Freenove C-Tutorial
--
procedure Sketch_04_1_Breathing_Light with
   No_Return
is
   use type Ada.Real_Time.Time_Span;
   use type Ada.Real_Time.Time;

   LED         : constant Pico.Analog.PWM_Point   := Pico.Analog.To_PWM (Pico.GP15);
   Hold_Period : constant Ada.Real_Time.Time_Span := Ada.Real_Time.To_Time_Span (0.05);
   Next        : Ada.Real_Time.Time               := Ada.Real_Time.Clock;
begin
   loop
      for I in 0 .. 100 loop
         Pico.Analog.Write_Analog (LED, Pico.Analog.Percentage (I));
         Next := @ + Hold_Period;
         delay until Next;
      end loop;

      for I in reverse 0 .. 100 loop
         Pico.Analog.Write_Analog (LED, Pico.Analog.Percentage (I));
         Next := @ + Hold_Period;
         delay until Next;
      end loop;
   end loop;
end Sketch_04_1_Breathing_Light;

Now that it’s finished it looks all nice and tidy but the was there was stony.

sketch_04_2_flowing_light_2.adb – The flowing light bar #

Now that we had a clean, reusable Pico.Analog package, the next logical step was to tackle the flowing light bar (sometimes called a “comet” or “cylon” effect) using ten PWM channels.

The C original uses a map function to calculate brightness. Even though we could have pre-calculated the values, I decided to implement a proper Map function in Pico.Utils because I know we will need linear interpolation again later.

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

---
--   General utility functions for the Raspberry Pi Pico.
--
--   This package contains small, reusable helper functions that I find myself
--   using again and again when working with the Pico. It is designed as a
--   universal support library and grows naturally alongside the Pi Ada Tutorial.
--
--   Current content:
--     * Map – linear interpolation (the famous Arduino map() function)
--
package Pico.Utils is

   ---
   --  Map a value from one range into another range using linear interpolation.
   --
   --  This is the well-known Arduino ``map()`` function, re-implemented cleanly in Ada. It performs a linear
   --  interpolation that scales the input value ``In_Value`` from the source range ``[In_Min .. In_Max]`` to the target
   --  range ``[Out_Min .. Out_Max]``.
   --
   --  The function is particularly useful when working with analogue sensors, PWM outputs, servo positions, or any
   --  situation where you need to convert a reading from one scale (e.g. raw ADC value) into a different scale (e.g.
   --  percentage, PWM duty cycle, servo angle in degrees).
   --
   --: Example:
   --:   --  Convert an ADC reading (0..4095) to a PWM percentage (0..100)
   --:    Duty_Cycle := Map (ADC_Value, 0, 4095, 0, 100);
   --
   --: @param In_Value Value to be mapped
   --: @param In_Min   Lower bound of the input range
   --: @param In_Max   Upper bound of the input range
   --: @param Out_Min  Lower bound of the output range
   --: @param Out_Max  Upper bound of the output range
   --: @return         The mapped value in the new range
   function Map
      (In_Value : in Integer;
       In_Min   : in Integer;
       In_Max   : in Integer;
       Out_Min  : in Integer;
       Out_Max  : in Integer)
       return Integer is ((In_Value - In_Min) * (Out_Max - Out_Min) / (In_Max - In_Min) + Out_Min) with
      Inline, Pure_Function;

end Pico.Utils;

The program itself looked straightforward at first — until I tested it on the Pico 2W.

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

with Pico.Analog;
with Ada.Real_Time;
---
--  Flowing LED sample from Chapter 3.1 from the Freenove C-Tutorial.
--
procedure Sketch_04_2_Flowing_Light_2 with
   No_Return
is
   use type Ada.Real_Time.Time_Span;
   use type Ada.Real_Time.Time;

   ---
   --  The LED bar has 10 LEDs
   --
   LED_Count : constant := 10;

   ---
   --   GPIO Pins the LEDs are connected to
   --
   LEDs : constant array (1 .. LED_Count)
      of Pico.Analog.PWM_Point :=
      [Pico.Analog.To_PWM (Pico.GP16),
      Pico.Analog.To_PWM (Pico.GP17),
      Pico.Analog.To_PWM (Pico.GP18),
      Pico.Analog.To_PWM (Pico.GP19),
      Pico.Analog.To_PWM (Pico.GP20),
      Pico.Analog.To_PWM (Pico.GP21),
      Pico.Analog.To_PWM (Pico.GP22),
      Pico.Analog.To_PWM (Pico.GP26),
      Pico.Analog.To_PWM (Pico.GP27),
      Pico.Analog.To_PWM (Pico.GP28)];

   ---
   --  Predefined duty-cycle table for creating a smooth "breathing" or fading effect.
   --
   --  This array contains 30 PWM values that start at 0, rise smoothly to full brightness (using powers of two), and
   --  then fall back to 0 again. It is ideal for LED breathing animations, status indicators, or any effect where you
   --  want a gentle fade-in / fade-out without doing the maths at runtime.
   --
   --  The values were generated with the ``Map`` function (see below) and are already scaled to the full 12-bit PWM
   --  range (0 .. 4095) used by the Pico.
   --
   --:  Typical usage:
   --:    for I in Duty_Cycle_Table'Range loop
   --:       Write_Analog (LED_Point, Duty_Cycle_Table (I));
   --:       delay 0.05;
   --:    end loop;
   --!pp off
   Duty_Cycle_Table : constant array (1 .. 30)
      of Integer := [
      0,     0,     0,     0,   0,   0,   0,  0,  0,  0,
      4_095, 2_047, 1_023, 512, 256, 128, 64, 32, 16, 8,
      0,     0,     0,     0,   0,   0,   0,  0,  0,  0];
   --!pp on

   Hold_Period : constant Ada.Real_Time.Time_Span := Ada.Real_Time.To_Time_Span (0.05);
   Next        : Ada.Real_Time.Time               := Ada.Real_Time.Clock;

begin
   loop
      --  Fowling light right to left
      for I in 0 .. 2 * LED_Count - 1 loop
         for J in LEDs'Range loop
            Pico.Analog.Write_Analog (LEDs (J), Pico.Analog.Map_Percentage (Duty_Cycle_Table (J + I), 0, 4_096));
         end loop;
         Next := @ + Hold_Period;
         delay until Next;
      end loop;

      --  Fowling light left to right
      for I in 0 .. 2 * LED_Count - 1 loop
         for J in reverse LEDs'Range loop
            Pico.Analog.Write_Analog
               (LEDs (J), Pico.Analog.Map_Percentage (Duty_Cycle_Table (LED_Count - J + I), 0, 4_096));
         end loop;
         Next := @ + Hold_Period;
         delay until Next;
      end loop;
   end loop;
end Sketch_04_2_Flowing_Light_2;

The Pico 2 debugging adventure #

Several LEDs stayed dark. At first I feared I had damaged the hardware. A quick continuity test on the LEDs and running the Chapter 3 cylon light on the Pico 2W proved the hardware was fine.

So the problem had to be in the software.

After some head-scratching and advice from Grok, I suspected PWM slice configuration issues. I created a minimal test with just two LEDs on GPIO14 and GPIO15 and added UART debug output.

procedure Breathing_2_Lights with
   No_Return
is
   use type Ada.Real_Time.Time_Span;
   use type Ada.Real_Time.Time;
   use type Pico.Analog.Analog_Level;

   ---
   --  Use GP14 and GP15 for the two external LEDs
   --
   LED_1_GPIO  : constant RP.GPIO.GPIO_Point      := Pico.GP14;
   LED_2_GPIO  : constant RP.GPIO.GPIO_Point      := Pico.GP15;
   LED_1_PWM   : constant RP.PWM.PWM_Point        := RP.PWM.To_PWM (LED_1_GPIO);
   LED_2_PWM   : constant RP.PWM.PWM_Point        := RP.PWM.To_PWM (LED_2_GPIO);
   Hold_Period : constant Ada.Real_Time.Time_Span := Ada.Real_Time.To_Time_Span (0.05);
   Next        : Ada.Real_Time.Time               := Ada.Real_Time.Clock;
begin
   Pico.UART_IO.Initialise;
   Pico.UART_IO.Put_Line ("+ Breathing_2_Lights");

   Pico.UART_IO.Put_Line ("> LED_1 Pin     => " & LED_1_GPIO.Pin'Image);
   Pico.UART_IO.Put_Line ("> LED_1 Slice   => " & LED_1_PWM.Slice'Image);
   Pico.UART_IO.Put_Line ("> LED_1 Channel => " & LED_1_PWM.Channel'Image);
   Pico.UART_IO.Put_Line ("> LED_2 Pin     => " & LED_2_GPIO.Pin'Image);
   Pico.UART_IO.Put_Line ("> LED_2 Slice   => " & LED_2_PWM.Slice'Image);
   Pico.UART_IO.Put_Line ("> LED_2 Channel => " & LED_2_PWM.Channel'Image);

   RP.PWM.Set_Divider (LED_1_PWM.Slice, Pico.Analog.Default_Divider);
   RP.PWM.Set_Interval (LED_1_PWM.Slice, Pico.Analog.Default_Reload);
   RP.PWM.Set_Interval (LED_2_PWM.Slice, Pico.Analog.Default_Reload);
   RP.PWM.Enable (LED_1_PWM.Slice);
   Pico.GP14.Configure (RP.GPIO.Output, RP.GPIO.Floating, RP.GPIO.PWM);
   Pico.GP15.Configure (RP.GPIO.Output, RP.GPIO.Floating, RP.GPIO.PWM);

   Pico.UART_IO.Put_Line ("> Entering main loop");
   loop
      for I in Pico.Analog.Analog_Level'Range loop
         RP.PWM.Set_Duty_Cycle (LED_1_PWM.Slice, LED_1_PWM.Channel, Pico.Analog."*" (Pico.Analog.Default_Reload, I));
         RP.PWM.Set_Duty_Cycle (LED_2_PWM.Slice, LED_2_PWM.Channel, Pico.Analog."*" (Pico.Analog.Default_Reload, I));

         Next := @ + Hold_Period;
         delay until Next;
      end loop;

      for I in reverse Pico.Analog.Analog_Level'Range loop
         RP.PWM.Set_Duty_Cycle (LED_1_PWM.Slice, LED_1_PWM.Channel, Pico.Analog."*" (Pico.Analog.Default_Reload, I));
         RP.PWM.Set_Duty_Cycle (LED_2_PWM.Slice, LED_2_PWM.Channel, Pico.Analog."*" (Pico.Analog.Default_Reload, I));

         Next := @ + Hold_Period;
         delay until Next;
      end loop;
   end loop;
end Breathing_2_Lights;

The result was surprising:

+ Breathing_2_Lights
> LED_1 Pin     =>  14
> LED_1 Slice   =>  6
> LED_1 Channel => A
> LED_2 Pin     =>  15
> LED_2 Slice   =>  7
> LED_2 Channel => B
> Entering main loop

According to the RP2350 datasheet, GPIO14 should be on slice 7, not 6. This was clearly a bug in the experimental RP2350 HAL.

Looking closer at RP.PWM.To_PWM, I noticed a classic copy-paste error: the function performed Shift_Right (Pin, 1) but then ignored the shifted value and used the original Pin for the mask. A one-line fix (Slice := Slice and 2#111#;) solved the problem.

I have submitted a pull request upstream. With the corrected HAL the flowing light bar now works beautifully on both the Pico 1 and Pico 2 / Pico 2W.

The final flowing light program #

The actual sketch is now as simple and elegant as the C version:

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

with Pico.Analog;
with Ada.Real_Time;

---
--  Flowing LED sample from Chapter 3.1 from the Freenove C-Tutorial.
--
procedure Sketch_04_2_Flowing_Light_2 with
   No_Return
is
   use type Ada.Real_Time.Time_Span;
   use type Ada.Real_Time.Time;

   ---
   --  The LED bar has 10 LEDs
   --
   LED_Count : constant := 10;

   ---
   --   GPIO Pins the LEDs are connected to
   --
   LEDs : constant array (1 .. LED_Count)
      of Pico.Analog.PWM_Point :=
      [Pico.Analog.To_PWM (Pico.GP16),
      Pico.Analog.To_PWM (Pico.GP17),
      Pico.Analog.To_PWM (Pico.GP18),
      Pico.Analog.To_PWM (Pico.GP19),
      Pico.Analog.To_PWM (Pico.GP20),
      Pico.Analog.To_PWM (Pico.GP21),
      Pico.Analog.To_PWM (Pico.GP22),
      Pico.Analog.To_PWM (Pico.GP26),
      Pico.Analog.To_PWM (Pico.GP27),
      Pico.Analog.To_PWM (Pico.GP28)];

   ---
   --  Predefined duty-cycle table for creating a smooth "breathing" or fading effect.
   --
   --  This array contains 30 PWM values that start at 0, rise smoothly to full brightness (using powers of two), and
   --  then fall back to 0 again. It is ideal for LED breathing animations, status indicators, or any effect where you
   --  want a gentle fade-in / fade-out without doing the maths at runtime.
   --
   --  The values were generated with the ``Map`` function (see below) and are already scaled to the full 12-bit PWM
   --  range (0 .. 4095) used by the Pico.
   --
   --:  Typical usage:
   --:    for I in Duty_Cycle_Table'Range loop
   --:       Write_Analog (LED_Point, Duty_Cycle_Table (I));
   --:       delay 0.05;
   --:    end loop;
   --!pp off
   Duty_Cycle_Table : constant array (1 .. 3 * LED_Count)
      of Integer := [
      0,     0,     0,     0,   0,   0,   0,  0,  0,  0,
      4_095, 2_047, 1_023, 512, 256, 128, 64, 32, 16, 8,
      0,     0,     0,     0,   0,   0,   0,  0,  0,  0];
   --!pp on

   Hold_Period : constant Ada.Real_Time.Time_Span := Ada.Real_Time.To_Time_Span (0.05);
   Next        : Ada.Real_Time.Time               := Ada.Real_Time.Clock;

begin
   loop
      --  Fowling light right to left
      for I in 0 .. 2 * LED_Count - 1 loop
         for J in LEDs'Range loop
            Pico.Analog.Write_Analog (LEDs (J), Pico.Analog.Map_Percentage (Duty_Cycle_Table (J + I), 0, 4_096));
         end loop;
         Next := @ + Hold_Period;
         delay until Next;
      end loop;

      --  Fowling light left to right
      for I in 0 .. 2 * LED_Count - 1 loop
         for J in reverse LEDs'Range loop
            Pico.Analog.Write_Analog
               (LEDs (J), Pico.Analog.Map_Percentage (Duty_Cycle_Table (LED_Count - J + I), 0, 4_096));
         end loop;
         Next := @ + Hold_Period;
         delay until Next;
      end loop;
   end loop;
end Sketch_04_2_Flowing_Light_2;

What I learned in Chapter 4 #

  • Always trust the oscilloscope first — it shows what the code is really doing.
  • Fixed-point arithmetic needs “multiply first, then divide” to keep precision, especially near the upper end of the range.
  • When working with the experimental Pico 2 support, never assume the HAL behaves exactly like the RP2040 version. Debugging with UART output and the scope is invaluable.
  • A modern USB-powered mixed-signal scope with screenshot capability is something I could only dream of as a teenager in the early 1980s. Capturing perfect images of my PWM signals and sharing them on the tutorial site still feels like a small miracle

Happy hacking!