Home > Enterprise >  Reading a 64 bit variable that is updated by an ISR
Reading a 64 bit variable that is updated by an ISR

Time:03-26

I am not finding much material on non-atomic operations.

Suppose I have a 32 bit processor and I want to keep count of microseconds in a 64 bit variable. An interrupt will update the variable every microsecond. The scheduler is non-preemptive. There will be a function to clear the variable and another to read it. Since it is a 32 bit processor then access will be non-atomic. Is there a “standard” or idiomatic way of handling this so that the reader function will not get a half-updated value?

CodePudding user response:

Within the ISR, a subsequent interrupt is usually prevented (unless a higher priority, but then the count is usually not touched there) so simply count_of_microseconds ;

Outside the ISR, to access (read or write) the count_of_microseconds you need interrupt protection or atomic access.

When atomic not available but interpret control is available:

uint64_t count_of_microseconds;
...
saved_interrupt_state();
disable_interrupts();
uint64_t my_count64 = count_of_microseconds;
restore_interrupt_state();
// now use my_count64 

else use

atomic_ullong count_of_microseconds;
...
unsigned long long my_count64 = count_of_microseconds;
// now use my_count64 

See How to use atomic variables in C?

CodePudding user response:

I am glad to hear the that the read twice method is workable. I had doubts, don't know why. In the meantime I came up with this:

struct
{
    uint64_t ticks;
    bool toggle;
} timeKeeper = {0};

void timeISR()
{
    ticks  ;
    toggle = !toggle;
}

uint64_t getTicks()
{
    uint64_t temp = 0;
    bool startToggle = false;
    
    do
    {
        startToggle = timeKeeper.toggle;
        temp = timekeeper.ticks;
    } while (startToggle != timeKeeper.toggle);
        
    return temp;
}

CodePudding user response:

Is there a “standard” or idiomatic way of handling this so that the reader function will not get a half-updated value?

What you need to do is use what I call "atomic access guards", or "interrupt guards". This is an area of interest of mine that I have spent a ton of time learning about and using in microcontrollers of various types.

@chux - Reinstate Monica, is correct, but here's some additional clarity I want to make:

For reading from volatile variables, make copies in order to read quickly:

Minimize time with the interrupts off by quickly copying out the variable, then using the copy in your calculation:

// ==========
// Do this:
// ==========

// global volatile variables for use in ISRs
volatile uint64_t u1;
volatile uint64_t u2;
volatile uint64_t u3;

int main()
{
    // main loop
    while (true)
    {
        uint64_t u1_copy;
        uint64_t u2_copy;
        uint64_t u3_copy;

        // use atomic access guards to copy out the volatile variables
        // 1. Save the current interrupt state
        const uint32_t INTERRUPT_STATE_BAK = INTERRUPT_STATE_REGISTER;
        // 2. Turn interrupts off
        interrupts_off();
        // copy your volatile variables out
        u1_copy = u1;
        u2_copy = u2;
        u3_copy = u3;
        // 3. Restore the interrupt state to what it was before disabling it.
        // This leaves interrupts disabled if they were previously disabled
        // (ex: inside an ISR where interrupts get disabled by default as it
        // enters--not all ISRs are this way, but many are, depending on your
        // device), and it re-enables interrupts if they were previously
        // enabled. Restoring interrupt state rather than enabling interrupts
        // is the right way to do it, and it enables this atomic access guard
        // style to be used both inside inside **and** outside ISRs.
        INTERRUPT_STATE_REGISTER = INTERRUPT_STATE_BAK;

        // Now use your copied variables in any calculations
    }
}

// ==========
// NOT this!
// ==========

volatile uint64_t u1;
volatile uint64_t u2;
volatile uint64_t u3;

int main()
{
    // main loop
    while (true)
    {
        // 1. Save the current interrupt state
        const uint32_t INTERRUPT_STATE_BAK = INTERRUPT_STATE_REGISTER;
        // 2. Turn interrupts off
        interrupts_off();

        // Now use your volatile variables in any long calculations
        // - This is not as good as using copies! This would leave interrupts
        //   off for an unnecessarily long time, introducing a ton of jitter
        //   into your measurements and code.

        // 3. Restore the interrupt state to what it was before disabling it.
        INTERRUPT_STATE_REGISTER = INTERRUPT_STATE_BAK;

    }
}

For writing to volatile variables, write quickly:

Minimize time with the interrupts off by quickly only disabling them while updating the volatile variables:

// global volatile variables for use in ISRs
volatile uint64_t u1;
volatile uint64_t u2;
volatile uint64_t u3;

int main()
{
    // main loop
    while (true)
    {
        // Do calculations here, **outside** the atomic access interrupt guards

        const uint32_t INTERRUPT_STATE_BAK = INTERRUPT_STATE_REGISTER;
        interrupts_off();
        // quickly update your variables and exit the guards
        u1 = 1234;
        u2 = 2345;
        u3 = 3456;
        INTERRUPT_STATE_REGISTER = INTERRUPT_STATE_BAK;
    }
}

Alternative: repeat read loop: doAtomicRead(): ensure atomic reads withOUT turning interrupts off!

An alternative to using atomic access guards, as shown above, is to read the variable repeatedly until it doesn't change, indicating that the variable was not updated mid-read after you read only some bytes of it.

Here is that approach. @Brendan and @chux-ReinstateMonica and I discussed some ideas of it under @chux-ReinstateMonica's answer.

#include <stdint.h>  // UINT64_MAX

#define MAX_NUM_ATOMIC_READ_ATTEMPTS 3

// errors
#define ATOMIC_READ_FAILED (UINT64_MAX)

/// @brief          Use a repeat-read loop to do atomic-access reads of a 
///     volatile variable, rather than using atomic access guards which
///     disable interrupts.
///
/// @param[in]      val             Ptr to a volatile variable which is updated
///                                 by an ISR and needs to be read atomically.
/// @return         A copy of an atomic read of the passed-in variable, 
///     if successful, or sentinel value ATOMIC_READ_FAILED if the max number
///     of attempts to do the atomic read was exceeded.
uint64_t doAtomicRead(const volatile uint64_t* val)
{
    uint64_t val_copy;
    uint64_t val_copy_atomic = ATOMIC_READ_FAILED;
    
    for (size_t i = 0; i < MAX_NUM_ATOMIC_READ_ATTEMPTS; i  )
    {
        val_copy = *val; 
        // an interrupt could have fired mid-read while doing the **non-atomic**
        // read above, resulting in 32-bits of the 64-bit variable being wrong
        // now, so verify the read above with a new read again
        if (val_copy == *val)
        {
            val_copy_atomic = val_copy;
            break;
        }
    }

    return val_copy_atomic;
}

Going further on this topic of atomic access guards, disabling interrupts, etc.

  1. [my answer] C decrementing an element of a single-byte (volatile) array is not atomic! WHY? (Also: how do I force atomicity in Atmel AVR mcus/Arduino)
  2. My long and detailed answer on Which Arduinos support ATOMIC_BLOCK? and:
    1. How are the ATOMIC_BLOCK macros implemented in C with the gcc compiler, and where can I see their source code?, and
    2. How could you implement the ATOMIC_BLOCK functionality in Arduino in C (as opposed to avrlibc's gcc C version)?
    3. I explain in detail how this really clever atomic access guard macro works in C via gcc extensions, and how it could easily be implemented in C :
      ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
      {
          my_var_copy = my_var;
      }
      
  3. [my Q&A] Which variable types/sizes are atomic on STM32 microcontrollers?
    1. Not all variables need atomic access guards for simple reads and writes (for increment/decrement they ALWAYS do!--see my first link in this list above!), as some variables have naturally atomic reads and writes for a given architecture.
      1. For 8-bit AVR microcontrollers (like ATmega328 on Arduino Uno): 8-bit variables have naturally atomic reads and writes.
      2. For 32-bit STM32 microcontrollers, all non-struct (simple) types 32-bits and smaller have naturally atomic reads and writes. See my answer above for details and source documentation and proof.
  4. Techniques to disable interrupts on STM32 mcus: https://stm32f4-discovery.net/2015/06/how-to-properly-enabledisable-interrupts-in-arm-cortex-m/
  5. [my answer] global volatile variable not being updated in ISR: How to recognize and fix race conditions in Arduino by using atomic access guards:
  6. [my answer] What are the various ways to disable and re-enable interrupts in STM32 microcontrollers in order to implement atomic access guards?
  • Related