ANN: Bareboard tasking runtimes for the RP2350 (Raspberry Pi Pico 2)

I finally got my hands on a Pico 2, so I’ve been able to release runtime crates for the RP2350. The new crates are:

Like the RP2040 crates I announced previously, these crates are also configurable through crate configuration variable. See the readmes on the GitHub page for more info on configuring them (in particular, README-RP2350.md)

For the moment the runtimes only support the Cortex-M33 cores. I might explore the RISC-V cores later if there’s demand for it.

6 Likes

Hi,

Thanks for your work. That’s very useful.

I don’t really understand the difference between both runtimes. It seems to be tasking related. But what are these differences ?

“light tasking” and “embedded” are two pre-defined runtime profiles for GNAT, which provide different levels of language support. For bareboard targets, there are generally three different profiles that can be supported:

  • “light” runtimes are the smallest runtimes but lack support for some Ada features. In particular, they don’t support tasking or exception propagation, and have a simple heap allocator (Unchecked_Deallocate is a no-op). They do, however, provide support for things like assertions, ’Image and ’Value attributes for scalar types, simplified Ada.Text_IO, secondary stack.
  • “light-tasking” runtimes include everything in “light”, but also add support for a tasking subset based on the Ravenscar and Jorvik profiles. For these RP2350 runtimes, this means you can declare multiple tasks and assign them to either of the two Cortex-M33 cores. For example, task My_Task with CPU => 2; declares a task that will run on the second core of the RP2350. You can also use Ada’s delay until statement for task delays, and use protected objects for inter-task communication and for interrupt handlers.
  • “embedded” runtimes include everything in “light-tasking”, but also adds support for exception propagation and has a full heap allocator (so you can use Unchecked_Deallocate to free memory).

For more information on what’s supported in each runtime, see this part of the GNAT User’s Guide Supplement for Cross Platforms: 5. Predefined GNAT Pro Run-Times — GNAT User's Guide Supplement for Cross Platforms 27.0w documentation

I hope this helps. Happy to answer any more questions if something isn’t clear.

Thanks for your answer.

Reading the GNAT documentation didn’t enlighten me on the differences between light-tasking and embedded runtimes.

Just to be sure, no profile allows a timeout in a task select. Right ?
Something like this :

   select
      data := GetData();
      Put_Line(data);
   or
      delay 1.0;
      Put_Line("timeout");
   end select;


That’s right. select statements are not allowed since only a subset of the tasking features are supported (Jorvik profile). You can read a bit more about the tasking restrictions for Ravenscar and Jorvik here: The Ravenscar and Jorvik Profiles

Thanks for your answer.

No Timeout… a significant limitation.

Nice.

How does this compare to:

?

No Timeout… a significant limitation.

Indeed, but there are other ways to achieve timeouts that fit within the restrictions of the Jorvik profile. For example, both the light-tasking and embedded runtimes have package Ada.Real_Time.Timing_Events which can be used to notify a protected object when a timeout has passed.

There’s also the Guide for the use of Ada Ravenscar Profile in high integrity systems technical report which has various examples of using the Ravenscar profile. Note that Ravenscar is a subset of Jorvik, so anything you can do within the Ravenscar profile you can also do within the Jorvik profile.

How does this compare to:

rp2040_hal and the runtime crates have different purposes, but complement each other. The purpose of the runtime is to implement the semantics of the Ada language needed at run-time. For example, when you use the ‘Image attribute, a call is made to the runtime to generate the string representation of the object. Similarly, when you declare a task, the runtime takes care of setting up and managing the execution of that task.

As mentioned above, there are different profiles available so you can choose which level of language support you want. The embedded profile provides the most language support for bare-metal targets like the RP2350, but it is also the largest and uses the most memory (code space, RAM). The light-tasking runtime is smaller and uses less resources, but also doesn’t provide some language features like exception propagation.

The goal of rp2040_hal on the other hand is to provide a nice API to access the various peripherals on the RP2040 chip (GPIO, SPI, DMA, etc).

Simply put: the runtime implements the dynamic semantics of the Ada language, and rp2040_hal provides access to the RP2040 peripherals.

To build a real application for the RP2040 you would need both a runtime (either a very minimal one like light-cortex-m0p, or a more featureful one like light_tasking_rp2040), plus rp2040_hal to be able to configure and use the peripherals.

Yes, I know. I studied all this few years ago. At that time Jorvik profile was still not official. I was wondering if it evolved on this aspect since then.

I think a full profile, or at least a much less restrictive profile, would be very useful in embedded world. I know the reasons of such restrictive profiles. Not every project needs to be provable. Not all projects use a microcontroller with very limited resources.

Thanks!!! Highly appreciated.

1 Like

Very good news! Do you think that sooner or later you runtime will be included in GNAT?

1 Like

No, there are no plans to distribute the runtime with GNAT since it’s distributed via Alire.

Alire makes it much easier for me to maintain the runtime since there’s no need to go through a third-party repository. I can also make more frequent releases without needing to wait for the next yearly GNAT FSF release.

Hi, this is great work! I’m a HW designer and new to Ada. A big boost is to do HW/SW codesign/cosim. Which means that one needs a way to configure the ADA Hardware Abstraction Layer (HAL) to either connect to a simulation, or (in the real world) run on bare metal. I know how to do it in C. I’m new enough to Ada that I don’t even know what that concept would be called in Ada, let alone what crates would support it. Would you possibly have any advice?

If I understand you correctly, you want one HAL interface that stays the same but has two different “backends” that implement the HAL for either a simulator or the real hardware. There’s a few ways you can do this, depending on how significant the changes are. In any case, Alire’s crate configuration variables provide a nice mechanism to do this, and this is what I have used to configure my runtimes.

With Alire’s crate configuration variables, your HAL crate could define something like this in its alire.toml file:

[configuration.variables]
Target = { type = "Enum", values = ["Simulator", "Hardware"], default = "Hardware }

This will declare a variable that can be configured for either the Simulator or the real target Hardware. Users of your HAL crate can then set this variable in their alire.toml (assuming your HAL crate is called my_hal for example:

[configuration.values]
my_hal.Target = "Simulator"

At build time, Alire will generate an Ada spec (.ads), GPR file (.gpr), and C header file (.h) that contain the configuration values. For example, a file called my_hal_config.ads will be generated.

You can then import these generated sources to configure your source code in various ways, depending on how significant the differences are between the simulator and real hardware. Here’s a use cases:

  1. If you just need to choose a different value between the hardware and simulator, then you can choose the value at compile time based on the. For example, to choose 1 for the simulator and 2 for the real hardware:
with My_Hal_Config; use My_Hal_Config;

package Example is
   My_Constant : constant := (if My_Hal_Config.Target = Simulator
                              then 1
                              else 2);
end Example;
  1. If you need certain code in a function or procedure to be executed only on the simulator, then you can use an if statement on a constant boolean for this:
with My_Hal_Config; use My_Hal_Config;

procedure Example is
   Is_Simulator : constant Boolean := My_Hal_Config.Target = Simulator;
begin
   --  A procedure call that is executed for both the sim and the real hardware
   Common_Code;

   --  Code that is only executed on the simulator
   if Is_Simulator then
      Do_Something_For_Simulator;
   end if;
end Example;
  1. If your implementations differ wildly between the two targets, then you can choose different source files at build time. To do this, you can create 2 different source subdirectories: e.g. one directory called Simulator and one directory called Hardware. You can then have two implementations of the same source file (e.g. hal.adb), one in each directory. Then, in your GPR file you can choose which sources you want to use by setting Source_Dirs. For example:
with "my_hal_config.gpr";

project My_HAL is

   for Source_Dirs use
     --  directory containing sources common to both impls
     ("src/common", 

     --  directory containing the sources for a specific target
      "src/" & My_Hal_Config.Target);

end My_HAL;

I’ve used all three of these approaches in the RP2350 runtimes. I’ve used #1 for setting clock configuration constants, #2 for omitting some multicore-only code for when the runtime is configured for a single core, and #3 for choosing different versions of a source file (for package Ada.Interrupts.Names) depending on single/multicore configuration.

2 Likes