Ada on the Raspberry Pi Pico

This project provides libraries and examples for the Ada programming language on the RP2040 microcontroller and the Raspberry Pi Pico development board.

Code snippets included here are meant to be illustrative but cannot be compiled standalone. For working examples, see pico_examples.

This document assumes you are familiar with the Ada language. If you are new to Ada, learn.adacore.com has an excellent introduction.


Getting Started

Install Alire

The Alire package manager is used to manage dependencies and build software using these libraries. You will need to download and install Alire before continuing.

Assuming you are running Linux on x86_64, the following commands should work.

curl -L -O https://github.com/alire-project/alire/releases/download/v1.1.0/alr-1.1.0-bin-x86_64-linux.zip
unzip alr-1.1.0-bin-x86_64-linux.zip
sudo cp bin/alr /usr/local/bin

Create a new project

Use Alire to create a skeleton project and add a dependency on pico_bsp.

alr init --bin hello_pico
cd hello_pico
alr with pico_bsp

If Alire prompts you to install tools like unzip and git, accept these changes. If prompted to select a toolchain, choose the most recent versions of gnat_arm_elf and gprbuild.

Next, edit hello_pico.gpr to import pico_bsp and add the Target, Runtime, and Linker configuration near the top.

with "config/hello_pico_config.gpr";
with "pico_bsp.gpr";
project Hello_Pico is
    for Target use "arm-eabi";
    for Runtime ("Ada") use "zfp-cortex-m0p";
    package Linker is
        for Switches ("Ada") use Pico_BSP.Linker_Switches;
    end Linker;

    for Source_Dirs use ("src");

Build the project.

alr build

If the build was successful, there will be an ELF binary in ./bin/hello_pico.

Flashing the Pico

There are several ways to get your compiled code onto the Pico. It is highly recommended that you setup a Serial Wire Debug (SWD) debugger so that you can run GDB. The following video shows how to setup a Raspberry Pi Zero as a SWD debugger, but other debug adapters like the Segger J-Link should work.

Using GDB, you can load the ELF binary and run it. If you are not using an OpenOCD based debugger, these commands may be slightly different. Consult your debugger’s documentation if this doesn’t work.

arm-eabi-gdb bin/hello_pico
target extended-remote localhost:3333
monitor arm semihosting enable
load
run

If you cannot use SWD, pico-debug can be loaded into memory on the Pico to allow you to debug over USB. If you use pico-debug, your code cannot make use of the USB port or the second CPU core and pico-debug will use up some memory. See the pico-debug README for more information.

elf2uf2 can convert a compiled ELF binary to UF2 format, which can be loaded onto the Pico over USB after holding the BOOTSEL button. If you choose this option, you will not be able to debug your code when it crashes.

Blinking an LED

You’ve probably noticed that we haven’t written any code yet! Let’s change that with the traditional blinking LED demo.

Edit src/hello_pico.adb

with RP.Device;
with RP.Clock;
with RP.GPIO;
with Pico;

procedure Hello_Pico is
begin
   RP.Clock.Initialize (Pico.XOSC_Frequency);
   RP.Device.Timer.Enable;
   Pico.LED.Configure (RP.GPIO.Output);

   loop
      Pico.LED.Toggle;
      RP.Device.Timer.Delay_Milliseconds (250);
   end loop;
end Hello_Pico;

At boot, the RP2040 is configured to use it’s internal oscillator to run the CPU’s clock. This oscillator is both slow and inaccurate, so an external 12 MHz crystal oscillator is included on the Pico board. The RP.Clock.Initialize procedure reconfigures the CPU to run at 125 MHz using the external crystal as a reference. This is needed for accurate timing and will almost always be the first call your program makes.

The internal Timer peripheral counts up at 1 MHz and can be used to implement delays. RP.Device.Timer.Enable configures an interrupt needed for Delay_Milliseconds.

By default, all of the GPIO pins are inputs, so we configure the pin named Pico.LED as an output.

The loop toggles the LED on and off every 250 milliseconds. Use alr build to recompile the project and load it using your debugger. If all goes well, the onboard LED will blink!

Development Guide

Source repositories

  • rp2040_hal contains drivers and register definitions for the RP2040’s onboard peripherals.

  • pico_bsp provides pin definitions and drivers for the Raspberry Pi Pico development board and compatible addons.

  • pico_examples demonstrates the functionality of rp2040_hal and pico_bsp and serves as a template for new projects.

Versioning

Releases are tagged and added to the Alire index regularly. The API provided by these drivers should not be considered stable until a 1.0.0 release is tagged. Until then, breaking changes are allowed. After 1.0.0, we plan to follow the Semver pattern for version numbers.

This documentation relates to the master branch of the source repositories and may differ from what’s available in Alire. If you need to use unreleased features or fixes, you can pin your project to the master branch. The master branch is unstable and may break your code, so this is not recommended unless you have a good reason.

alr pin --use=https://github.com/JeremyGrosser/rp2040_hal rp2040_hal

Hardware overview

The Raspberry Pi RP2040 is an ARM Cortex-M0+ microcontroller. Detailed specifications and low level documentation can be found in the RP2040 datasheet.

Pico pinout

Pin diagram
Figure 1. Pin diagram

The Raspberry Pi Pico is a low cost development board that includes the RP2040, a micro USB port, crystal oscillator, voltage regulator, and a green LED. The Pico datasheet includes schematics, pinout diagrams, and design recommendations.

If you wish to develop a custom board using the RP2040, the hardware design guide contains reference designs and layout tips.

Clock configuration

Clock tree
Figure 2. Clock tree

See section 2.15 of the RP2040 datasheet for a detailed description of the clock hierarchy.

procedure RP.Clock.Initialize
    (XOSC_Frequency     : XOSC_Hertz := 0;
     XOSC_Startup_Delay : XOSC_Cycles := 12_032);

Some oscillators may need a longer startup delay. If your system clock doesn’t run at the frequency you expect, try adjusting this. NXP’s Crystal Oscillator Troubleshooting Guide has some good advice if you’re building a custom board.

Table 1. Clock configuration after Initialize

Name

Frequency

Enabled

clk_sys

125 MHz

Yes

clk_peri

125 MHz

No

clk_ref

1 MHz

Yes

clk_rtc

1 MHz

No

clk_usb

48 MHz

No

clk_adc

48 MHz

No

clk_gpout0-3

Not configured

No

Some peripherals, such as the UART, need a clock enabled before they can be used.

RP.Clock.Enable (RP.Clock.PERI);

Clocks may be disabled to reduce power consumption.

RP.Clock.Disable (RP.Clock.PERI);

The RP.Clock.Frequency function uses the internal frequency counter to measure internal clocks. This is used to configure dividers and enforce minimum operating frequencies in preconditions. Most drivers provide convenience wrappers that perform these calculations for you.

declare
    Target_Frequency : constant Hertz := 1_000_000;
    Divider          : constant Positive := Positive (RP.Clock.Frequency (RP.Clock.SYS) / Target_Frequency);
begin
    Put_Line (Divider'Image);
end;

GPIO

IO pad diagram
Figure 3. IO pad diagram

The RP2040 has 29 GPIO pins. The RP.GPIO package implements the abstract HAL.GPIO interface.

type GPIO_Pin is range 0 .. 29;
type GPIO_Point is new HAL.GPIO.GPIO_Point with record
    Pin : GPIO_Pin;
end record;

The GPIO peripheral must be enabled before configuring and manipulating pin states.

RP.GPIO.Enable;

Naming pins

BSP packages contain names for pins that map to a schematic net name or silkscreened label familiar to the user. For example, package Pico contains the following definitions that match the board’s documentation.

   GP25 : aliased GPIO_Point := (Pin => 25);
   GP26 : aliased GPIO_Point := (Pin => 26);
   GP27 : aliased GPIO_Point := (Pin => 27);
   GP28 : aliased GPIO_Point := (Pin => 28);

   LED  : GPIO_Point renames GP25;

Basic operation

RP.GPIO.Configure has several optional parameters. The default values will configure the pin for basic Get, Set, Clear, Toggle operation.

Pico.LED.Configure (RP.GPIO.Output);
loop
    Pico.LED.Toggle;
end loop;

Pin functions

The Func parameter connects the pin to an internal peripheral, such as I2C. See 2.19.2. Function Select in the RP2040 datasheet or the Pico pin diagram for the mapping between pins and peripherals. This example also shows how Ada’s renames can be used to make it clear what a pin will be used for.

declare
    use RP.GPIO;
    SCL : GPIO_Point renames Pico.GP27;
    SDA : GPIO_Point renames Pico.GP26;
begin
    SCL.Configure
        (Mode       => Output,
         Pull       => Pull_Up,
         Func       => I2C,
         Schmitt    => True,
         Slew_Fast  => False);
    SDA.Configure
        (Mode       => Output,
         Pull       => Pull_Up,
         Func       => I2C,
         Schmitt    => True,
         Slew_Fast  => False);
end;

Interrupts

Level- and edge-triggered interrupts can be configured to call a procedure when a pin’s state changes.

declare
    use RP.GPIO;

    procedure Switch_Changed
        (Pin     : GPIO_Pin;
         Trigger : Interrupt_Triggers)
    is
    begin
        case Trigger is
            when Rising_Edge =>
                LED.Set;
            when Falling_Edge =>
                LED.Clear;
        end case;
    end Switch_Changed;
begin
    GP22.Configure (Input, Floating);
    GP22.Set_Interrupt_Handler (Switch_Changed'Unchecked_Access);
    --  Move Switch_Changed into a package to avoid the need to use Unchecked_Access here.
    GP22.Enable_Interrupt (Rising_Edge);
    GP22.Enable_Interrupt (Falling_Edge);
end;

Timing

Timer

The Timer peripheral runs at 1 MHz (1 microsecond per tick) and counts up from zero at power on. The counter is 64 bits wide and will not overflow in your lifetime. The RP.Timer package defines several procedures for getting the current timer value and delaying for a period of time. This package implements the HAL.Time interface.

Clock returns the current value of the timer, microseconds since power on.

declare
    Now : constant RP.Timer.Time := RP.Timer.Clock;
begin
    Put_Line (Now'Image);
end;

Delay_Microseconds polls Clock until a number of microseconds has elapsed. This method is recommended if you need accurate, short delays.

RP.Device.Timer.Delay_Microseconds (1_000); --  one millisecond

All of the other procedures in RP.Timer configure TIMER_IRQ_2 interrupt to fire after a time has elapsed. This allows the chip to go into a low power mode while waiting. Enable must be called before any of the interrupt driven delays may be used. Handling the timer interrupt does incur latency on the order of a few microseconds, so the actual time elapsed may be slightly longer than requested.

Use Delay_Milliseconds if you need delays longer than a few microseconds and don’t care about interrupt latency.

RP.Device.Timer.Enable;
RP.Device.Timer.Delay_Milliseconds (1_000);

A common pattern is to use Delay_Until to ensure that a loop executes on a fixed schedule. Delay_Until is the best of both approaches as it uses interrupts for low power and returns very close to the specified time.

declare
    use RP.Timer;
    T : Time := Clock; --  RP.Timer.Clock does not use interrupts, it can be called before Enable
begin
    RP.Device.Timer.Enable;

    loop
        T := T + Milliseconds (100);
        RP.Device.Timer.Delay_Until (T);

        --  Do some work here. As long as it takes less than 100ms to execute,
        --  this loop will run at precise 100ms intervals.
    end loop;
end;

SysTick

RP.SysTick implements the HAL.Time interface using the standard 24-bit SysTick timer included with each ARM Cortex-M0+ core. SysTick runs at 1 KHz (1 millisecond per tick). Delay_Microseconds is not supported with this driver. Interrupts are used to implement all of the delay procedures. As the timer is only 24 bits wide, it will overflow every 4.66 hours. The RP.SysTick driver will handle overflow correctly as long as your delay is shorter than that.

RP.SysTick is not recommended as long as RP.Timer can be used. SysTick is provided for programs that need portability to other ARM microcontrollers.

Real Time Clock (RTC)

The RTC tracks the date and time in years, months, days, day of week, hours, minutes, and seconds.

Due to issues with clock configuration, this RTC implementation is only accurate to within a few minutes per day. It is recommended that you use an external RTC or reference clock if the RTC cannot be synchronized on a regular basis. We hope to improve this in the future.

The RTC is not running by default, but once initialized, will continue running as long as power is supplied. This means the time can persist through a reset. In most cases, you’ll want to check if the RTC is already running before initializing it.

if not RP.Device.RTC.Running then
    RP.Device.RTC.Initialize;
end if;

RP.RTC implements the HAL.Real_Time_Clock interface. Date and time are represented by the RTC_Date and RTC_Time records.

with HAL.Real_Time_Clock; use HAL.Real_Time_Clock;
with RP.Device;

RP.Device.RTC.Set
    (Time =>
        (Hour => 12,
         Min  => 0,
         Sec  => 0),
     Date =>
        (Day_Of_Week => Friday,
         Year        => 21,
         Month       => October,
         Day         => 1));

declare
    Time : RTC_Time;
    Date : RTC_Date;
begin
    RP.Device.RTC.Get (Time, Date);
end;

The RTC_Year type has the range 0 .. 99, so you will likely need to add an offset to this value for display purposes. The rtc example includes a To_String function that converts RTC_Time and RTC_Date into an ISO 8601 string.

No support is provided for time zones, leap seconds, or daylight savings time conversion.

Pause and Resume procedures are provided to temporarily stop the RTC. Note that the Running function will return False while the RTC is paused. If the chip is reset while the RTC is paused, the RP.RTC package cannot differentiate between a paused RTC and an uninitialized one.

Serial interfaces

The UART, SPI, and I2C drivers implement the HAL.UART, HAL.SPI, and HAL.I2C interfaces to provide compatibility with other platforms. The interfaces all have Transmit and Receive procedures that provide status information to an out parameter. The RP.UART, RP.SPI, and RP.I2C_Master packages provide platform specific initialization, configuration, and status procedures that are not included in the HAL interfaces. Timeouts are implemented by polling RP.Timer.Clock.

All of the serial interfaces require clk_peri to be running.

RP.Clock.Enable (RP.Clock.PERI);

GPIO pins need to be configured for the serial peripherals to interact with the outside world. See the pin diagram for information on which pins can be associated with serial functions. All serial pins should use the Output mode, regardless of the serial data direction. External pull-up resistors are recommended for buses that require them, rather than exposing the internal pull-ups to bus currents.

UART

with HAL.UART;
with RP.GPIO; use RP.GPIO;
with RP.Clock;
with RP.Device;
with Pico;

procedure UART_Example is
    UART_TX : GPIO_Point renames Pico.GP0;
    UART_RX : GPIO_Point renames Pico.GP1;
    Port    : RP.UART.UART_Port renames RP.Device.UART_0;

    Message : constant String := "Hello, Pico!";
    Data    : HAL.UART.UART_Data_8b (Message'Range)
        with Address => Message'Address;
    --  This is an implicit unchecked conversion that takes advantage of the
    --  fact that Character is always 8 bits wide.

    Status  : HAL.UART.UART_Status;
begin
    RP.Clock.Initialize (Pico.XOSC_Frequency);
    RP.Clock.Enable (RP.Clock.PERI);

    UART_TX.Configure (Output, Floating, UART);
    UART_RX.Configure (Output, Floating, UART);

    --  The default config is 115200 8n1, this example just overrides the baud rate
    Port.Configure (Config => (Baud => 9600, others => <>));

    --  A Timeout of 0 means this procedure may block forever. The default timeout is one second.
    UART_TX.Transmit (Data, Status, Timeout => 0);
end;

SPI

SPI is a synchronous interface. For every word that is transmitted, another is received and buffered simultaneously. The SPI peripheral can buffer up to 8 words in the Transmit and Receive FIFOs. The bus clock is generated when Transmit is called. Receive should be called after every Transmit to read the same number of bytes from the FIFO buffer. If Receive is not called after every 8 transmitted words, the receive buffer will overflow and data will be silently dropped.

The chip select signal, often referred to as CSn, CS, NSS, CS, or SS, is optional. If specified, it will be pulled low while a SPI transfer is in progress. Some SPI devices require this signal to be held low across multiple transfers. For those devices, configure this as a normal output pin and toggle it directly, rather than letting the SPI peripheral control it.

If the Blocking configuration flag is True, then the Transmit and Receive procedures will wait until the entire Data array has been processed or the timeout has expired before returning. If Timeout is set to 0, these procedures will block forever.

If Blocking is False, then Transmit returns as soon as all words have been delivered to the FIFO, but may not all be clocked out to the bus yet. The SPI peripheral will continue clocking and transmitting words in the background until the transmit FIFO is empty. In non-blocking mode, the Receive procedure will copy data from the receive FIFO into the supplied Data array until the array is full, the timeout expires, or the receive FIFO is empty. This may result in a partially filled Data array. The Transmit_Status and Receive_Status functions can be used to anticipate the behavior of Transmit and Receive in non-blocking mode.

with HAL.SPI; use HAL.SPI;
with RP.GPIO; use RP.GPIO;
with RP.SPI;  use RP.SPI;
with RP.Device;
with RP.Clock;
with Pico;

procedure SPI_Example is
    SCK    : GPIO_Point renames Pico.GP2;
    MOSI   : GPIO_Point renames Pico.GP3;
    MISO   : GPIO_Point renames Pico.GP4;
    CS     : GPIO_Point renames Pico.GP5;
    Port   : RP.SPI.SPI_Port renames RP.Device.SPI_0;

    Data   : HAL.SPI.SPI_Data_8b (1 .. 4) := (41, 42, 43, 44);
    Status : HAL.SPI.SPI_Status;
begin
    RP.Clock.Initialize (Pico.XOSC_Frequency);
    RP.Clock.Enable (RP.Clock.PERI);

    SCK.Configure (Output, Floating, SPI);
    MOSI.Configure (Output, Floating, SPI);
    MISO.Configure (Output, Floating, SPI);
    CS.Configure (Output, Floating, SPI);
    Port.Configure (Config =>
        (Role      => Master,
         Baud      => 10_000_000, --  up to 50 MHz is supported
         Data_Size => Data_Size_8b,
         Polarity  => Active_Low,
         Phase     => Rising_Edge,
         Blocking  => True));

    Port.Transmit (Data, Status);
    Port.Receive (Data, Status);
end SPI_Example;

I2C

The I2C peripheral is similar to the other serial interfaces in operation but is more complicated due to the state machine that controls the bus. The I2C hardware takes care of most of this complexity for you. Pull-up resistors should be added to the SCL and SDA signals. While the RP2040’s internal GPIO pull-ups may be sufficient, this is not recommended as bus shorts and fault conditions can drive too much current through the pull-ups and cause damage. External 4.7k ohm resistors are usually sufficient, but this value may need to be adjusted to keep the rise time within spec. See the I2C bus specification for more information.

The RP2040 datasheet recommends enabling Schmitt triggering for the I2C pins.

This driver only supports I2C master mode. The hardware is capable of I2C slave operation as well, but we haven’t written a driver for this yet.

with RP.GPIO; use RP.GPIO;
with RP.I2C_Master;
with RP.Device;
with RP.Clock;
with HAL.I2C;
with Pico;

procedure I2C_Demo is
    Port   : RP.I2C_Master.I2C_Master_Port renames RP.Device.I2C_0;
    SDA    : RP.GPIO.GPIO_Point renames Pico.GP0;
    SCL    : RP.GPIO.GPIO_Point renames Pico.GP1;
    Data   : HAL.I2C.I2C_Data_8b (1 .. 2) := (others => 0);
    Status : HAL.I2C.I2C_Status;
begin
    RP.Clock.Initialize (Pico.XOSC_Frequency);
    RP.Clock.Enable (RP.Clock.PERI);
    SDA.Configure (Output, Floating, RP.GPIO.I2C, Schmitt => True);
    SCL.Configure (Output, Floating, RP.GPIO.I2C, Schmitt => True);
    Port.Enable (Baudrate => 400_000); --  I2C fast mode

    --  The address and data format vary widely depending on the I2C slave
    --  device you're connected to. See your I2C slave's datasheet for details.
    Port.I2C_Master_Transmit
        (Addr   => 16#42#,
         Data   => Data,
         Status => Status);
end I2C_Demo;

USB device controller

The RP.USB_Device package implements the USB.HAL.Device.USB_Device_Controller interface from the usb_embedded crate.

After initialization, all USB events are handled by the Poll procedure. Poll should be called regularly in a main loop, or in response to the USBCTRL interrupt.

The usb_echo example configures the USB controller to implement the USB serial (CDC ACM) protocol.

Pulse Width Modulation (PWM)

PWM pin mapping
Figure 4. PWM pin mapping

The PWM is split into 8 slices, each slice has 2 channels, and each channel can be output to 2 pins simultaneously. The B channel of each slice can be configured as an input and used as a gate or trigger for channel A. Even numbered GPIO pins are associated with Channel A and odd numbered pins are Channel B.

The To_PWM function can help you find the Slice and Channel associated with a GPIO pin.

with RP.PWM; use RP.PWM;
with RP.Clock;
with RP.GPIO;
with Pico;

procedure PWM_Demo is
    P : constant PWM_Point := To_PWM (Pico.GP0);
begin
    RP.Clock.Initialize (Pico.XOSC_Frequency);
    RP.PWM.Initialize;
    Pico.GP0.Configure (RP.GPIO.Output, RP.GPIO.Pull_Up, RP.GPIO.PWM);

    Set_Mode (P.Slice, Free_Running);
    Set_Frequency (P.Slice, 10_000_000);
    Set_Interval (P.Slice, 10_000);
    Set_Duty_Cycle (P.Slice, P.Channel, 5_000);
    Enable (P.Slice);
end PWM_Demo;

In this example, the PWM slice is configured to run at 10 MHz with an interval of 10,000. The output frequency is (Frequency / Interval). In this case, 1 KHz. The output will be high as long as the counter is less than or equal to Period, after which it transitions to low until the counter reaches Interval. The counter then resets to zero and begins again. In this example, the period is half of the interval, so the output has a 50% duty cycle. If the period is greater than or equal to the interval, the output is high for the entire cycle. If period is zero, the output is low for the entire cycle. Each channel’s output may be inverted using the Set_Invert procedure.

All of these parameters may be changed while the clock is running to modulate the output. See section 4.5.2.1. Pulse Width Modulation of the RP2040 datasheet for specifics about when changes to a running PWM slice take effect.

The Set_Frequency procedure attempts to calculate the correct divider value to get the desired PWM frequency. This is not always possible as the PWM divider has a fixed resolution. For accurate PWM frequency setting, use the Set_Divider procedure in combination with RP.Clock.Frequency (RP.Clock.SYS) instead, which uses the Divider fixed point type defined with the same resolution and range as the hardware divider. This allows you to calculate the error from the target frequency. Note that this is just the input frequency to the PWM, which will be divided further by the value set by Set_Interval.

Input_Clock      : constant Hertz := RP.Clock.Frequency (RP.Clock.SYS);
Target_Frequency : constant Hertz := 1_000_000;
Target_Divider   : constant Float := Float (Input_Clock) / Float (Target_Frequency);
Actual_Divider   : constant RP.PWM.Divider := RP.PWM.Divider (Target_Divider);
Divider_Error    : constant Float := Target_Divider - Float (Actual_Divider);
Actual_Frequency : constant Float := Float (Input_Clock) / Float (Actual_Divider);
Frequency_Error  : constant Float := Float (Target_Frequency) - Actual_Frequency;

Analog to Digital Converter (ADC)

The ADC is multiplexed across five channels, one of which is reserved for the internal temperature sensor. The ADC generates 12-bit samples (8.5 ENOB) at up to 500,000 samples per second.

Before ADC measurements can be done, the ADC clock must be enabled and a GPIO pin configured for Analog mode.

Configuring the ADC

RP.GPIO.Enable;
Pico.GP26.Configure (Analog);

RP.Clock.Enable (RP.Clock.ADC);

declare
    use RP.ADC;
    Channel : ADC_Channel := To_ADC_Channel (Pico.GP26);
    Value   : Analog_Value;
begin
    RP.ADC.Configure (Chan);
    Value := RP.ADC.Read (Channel);
end;

Voltage measurement

If a known reference voltage is applied to the ADC_VREF pin, the Read_Microvolts function can calculate the Microvolts integer type for you. On the Pico board, this reference is set to 3.3V unless your board has been modified. This is the default value for the VREF parameter.

declare
    use RP.ADC;
    mV : Microvolts;
begin
    mV := RP.ADC.Read_Microvolts
        (Channel => 0,
         VREF    => 3_300_000);
end;

Temperature sensor

The RP2040 has an internal temperature sensor attached to ADC channel 4. The Temperature function converts the value of this ADC channel to Celsius based on some reference values. See 4.9.5. Temperature Sensor in the RP2040 datasheet for accuracy and calibration information.

The Temperature function enables the temperature sensor for the duration of the reading and the ADC channel does not need to be configured beforehand.

declare
    C : RP.ADC.Celsius;
begin
    RP.ADC.Enable;
    C := RP.ADC.Temperature;
end;

Round robin

In round robin mode, the ADC will switch to the next enabled channel after each conversion. This allows you to sample many channels is rapid succession.

RP.ADC.Set_Round_Robin
    (Channels =>
        (0      => True,
         1      => True,
         others => False);
Sample := RP.ADC.Read;  --  Reads channel 0
Sample := RP.ADC.Read;  --  Reads channel 1
Sample := RP.ADC.Read;  --  Reads channel 0
Sample := RP.ADC.Read;  --  Reads channel 1

Continuous conversion

The ADC can be configured to continuously sample its inputs at a fixed rate. This is useful if you want to capture audio, for example. Sampling begins as soon as RP.ADC.Set_Mode is called.

declare
    use RP.ADC;
    Sample : Analog_Value;
begin
    Set_Sample_Rate (48_000);
    Set_Mode (Free_Running);
end;

Samples will be dropped if Read isn’t called fast enough. In most cases, you will want to setup a DMA channel triggered on the ADC to copy samples to a buffer while the CPU does processing. The adc_continuous example demonstrates using DMA and double buffering to capture and process two channels simultaneously.

Direct Memory Access (DMA)

The DMA peripheral can copy data from one memory location to another, freeing up the CPU to do other work while a transfer is in progress. A DMA channel is configured with a Source and Target System.Address and can be triggered by almost every other peripheral on the chip. The DMA peripheral can increment the source and/or target address after each transfer, or always use the same address.

For example, if you had a large buffer that needed to be transmitted via SPI, you could set the Source address to the beginning of the buffer and the target address to the SPI FIFO address, with the DMA channel configured to increment the source address until it reaches the end of the buffer.

with HAL.SPI;
with HAL;
with RP.DMA;  use RP.DMA;
with RP.Device;
with RP.SPI;

procedure DMA_Demo is
    Port    : RP.SPI.SPI_Port renames RP.Device.SPI_0;
    Buffer  : HAL.SPI.SPI_Data_8b (1 .. 512);
    Channel : constant RP.DMA.DMA_Channel_Id := 0;
begin
    --  Fill a buffer with sequential numbers
    for I in Buffer'Range loop
        Buffer (I) := HAL.UInt8 (I mod 255);
    end loop;
    RP.Clock.Enable (RP.Clock.PERI);
    Port.Configure (Config => (others => <>));

    RP.DMA.Enable;
    RP.DMA.Configure
        (Channel => Channel,
         Config  =>
            (Transfer_Size   => Transfer_8,
             Increment_Read  => True,
             Increment_Write => False,
             Trigger         => SPI0_TX,
             others          => <>));
    RP.DMA.Start
        (Channel => Channel,
         From    => Buffer'Address,
         To      => Port.FIFO_Address,
         Count   => HAL.UInt32 (Buffer'Length));

    loop
        --  Do some other work while the transfer is in progress

        if not RP.DMA.Busy (Channel) then
            --  Transfer completed, maybe begin another one.
        end if;
    end loop;
end DMA_Demo;

The DMA_Configuration record contains many other options such as reading/writing ring buffers, chaining another DMA channel after a transfer has finished, and generating checksums of data transferred through a DMA channel. See the RP2040 datasheet for more information on these options.

Programmable I/O (PIO)

The PIO peripheral consists of several state machines connected to all of the RP2040’s I/O pins. The PIO has it’s own opcodes and assembly language documented in the RP2040 datasheet. The pioasm assembler can output assembled code as a .ads file with the compiled binary represented as an array.

pioasm -p Hello -o ada src/hello.pio gen/hello.ads

The RP.PIO driver is very similar to the interface of the pico-sdk C API, as most available example code and documentation for PIO use that nomenclature. The pio_blink example demonstrates loading an assembled PIO program and executing it.

A more involved example is the Pico.Audio_I2S driver, which uses PIO to generate an I2S audio stream for use with the Pimoroni Audio Pack example. The PIO program in this example was copied from the raspberrypi/pico-extras repository and it’s C startup code ported to Ada.

Interpolator

Interpolator data flow
Figure 5. Interpolator data flow

The Interpolator is capable of performing an Add, Right Shift, Mask, and Sign Extend all within a single CPU cycle, with feedback from previous cycles into the input. The datasheet indicates that this is useful for pixel blending and certain audio synthesis operations.

The RP.Interpolator package directly exposes the registers of the Interpolator peripheral. We don’t have an example for this yet because we don’t entirely understand it. You could write one!

ROM functions

The RP2040’s ROM contains a library of useful functions that can be accessed via a symbol lookup table. The RP.ROM package provides an Ada binding to this library.

Floating point library

The RP2040’s ROM includes a set of floating point functions using the CORDIC algorithm, implemented in assembly. The Cortex-M0+ has no hardware floating point unit, so if you need to do floating point math, this is as fast as it’s gonna get. GCC will use it’s own math library by default, which will generally be slower as fetching code from flash is slower than ROM access.

A : constant Float := RP.ROM.Floating_Point.fdiv (24.0, 2.0)

GCC uses weak _aeabi symbols to point to the floating point functions. These symbols can be overridden to point to the ROM functions if you prefer to use those for all floating point math operations. The Ravenscar runtimes do this for you. See s-bootro.ads and s-bootro.adb for examples of how to override these symbols.

Custom board support

The focus of this project is to support the RP2040 on the Raspberry Pi Pico development board, but these libraries are portable to any board that uses the RP2040 chip. We recommend creating a new yourboardname_bsp Alire crate that contains boot code, pin definitions, and peripheral drivers for your custom board’s components.

Boot code

As the RP2040 has no onboard flash, code is executed from an external flash chip. The XIP_SSI (Execute In Place Slave Serial Interface) peripheral needs to be configured for the flash chip in use. At power on, the RP2040’s ROM uses a slow 0x03 SPI flash read command to load the first 256 bytes of flash into SRAM, verify a checksum at the end of that page, then jumps to it and begins executing. This section is known as .boot2 in linker scripts. boot2 should disable the SSI, reconfigure it for the specific flash chip on the board, then re-enable it and jump to the beginning of user code, now executing from the memory mapped flash chip.

rp2040_hal includes a boot2 section written in Ada. As the compiled boot2 needs to be padded to 256 bytes with a checksum added, the rp2040_hal repository contains pre-compiled boot2 code that will be linked automatically.

Your BSP crate should set the Flash_Chip variable in alire.toml to select the correct boot2 for your board.

[configuration.values]
rp2040_hal.Flash_Chip = "w25qxx"

The following boot2 variants are supported:

generic_03

The generic_03 boot2 is compatible with any flash chip that supports the 03h read command (which is most serial NOR flash devices). The downside is that it operates in standard SPI mode so is about 3x slower than QSPI.

generic_qspi

The generic_qspi boot2 is compatible with most (but not all) QSPI NOR flash chips. It is compatible with at least the following flash chips:

  • AT25SF128A

  • GD25Q64C

  • IS25LP128F

  • W25Q16JV

  • W25Q32JV

  • W25Q64JV

  • W25Q128JV

In general, it should be compatible with any devices that meet the following requirements:

  • The "Fast Read Quad I/O" (EBh) command is supported.

  • The "Fast Read Quad I/O" instruction has 4 dummy cycles between the mode bits (M7-M0) and the first output data from the flash.

  • "Continuous read" mode is supported with mode bits (M7-M0) = A0h

  • The "Read JEDEC ID" (9Fh) command is supported.

  • The "Write Enable" (06h) command is supported.

  • If the JEDEC manufacturer ID = 9Dh (ISSI devices), then the "Quad Enable" (QE) flag is in bit 2 of Status Register 1 and the "Write Status Register 1" (01h) command is supported.

  • If the JEDEC manufacturer ID /= 9Dh, then QE flag is in bit 6 of Status Register 2 and the "Write Status Register 2" (31h) instruction is supported.

Note that this version is NOT compatible with older Winbond devices, such as the W2580DV. Those devices MUST use the w25qxx variant of boot2.

w25qxx

The w25qxx boot2 is compatible with Winbond devices that have the QE flag in bit 6 of Status Register 2, and support writing to it via a "continuous write" from Status Register 1. It is compatible with at least the following Winbond devices:

  • W2580DV

  • W25Q16JV

  • W25Q32JV

  • W25Q64JV

  • W25Q128JV

Linker script

Some startup code and a linker script are needed to generate an executable binary for the RP2040. If you are using a ZFP runtime, rp2040_hal provides reasonable defaults in the ld and startup directories. If you wish to customize these, copy both directories to your BSP crate and add the following to your alire.toml.

[configuration.values]
rp2040_hal.Use_Startup = false

Pin definitions

It is unlikely that your custom board has the same pinout as the Pico. Most BSP crates will include a package that defines symbolic names for GPIO pins that match a schematic net name or silkscreen that a user of your board would recognize.

The Adafruit Feather RP2040 example provides pin definitions for a custom board.

Optimization

While compile-time optimizations are no substitute for efficient algorithms, GCC can usually make your code fast and small enough for all but the most demanding tasks. Flags can be passed to the compiler and linker in a gpr project file using the Switches option.

The GNAT user guide includes a list of all available switches. The example below shows the ones I use most frequently.

   package Compiler is
       for Switches ("Ada") use (
           "-Os",           --  Optimize for size. Use -O2 or -O3 for performance optimization
           "-g",            --  Generate debug symbols
           "-gnatwa",       --  All warnings
           "-gnatwl",       --  Elaboration warnings
           "-gnatVa",       --  Validity checks. Execution faster without these, but
                            --  constraints are a good idea.
           "-gnatyg",       --  Style checks. Not required, but recommended.
           "-gnatyM120",    --  Line length. Controversial.
           "-gnatyO",       --  Overriding subprograms must be explicitly marked
           "-gnata",        --  Enable assertions. Pre and Post conditions are enforced.
           "-gnatn",        --  Enable inlining. Procedures also need to be marked "with Inline" and
                            --  compiled with -O2 to actually be inlined
           "-gnatQ",        --  Keep going after errors
           "-gnatw.X",      --  Hide No_Exception_Propagation warnings.
                            --  Exceptions in ZFP are always crashes, we don't need to be reminded.
           "-gnatwB",       --  Hide Warn_On_Bad_Fixed_Value. Values assigned to fixed types may be
                            --  truncated to the nearest 'Small
           "-fstack-usage", --  Output stack usage information.
                            --  Generates .su files that can be used for stack analysis later.
           "-ffunction-sections", --  Every function gets it's own ELF section
           "-fdata-sections"      --  Every variable gets it's own ELF section
       );
   end Compiler;

   package Linker is
      for Default_Switches ("Ada") use
          Pico_BSP.Linker_Switches & (
              "-Wl,-gc-sections",        --  Discard unused ELF sections
              "-Wl,-print-memory-usage", --  Outputs memory and flash usage after linking
              "-Wl,-Map=main.map"        --  Generate a memory map file for later analysis
          );
   end Linker;

Ravenscar runtimes

There is a pull request to add ravenscar-sfp and ravenscar-full runtime support for the RP2040 to bb-runtimes. At the moment, these patches are not distributed with any toolchain. Building these runtimes is a bit tricky, so if you really need this and can’t figure out how get it working, come say hi on Gitter.

API reference

We have generated API documentation but in most cases it’s easier to just read the .ads files in rp2040_hal.

Community Support

Bugs and usability issues may be reported by opening a GitHub issue. For more general questions and feedback, join the community on Gitter.im.

License

All source code in rp2040_hal, pico_bsp, and pico_examples is available under the BSD-3-Clause license. Some portions (SVD definitions, startup code) of these libraries are copied or generated from pico-sdk and are Copyright © Raspberry Pi (Trading) Ltd. and distributed under the same BSD-3-Clause license.

This documentation is made available under a Creative Commons Attribution-ShareAlike 4.0 International License.

Excerpts from the Raspberry Pi documentation are also licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.

Change log

0.7.0 (UNRELEASED)

New features

Documentation

Documentation has been written for most of the drivers and is now available at pico-doc.synack.me.

USB device controller

The RP.USB_Device driver now implements the USB.HAL.Device.USB_Device_Controller interface. This adds a dependency on the usb_embedded crate, which in turn depends on bbqueue-spark and atomic. This driver does not support USB host mode or double buffering.

The upstream SVD was updated to include USB_DPRAM registers, so all of the RP2040_SVD packages have been regenerated from source.

ADC round robin and free running mode

RP.ADC.Set_Round_Robin can be used to select multiple ADC channels to be read sequentially. RP.ADC.Set_Mode (Free_Running) will cause the ADC to continuously sample the selected channels. Paired with DMA, this means the ADC can run at up to 500,000 samples per second.

Ada boot2 code

Thanks to Daniel King, we now have a working implementation of boot2 in Ada. boot2 has been moved from pico_bsp to rp2040_hal and the flash chip may be selected with the Flash_Chip Alire configuration variable.

ROM AEABI functions

The RP.ROM.AEABI package now connects the ROM floating point library to GCC’s __aeabi symbols. This means smaller binaries and faster execution for many floating point operations. This does not include support for the hardware integer divider, yet.

Breaking changes

Toolchain dependency

rp2040_hal now depends on the gnat_arm_elf toolchain in Alire. While the GNAT Community toolchains should continue to work, the FSF GNAT toolchain is the only one we will test going forward.

Startup code conflicts with Ravenscar runtimes

crt0.S and package Runtime have been moved from pico_bsp into rp2040_hal. If rp2040_hal is used as a dependency of a project built with one of the Ravenscar runtimes, rp2040_hal’s startup code will conflict with that provided by the runtime. The Use_Startup = false Alire configuration variable will prevent rp2040_hal from compiling and linking it’s startup code.

Bugs fixed

Oscillator startup delay for Feather boards

Some Adafruit Feather RP2040 boards have higher than expected capacitance on the XOSC traces and need a bit more time for the oscillator to stabilize. The XOSC_Startup_Delay parameter was added to RP.Clock.Initialize to allow BSPs to override the default startup delay. The default value should still be fine for most boards.

0.6.0

New features

Clocks can be disabled

To save power, peripheral clocks can be disabled with RP.Clock.Disable. Some peripherals may exhibit unexpected behavior if their clocks are disabled. Use at your own risk.

RTC can be paused

The RP.RTC.Pause and RP.RTC.Resume procedures stop and start the RTC. This is useful if you want the RTC to stop ticking while a user is setting the time. Preconditions requiring the clock to be running have been removed from the RTC procedures. RP.RTC.Initialize still needs to be called at least once, but can be skipped if RP.RTC.Running returns True, implying that the RTC is already Initialized.

Continuous integration

A CircleCI project has been setup to compile rp2040_hal upon commit and email the author if the build fails. This is not meant to replace actual user testing on real hardware. This is just a quick check for broken builds.

Breaking changes

Delay_Microseconds no longer uses interrupts

RP.Timer.Delay_Microseconds now polls the timer registers in a busy loop, rather than setting up an alarm interrupt. This should make shorter (< 10 microsecond) delays more accurate as interrupt latency is no longer a factor. RP.Timer.Delay_Until can still be used to perform interrupt-based delays with microsecond precision.

Bugs fixed

16-bit RP.SPI.Transmit did not respect the Blocking configuration option

Issue #3: If Blocking was set in the SPI_Configuration and the 16-bit version of the Transmit procedure was used, Transmit would return before all data was clocked out. Thanks to @hgrodriguez for discovering this

RP.PWM did not check that Initialize was called first

If RP.PWM.Initialize was not called before configuring PWM slices, the configuration would succeed but would generate no output. An Initialized variable has been added to RP.PWM along with a precondition on all procedures that modify PWM slices to ensure that Initialized is True. If you forget to call RP.PWM.Initialize, your program will crash on the first run.

RP.ADC.Temperature could return incorrect data

If RP.ADC.Configure (Temperature_Sensor) was not called before RP.ADC.Temperature, incorrect temperature readings would be returned. RP.ADC.Temperature now ensures the temperature sensor is configured on every call, eliminating the need to call Configure for the temperature sensor.

0.5.0

New features

UART enhancements

RP.UART now allows configuration of baud, word size, parity, and stop bits via the UART_Configuration record. The default values for the UART_Configuration record represent the typical 115200 8n1 setup.

The UART now has a Send_Break procedure, which holds TX in an active state (usually low) for at least two frame periods. Some protocols use the UART break condition to indicate the start of a new packet.

RP.UART.Receive now sets Status = Busy and returns immediately if a break condition is detected.

UART Transmit and Receive procedures now return as soon as all words have been delivered to the FIFO. FIFO status is exposed by the Transmit_Status and Receive_Status functions. This interface is the same as the I2C and SPI drivers.

The uart_echo example has been updated to demonstrate these new features.

RTC driver

The real time clock is now exposed by the RP.RTC package. It implements the HAL.Real_Time_Clock interface for getting and setting the date and time. An example project demonstrates use of the RTC. RTC alarm interrupts are not yet implemented.

Interpolator driver

The RP2040 has two interpolators per core embedded in the SIO peripheral. The RP.Interpolator package make their registers available. Some of the registers in this block support single-cycle operation, so it would be counter productive to wrap them up in procedures that may not be inlined by the compiler. There are examples in the datasheet for working with the interpolators, but I’m still trying to wrap my head around it, so there is no example here yet.

Breaking changes

UART.Enable is replaced with UART.Configure

To match the nomenclature of the other serial drivers (SPI, I2C), RP.UART now has a Configure procedure instead of Enable.

I2C addresses should include the R/W bit

The RP.I2C driver was expecting 7-bit I2C addresses to not include the R/W bit in the LSB. This was inconsistent with the other HAL.I2C implementations and would result in incorrect I2C addressing. Now, 7-bit I2C addresses should be represented as a UInt8 with the LSB set to 0. If this breaks your code, shift your I2C address left by one bit.

Bugs fixed

Improper use of the Pack clause

The Pack clause was used to enforce the memory layout of some records.

It is important to realize that pragma Pack must not be used to specify the exact representation of a data type, but to help the compiler to improve the efficiency of the generated code. Source

The Pack clause has been replaced with Component_Size and Size clauses where necessary. Thanks to @onox for pointing this out!

Use of access PIO_Device as a type discriminant

Projects depending on pico_bsp failed gnatprove in SPARK mode as the Pico.Audio_I2S package was using not null access PIO_Device as a discriminant. PIO_Device is now tagged and Pico.Audio_I2S uses not null access PIO_Device’Class, which is valid under SPARK. gnatprove still throws many warnings about side effects in the rp2040_hal drivers, but no fatal errors.

RP.ADC.Read_Microvolts was rounding incorrectly

Read_Microvolts was using Integer arithmetic to calculate VREF / Analog_Value’Last, which does not divide evenly for common VREF values. When that value was multiplied by an ADC reading, Read_Microvolts would return lower than expected results. Read_Microvolts now uses floating point to multiply ADC counts before converting the return value to Integer.

UART Transmit and Receive did not respect Timeout

The UART driver has been modified to use RP.Timer to implement timeouts and monitor FIFO status, similar to RP.SPI and RP.I2C.

SPI Transmit was nonblocking

The SPI Transmit procedure would return immediately after the last byte was written to the FIFO, but before the FIFO became empty. This behavior breaks some drivers that depend on all bytes being clocked out before proceeding. A configuration flag for Blocking behavior has been added and defaults to True.

0.4.0

New features

DMA driver

The RP.DMA package allows out of band copies between a source and target System.Address and may be triggered by a variety of events. The PIO and SPI drivers have been tested with DMA and have new functions that return their FIFO addresses.

I/O Schmitt triggers

The RP.GPIO.Configure procedure now takes optional Schmitt and Slew_Fast boolean parameters that control the behavior of I/O pads. The RP2040 documentation recommends enabling the Schmitt trigger for I2C operation.

RP.ROM.Floating_Point

The ROM floating point library is now exposed in the RP.ROM.Floating_Point package. GNAT will use gcc’s soft float implementation by default, but you may call the optimized versions in the ROM directly. The Ravenscar runtimes will replace the gcc functions with these ROM calls automatically.

I2C and SPI Timeouts

Previously, the I2C and SPI drivers did not use the Timeout argument. They now use RP.Timer to implement a timeout for all blocking operations and set Status to Err_Timeout if it expires before the blocking operation completes. The I2C peripheral may require a reset after a timeout as the bus may be in an unknown state.

SPI FIFO status is exposed with Transmit_Status and Receive_Status

You can use these functions to determine if the Transmit or Receive procedures would block. See the new spi_loopback example.

Breaking changes

PWM Set_Duty_Cycle and Set_Invert no longer use PWM_Point

These procedures have changed to take a PWM_Slice as the first argument to make them more consistent with the rest of the driver. These procedures now set both channels of a slice nearly simultaneously.

PWM Initialize must be called before any other PWM configuration

This procedure was added to fix the corruption bug discussed below.

SPI.Enable is replaced with SPI.Configure

The Configure procedure takes a SPI_Configuration record as an argument for easy static configuration.

Bugs fixed

PWM configuration is corrupted after power cycle

RP.PWM.Enable is called after configuring a PWM slice to enable it. This procedure was incorrectly resetting the PWM peripheral before enabling the slice. RP.PWM.Initialize now performs the reset and all peripheral resets have been moved to RP.Reset to avoid this mistake in the future.

PWM dividers can have a value of zero

The documentation is unclear on what this means, but my testing shows that it acts like a divider of 1, which outputs the clk_sys frequency.

Fast I2C writes would result in dropped bytes

The RP.I2C_Master driver has been modified to wait for the TX FIFO to be empty before writing a byte. This effectively reduces the FIFO depth to 1 byte. This is the same behavior as the upstream SDK.

Known issues

I2C clock is slower than expected

In 400 KHz (fast mode) operation, the I2C master generates SCL at approximately 380 KHz. I believe this is due to clock stretching caused by the new TX FIFO blocking behavior. The upstream SDK has the same behavior. According to the I2C specification, a fast mode clock may be up to 400 KHz, but specifies no minimum frequency. It may be possible to workaround this by using DMA to write to the I2C FIFO, but this is untested.