Electronic – Understanding volatile class fields in AVR C++ programs

avrcinterruptsmicrocontroller

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?

Best Answer

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.