1

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?

Gabriel Staples
  • 22,024
  • 5
  • 133
  • 166
MaryK
  • 33
  • 5
  • has long long https://en.cppreference.com/w/c/atomic – pm100 Mar 25 '22 at 23:33
  • 3
    I used to do similar (with 8-bit processor) by reading the high part, then the low part, then comparing the high part. If it was different, repeat. – Weather Vane Mar 25 '22 at 23:34
  • Take a page out of the book of FPGA development on how to cross clock domain boundaries: Use gray code to cross between ISR and reader code. This works, because when incrementing gray code counters, only one bit will change at a time. – datenwolf Mar 25 '22 at 23:38
  • Should have stated C99 is being used. – MaryK Mar 26 '22 at 03:38
  • 1
    Is this on a microcontroller? With RTOS, or bare metal? – Gabriel Staples Mar 26 '22 at 05:11
  • @pm100 but whether it's non-lock or not depends on the architecture and you have to check `ATOMIC_LLONG_LOCK_FREE` to know that. If it isn't lock-free then it'll be quite expensive – phuclv Mar 26 '22 at 06:12

3 Answers3

2

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*1 but interpret control is available:

volatile 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?

Since C89, use volatile with count_of_microseconds.


[Update]

Regardless of the approach used (this answer or others) in the non-ISR code to read/write the counter, I recommend to enclose the read/write code in a helper function to isolate this critical set of operations.


*1 <stdatomic.h> available since C11 and __STDC_NO_ATOMICS__ not deifned.

#if __STDC_VERSION__ >= 201112
#ifndef __STDC_NO_ATOMICS__
#include <stdatomic.h>
#endif
#endif
chux - Reinstate Monica
  • 127,356
  • 13
  • 118
  • 231
  • 1
    There's a third alternative - a loop that reads the high half, reads the low half, then reads the high half again; until the high half is the same twice in a row - e.g. like maybe `new_high = counter[1]; do { old_high = new_high; low = counter[0]; new_high = counter[1]; } while (old_high != new_high);`. – Brendan Mar 26 '22 at 05:18
  • @Brendan, although that would work in many cases, I don't think that's fool-proof. Imagine an edge-case scenario where the interrupt was constantly firing back-to-back, updating the variable each time you read 32-bits of the 64-bit variable in the main loop. In such a scenario, the high half would never read the same twice in a row. Also, this seems to be endianness/architecture dependent, and imply the architecture is such that it writes the high-half first. If it writes the low-half first, you'd need to read in the other way: read low half, then high, then low again until low is same twice. – Gabriel Staples Mar 26 '22 at 05:27
  • 2
    @Brendan A simply variation of that idea is to read the item twice. If same we are done. Else read 3rd. If 2nd and 3rd the same, we are done, else program fault. I am leery of infinite loops. l'd rather avoid access contention, hence the interrupt disable idea. – chux - Reinstate Monica Mar 26 '22 at 05:27
  • 1
    @Brendan, in other words, your solution seems to have broken edge cases and be architecture-dependent. chux's solution just works all the time, period. – Gabriel Staples Mar 26 '22 at 05:28
  • 1
    @GabrielStaples One thing I learn was not to do `disable(); read; enable()` as interrupts might have already been disabled and it is better to restore the state than enable it. – chux - Reinstate Monica Mar 26 '22 at 05:30
  • 1
    @chux-ReinstateMonica, 100% correct! I've had HUGE bugs that way, by using a function which does that inside of an ISR where interrupts were disabled by default, thereby enabling nested interrupts accidentally. HUGE bug. Hard to find. – Gabriel Staples Mar 26 '22 at 05:31
  • @GabrielStaples: For a counter, the only case it'd be broken is if 1 iteration of the loop takes so long that the high half wraps around to match the old high half. For a microsecond counter, this means 1 iteration of the loop will need to take more than 50 million years. – Brendan Mar 26 '22 at 05:36
  • @Brendan and chux, I added `doAtomicRead()` to [the bottom of my answer](https://stackoverflow.com/a/71625693/4561887). I actually like this approach now. I'll plan to use and test it more in future work. – Gabriel Staples Mar 26 '22 at 06:21
  • @Brendan, regarding the `uint64_t` _counter_....ohhhhh! Right, it's an incrementing _counter_. I was trying to account for _any_ type of variable, not just a counter. For a variable which could change in _any_ non-predictable way, such as a measurement, it has more limitations I think. – Gabriel Staples Mar 26 '22 at 06:25
  • @chux-ReinstateMonica, I posted [this Q&A just now](https://stackoverflow.com/q/71626597/4561887) but see another answer there I'd like your opinion on. I want to know if I'm missing something big here. – Gabriel Staples Mar 26 '22 at 09:28
  • 1
    How do you know when [atomic types](https://en.cppreference.com/w/c/language/atomic) are or are not available? Are they available on stm32 mcus? On AVR mcus? – Gabriel Staples Mar 26 '22 at 09:34
  • 1
    @Brendan both [loop](https://stackoverflow.com/questions/71624109/reading-a-64-bit-variable-that-is-updated-by-an-isr/71625693#comment126587664_71625379) and [3 reads](https://stackoverflow.com/questions/71624109/reading-a-64-bit-variable-that-is-updated-by-an-isr/71625693#comment126587730_71625379) can fail when the objects lacks `volatile`. Without that, re-reads of the object may get optimized out. – chux - Reinstate Monica Mar 26 '22 at 15:10
  • For an incrementing counter (so we are assured the time it takes to read is low compared to the time it takes between increments of the high half), there is no need for any loop. We can use `high0 = GetHighHalf(); low = GetLowHalf(); high1 = GetHighHalf(); high = HighBitOf(low) ? high0 : high1;`. That is, get the high half twice and use the earlier or later value according to whether the low half is closer to approaching or leaving a transition in the high half. – Eric Postpischil Mar 26 '22 at 15:14
  • 1
    @GabrielStaples See update. For the various platforms, it depends on the compiler. I have not recently used _mcus_. – chux - Reinstate Monica Mar 26 '22 at 15:19
  • @chux: If `high0` and `high1` differ and `low` is binary 1xxx…xxxx, then `low` must have come from before the increment in the high half, because it has not had time to roll over to zero and get back to 1xxx…xxxx. Therefore, at the time `low` was read, the high value was `high0`. Similarly, if `low` is 0xxx…xxxx, then it must have come from after the increment, because it did not have time to go from 0xxx…xxxx to rolling over after 1111…1111. Therefore, at the time `low` was read, the high value was `high1`. – Eric Postpischil Mar 26 '22 at 15:28
  • @EricPostpischil OK, I thought final selection was steered on something other than `HighBitOf(low)`. – chux - Reinstate Monica Mar 26 '22 at 15:36
  • 1
    @EricPostpischil OP says 32-bit machine & the atomic-ness of reading 32-bits is then assumed & critical to the interesting [idea](https://stackoverflow.com/questions/71624109/reading-a-64-bit-variable-that-is-updated-by-an-isr/71625379?noredirect=1#comment126593552_71625379). Yet if code is ported to 16-bit machines or object was on a less than optimal aligned 32-bit obliging multiple accesses, we are back to a potential problem of a mis-read in re-combining the counter. OP's "is a 32 bit processor" I see as insufficient guarantee for atomic 32-bit access nor is code portable to smaller CPUs. – chux - Reinstate Monica Mar 26 '22 at 15:47
0

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;
}
MaryK
  • 33
  • 5
  • Of course there is a problem if the code gets delayed by something, and the timer ticks twice during one iteration of the loop. So instead of just a toggle you want a counter ... oh but wait, `ticks` is already a counter. So that would essentially reduce to "read twice" anyway. – Nate Eldredge Apr 11 '22 at 06:35
0

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;
}

Usage Example:

// global volatile variable shared between ISRs and main code
volatile uint64_t u1;

// Inside your function: "atomically" read and copy the volatile variable
uint64_t u1_copy = doAtomicRead(&u1);
if (u1_copy == ATOMIC_READ_FAILED)
{
    printf("Failed to atomically read variable `u1`.\n");

    // Now do whatever is appropriate for error handling; examples: 
    goto done;
    // OR:
    return;
    // etc.
}

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?
Gabriel Staples
  • 22,024
  • 5
  • 133
  • 166