c++volatilec++20bitflags

set a flag in int variable from different context on bare metal controller


For avr 8 bit micro controller a single bit ( flag ) must be set or cleared in some 8 bit integer variable. This set/clear function can be called from normal context ( main ) and also from interrupt handler ( Isr() ). As this, the variable must be made volatile to prevent it from reordering or keeping it somewhere in registers. std::atomic is not a valid option here, as there is no underlying OS nor a multi cpu core nor caches so some kind of memory fences are not needed. Even std::atomic is not part of any avr c++ library.

The operation to set a flag is something like: some_flags|= new_set_flags.

But with c++20 I get the warning: warning: compound assignment with 'volatile'-qualified left operand is deprecated [-Wvolatile].

It is no problem to rewrite the functions by using a temporary variable, but it feels that this is not the intend of making the volatile keyword deprecated in this situation.

BTW: As the variable is stored in RAM, the cpu is not able to set a bit in the memory in a single assembler instruction. As this, the whole operation must be atomic. For that use case the avr-lib has ATOMIC_BLOCK(ATOMIC_RESTORESTATE) which simply disables interrupts.

#include <util/atomic.h>

volatile uint8_t some_flags;

void SetFlag( uint8_t new_set_flags )
{
    ATOMIC_BLOCK(ATOMIC_RESTORESTATE) 
    {   
        uint8_t tmp = some_flags;
        tmp |= new_set_flags;
        some_flags = tmp;

        ... vs ...

        some_flags|= new_set_flags; // main.cpp:65:19: warning: compound assignment with 'volatile'-qualified left operand is deprecated [-Wvolatile]
    }   
}

void SomeIsr()
{
    SetFlag( 0x02 );
}

int main()
{
    SetFlag( 0x01);
}

Question: What is the "correct" way of setting a flag to a int variable if this flags/variable is used in different context on a bare metal controller with a single core without OS nor MMU.

It becomes very curious if you want to set a bit on a IO register, which is on AVR something like PORTA|=0x01; and the compiler can take that action within a single assembler instruction.

#include <avr/io.h>

int main()
{
    PORTA|=0x01; // main.cpp:57:10: warning: compound assignment with 'volatile'-qualified left operand is deprecated [-Wvolatile]
}

0000006c <main>:
  6c:   d8 9a           sbi 0x1b, 0 ; 27
  6e:   90 e0           ldi r25, 0x00   ; 0
  70:   80 e0           ldi r24, 0x00   ; 0
  72:   08 95           ret

Solution

  • The rationale is that compound assignments or pre-post incrementations or decrementations are not atomic even on a volatile variable, while a programmer could see it as a single operation. Moreover, the standard says that E1 op= E2 is the same as E1 = E1 op E2 except that E1 is evaluated only once.

    That means that non cautious programmers could use

    volatile uint8_t some_flags;
    ...
    some_flags|= new_set_flags;
    

    with the expectation that it will be atomic, even in presence of hardware interruptions while it is not required to be.

    At the machine level it looks like 3 operations:

    load value from memory
    update accumulator register
    store value to memory
    

    That means that without more precautions, a race condition occurs if 2 executions threads (here normal processing and an ISR) are interleaved:

    normal loads
    ! ISR takes the processor
    ISR loads updates and stores
    ! return from ISR
    normal updates and stores erasing the change from ISR
    

    When the program uses a temp variable it is evident that race conditions could occur.

    What is bad for you, it that the C++ commitee has deprecated that use with the intention to later fully remove it.

    So you can:

    I prefere the last way because for a volatile byte, there are no reason that the compiler produces a less efficient code, and it is conformant from early C version to the last C++ one


    References: