Tasking with embedded_rp2040

When it comes to multi tasking what i actually possible with embedded_rp2040? I already learned that rendezvous don’t work. Now I wonder how many task you can use? What is possible with Jorvik?

Have you seen these

Both light_tasking_rp2040 and embedded_rp2040 support the Jorvik tasking profile. This is a subset of Ada’s tasking features that is designed for safety-critical and hard real-time computing. Excluding more complex tasking features (like rendezvous as you mentioned) also means the implementation can be simpler and more efficient on bare-metal targets like the RP2040.

Compared to Ada’s full tasking supports, the Jorvik profile excludes:

  • task rendezvous
  • dynamic task allocation (all tasks must be declared at library level)
  • dynamic protected object allocation (all protected objects must be declared at library level)
  • complex entry barrier expressions. Jorvik uses the Pure_Barriers restriction which means you can do simple expressions, but cannot do things that could have side effects like make function calls.
  • dynamic interrupt handler attachment (all interrupts must be statically bound to a protected procedure via Attach_Handler)

You can, however, declare as many tasks and protected objects as you want (as long as you have the memory for it).

Both the light_tasking_rp2040 and embedded_rp2040 runtimes support tasking under the Jorvik profile.

These runtimes also support pinninig tasks to either of the RP2040’s two cores. By default, tasks are scheduled on the first core, but you can pin a task to the second core using the CPU aspect. For example:

task My_Task with CPU => 2;

You can also assign priorities to tasks so that higher-priority tasks can preempt lower-priority tasks on the same core. For example:

task Low_Priority_Task with Priority => 10;
task Highest_Priority_Task with Priority => System.Priority'Last;

You can use protected objects to install interrupt handlers. Here’s an example that will allow a task to block (wait) for the UART0 interrupt to occur.

protected Example_PO 
  with Interrupt_Priority => System.Interrupt_Priority'First 
is
   entry Wait_For_Interrupt;

private
   procedure Interrupt_Handler 
   with Attach_Handler => Ada.Interrupts.Names.UART0_Interrupt_CPU_1;

   Triggered : Boolean := False;
end Example_PO;

protected body Example_PO is
   entry Wait_For_Interrupt when Triggered is
   begin
      Triggered := False;
   end Wait_For_Interrupt;

   procedure Interrupt_Handler is
   begin
      --  Insert code here to poke the appropriate UART0 register to
      --  acknowledge the interrupt, if necessary.

      Triggered := True;
   end Interrupt_Handler.
end Example_PO;

Notice that the RP2040 runtimes allow interrupts to be attached to either core. This example attached it to CPU 1 (the first CPU).

I hope this gives a little taste of what is possible.

Why interrupt is attached to CPU 1? Is it default?

It’s the suffix of the interrupt name that determines which CPU the interrupt is attached to. Each interrupt in the RP2040 runtime is given two names in Ada.Interrupts.Names; one which attaches to CPU 1 and another which attaches to CPU 2. For example, the UART0 interrupt has the names:

  • UART0_Interrupt_CPU_1
  • UART0_Interrupt_CPU_2

So if you want your UART0 interrupt handler to run on CPU 1, then use UART0_Interrupt_CPU_1. Otherwise, if you want it on CPU 2 then use UART0_Interrupt_CPU_2.

In theory you could install two different handlers for the same interrupt - one on each CPU - and it would work, but it’s probably not very useful.

While I’m on this subject, I’ll say that if you do assign different interrupt handlers to different CPUs, then I would recommend also putting them in separate protected objects if possible. If you put two interrupt handlers on different CPUs in the same protected object, then if both interrupts are active at the same time then one CPU will “win” and get access to the protected object first and the other CPU will have to wait (spin on a spinlock) until the first CPU has finished before it gets access to the PO. This would block the “losing” CPU from doing anything useful for a short while.

Just to add to this list I believe Jorvik also excludes things like Ada.Dynamic_Priorities, Ada.Execution_Time.* and ATC?

Assume these would most likely be available in an OS-based setup?

Jorvik excludes Ada.Dynamic_Priorities, so all task priorities must be static (you can see the full list of restrictions for Ravenscar and Jorvik here). It also excludes Ada.Asynchronous_Task_Control.

Jorvik doesn’t say anything about Ada.Execution_Time, but GNAT bareboard runtimes do support this package for the “light-tasking” and “embedded” runtime profiles.

Yeah, with the “full” Ada runtime running on an OS, these packages are all supported.