The scheduler is used to off-load intensive processing from the interrupt routines.

Consider an example where each of the IMU components (accelerometer(A), compass(C), gyro(G)) generates an interrupt, perhaps at 100 Hz.  It’s not hard to imagine cases where all three interrupts may be triggered by the same physical activity.   Each interrupt may demand some substantial signal processing that can itself take some time.

First come, first served

The simplest programs will process each interrupt to completion.  While, for example, interrupt A is being processed, interrupts from C and G are disabled, and are only re-enabled when A is finished. If handling A takes a long time to process, say longer than 10ms for 100Hz sampling, then C and G may have had to wait so long, that the hardware has already experienced a new interrupt condition, and has updated it’s internal registers with new data before the processor has had a chance to read the first lot.  This is known as exceeding the “crisis time” of the device.

void Accel_IRQ_Handler(void) {
  xyzAccelData = getAccelDataFromIMU(); 
  doAccelSignalProcessing(xyzAccelData); 

Prioritized interrupt handling

A more sophisticated approach is to use interrupt priorities to make sure that the interrupt with the shortest crisis time will preempt interrupts with less critical timing requirements.  There are not usually very many priority levels, and a bunch of them are usually assigned to processor-related interrupts, so there may only be two, or three priority levels anyway.   But let’s assume that we have enough priority levels available.

When an interrupt occurs, the processor saves the current values of it’s various registers onto the program stack, and transfers control to the appropriate interrupt request handler (IRQ).  This then uses the stack to do some processing and returns.  The processor pops stuff off the stack back into the hardware registers, and continues where it left off, unless…  a higher priority interrupt occurs before the original IRQ has finished.  So more registers get pushed onto the stack.  And if another even higher priority interrupt occurs, then even more get pushed onto the stack.  This means that we have to make the stack size, at least “three interrupts worth of register space” bigger than the maximum it can ever reach during normal background processing.  Otherwise we get stack overflow, which provoke squirrelly behavior that is often very hard to diagnose.  And remember that this whole approach only works if we have enough different levels of priority, and IRQs can complete their tasks within the slowest interrupt crisis time.

Off-loading interrupt processing

To overcome these issues a real-time system generally attempts to minimize the time spent inside an IRQ, thus allowing all other IRQs to run before their crisis time expires.  The way they do that is to capture, and save the interrupt data, and not to think about it too much.  They off-load the processing task to a lower-priority process – normally the main loop.  When all the IRQs have serviced their interrupts, saved their data, and got their pants pulled up, the main loop resumes.  The main loop looks to see if any interrupt routines has indicated that there is some more work to be done – some heavy signal processing, for example.  If there are several tasks, it figures out the order to do them in, and gets on with the job.

It’s worth noting here that there are situations where further signal processing does not make much sense until multiple devices have captured data.  For example, and IMU may prefer to wait until it has data from A, C and G interrupts before it attempts to update it’s new view of the world.  Updating it’s view using data from just one device may actually we worse than waiting for three samples that share the same approximate time stamp.

A simple way to do this is for each IRQ to set some kind of flag that the main loop can read.  For example, the IRQ for A may look like this:

void Accel_IRQ_Handler(void) {
  xyzAccelData = getAccelDataFromIMU(); 
  accelWorkToDoFlag = true; 

The main loop look like this:

main() {
  sleep(); // processor sleeps till next IRQ has been handled
  if (accelWorkToDoFlag) {
    accelWorkToDoFlag = false;
    doAccelSignalProcessing(
xyzAccelData);
  }
}

Scheduler 

The scheduler provides a formalized, more disciplined and flexible way to deal with this situation. The programmer defines an IRQ post processor to do the hard signal processing, for example.

postProcessor(void *data) {
  
doAccelSignalProcessing(data);
}

Essentially the scheduler manages a queue of post-processing tasks submitted by the various IRQs. The scheduler runs in the main loop.  It takes each each task description, starting at the head of the queue, and calls the designated post-processor to deal with it.

void Accel_IRQ_Handler(void) {
  xyzAccelData = getAccelDataFromIMU(); 
  scheduler_enQueue(&xyzAccelData, postProcessor); 

main() {
  sleep(); // processor sleeps till next IRQ has been handled
  schedule();
}

Leave a Reply