Close

Interrupts, Volatiles, and Atomics... and maybe threads?

A project log for PotentiallyUseful/Frustrating/Obscure C/GCC/Make

Notes about unexpected findings in C/GCC... (and Make, and plausibly Bash, etc.)

eric-hertzEric Hertz 11/13/2016 at 16:540 Comments

When using global-variables shared between your main() function and an interrupt (or two threads?), there are a lot of potential disasters one might not expect...

----------

I'm no expert, here... but I've learnt a few things along the way, which could be handy for others. There's probably an entire course in most Computer-Science degrees on the matter, but maybe this is a decent starting-point for those who haven't run into it...

----------

First: Use "volatile" correctly...

Here's a pretty great writeup on the matter, so I won't go into it: http://www.barrgroup.com/Embedded-Systems/How-To/C-Volatile-Keyword

Read that, because some of the following relies on your having-done...

---------

Now for the fun stuff... That page doesn't describe several other common pitfalls...

E.G. say you have:

while( volatile_int != 0 ) 
{ 
   do something with volatile_int 
}

Will it reread volatile_int *each* time it's referenced within that while-loop, or will that entire iteration of the while loop use that value of volatile_int? Or worse, maybe:

if( volatile_int > 0 )
{ 
  do something with volatile_int 
} 
else if ( volatile_int < 0 )
{
  do something else with volatile_int
}
else // volatile_int == 0
{
  do something else, again, with volatile_int
}

In cases like these, it's too confusing (for me) to rely on whatever the standard might claim in some vague reference-manual somewhere hard to find... Nevermind what would happen when an interrupt occurs between testing the first "if" and the next "else if"...

So best to use a separate *local* variable... e.g.

int temp_int;
while( (temp_int = volatile_int) !=0 )
{ 
  do something with temp_int 
}

or

int temp_int = volatile_int;
if( temp_int > 0 )
{ 
  do something with temp_int 
} 
else if ( temp_int < 0 )
{
  do something else with temp_int
}
else // temp_int == 0
{
  do something else, again, with temp_int
}

And, when needing to write back to volatile_int...?

Best to write to a temp variable and *only once* write that temp-variable back to the global.

Even still, there's some difficulty. If your interrupt *and* main both write to the same variable you can get into some trouble, depending on the implementation... This is where mutual-exclusion comes into play, and I thankfully haven't had to mess with that much.

---------

(For most of my needs, an interrupt writes a variable, main reads it back, and *if* main needs to write it, it's usually just as an indicator that the value has been processed... more on that in a second).

---------

And, finally, it may not be *as* common on a 32-bit device, but definitely a common problem with 8-bitters... Consider a global volatile int64_t being written in an interrupt and used in main():

temp_int64 = volatile_int64;

On a 32-bit system, that will most-likely require *two* instructions, each writing 32-bits of temp_int, one 32-bit word at a time. If the interrupt occurs *between* those writes, temp_int64 will contain a mess. Here's a 16-bit example on an 8-bit processor:

volatile_int16 = 0x00ff;

main: 
 temp_int16 = volatile_int16; 
 // -> temp_int16[low byte] = 0xff; 
 // <INTERRUPT>

interrupt: 
 volatile_int16 = 0xff00; 
 // <return to main>

main: 
 temp_int16 = volatile_int16; //(continued) 
 // -> temp_int16[high byte] = 0xff;

//now, in main, temp_int16 == 0xffff

Bad news!

This one's a real problem, because most people don't see it just looking at the code. *And* it happens *very* rarely that the interrupt will occur at *exactly* that moment.

But if you're not careful the consequences could be horrendous

(sudden-acceleration of cars? pace-makers firing when not needed? etc. etc. etc.)

So, then, it's somewhat common to change it in main:

<disable interrupts>; 
temp_int16 = volatile_int16; 
<reenable interrupts>

This makes the two-instruction assignment "atomic", it cannot be interrupted.

Of course, this assumes you already had interrupts enabled and want them reenabled after your assignment to temp_int16... but what if you don't *know* whether interrupts are enabled but want to make sure your assignment to temp_int16 remains atomic? It gets a bit more complicated... so don't just go throwing around cli() (clear interrupts, on AVRs) and sei() (set interrupts) willy-nilly.

There are probably more-common and better ways of handling this...

E.G. on AVRs there's "ATOMIC_BLOCK()"

Check out http://www.nongnu.org/avr-libc/user-manual/group__util__atomic.html for some more examples.

----------

Personally, ATOMIC_BLOCK is a bit too complex for my normal needs, ("WTF is this argument?!") so I've written "CLI_SAFE" and "SEI_RESTORE" for most architectures I've worked with... here's the AVR example:

#define CLI_SAFE(uint8_name)        (uint8_name) = (SREG); cli()
#define SEI_RESTORE(uint8_name)     (SREG) = (uint8_name)
Now I'd rewrite the temp_int16 assignment (in main) as:
uint8_t oldInterruptState;

CLI_SAFE(oldInterruptState);
  temp_int16 = volatile_int16;
SEI_RESTORE(oldInterruptState);

One handy side-effect of how it's written... you don't need the explicit declaration of oldInterruptState:

uint8_t CLI_SAFE(oldInterruptState);
  temp_int16 = volatile_int16;
SEI_RESTORE(oldInterruptState);

----------

So, earlier I said that for most of my needs, an interrupt writes a variable, main reads it back, and *if* main needs to write it, it's usually just as an indicator that the value has been processed, so it won't be processed again.

E.G.

volatile int16_t receivedByte = -1;

void interrupt_handler(void)
{
  //Unfortunately, this'll overwrite the last value
  // if not processed quickly-enough by main()!
  receivedByte = <read UART Rx-Register>;
}

int main(void)
{
 while(1)
 {
  uint8_t CLI_SAFE(oldState);
   int16_t tempRx = receivedByte;
   receivedByte = -1;
  SEI_RESTORE(oldState)
  
  if( tempRx >= 0 )
  {
      //display the hex-value of the byte received:
      printf( "Received 0x%2x", tempRx);
  }
 }
}
Note how the read *and* modification of receivedByte occur within the same "atomic block" in main.

Anything more complicated than that is too much for this write-up... Just be aware of these pitfalls, and don't assume that I actually know what I'm talking about, and *don't* use my code/explanations for designing self-driving cars or anything else that could harm a person, fergodsakes.

Discussions