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.
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
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.
If the build was successful, there will be an ELF binary in
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.
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
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!
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.
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
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.
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.
Some peripherals, such as the UART, need a clock enabled before they can be used.
Clocks may be disabled to reduce power consumption.
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;
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.
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;
RP.GPIO.Configure has several optional parameters. The default values will configure the pin for basic
Pico.LED.Configure (RP.GPIO.Output); loop Pico.LED.Toggle; end loop;
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;
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;
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;
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
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.
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;
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;
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;
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_Date into an ISO 8601 string.
No support is provided for time zones, leap seconds, or daylight savings time conversion.
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.
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
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
All of the serial interfaces require
clk_peri to be running.
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.
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 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
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.
Blocking configuration flag is
True, then the
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.
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
Receive_Status functions can be used to anticipate the behavior of
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;
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
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
Pulse Width Modulation (PWM)
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.
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
All of these parameters may be changed while the clock is running to modulate the output. See section 220.127.116.11. Pulse Width Modulation of the RP2040 datasheet for specifics about when changes to a running PWM slice take effect.
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
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;
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
declare use RP.ADC; mV : Microvolts; begin mV := RP.ADC.Read_Microvolts (Channel => 0, VREF => 3_300_000); end;
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.
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;
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
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;
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.
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!
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.
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:
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.
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:
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.
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:
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
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.
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
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;
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.
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.
Documentation has been written for most of the drivers and is now available at pico-doc.synack.me.
USB device controller
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
Ada boot2 code
ROM AEABI functions
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.
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
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.
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.
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
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
True, implying that the RTC is already Initialized.
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.
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.
16-bit RP.SPI.Transmit did not respect the Blocking configuration option
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
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.
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.
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.
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.
Improper use of the Pack clause
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
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
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.
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 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.
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.
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.
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.