rem
rem

Reputation: 1291

volatile and variable modification in C

A structure TsMyStruct is given as parameter to some functions :

typedef struct
{
    uint16_t inc1;
    uint16_t inc2;
}TsMyStruct;


void func1(TsMyStruct* myStruct)
{
    myStruct->inc1 += 1;
}

void func2(TsMyStruct* myStruct)
{
    myStruct->inc1 += 2;
    myStruct->inc2 += 3;
}

func1 is called under non-interrupt context and func2 is called under interrupt context. Call stack of func2 has an interrupt vector as origin. C compiler does not know func2 can be called (but code isn't considered as "unused" code as linker needs it in interrupt vector table memory section), so some code reading myStruct->inc2 outside func2 can be possibly optimized preventing myStruct->inc2 to be reloaded from ram. It is true for C basic types, but is it true for inc2 structure member or some array...? Is it true for function parameters?

As a general rule, can I say "every memory zone (of basic type? or not?) modified in interrupt context and read elsewhere must be declared as volatile"?

Upvotes: 1

Views: 111

Answers (2)

zneak
zneak

Reputation: 138031

Yes, any memory that is used both inside and outside of an interrupt handler should be volatile, including structs and arrays, and pointers passed as function parameters. Assuming that you are targeting a single-core device, you do not need additional synchronization.

Still, you have to consider that func1 could be interrupted anywhere, which may lead to inconsistent results if you're not careful. For instance, consider this:

void func1(volatile TsMyStruct* myStruct)
{
    myStruct->inc1 += 1;
    if (myStruct->inc1 == 4)
    {
        print(myStruct->inc1); // assume "print" exists
    }
}

void func2(volatile TsMyStruct* myStruct)
{
    myStruct->inc1 += 2;
    myStruct->inc2 += 3;
}

Since interrupts are asynchronous, this could print numbers different from 4. That would happen, for instance, if func1 is interrupted after the check but before the print call.

Upvotes: 2

ensc
ensc

Reputation: 6984

no. volatile is not enough. You have both to set an optimization barrier for the compiler (which can be volatile) and for the processor. E.g. when a CPU core writes data, this can go into some cache and won't be visible for another core.

Usually, you need some locking in your code (spin locks, or mutex). Such functions contain usually an optimization barrier so you do not need an volatile.

Your code is racy, with proper locking it would look like

void func1(TsMyStruct* myStruct)
{
    lock();
    myStruct->inc1 += 1;
    unlock();
}

void func2(TsMyStruct* myStruct)
{
    lock();
    myStruct->inc1 += 2;
    unlock();
    myStruct->inc1 += 3;
}

and the lock() + unlock() functions contain optimization barriers (e.g. __asm__ __volatile__("" ::: "memory") or just a call to a global function) which will cause the compiler to reload myStruct.

For nitpicking: lock() and unlock() are expected to do the right thing (e.g. disable irqs). Real world implementations would be e.g. spin_lock_irqsave() + spin_lock_irqrestore() in linux.

Upvotes: 0

Related Questions