Ada on RP2350
On Mon, Aug 4, 2025 at 9:31 PM Zeynep Tasci wrote:
I saw that you started writing the rp-clock.ads while porting the 2040 hal to 2350. I’ve just finished the Ada tutorial on learn.adacore.com and would like to get some hands-on experience with the language. Would you recommend I continue implementing the timer? Do you think it is beginner friendly enough or do you have some other similar ideas in mind?
This is gonna be a bit of stream-of-consciousness… I have a lot of thoughts about these chips and all of the different approaches to working with them in Ada.
I think you’re looking at my rp2040_hal branch which is in a very unfinished state. I do remember rewriting RP.Clock
, but it’s different enough from RP2040 that I’m not sure it makes sense to try to maintain the same spec for both chips. This opens up a broader question: Do these things even belong in the same crate? The boot sequence on RP2350 is different enough that none of that code is shared, the timers have different semantics because there are now two of them, and you’d need some conditional compilation or discriminated record types for things like the PIO, which now has additional registers. All of these things lead me to thinking we should have an rp_common
crate, and separate rp2040_hal
and rp2350_hal
crates that depend on it, similar to what pico-sdk does.
Cortex-M33 adds a lot of new features that only make sense on an RTOS. rp2040_hal is mostly focused on a “light” or “zero footprint” runtime that does not have tasking support. While rp2040_hal can coexist with the Ravenscar bb-runtimes RTOS, the interrupt handling goes through the RP_Interrupts package which has two implementations, one for the RTOS, one bare metal. This has always felt awkward and annoying to use, especially with regard to timers. I think it might be better to get bb-runtimes ported to RP2350 first, similar to what damaki/nrf52-runtimes does. That way, the runtime takes over timer interrupts and enables the standard Ada.Real_Time interface and delay until statements, which can coexist with tasking gracefully. Another approach would be to build atop godunko’s a0b infrastructure with separate crates for each chip and peripheral. I haven’t experimented too much with this, but it looks very flexible.
I also started a new rp2350 library attempting to support both the RISC-V and Cortex-M33 cores. I was writing all of the register type definitions by hand rather than relying on svd2ada. The APB bus that these chips use for the low speed peripherals only supports 32-bit aligned writes but gcc/gnat will try to use byte instructions on records not explicitly marked Volatile_Full_Access
. I’ve had a few nasty bugs due to these misaligned writes that make me cautious about SVD generated types. Handwritten types also allow some flexibility in choosing to use arrays, records, or just bare UInt32
where appropriate. This can simplify some driver code and lead to better codegen in some cases.
I lost interest in the RISC-V cores when I found out you can’t use the FPU with them as it’s a “co-processor” that can only be accessed via special ARM instructions. That, combined with the I/O pad errata and tarriff situation led me to shelve this whole thing and go back to the MSPM0 that I’ve quite enjoyed working with. I saw that they’ve fixed the errata with a silicon revision, so I guess that makes RP2350 a bit more appealing again.
So, where should you start? I have no idea. I think getting an Ada runtime with some sort of tasking support running would be much more useful/interesting than writing a bunch of HAL drivers. The HAL interfaces are nice to have, but constrain a lot of design decisions (eg. tagged discriminated records vs generics) that I’m not sure I agree with.
If you want to start with something smaller, I’d recommend building a project like a kitchen timer or some sort of robot, rather than focusing on the libraries. You’ll quickly discover what pieces are missing and what’s important to you.