Tuesday, November 1, 2011

Demonstration: Atomic Access and Interrupt Routines

This is a post I made a while back on the Arduino Forums.  It has since been buried, so I figured I'd repost it here so I have easier access to it in the future.

Had an idea this morning for what I thought would be an interesting little demonstration of why Atomic access is necessary when working with variables that are used in Interrupt routines.

Demonstration code:
#include "TimerOne.h"
#include <util/atomic.h>

volatile unsigned int test = 0xFF00; 
void setup()
{
  
  Serial.begin(115200);
  Timer1.initialize(10000); //Call interrupt every 10ms
  Timer1.attachInterrupt(myInterrupt);
  Timer1.start();

}
 
void myInterrupt()
{
  //Interrupt just toggles the value of test.  Either 0x00FF or 0xFF00
  static bool alt = true;
  if(alt) test = 0x00FF;
  else    test = 0xFF00;
  alt = !alt;
}
 
void loop()
{
  unsigned int local;
  //ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
  {
     local = test;
  }
  
  //Test value of local.  Should only ever be 0x00FF or 0xFF00
  if(!(local == 0xFF00 || local == 0x00FF))
  {
    //local value incorrect due to nonatomic access of test
    Serial.println(local, HEX);
  }
}

If you download and run this code as is, unmodified, you will see intermittent outputs on the Serial port of 'FFFF' and '0'.

If the variable test is only ever assigned the values of 0x00FF or 0xFF00, then why is the value of our local variable occasionally 0x0000 or 0xFFFF?

The reason is because the assignment of the 16 bit value stored in our variable test to our variable local is not what we call atomic.  What do I mean by atomic?  Essentially it means that the assignment takes multiple cycles for the microcontroller is performed.  This is because the microcontroller is an 8-bit processor, so it only handles data in 8 bit chunks. When we are working with 16 bit data, the processor still only handles that data 8 bits at a time, so will always take multiple cycles to perform even the simplest operations on that data.

Because it takes multiple cycles even to copy data from one variable to another, there is the possibility that the interrupt will occur in the middle of that assignment.  When this happens, the processor stops what it is currently doing, switches to processing the ISR, and then returns back to where it left off.  The processor won't care if the ISR changed the value of it's variable while it was in the middle of assigning that value to another variable, so we get these situations where it assignments half of one value to our local variable, gets interrupted by the ISR, and then returns to copy the other half of the second value to our local variable.  And now our local variable contains invalid data.

The solution to this problem is to force the compiler to perform the operation without allowing any interruptions to occur.  There are several methods of doing this, but the basic requirement is that interrupts be disabled prior to performing the operation that requires atomicity, thus ensuring that the entire operation is completed without interruption.

If you uncomment the one line in my demonstration code: ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
the code will then run indefinitely without any output to the Serial port.  ATOMIC_BLOCK() is a macro provided by the AVR LibC library that provides the ability to designate a block of code to be performed atomically (ie, without interruption).

It is similar to using sei() and cli(), or Arduino's wrappers interrupts() nointerrupts() with a couple potentially key differences.  With the parameter ATOMIC_RESTORESTATE passed in, it doesn't simply re-enable interrupts upon exiting the atomic block.  It restores it to it's state previous to entering the atomic block.  In this simple demonstration code, the difference may not be apparent.

In more complicated code, with multiple paths of execution and potentially convoluted execution of various functions, the state of interrupts won't necessarily be known upon executing an ATOMIC_BLOCK(), so it may be necessary to restore the previous state as opposed to arbitrarily re-enabling interrupts.  Perhaps they were already disabled prior to entering the atomic block.

The other benefit is that you are forced to define a block of code to make Atomic, by way of coding braces ({ and }).  What this means is that it's not possible to forget to restore your interrupt state.  Again, in more complicated code, if you use say nointerrupts(), it's possible you could forget to call interrupts() to reenable them, and then have to deal with debugging why your interrupts aren't working. If it is code that is called intermittently, or under certain/rare conditions, it can be very hard to debug.

With ATOMIC_BLOCK() there's still the possibility that you've got too much code in your block, or not all the code that needs to be atomic, but it will always either re-enable interrupts, or restore them to the state they were in prior to the atomic block.  If you forget to close the block, you get a compile error.

No comments:

Post a Comment