Using a “Run To Completion Task” model in embedded programming

This is about the architecture or high-level structure of an embedded app.  You don’t always need an RTOS, or you can implement a very simple one.

References

I read these sources prior to this post.   This post is largely a hasty summary of these links.

Super Simple Tasker

Real Time For the Masses

Fearless Concurrency in your microcontroller

Context

Embedded programming is event/interrupt driven.

Embedded programming is multiprocessing:  devices are processors too.

Some chips e.g. Nordic NRF52 chips have event and task notions implemented in hardware.

Most chips have a NVIC (Nested, Vectored Interrupt Controller).

Traditional advice about ISR’s:

Interrupt service routines (ISR) should be kept short.   The references above dispute that dictum.

Traditional, typical software architecture

Your ISR’s are short and set flags (events).  Your main loop is an event loop:

main() 
  while(true)
    if (eventFlagA)
        taskA()
    if (eventFlagB)
        taskB()
    wait for event (WEV or WFI instructions on ARM)

Tasks are just functions.  They run in the main context (not in an ISR) where interrupts are enabled but no interrupt is being serviced.  Tasks are interruptible by any interrupt.  Interrupts come from concurrent device operations that your software Tasks start.  (And some chips call device operations “tasks”, that eventually generate “events” that trigger interrupts.)

Another example might be:

main()
   while true
      taskA()
      sleep(duration)
      taskB()
      sleep(duration)

sleep(duration)
   start Alarm
   while()
      wait for event
      if event is Alarm event
          return
      else
          handle other event, set flags, etc.

Again, your code dispatches ( an if sequence) on events.  You might need several sleep functions, each anticipating a different set of possible events.

Run to completion task architecture

main()
   initialize things
   schedule(taskA)
   while (true)
       wait for Event

taskA()
    do something useful
    schedule(taskB)

taskB()
   do something useful
   start a task on a device  (to generate event for taskC)

taskC()
   service device event
   schedule(taskA)
   
etc....

Here each task runs to completion.  That means it doesn’t sleep.  It does not mean that it can’t be interrupted by a higher priority task.  Each task is-a  ISR for an event/interrupt.

Here, it is important that each task runs to completion and insures that some other task is scheduled or that some device task is running and will generate an event/interrupt that will in turn schedule another task.  I.E. you must insure that you don’t sleep forever.

As discussed above for SST, the NVIC (Nested, Vectored Interrupt Controller) is the dispatcher.  Your code does not dispatch, the HW does (or an “OS” scheduler.)

You can schedule many tasks to run at the same priority at the same time in the future.  When they run, they execute pseudo-concurrently.  One will run, to completion, when the interrupt triggers, from its scheduled alarm.  The interrupt from the other task’s scheduled alarm will trigger at the same time, but since it is at the same priority, will be pended and be serviced only when the first task completes.

Difference between this and SST

SST described above has a broader notion of Task:

  • implemented in an “OS” i.e. by a Scheduler
  • has-a priority
  • that other Tasks can schedule, i.e. put in a list of ready tasks
  • that does not run on events, only when the Scheduler jumps to it.

But the “OS” scheduler can often be simpler: just set an Alarm on a HW timer, arranging that the task runs in response the the alarm, as the ISR of the HW timer.  You can use such a simpler scheduler when you don’t need the notion of a set of prioritized, software tasks.  The HW interrupt priorities can still insure that time critical code is run (to service devices) but your lowest priority tasks (the logic of your main application) don’t need to be structured as a set, each having different priority.

When you want to schedule for immediate, next execution, you can just pend the Alarm interrupt, or use a SWI?

Wrapping your mind around it

In the traditional architecture, the code structure shows the most likely (anticipated) sequence of events, and code executed in response (e.g. taskA, taskB.)  It might be easier for readers to grasp.

In the RTC Task architecture, it is harder for readers to see the sequence.  But it is easier to interpret the code as the “actions” for state machine transitions.  You can label each task to indicate its event, e.g. “onEventA.”

When to use this architecture

As discussed above for SST, when your application is a state machine.

 

 

 

 

Advertisements