It’s been 11 months since the first Ada code running on an RP2040 blinked an LED. In that time, rp2040_hal has grown considerably, with support for a veritable alphabet soup of peripherals; UART, SPI, I2C, USB, DMA, PWM, PIO, RTC, Timer, and ADC.
I’m pleased with the state of things and feel like rp2040_hal is a solid foundation for other projects to build upon. Up to this point, each release came with exciting new features and functionality. This release marks a new stage of development, where the focus is on providing bug fixes, performance improvements, and a stable API. In the words of Linus, "We do not break userspace."
If you have questions about Ada, the RP2040, or just microcontrollers in general, come join the conversation at ada-lang/raspberrypi-pico on gitter.im. Gitter.im is available over Matrix if you’d prefer to bring your own chat client.
Demos
I often share what I’m working on via on Twitter @JeremyGrosser. I’ve embedded some tweets about my RP2040 Ada projects below.
I needed to charge some NiMH batteries but I was worried they'd overheat. Only took an hour to build a nice little temperature monitor! pic.twitter.com/pcJXoAt8yk
I've got working bitmap tiles on the Picosystem now. I went with 2-bit 8x8 bitmaps, with switchable color palettes, similar to the Game Boy. pic.twitter.com/Gv68XG2t4N
Pretty happy with how my portable Pico dev kit is turning out. Still need to 3d print a mount for the battery and make some shorter wires. pic.twitter.com/a3LFRChFwn
RP.DMA can now configure interrupt masks for each DMA channel. If DMA_Configuration.Quiet = False, the interrupt will fire when a transfer is completed.
Unit tests
We’ve begun writing tests for rp2040_hal with the AUnit Testing Framework. Currently, there are tests for Clock, UART, SPI, GPIO, and DMA. These tests have already led to several bug fixes and we will continue to work toward more complete unit test coverage.
Bugs fixed
GPIO.Mode returned incorrect values
GPIO.Mode was returning the mode of the wrong pin.
PWM divider edge cases
The minimum and maximum PWM divider values were calculated incorrectly. The calculation and constraints on RP.PWM.Divider have been fixed.
RP.DMA.Status returned incorrect Transfers_Remaining
The DMA alias register layouts were incorrect. The only visible effect of this error was that RP.DMA.Status returned an incorrect value for Transfers_Remaining.
Some DMA triggers didn’t work
The DREQ register values did not have a representation clause specified, which caused triggers internal to the DMA peripheral (pacing timers and permanent triggers) to be nonfunctional.
I’ve begun work on a board support package called picosystem_bsp for the Pimoroni Picosystem, which is a handheld game console that includes an RP2040, 240x240 screen, piezo speaker, buttons, and a battery. I’ll be building games on this platform soon!
I’ve written some documentation for Ada on the Raspberry Pi Pico. This is still a work in progress, but provides quite a lot of context around the usage of the libraries and how to build a project on this platform.
I’ve tagged release 0.6.0 of the rp2040_hal, pico_bsp, and pico_examples libraries for the Raspberry Pi Pico. Release notes are at the end of this page.
This is a relatively small release as I’ve found that these drivers are working well for my projects. If things keep going this way, I’ll be comfortable tagging a stable 1.0 release before the end of the year and promising no breaking changes without a major version bump.
Work needed
While we have simple drivers for most of the RP2040’s internal peripherals, there are a few tricky bits that still need work. I’d appreciate help with these.
Ravenscar Runtime
To take advantage of some of the more interesting features of Ada (tasking, protected types, etc) RP2040 support needs to be merged into bb-runtimes. On the rpi-pico-2021 branch I’ve rebased damaki’s branch on the community-2021 release of bb-runtimes. This branch cannot be merged upstream as it still uses a stage 2 boot (boot2) loader assembled from the upstream pico-sdk, which is licensed as BSD-3-Clause and copyright Raspberry Pi Foundation. AdaCore prefers that all contributions to bb-runtimes be licensed as GPL-3 and their copyright assigned to AdaCore. Therefore, we need to write new boot2 code that can be licensed as such.
I’ve made an attempt to reimplement the generic_03h version of boot2 in Ada, but this is broken. The ROM expects a .boot2 section at the beginning of flash to be padded to 256 bytes, with the last four bytes containing a CRC32. As far as I know, there’s no easy way to do the padding and checksum within the bb-runtimes build system. I’d be comfortable with having an out-of-band build process that generates a .S with the boot2 binary that can be committed to the bb-runtimes repository. In addition to the missing checksum, I believe my boot2 implementation does not save the link register and jump to the correct entry point after executing. I don’t know how to do that without adding inline assembly, which seems inelegant.
I’d appreciate help with getting the Ada boot2 implementation working so that the rpi-pico-2021 branch can be merged to bb-runtimes.
USB
I’ve made some progress on a USB driver, but it’s far from complete and too disorganized to share right now. While it’s certainly possible to build a USB driver within the ZFP runtime restrictions, the implementation would be much easier and cleaner if tasking and storage pools were available. To that end, I’m prioritizing work on the Ravenscar runtime ahead of the USB stack. If anybody does want to work on USB, most of the device stack is already in usb_embedded, but that library requires hardware atomics that the Cortex-M0+ does not have. I have a branch with the atomics stuff removed that kinda works, but this feels like playing with fire.
Dual CPU
The bb-runtimes branch already has support for multiprocessing so I’m hesitant to duplicate that work in rp2040_hal. Even if I were to add SMP support to rp2040_hal, doing tasking without using Ada’s tasking features just seems wrong. Once again, this reinforces the importance of the Ravenscar work.
Integer divider
There’s a hardware integer divider in the SIO block that would significantly improve the performance of many programs. The assembly code used to hook this up to gcc’s EABI in pico-sdk is intimidating and more complicated than you’d expect.
Watchdog
I wrote a driver for it, but I’m not convinced that it works. Watchdogs are hard to test.
rp2040_hal 0.6.0 Release Notes
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.
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
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.
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.
I’ve tagged release 0.4.0 of the rp2040_hal, pico_bsp, and pico_examples libraries for the Raspberry Pi Pico. Lots of testing and bug fixes on SPI and I2C in this release and both interfaces should be much more stable now. DMA to PIO works well and opens up a lot of possibilities for high speed signaling. Examples have been added for a rigorous SPI loopback test and a custom BSP implementation for the Adafruit Feather RP2040, which should serve as a template for running on any board that uses the RP2040. The Pimoroni Audio Pack example has been updated to use the ROM floating point library to generate a sampled sine output.
rp2040_hal 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.
Twiddling bits in registers doesn’t make the most intuitive, readable, or portable code. This is why we write drivers. I’ve created three Alire packages, rp2040_hal, pico_bsp, and pico_examples. rp2040_hal contains all of the drivers for the chip’s internal peripherals, pico_bsp contains some details about the Pico board and drivers for the Pimoroni Pico addons, and pico_examples contains, you guessed it, example code. At the moment, pico_bsp cannot be used with the Ravenscar runtime without modification, so I’ll ignore that and focus on rp2040_hal for right now. The examples repository contains lots of code that uses the pico_bsp if you’d like to see how that works.
Now that I have drivers, I’ve removed all of the interfaces generated from SVD from our project. These still live in the rp2040_hal package but as long as the drivers implement all of the right interfaces, we shouldn’t need them. If you find something you can’t do with the HAL drivers that seems important, please send a pull request to the rp2040_hal repo. I’ve refrained from tagging a 1.0 release because I’m still not satisfied with all of the interfaces and retain the right to break the API.
You can see that the Main procedure is much more compact and readable now. The PADS_BANK and IO_BANK stuff is wrapped up in a lovely Configure interface and the GPIO is abstracted into an object with a very convenient Toggle method.
Building the code is done using Alire now, rather than calling gprbuild directly. Alire is analogous to Rust’s Cargo or Python’s pip. Alire keeps track of all of the dependencies and can pull in new ones with the alr with command.
git clone https://github.com/JeremyGrosser/rp2040_hal
cd 04-hal-blink
alr build
A month later, much has happened! I’ve implemented drivers for nearly all of the RP2040’s peripherals, Fabien Chouteau contributed an I2C driver, and Daniel King has ported bb-runtimes, including multiprocessing support. Let’s get that blink example updated!
There are lots of pieces that can be abstracted away and made more flexible. For example, Ticks shouldn’t be a public variable that can be modified from anywhere. I should also define a new Time type to differentiate it from other Integers. If you follow these changes to their logical conclusion, you get something that looks like Ada.Real_Time which already exists in the Ravenscar runtimes.
The Ada language provides some fairly high level constructs related to tasking, memory management, and timing that aren’t easy or practical to implement on every platform and the use of some of those features may violate a project’s certification requirements. For this reason, there several runtime profiles with varying levels of functionality. So far, I’ve been using a Zero Footprint (ZFP) runtime, which provides only the bare minimum to allocate some stack space, pass arguments, and call a procedure. No batteries included. The next step up would be the Ravenscar profile, which allows a broader set of builtin functionality, including tasking and synchronization constructs you’d expect to find in an RTOS. An open source implementation of Ravenscar is available in the bb-runtimes repository, though porting it to a new chip is not a small task. There are other runtime implementations that wrap existing RTOS libraries like FreeRTOS and RTEMS. GNAT GCC also includes runtimes for Linux, FreeBSD, Solaris, HP-UX, VxWorks, LynxOS, QNX, and Windows that implement the full set of libraries in the Ada language specification.
I’ll port our blink example to a Ravenscar RTS (Run-Time System) from bb-runtimes. As the RP2040 is still a new platform, its runtimes haven’t been merged yet and aren’t distributed in the GNAT Community 2020 bundle so I’ll need to build it from source.
git clone -b rpi-pico https://github.com/damaki/bb-runtimes
cd bb-runtimes
./build_rts.py --build rpi-pico-mp
cd ../02-ravenscar-blink
gprbuild -P rpsimple.gpr
You’ll notice that the boot2, crt0, and linker script aren’t needed anymore as they’re included with the runtime. I’ve replaced all of the SysTick stuff with a single delay 1.0; statement. The RTS has configured the PLLs and the TIMER peripheral, so I now have microsecond resolution tickless operation. We can still do better! The single-cycle loop still takes time to execute, and waking from sleep takes a few cycles too, so the delay between blinks is still not precisely one second.
In 03-realtime-blink I’ve imported Ada.Real_Time and declared a Next_Blink variable with type Time. Time is a private type as the storage representation of time is a non-portable implementation detail. Initializing Next_Blink with a call to the Clock function means that time doesn’t even have to start at 0!
Now that I know when the next blink should happen, I can use a delay until statement for precision waiting.
Recently, the Raspberry Pi Foundation launched their new RP2040 SoC on a $4 development board called Pico. As the foundation’s goal is primarily education, they’ve provided lots of high quality documentation, libraries, and examples for the new chip. Naturally, I’m going to ignore most of that and roll my own. Why? Because this is my idea of fun and I’m trapped inside during a pandemic.
If you’d like to build the examples and follow along, you’ll need GNAT Community 2020 ARM ELF installed in your PATH. I’ve only tested this on x86_64 Debian, if you’re on another platform I can’t help you. You’ll also want an SWD debugger that works with the RP2040. I use openocd on a Raspberry Pi, I hear Segger J-Link support is coming soon. Technically you could use elf2uf2 and load binaries over USB, but that’s gonna get tedious for any nontrivial debugging.
The RP2040 has no internal flash. The boot ROM loads 256 bytes of code from an external SPI flash and executes it. This “second stage bootloader” is expected to configure the XIP (eXecute In Place) peripheral with clock and timing details specific to the flash chip in use, then jump to the start of the user code in the memory mapped to the flash.
I tried to rewrite the boot2.S bootloader from pico-sdk in Ada, but I couldn’t get it to fit inside 256 bytes. It might be possible, but not today. I ran gcc’s preprocessor on boot2.S to generate a single assembly file I could link. I copied the linker script and crt0.S from pico-sdk too.
The zfp-cortex-m0p Ada runtime does most of the boilerplate Cortex-M0+ setup and implements Ada.Text_IO with semihosting over the debug interface, so I wrote a hello world and copypasta’d a GPR project file to build it.
It took me a few hours to figure out the right incantation to get the linker to put the .boot2 section at the start of the flash chip along with crt0.S and the Ada runtime’s init code. The result is an ugly mess, which is why I’m omitting it here. If you want to see how to do things the quick and dirty way, my initial attempt is on github.
I soldered some headers for the Pico’s SWD port, wired it to a Raspberry Pi 4, and compiled the raspberrypi branch of openocd. As far as I can tell, nobody distributes a toolchain that can cross-compile Ada arm-eabi binaries on aarch64 and I don’t want to spend my time building binutils and gcc right now, so I setup an ssh tunnel from my x86_64 workstation to the Pi. I run arm-eabi-gdb locally and connect to openocd over the tunnel. Most of this will go in a .gdbinit script later.
ssh [email protected] -L3333:localhost:3333
openocd -f interface/raspberrypi-swd.cfg -f target/rp2040.cfg
arm-eabi-gdb obj/main
target extended-remote localhost:3333
monitor arm semihosting enable
load
run
Let’s write some code! For this example, I’m not going to do anything particularly complicated or idiomatic to Ada in order to keep things as simple as possible. This is going to be a very imperative program assigning values to registers and not much more.
ARM’s tooling for silicon vendors generates an SVD file, which is a pile of XML that lists all of the peripheral addresses and register offsets with vaguely human readable names. The svd2ada program translates this into Ada spec files with record types and representation clauses. Unfortunately, it crashed with the RP2040 SVD file. svd2ada expects a <size> to be specified on every register but the RP2040 SVD defines <size> at the peripheral level. The SVD format says this should then be inherited by the registers in the peripheral, but svd2ada doesn’t do that. I couldn’t figure out how to fix svd2ada so I wrote a quick Python script to modify the SVD file by copying the <size> field to every register within a peripheral. Now svd2ada works and generates a .ads file for each peripheral on the RP2040.
Normally the next step in bringing up a new chip would be to get all of the clocks configured, which is usually pretty boring code to write. At startup, the RP2040’s system clock runs from an internal ring oscillator with a not at all predictable or stable frequency between 1.8 and 12 MHz, which is good enough for some blinking. I skipped clock configuration and went straight for the GPIO.
There are four peripherals that need to be configured to toggle a pin: RESETS, PADS_BANK, IO_BANK, and SIO. RESETS, as you might expect, controls the reset state of the other peripherals. I pull the IO_BANK and PADS_BANK out of reset and wait for any initialization these peripherals might need to do in a busy loop.
Next, PADS_BANK enables the output driver on GPIO25, which is connected to the LED on the Pico board.
IO_BANK selects a peripheral to connect the pad to.
The SIO (single-cycle IO) peripheral can toggle pins. I added a Pin_Mask constant in the declare block with bit 25 set. svd2ada generates nice subtype definitions for each register field so that I don’t need to know that GPIO_OUT is 30 bits wide. The immediate value here is just Shift_Left (1, 25), but using Shift_Left here would require some type conversion that I want to avoid in this example.
Enable the output in the SIO peripheral
Now we blink!
Assuming those calls actually get compiled into single cycle writes, that’s gonna be blinking at half the system clock frequency, far faster than the human eye can see, but good enough for an oscilloscope. The SIO peripheral has an XOR register that allows us to make this code even shorter.
This is where the narrative diverges from reality. After I got to this point, I spent a few days refactoring the code into a package with well defined types and interfaces that conform to the Ada HAL package. For the sake of this example, I’m going to gloss over some of those organizational details and pretend things are still mostly happening in a single Main procedure.
Next I need a delay in that loop to blink at a rate that’s perceptible to humans. I could call a bunch of nop instructions and waste cycles, but that’s just silly. The RP2040 has a very nice 64-bit timer peripheral that I completely ignored because the chip also has the standard ARM SysTick peripheral that I’m already familiar with. Exported symbols need to be defined at the package level, so I’ve put this code into a SysTick package. I’ve only reproduced the implementation bits here, just know that this is happening in a new file.
I need an interrupt handler to increment a counter for every tick. crt0.S exports a weak isr_systick symbol that I can implement.
I’ll also add a wrapper around the wfi (wait for interrupt) assembly instruction in the same package
Back in the blink loop, I call the wait for interrupt instruction and check the value of Ticks.
IT BLINKS! Not precisely at 1 Hz, as the ring oscillator isn’t accurate, but close enough for this demo.
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.
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.
I’ve tagged release 0.4.0 of the rp2040_hal, pico_bsp, and pico_examples libraries for the Raspberry Pi Pico. Lots of testing and bug fixes on SPI and I2C in this release and both interfaces should be much more stable now. DMA to PIO works well and opens up a lot of possibilities for high speed signaling. Examples have been added for a rigorous SPI loopback test and a custom BSP implementation for the Adafruit Feather RP2040, which should serve as a template for running on any board that uses the RP2040. The Pimoroni Audio Pack example has been updated to use the ROM floating point library to generate a sampled sine output.
rp2040_hal 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.
I’m building an audio synthesizer with Ada on the Raspberry Pi Pico’s RP2040 chip. See my earlierposts about board bring up. This chip has 256 KB of memory, no floating point unit, and no DAC or I2S peripheral. But, it does have some very flexible I/O state machines and a well optimized soft float library in ROM.
There’s a PIO program in pico-extras called audio_i2s.pio that handles the timing sensitive clocking and data bits of I2S. All I need to do is push 16-bit two’s complement signed integers into the PIO FIFO and the state machine handles the rest!
The PIO program includes a block of C code that calls pico-sdk to configure the state machine’s pin muxing and jump to the program’s entry point. I stumbled a bit trying to rewrite this in Ada, but things went much smoother after I refactored my RP.PIO driver to more closely match pico-sdk’s interfaces. This code populates a Configuration record with a bunch of settings, then applies them to the PIO both by setting registers directly and executing a few instructions on the state machine.
The rest of the clock and GPIO configuration for the PIO is pretty straightforward and I was able to get things up and running pretty easily… Except for my Pico’s broken internal pull down resistor, which took me a while to debug and ended with a silly looking 0603 resistor pulling the I2S data pin to GND.
The RP.PIO driver only supports blocking writes to the FIFO, which wastes a lot of cycles that could be used to generate more interesting audio. I had never implemented a DMA driver before and was expecting a challenge, but it was suprisingly simple! Load some configuration registers, set a source and destination address and buffer size, and pull the trigger. The RP2040’s DMA channels have a CTRL register that is aliased four times at different memory addresses and the triggering behavior is slightly different depending on which one you write to. This was a little difficult to understand from the docs, but I eventually figured it out.
HAL.Audio.Audio_Buffer is an array of 16-bit integers. If stereo audio is used, the samples are interleaved. The PIO FIFO buffer is 32 bits wide and the PIO program expects two 16-bit samples in each FIFO write, one for each channel. For mono audio, the top 16 bits are zeroed. The DMA channel is configured for 16 or 32 bit writes depending on This.Channels. As long as Data is 32 bit aligned, this works out perfectly.
After I got DMA working, I spent a few days on audio synthesis. I’d never written any software to generate audio before, so I had a bit of domain knowledge to catch up on but now that I understand it, it’s pretty straightforward. The only real complication is the need to call the RP2040’s ROM floating point routines if performance or code size is a concern. Daniel King already did most of the hard work in making those library symbols available in Ada and my RP.ROM.Floating_Point mostly follows the same pattern.
Neither bb-runtimes nor rp2040_hal override gcc’s trigonometry functions (sinf, cosf, tanf, etc) with the ROM’s implementations as the ROM’s floating point library isn’t strictly compatible with what gcc expects. It would likely work for our use case, but I don’t want to make that decision for other people. Instead, if you need the speed of the ROM functions, you can call them directly. Note that this is different from pico-sdk’s behavior, which uses the ROM functions by default.
Now I have a working wavetable oscillator, ADSR envelope filter, and IIR low pass filter on the Pico. I’m starting to think about what kind of user interface I want on this synthesizer and think it would be neat to use the Pimoroni RGB Keypad as a sort of crossbar selector for connecting four oscillators to four filters. Unfortunately, the way the Pico Audio Pack is designed, I can’t have it plugged into a Pico at the same time as the keypad, so I’m probably going to need to design a PCB to get it all wired up.
The PCM5100A I2S DAC that the audio pack uses looks to be in short supply at the moment, but its older sibling, PCM1754 is cheap and widely available. The most significant difference between the two is that the PCM1754 needs a MCLK signal running at a multiple of BCLK, whereas the PCM5100A generates MCLK internally by doing clock recovery from BCLK with a PLL. I think the easiest way to generate MCLK from the RP2040 would be to run a second PIO state machine at the higher clock speed and start it in sync with the I2S state machine. The MCLK PIO program should be very simple, just toggling MCLK on each cycle.
Hopefully next month I’ll have some fun sounds and pretty PCB layouts to show you!
Twiddling bits in registers doesn’t make the most intuitive, readable, or portable code. This is why we write drivers. I’ve created three Alire packages, rp2040_hal, pico_bsp, and pico_examples. rp2040_hal contains all of the drivers for the chip’s internal peripherals, pico_bsp contains some details about the Pico board and drivers for the Pimoroni Pico addons, and pico_examples contains, you guessed it, example code. At the moment, pico_bsp cannot be used with the Ravenscar runtime without modification, so I’ll ignore that and focus on rp2040_hal for right now. The examples repository contains lots of code that uses the pico_bsp if you’d like to see how that works.
Now that I have drivers, I’ve removed all of the interfaces generated from SVD from our project. These still live in the rp2040_hal package but as long as the drivers implement all of the right interfaces, we shouldn’t need them. If you find something you can’t do with the HAL drivers that seems important, please send a pull request to the rp2040_hal repo. I’ve refrained from tagging a 1.0 release because I’m still not satisfied with all of the interfaces and retain the right to break the API.
You can see that the Main procedure is much more compact and readable now. The PADS_BANK and IO_BANK stuff is wrapped up in a lovely Configure interface and the GPIO is abstracted into an object with a very convenient Toggle method.
Building the code is done using Alire now, rather than calling gprbuild directly. Alire is analogous to Rust’s Cargo or Python’s pip. Alire keeps track of all of the dependencies and can pull in new ones with the alr with command.
git clone https://github.com/JeremyGrosser/rp2040_hal
cd 04-hal-blink
alr build
A month later, much has happened! I’ve implemented drivers for nearly all of the RP2040’s peripherals, Fabien Chouteau contributed an I2C driver, and Daniel King has ported bb-runtimes, including multiprocessing support. Let’s get that blink example updated!
There are lots of pieces that can be abstracted away and made more flexible. For example, Ticks shouldn’t be a public variable that can be modified from anywhere. I should also define a new Time type to differentiate it from other Integers. If you follow these changes to their logical conclusion, you get something that looks like Ada.Real_Time which already exists in the Ravenscar runtimes.
The Ada language provides some fairly high level constructs related to tasking, memory management, and timing that aren’t easy or practical to implement on every platform and the use of some of those features may violate a project’s certification requirements. For this reason, there several runtime profiles with varying levels of functionality. So far, I’ve been using a Zero Footprint (ZFP) runtime, which provides only the bare minimum to allocate some stack space, pass arguments, and call a procedure. No batteries included. The next step up would be the Ravenscar profile, which allows a broader set of builtin functionality, including tasking and synchronization constructs you’d expect to find in an RTOS. An open source implementation of Ravenscar is available in the bb-runtimes repository, though porting it to a new chip is not a small task. There are other runtime implementations that wrap existing RTOS libraries like FreeRTOS and RTEMS. GNAT GCC also includes runtimes for Linux, FreeBSD, Solaris, HP-UX, VxWorks, LynxOS, QNX, and Windows that implement the full set of libraries in the Ada language specification.
I’ll port our blink example to a Ravenscar RTS (Run-Time System) from bb-runtimes. As the RP2040 is still a new platform, its runtimes haven’t been merged yet and aren’t distributed in the GNAT Community 2020 bundle so I’ll need to build it from source.
git clone -b rpi-pico https://github.com/damaki/bb-runtimes
cd bb-runtimes
./build_rts.py --build rpi-pico-mp
cd ../02-ravenscar-blink
gprbuild -P rpsimple.gpr
You’ll notice that the boot2, crt0, and linker script aren’t needed anymore as they’re included with the runtime. I’ve replaced all of the SysTick stuff with a single delay 1.0; statement. The RTS has configured the PLLs and the TIMER peripheral, so I now have microsecond resolution tickless operation. We can still do better! The single-cycle loop still takes time to execute, and waking from sleep takes a few cycles too, so the delay between blinks is still not precisely one second.
In 03-realtime-blink I’ve imported Ada.Real_Time and declared a Next_Blink variable with type Time. Time is a private type as the storage representation of time is a non-portable implementation detail. Initializing Next_Blink with a call to the Clock function means that time doesn’t even have to start at 0!
Now that I know when the next blink should happen, I can use a delay until statement for precision waiting.
Recently, the Raspberry Pi Foundation launched their new RP2040 SoC on a $4 development board called Pico. As the foundation’s goal is primarily education, they’ve provided lots of high quality documentation, libraries, and examples for the new chip. Naturally, I’m going to ignore most of that and roll my own. Why? Because this is my idea of fun and I’m trapped inside during a pandemic.
If you’d like to build the examples and follow along, you’ll need GNAT Community 2020 ARM ELF installed in your PATH. I’ve only tested this on x86_64 Debian, if you’re on another platform I can’t help you. You’ll also want an SWD debugger that works with the RP2040. I use openocd on a Raspberry Pi, I hear Segger J-Link support is coming soon. Technically you could use elf2uf2 and load binaries over USB, but that’s gonna get tedious for any nontrivial debugging.
The RP2040 has no internal flash. The boot ROM loads 256 bytes of code from an external SPI flash and executes it. This “second stage bootloader” is expected to configure the XIP (eXecute In Place) peripheral with clock and timing details specific to the flash chip in use, then jump to the start of the user code in the memory mapped to the flash.
I tried to rewrite the boot2.S bootloader from pico-sdk in Ada, but I couldn’t get it to fit inside 256 bytes. It might be possible, but not today. I ran gcc’s preprocessor on boot2.S to generate a single assembly file I could link. I copied the linker script and crt0.S from pico-sdk too.
The zfp-cortex-m0p Ada runtime does most of the boilerplate Cortex-M0+ setup and implements Ada.Text_IO with semihosting over the debug interface, so I wrote a hello world and copypasta’d a GPR project file to build it.
It took me a few hours to figure out the right incantation to get the linker to put the .boot2 section at the start of the flash chip along with crt0.S and the Ada runtime’s init code. The result is an ugly mess, which is why I’m omitting it here. If you want to see how to do things the quick and dirty way, my initial attempt is on github.
I soldered some headers for the Pico’s SWD port, wired it to a Raspberry Pi 4, and compiled the raspberrypi branch of openocd. As far as I can tell, nobody distributes a toolchain that can cross-compile Ada arm-eabi binaries on aarch64 and I don’t want to spend my time building binutils and gcc right now, so I setup an ssh tunnel from my x86_64 workstation to the Pi. I run arm-eabi-gdb locally and connect to openocd over the tunnel. Most of this will go in a .gdbinit script later.
ssh [email protected] -L3333:localhost:3333
openocd -f interface/raspberrypi-swd.cfg -f target/rp2040.cfg
arm-eabi-gdb obj/main
target extended-remote localhost:3333
monitor arm semihosting enable
load
run
Let’s write some code! For this example, I’m not going to do anything particularly complicated or idiomatic to Ada in order to keep things as simple as possible. This is going to be a very imperative program assigning values to registers and not much more.
ARM’s tooling for silicon vendors generates an SVD file, which is a pile of XML that lists all of the peripheral addresses and register offsets with vaguely human readable names. The svd2ada program translates this into Ada spec files with record types and representation clauses. Unfortunately, it crashed with the RP2040 SVD file. svd2ada expects a <size> to be specified on every register but the RP2040 SVD defines <size> at the peripheral level. The SVD format says this should then be inherited by the registers in the peripheral, but svd2ada doesn’t do that. I couldn’t figure out how to fix svd2ada so I wrote a quick Python script to modify the SVD file by copying the <size> field to every register within a peripheral. Now svd2ada works and generates a .ads file for each peripheral on the RP2040.
Normally the next step in bringing up a new chip would be to get all of the clocks configured, which is usually pretty boring code to write. At startup, the RP2040’s system clock runs from an internal ring oscillator with a not at all predictable or stable frequency between 1.8 and 12 MHz, which is good enough for some blinking. I skipped clock configuration and went straight for the GPIO.
There are four peripherals that need to be configured to toggle a pin: RESETS, PADS_BANK, IO_BANK, and SIO. RESETS, as you might expect, controls the reset state of the other peripherals. I pull the IO_BANK and PADS_BANK out of reset and wait for any initialization these peripherals might need to do in a busy loop.
Next, PADS_BANK enables the output driver on GPIO25, which is connected to the LED on the Pico board.
IO_BANK selects a peripheral to connect the pad to.
The SIO (single-cycle IO) peripheral can toggle pins. I added a Pin_Mask constant in the declare block with bit 25 set. svd2ada generates nice subtype definitions for each register field so that I don’t need to know that GPIO_OUT is 30 bits wide. The immediate value here is just Shift_Left (1, 25), but using Shift_Left here would require some type conversion that I want to avoid in this example.
Enable the output in the SIO peripheral
Now we blink!
Assuming those calls actually get compiled into single cycle writes, that’s gonna be blinking at half the system clock frequency, far faster than the human eye can see, but good enough for an oscilloscope. The SIO peripheral has an XOR register that allows us to make this code even shorter.
This is where the narrative diverges from reality. After I got to this point, I spent a few days refactoring the code into a package with well defined types and interfaces that conform to the Ada HAL package. For the sake of this example, I’m going to gloss over some of those organizational details and pretend things are still mostly happening in a single Main procedure.
Next I need a delay in that loop to blink at a rate that’s perceptible to humans. I could call a bunch of nop instructions and waste cycles, but that’s just silly. The RP2040 has a very nice 64-bit timer peripheral that I completely ignored because the chip also has the standard ARM SysTick peripheral that I’m already familiar with. Exported symbols need to be defined at the package level, so I’ve put this code into a SysTick package. I’ve only reproduced the implementation bits here, just know that this is happening in a new file.
I need an interrupt handler to increment a counter for every tick. crt0.S exports a weak isr_systick symbol that I can implement.
I’ll also add a wrapper around the wfi (wait for interrupt) assembly instruction in the same package
Back in the blink loop, I call the wait for interrupt instruction and check the value of Ticks.
IT BLINKS! Not precisely at 1 Hz, as the ring oscillator isn’t accurate, but close enough for this demo.