2

I'm currently writing a time-dependant embedded systems application, specifically on an ARM Cortex M4. It has a series of tasks that happen at different frequencies, and so, we're using the ARMs SysTick timer to keep track of time within a 1 second frame. A main busy loop waits until the timer has incremented and starts a task, as so:

void main(){
 while(1){
  if(!timerTicked)
     continue; //timer hasn't updated since last loop, do nothing. 
  if(time==0)
    toggleStatusLED();
  if(time==0 || time==500){
    twoHzTask(); //some task that must run twice a second
  }
  if(time%300==5){ //run at ms=5, 305 and 605
    threeHzTask();  //some task that must run three times a second
  }
 }
}

void SysTick_increment(){ //Interrupt Service Routine for SysTick timer
 time=(time+1)%1000;
}

This works perfectly for high-priority tasks that can't hang. In addition, high-priority tasks are allowed to take as long as they need to complete, however going over time may cause the system to go into fail-safe, depending on the task. However, I'd like to add a simple mechanism to abort non-mission-critical code that takes too long, like this:

void main(){
 while(1){
  if(time==500){ //run at ms=500
   savingWorld=1;
   saveTheWorld();  //super important task
   savingWorld=0;
  }
  else if(time==750){
   spinningWheels=1;
   spinWheels(); //not very important task. Code for this function is immutable, can not alter it to support task cancellation. 
   spinningWheels=0;
  }
 }
}

void SysTick_increment(){ //Interrupt Service Routine for SysTick timer
 time=(time+1)%1000;
 if(time==900 && spinningWheels){
  spinningWheels_cancel(); //we have more important stuff to do 
 }
}

As this is a basic embedded system I'd prefer not to introduce the complexity of an operating system or a multithreading mechanism. In addition, the rest of the system should be in a stable state after spinningWheels_cancel() is called. That is to say, saveTheWorld() should still work as expected. What's the simplest way to accomplish this?

Chris
  • 1,037
  • 15
  • 26
  • 1
    If you cannot alter spinWheels() then you cannot cancel it once it starts, so you have no choice but to "not execute it" if you are too close to the execution time of the saveTheWorld() task no? So if spinWheels() takes more than 750 cycles in your system it will still be running when its time to run saveTheWorld() and nothing can be done about it. An alternate may be to run saveTheWord() in the ISR, but depends on what it does. – Chris Nov 21 '14 at 23:30
  • Well, your idea kind of works, though time rigidly be 900 is probably non-ideal. >=900 would be better incase when time is exactly 900 and you interrupt inbetween the `else if (time == 750)` and the `spinningWheels = 1;`, you dont get caught not stopping the wheels spinning but still enter the task once you return from the ISR. While you dont need a full RTOS for this, you are going to have to implement parts of a basic one. Mostly, Im thinking the return address of your `SysTick_increment` will have to change if you cancel wheel spin to avoid returning back into that task. – Unn Nov 21 '14 at 23:37
  • I think Chris has a good idea though, using ISRs for your most important tasks as that way, they can interrupt the less important ones. (Though be careful they dont block eachother).... – Unn Nov 21 '14 at 23:38
  • You could try setjmp and longjmp from an alarm handler, [sort of like this](http://stackoverflow.com/questions/1715413/longjmp-from-signal-handler). – JS1 Nov 22 '14 at 10:25
  • @JS1 I once implemented a cooperative tasking kernel using nothing but `setjmp()` and `longjmp()`. It worked, and achieved the project goals, but it was a bear to maintain and debug. – RBerteig Nov 25 '14 at 19:35

2 Answers2

2

You've implemented a collection of cooperative tasks. A key attribute of this mechanism is that multitasking only works if all tasks follow the rules and do not overrun their slots. One way to guarantee that is to break the implementation of a long, slow, low-priority task such as your spinningWheels() into segments that each meet the time constraint.

Then spinningWheels() would use private state variables to remember where it left off and continue spinning for a time. Depending on its nature, that might be a tuned number of iterations of an inner loop, or it might mean sampling time and giving up when it is too big.

In embedded systems I've built, I often end up implementing similar long-running tasks with a finite state machine that is called periodically. That is often a good fit to implementation of communications protocols which often need to be resilient in the face of unexpected delays from the other end of a wire.

Given only cooperative tasks, consider what your proposed function spinningWheels_cancel() would have to be. It is called from the timer tick interrupt, so it is limited to functions known to be reentrant. Since you don't have a formal scheduler and separate tasks, it cannot actually "kill" a task. So its implementation can only set an atomic flag that is tested by all inner loops in spinningWheels() and used to signal an early exit.

Alternatively, this requirement could be the motivation to move to some RTOS kernel. With preemptive scheduling, the slow task would overrun its time slot, but would naturally be lower priority than the more important tasks and as long as there are enough total cycles available to complete its work on average that overrun might not be so important.

There are quite a few RTOS kernels in the wild that are light weight, provide for preemptive scheduling, and supported on ARM. Both commercial (with a wide range of licensing models) and free candidates are out there, but expect the selection process to be non-trivial.

RBerteig
  • 41,948
  • 7
  • 88
  • 128
  • Chris does not want to get involved in threading or other OS scheduling operations but I think he may have to, to really solve this task. The first suggestion of having an atomic escape flag requesting early task exit is out as he says tasks are un-modifible. Preemption would be the proper way of doing it. He may be able to cheat if he creates a thread to run the high-priority tasks, making the thread higher priority, not sure he wants to do that. The other – Chris Nov 21 '14 at 23:59
  • If tasks are truly unmodifiable then there is no hope without some sort of task management. Assuming a single-core, an atomic flag stored in a global variable is a very common way to implement a request to cancel. But sampling the existing atomic global `time` just like the main loop does would be equivalent. I hope they arranged for `time` to be sufficiently atomic... – RBerteig Nov 22 '14 at 00:04
2

I think an RTOS scheduler would introduce simplicity rather than introduce complexity (one you have it running on your target that is). Independent concurrently schedulable tasks can enhance cohesion and reduce coupling - both desirable aims in any system.

However an alternative architecture is to change all your "tasks" from run-to-task-completion to state-machines that can be called repeatedly and handle their own timing and perform only a part of a task in each call, maintaining "state" to determine what should be done and avoiding "busy waits".

If you run your system tick much faster, and rather than calling your "tasks" at the required time, you simply call them as fast as possible in the main loop, and make them individually responsible for their triggering time or state switching, that gives you far greater flexibility and allows your processor to be doing something useful as "background tasks" rather than just waiting for a one-second tick. That is a far more efficient use of the processor, gives greater responsiveness, and allows work to be done asynchronously to some arbitrary time tick.

For example, based on your outline, but perhaps somewhat contrived for the purposes of illustration and due to lack of information:

#include <stdio.h>

#define TICKS_PER_SECOND = 100u ;  // for example
volatile uint32_t tick ;

void SysTick_increment()
{ 
     tick++ ;
}

void main()
{
    for(;;)
    {
        LedUpdate() ;
        TwoHzTask() ;
        ThreeHzTask() ;
        SpinningWheels() ;
    }
 }

 void LedUpdate()
 {
     static uint32_t last_event_timestamp = tick ;
     uint32_t now = tick ;

     if( now - last_event_timestamp >= TICKS_PER_SECOND )
     {
        last_event_timestamp = now ;
        toggleStatusLED() ;
     }
 }

void TwoHzTask()
{
     static uint32_t last_event_timestamp = tick ;
     uint32_t now = tick ;

     if( now - last_event_timestamp >= 2 * TICKS_PER_SECOND )
     {
        last_event_timestamp = now ;
        // Do 2Hz task...
     }
}

void ThreeHzTask()
{
     static uint32_t last_event_timestamp = tick ;
     uint32_t now = tick ;

     if( now - last_event_timestamp >= 3 * TICKS_PER_SECOND )
     {
        last_event_timestamp = now ;
        // Do 3Hz task...
     }
}

void SpinningWheels()
{
    static enum
    {
        IDLE, SPINNING;
    } state = IDLE ;

    switch( state )
    {
        case IDLE ;
        {
            if( startSpinning() )
            {
                state = SPINNING ;
            }
        } 
        break ;

        case SPINNING ;
        {
            bool keep_spinning = doOneSpin() ;
            if( !keep_spinning )
            {
                state = IDLE ;
            }
        } 
        break ;
    }
}

The important thing to note is that none of the four "task" functions is dependent on the others to determine when or whether they run except for the fact that a "well behaved" task will not block in order to wait for any event - it simply sets its state to indicate that it is waiting for that event, and switched state and/or performs some action when that event occurs. A "task" must not busy-wait or perform any lengthy processing that will not meat the real-time requirements of other tasks. So for example if you might have a length or non-deterministic loop, you would re-factor that to be in an "iteration state", where one iteration is performed per call (like the doOneSpin() function in the example above).

Note that in your original systick, the expression time=(time+1)%1000 is a really bad idea and should be avoided. It makes handling the wrap-around of time problematic if you force it to wrap every 1000 ticks. Ideally it should wrap a some power of 2, and since all integer types have a power of two range in any case, you can simply increment it as I have done. If you do that the expressions such as now - last_event_timestamp give the correct time difference even when the counter has wrapped around (so long as it has not wrapped around twice in that time), for example 1 - 999 will give the answer -998 (or 4294966298 if unsigned), whereas 1 - 0xffffffff = 2.

Another reason to avoid the expression in the ISR is that the % operator has an implicit divide operation which is relatively expensive and takes a variable number of clock cycles. We are talking tiny amounts of time of course, but in some hard real-time applications the jitter caused may be undesirable.

Clifford
  • 88,407
  • 13
  • 85
  • 165