Task creation issue in ravenscar-full-stm32f429disco runtime

I am trying to implement some logic which is meant to be enforcing an MIT constraint in the way some specific interrupt is handled in a stm32f429 board (so building against the ravenscar-full-stm32f429disco runtime).

For context, I have configured the MC01 pin of this board to pulsate at a rate of 3.2MHz (so actually period and not sporadic but probably somewhat irrelevant to my question) and have then jumpered this pin to another one which has been configured as an interrupt receiver for the EXTI4_Interrupt interrupt. The AHB bus is running at the default 16MHz.

The MIT enforcer is implemented by way of the following protected object:

with Ada.Interrupts.Names;
with System; use System;
with Ada.Real_Time.Timing_Events; use Ada.Real_Time.Timing_Events;
use Ada.Real_Time;

package Interrupt_Controllers with Elaborate_Body is
      
   protected MIT_Controller with
     Interrupt_Priority => Interrupt_Priority'Last
   is
      procedure Handle_Interrupt with
        Attach_Handler => Ada.Interrupts.Names.EXTI4_Interrupt;
   
      entry Wait_For_Next_Interrupt;
   private
      Event: Timing_Event;
      procedure Handle_Timeout(E: in out Timing_Event);
      Arrived: Boolean;
      MIT: Time_Span := Milliseconds(100);
   end MIT_Controller;
   
   task type Interrupt_Action;
   
end Interrupt_Controllers;

with the body being:

with EXTI; use EXTI;
with Common_Types; use Common_Types;

package body Interrupt_Controllers is
      
   protected body MIT_Controller is
      procedure Handle_Interrupt is
      begin
         EXTI_IMR_Register.MR4 := Off; -- disable interrupt
         EXTI_PR_Register.PR4 := On; -- clear pending status (bit needs to be set HIGH in order to do that)
         Set_Handler(Event, Clock + MIT, Handle_Timeout'Access); -- setup interrupt-enable event to fire after MIT
         Arrived := True;
      end Handle_Interrupt;
         
      entry Wait_For_Next_Interrupt when Arrived is
      begin
         Arrived := False;
      end Wait_For_Next_Interrupt;
   
      procedure Handle_Timeout(E: in out Timing_Event) is
      begin
         EXTI_IMR_Register.MR4 := On; -- enable the interrupt
      end Handle_Timeout;
   end MIT_Controller;
   
   type Count is mod Natural'Last;
   
   task body Interrupt_Action is
        C: Count := 0;
   begin
      loop
         MIT_Controller.Wait_For_Next_Interrupt;
         C := C + 1;
      end loop;
   end Interrupt_Action;
   
   IA: Interrupt_Action;
   
end Interrupt_Controllers;

and in the “main” procedure I just:

with Interrupt_Controllers;
pragma Elaborate(Interrupt_Controllers);

which then just enters an infinite loop i.e.

loop
  null;
end loop;

Now when I run this in gdb and put a few brakepoints inside Handle_Interrupt, Handle_Timeout and the Interrupt_Action task I only get hits for the first two but not the task. I.e. if the execution is constantly continued breakpoint hitting alternates between the interrupt handler and the timeout handler but never goes inside the task.

One thought I had was that something is incomplete with regards to program elaboration so I added the with Elaborate_Body to the package specification but also the Elaborate pragma in “main” as shown above but that didn’t change much (gdb was hitting the interrupt handler breakpoints anyway even without these).

Running info threads whilst inside Handle_Interrupt yields:

  Id   Target Id         Frame
* 1    Thread <main>     interrupt_controllers.mit_controller.handle_interrupt (<_object>=...) at /home/...

which is somewhat interesting as it kinda indicates the runtime somehow hijacks the “main” thread to run the interrupt handler?

Another thought was that maybe Interrupt_Action running on the default priority gets constantly preempted by the interrupt handler so never gets a chance to run. But that seems somewhat unlikely as there’s an MIT of 100ms and the AHB bus is running at a much higher rate than the interrupt rate.

Obviously, I can’t declare the task object inside “main” because of the No_Task_Hierarchy restriction in Ravenscar but isn’t the above what at library level means? (i.e. declare within the declarative part of a package specification/body)

I’m clearly missing something basic here. :slightly_smiling_face:

Any pointers would be much appreciated. - Thanks.

I’m not a bareboard expert. From my point of view, you have two tasks with equal (default) priority. Both tasks eat 100% of CPU. In my opinion, in this condition Ada doesn’t guarantee that both task get CPU ticks. I would add delay 0.0; (or delay until ...; equivalent) statements in loops in both tasks in a hope that it force the scheduler to switch context.

Have you actually declared an instance of task type Interrupt_Action?

For a simple test like this, you could just declare a single instance (task Interrupt_Action).

Yeah, I did think about that too so I tried a couple of things:

First, I tried increasing significantly the MIT in hope that the interrupt handler would execute a lot less frequently but that didn’t change anything. But also as an even more drastic approach I tried removing the jumper wire between the interrupt source and the interrupt sink (which means that zero interrupts are occurring on the pin) so the interrupt handler isn’t actually getting invoked at all, but no luck.

What’s a bit strange is the fact that, as mentioned above, the info threads isn’t reporting any other tasks except “main”. I’m not sure how tasks are implemented in the Ravenscar runtime but I would except some indication of a task other than “main” to be present.

Thank you.

Yep, there’s a task object declared inside Interrupt_Controllers (i.e. IA: Interrupt_Action; if you could scroll a bit further down in that file).

I appreciate the runtimes are widely different but implementing some similar logic in a desktop executable (i.e. declare a few task objects in some package and with that package in “main”) and running info threads in gdb I can see these tasks listed in the command output.

So, a bit confused.

Thank you.

Could you try putting a delay in the main program? If there’s nothing else for it to do, I sometimes replace the final loop by delay until Ada.Real_Time.Time_Last;.

Or you could move the code in Interrupt_Action to the main program.

Or you could raise the priority of Interrupt_Action.

What I suspect is happening is that there’s no reason for the runtime to stop running the main program and run Interrupt_Action.

1 Like

Great, both suggestions worked!

This would make a lot of sense as both the main and the Interrupt_Action tasks were running with the same priority (Deafult) and assuming the default dispatching policy is FIFO_Within_Priorities this tie was always broken in favour of main since it started first and never gave the processor up. Perhaps with a Round_Robin policy Interrupt_Action would get a slice.
So, declaring the task type with Priority => Default_Priority + 1 does the trick.

I’m still curious to understand how tasking is implemented in this runtime but might try and look into that a bit later. Running a backtrace from within the Interrupt_Action shows:

#0  interrupt_controllers.interrupt_action (<_task>=0x20001518 <interrupt_controllers.ia>) at /home/savvas/...
#1  0x080030ec in system.tasking.restricted.stages.task_wrapper ()

so, the clue might be in task_wrapper()

Thanks very much.

Since your main-program procedure does nothing, its body should just be

null;

Your program won’t end until the task in the package ends, and it never does. That way you should only have one task running.

1 Like

hmm…so are you saying the main task is used to run the code in Interrupt_Action too?

@JC001 I was going to say that that won’t work in Ravenscar … I know it won’t in FreeRTOS-Ada’s implementation, because I made it so, but not sure about AdaCore’s version.

No. I’m saying the Environment Task will block after it has executed the main-program procedure until the IA task has terminated.

The ARM defines the semantics as the Environment Task does all elaboration, then calls the main-program procedure, then waits for all tasks to terminate, then does any remaining finalization. The restrictions for Ravenscar shouldn’t make any difference to this. So there should be no need for a loop or delay in the main-program procedure when it has nothing else to do. If you have a compiler that doesn’t do this, then it’s not Ada.

1 Like

Well, the simple-minded answer is, it’s not Ada, it’s Ravenscar.

AdaCore’s simple tasking RTS for the STM32F4 just drops off the bottom of the environment thread and tries to restart the machine.

FreeRTOS-Ada has the grace to call Last_Chance_Handler (with parameter “S” - ?? - should be “task exited, not allowed in Ravenscar”.

What should happen … I couldn’t get sensible answers from the Ravenscar documents I could find.

It doesn’t actually do that. If I remove the loop, it simply terminates the program immediately (i.e. SIGTRAP’s).

I believe the reason for that is because there is no task hierarchy formed (which is not allowed in Ravenscar anyway) so that the parent task doesn’t wait for its child tasks to complete.

My understanding is that the sequence of events is something along the lines of:

  • The environment task (ET) elaborates Interrupt_Controllers and creates the IA object.
  • The IA object begins activation (there’s no begin tag in the package body to block right there waiting for the task to activate).
  • The IA object completes activation (which in this case is probably a zero-op since there’s no declarative section in the task declaration) and starts running with a higher priority than main.
  • When IA calls the blocking entry from the protected object (and the interrupt handler is not active) the ET calls the main procedure which simply reaches the end statement.
  • Since there are no child tasks to wait upon main terminates.

I could be wrong but this is my interpretation of the state of affairs for this program.

Ravenscar is Ada with some restrictions. Using the Ravenscar profile does not change Ada’s tasking semantics.

These compilers clearly have errors. I have written many programs where the main-program procedure is simply null;, everything is done by tasks in withed packages, and they work fine. Some of these were embedded programs running under VxWorks.

Apparently you are OK with working around these compiler errors.

package Library_Task is
   pragma Elaborate_Body;
end Library_Task;

pragma Profile (Ravenscar);

with Ada.Real_Time;
with Ada.Text_IO;

package body Library_Task is
   task Counter;

   task body Counter is
      type Count_Value is mod 10;

      use type Ada.Real_Time.Time;

      Count : Count_Value := 0;
      Next  : Ada.Real_Time.Time := Ada.Real_Time.Clock + Ada.Real_Time.Milliseconds (100);
   begin -- Counter
      Counting : loop
         Ada.Text_IO.Put_Line (Item => Count'Image);
         Count := Count + 1;

         delay until Next;

         Next := Next + Ada.Real_Time.Milliseconds (100);
      end loop Counting;
   end Counter;
end Library_Task;

pragma Profile (Ravenscar);

with Ada.Text_IO;
with Library_Task;

procedure Library_Task_Test is
   -- Empty
begin -- Library_Task_Test
   Ada.Text_IO.Put_Line (File => Ada.Text_IO.Standard_Error, Item => "Main program procedure");
end Library_Task_Test;

This compiles fine with GNAT 12.3.0/Xubuntu 24.04. Behavior is as expected:

$ ./library_task_test 
 0
Main program procedure
 1
 2
 3
 4
 5
 6
 7
 8
 9
 0
 1
 2
 3
 4
^C

One of the restrictions for Ravenscar is No_Task_Termination. This code satisfies that because the Counter task never terminates, and the Environment Task cannot terminate until the Counter task does.

Ravenscar is Ada with some restrictions. Using the Ravenscar profile does not change Ada’s tasking semantics.

Ravenscar is entirely about tasking!

I have written many programs where the main-program procedure is simply null; , everything is done by tasks in withed packages, and they work fine. Some of these were embedded programs running under VxWorks.

I’ve written embedded programs under VxWorks; GNAT for VxWorks 5.3 was a full Ada compiler, not Ravenscar.

Apparently you are OK with working around these compiler errors.

All compilers have errors, and some have features you wish they did differently, or at all. I’m not at all sure these are errors; given that Ravenscar tasks mustn’t terminate, there’s no future in arguing about how the main program should behave if any of them do.

Given our context is a restricted environment, it’s frequently recommended not to have a main program that does nothing (except delaying), which wastes resources (particularly stack); instead, find something for it to do!

We have the same philosophy. We take it a step farther: If your main is empty and is doing nothing, then either you are doing something wrong in your interrupts (too much code usually…interrupts should be “get in, get out quickly”) or you have one too many tasks (one of those tasks should just be done in main).

Additionally, if the system is safety critical we don’t allow fall through mains at all. It covers the case where we have a logic error that only minifests in the field and ends up unexpected completing a task (bad goto, missed loop exit used for debugging, etc). Once main finishes, falling off the edge of the world is a scary place to be in a micro.

1 Like

It makes sense, I guess, to piggyback some work on the main task than just leave it blocked for ever as although it may have some special role it still is a full-blown task nevertheless (also, as mentioned the stack has been allocated anyway, so why waste that).

The Ravenscar profile does look like it diverges a bit from the Ada semantics in the sense that, as mentioned, main will block waiting for the with’ed tasks to complete in a similar desktop program (I’ve run this on an Ubuntu 22.04).

Perhaps stretching a bit the scope of my original question but does anyone know if this is the case and if so why? It can’t be that a task hierarchy is formed between the main task and the with’ed tasks or the compiler would have disallowed that? So, there must be some other rule in place?

Going through an article titled “Guide for the use of the Ada Ravenscar Profile in high integrity systems” (Alan Burns, Brian Dobbing and Tullio Vardanega) the section on the profile restrictions (4.1) mentions:

No_Task_Termination

[AI 305] All tasks are non-terminating. It is implementation-defined what happens if a task attempts to terminate.

The restriction attempts to mitigate the hazard that may be caused by tasks terminating silently.
Real-time tasks normally have an infinite loop as their last statement.

which kinda confirms what was mentioned above, that tasks in Ravenscar should absolutely not terminate (voluntarily at least, I guess).

Thanks very much for your input and insights!