3
\$\begingroup\$

I'm having some confusion about what members to declare volatile in a program I'm writing in C++ for an AVR microcontroller, with interrupts. When it's plain C it makes sense - the compiler doesn't know that interrupt ISR's can modify variables at any time, so those variables need to be declared volatile. But I have an architecture that's somewhat like the following (distilled example):

class FifoBuffer {
    public: 
    int Size;
    int BeginIndex;
    void Push() {
        Size++;
        // ...
    } 
}

class RadioInterface {
    public: 
    FifoBuffer RxBuffer;
    void HandleInterrupts() {
        RxBuffer.Push( 123 );
        // ...
    }
    int GetPacket() {
        RxBuffer.Pop();
        // ...
    }
};

RadioInterface g_Radio;

ISR( INT0_vect ) {
    g_Radio.HandleInterrupts();
};

void main() {
    while ( true ) {
        g_Radio.GetPacket();
    }
}

My basic question - what needs to be volatile here?

When the ISR calls RadioInterface::HandleInterrupts(), are we back into a safe non-volatile context that the compiler can figure out and optimize? Or do I need to recurse down the call tree and figure out everything that could potentially get touched from main() and from ISR(...) and make it volatile?

\$\endgroup\$
4
\$\begingroup\$

It's essentially the same problem as writing a multithreaded program, except that you can't use mutexes to protect your critical sections. Everything that can be touched from more than one context should be marked as volatile. But that may not be enough - any read-modify-write pattern needs to be scrutinised. It is at least asymmetrical; the interrupt handler can't be interrupted by the main execution, only the other way round.

There are two usual techniques for ensuring reliable results:

A) Disable interrupts while working on the shared variables, then re-enable them. Will increase interrupt worst-case latency.

B) Avoid having variables written by both the interrupt handler and the main "thread". Each variable is written by one or the other. For example, use a circular buffer as a FIFO: there is a read pointer, which is only altered by main, and a write pointer, which is only altered by the interrupt handler.

Researching "lockless" data structures may also be informative.

| improve this answer | |
\$\endgroup\$
  • 3
    \$\begingroup\$ Also keep in mind that on the AVR 8bit architecture, each access to a variable that is more than 8bit long (I have seen int in the program) must be considered a critical section. So do make those read and write pointers uint8_t, not int. \$\endgroup\$ – ndim Apr 25 '13 at 8:40
5
\$\begingroup\$

I would just like to point out that pjc50 actually answered a different question, relating to synchronization and atomic operations, which is completely orthogonal to the issue of whether variables should be marked volatile for the compiler.

volatile is no different in C++ than it is in C — the compiler does not have global knowledge about how variables are used, and cannot in general figure out on its own which ones might be shared among multiple threads. Keep in mind that the modules used in these threads might be separately compiled.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Thanks - that answer was more atomic-operation focused, but pjc50 did mention some criteria for what to declare volatile, so it was a helpful answer from my standpoint. Was able to work out the concurrency kinks in my program based on that. \$\endgroup\$ – QuadrupleA Apr 25 '13 at 18:42

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.