5

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?

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
MaryK
  • 71
  • 6
  • 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

5

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: lock-free atomic reads via a 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.
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; 
        if (val_copy == *val)
        {
            val_copy_atomic = val_copy;
            break;
        }
    }

    return val_copy_atomic;
}

If you want to understand deeper, here is the same doAtomicRead() function again, but this time with extensive explanatory comments. I also show a commented-out slight variation to it which may be helpful in some cases, as explained in the comments.

/// @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;
    
    // In case we get interrupted during this code block, and `val` gets updated
    // in that interrupt's ISR, try `MAX_NUM_ATOMIC_READ_ATTEMPTS` times to get
    // an atomic read of `val`.
    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, updating the 64-bit value in the ISR and resulting in
        // 32-bits of the old value in the 64-bit variable being wrong now
        // (since the whole 64-bit value has just been updated with a new
        // value), so verify the read above with a new read again.
        // 
        // Caveat: 
        //
        // Note that this method is **not _always_** foolproof, as technically
        // the interrupt could fire off and run again during this 2nd read,
        // causing a very rare edge-case where the exact same incorrect value
        // gets read again, resulting in a false positive where it assigns an
        // erroneous value to `val_copy_atomic`! HOWEVER, that is for **you or
        // I** to design and decide as the architect. 
        //
        // Is it _possible_ for the ISR to really fire off again immediately
        // after returning? Or, would that never happen because we are
        // guaranteed some minimum time gap between interrupts? If the former,
        // you should read the variable again a 3rd or 4th time by uncommenting
        // the extra code block below in order to check for consistency and
        // minimize the chance of an erroneous `val_copy_atomic` value. If the
        // latter, however, and you know the ISR won't fire off again for at
        // least some minimum time value which is large enough for this 2nd
        // read to occur **first**, **before** the ISR gets run for the 2nd
        // time, then you can safely say that this 2nd read is sufficient, and
        // you are done.
        if (val_copy == *val)
        {
            val_copy_atomic = val_copy;
            break;
        }

        // Optionally delete the "if" statement just above and do this instead.
        // Refer to the long "caveat" note above to see if this might be
        // necessary. It is only necessary if your ISR might fire back-to-back
        // with essentially zero time delay between each interrupt.
        // for (size_t j = 0; j < 4; j++)
        // {
        //     if (val_copy == *val)
        //     {
        //         val_copy_atomic = val_copy;
        //         break;
        //     }
        // }
    }

    return val_copy_atomic;
}

The above could be optimized to only obtain a new reading of *val only once per iteration, instead of twice, by adding one extra read before the start of the loop, and reading only once in the loop, like this:

[This is my favorite version:]

uint64_t doAtomicRead(const volatile uint64_t* val)
{
    uint64_t val_copy_new;
    uint64_t val_copy_old = *val;
    uint64_t val_copy_atomic = ATOMIC_READ_FAILED;
    
    for (size_t i = 0; i < MAX_NUM_ATOMIC_READ_ATTEMPTS; i++)
    {
        val_copy_new = *val; 
        if (val_copy_new == val_copy_old)
        {
            // no change in the new reading, so we can assume the read was not 
            // interrupted during the first reading
            val_copy_atomic = val_copy_new;
            break;
        }
        // update the old reading, to compare it with the new reading in the
        // next iteration
        val_copy_old = val_copy_new;  
    }

    return val_copy_atomic;
}

General usage Example of doAtomicRead():

// 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.
}

This requires the writer to be atomic with respect to any readers, which is true, for example, in the case of a single writer writing to this variable. This write might occur inside an ISR, for example. We're detecting only torn reads (due to the reader being interrupted) and retrying. If the 64-bit value was ever already in a torn written state in memory when this reader ran, the reader could erroneously see it as valid.

A SeqLock doesn't have that limitation, so is useful for multi-core cases. But, if you don't need that (ex: you have a single-core microcontroller), it is probably less efficient, and the doAtomicRead() trick works just fine.

For the special edge case of a monotonically-incrementing counter (not for a variable which can be updated with any value, such as a variable storing a sensor reading!), as Brendan suggested here you only need to re-read the most-significant half of the 64-bit value and check that it didn't change. So, to (probably) slightly improve the efficiency of the doAtomicRead() function above, update it to do that. The only possible tearing (unless you miss 2^32 counts) is when the low half wraps and the high half gets incremented. This is like checking the whole thing, but retries will be even less frequent.

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

  1. My c/containers_ring_buffer_FIFO_GREAT.c demo from my eRCaGuy_hello_world repo. Description of this code example from my comments at the top of this file:

    Demonstrate a basic, efficient lock-free SPSC (Single-Producer Single-Consumer) ring buffer FIFO queue in C (that also runs in C++).

    This queue is intended to work lock-free in a SPSC context only, such as on a bare-metal microcontroller where an ISR needs to send data to the main loop, for example.

  2. [Peter Cordes's answer on the SeqLock ("sequence lock") pattern] Implementing 64 bit atomic counter with 32 bit atomics

  3. [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)

  4. 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;
      }
      
  5. [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.
  6. Techniques to disable interrupts on STM32 mcus: https://stm32f4-discovery.net/2015/06/how-to-properly-enabledisable-interrupts-in-arm-cortex-m/

  7. [my answer] global volatile variable not being updated in ISR: How to recognize and fix race conditions in Arduino by using atomic access guards:

  8. [my answer] What are the various ways to disable and re-enable interrupts in STM32 microcontrollers in order to implement atomic access guards?

  9. https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock

Gabriel Staples
  • 36,492
  • 15
  • 194
  • 265
  • Your read-retry loop with `volatile uint64_t* val` dereferences the volatile twice on every iteration, instead of once (and once before the loop). Read into a 2nd temporary, so on mismatch you can assign that to `val_copy`. ARM is a RISC so the compare has to load into registers anyway, might as well tell the compiler it can copy those registers instead of doing another load. (Although possibly `ldrd` is one instruction to load two registers vs. two `mov` instructions to copy two regs. On microcontrollers you often don't have data cache, but often not instruction cache either.) – Peter Cordes Aug 24 '22 at 19:28
  • For monotonically-incrementing counters, [as Brendan suggested](https://stackoverflow.com/questions/71624109/reading-a-64-bit-variable-that-is-updated-by-an-isr/71625693#comment126587664_71625379) you only need to re-read and check that the most-significant half didn't change, using it to detect tearing on carry propagation into the high half as the low half wraps. That's much less likely to need to retry, better even than a SeqLock ([Implementing 64 bit atomic counter with 32 bit atomics](https://stackoverflow.com/q/54611003)) – Peter Cordes Aug 24 '22 at 19:30
  • 1
    Note that your re-read strategy relies on it being written atomically (wrt. the reader, so in an ISR on a single-core machine is ok). A SeqLock doesn't have that requirement; it's SMP safe for readers on other cores. But it's not lock-free, and if there are multiple writers they need to make sure they aren't writing at once. – Peter Cordes Aug 24 '22 at 19:32
  • @PeterCordes, can you demonstrate in code this part? _The above could be optimized in assembly to only dereference `*val` once per iteration, with one extra load before the first._ – Gabriel Staples Aug 24 '22 at 21:47
  • Oops, didn't mean to write "in assembly". I just mean in plain C, using two temp vars, one init outside the loop, one loaded inside the loop. Like maybe a do/while loop especially if you don't bother with extra code to set an iteration limit, otherwise keep the if/break after the load, with `tmp1 = tmp2` after that. – Peter Cordes Aug 24 '22 at 22:13
  • @PeterCordes, I'm still not following. Feel free to update the code block containing `///todo///` with what you mean. – Gabriel Staples Aug 24 '22 at 22:19
  • 1
    Ok, done. https://godbolt.org/z/Me5oWKoPG shows how with `MAX_NUM_ATOMIC_READ_ATTEMPTS` = 3, GCC `-Os` still fully unrolls the loop, so `read1 = read2` costs 0 instructions. Use 300 or whatever to see compilers make loops, where 2x `ldrd` is fewer instructions that `ldrd` + 2x `mov`, but not by much. – Peter Cordes Aug 24 '22 at 22:35
  • @PeterCordes, thank you! I get it now. I was doing two new reads of `*val` each loop iteration, when really I just needed one new read. I see the difference now. You stuck your change in a spot different than what I was expecting, so I reverted your latest change, tweaked it to my coding style and liking, and stuck it back in as "my favorite version" in the answer now. Thanks again. – Gabriel Staples Aug 25 '22 at 06:01
  • Ah, I think I found the `todo` comment when I started to edit, but restarted the edit after screwing something up and apparently copy/pasted from Godbolt over part of the wrong code block that only looked similar to the one I'd been looking at before. Not sure why you wanted to keep the version with 2 reads, and show it first. There's not a significant difference, so I think future readers might just get bogged down with multiple implementations of the same algorithm; they're both readable. It's your answer so it's of course up to you, this is just my opinion / impression. – Peter Cordes Aug 25 '22 at 12:02
  • Also, I liked my version's style better; it made it clearer that only two `uint64_t` vars were needed at any given time (4 registers), vs. assigning an error code to a 3rd var with a confusing name (atomic?) before the loop. Hopefully they still compile the same, but I find it more mental work to follow assigning a loaded value to some other variable and then `break` to get to a `return`, vs. returning on the spot after a match. Otherwise fall out of the loop to return error. If all your other code is written in your style (avoiding multiple exit points in functions?) then sure I guess. – Peter Cordes Aug 25 '22 at 12:05
3

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
  • 143,097
  • 13
  • 135
  • 256
  • 2
    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
  • 1
    @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
  • 3
    @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
  • 1
    @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
  • 2
    @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
  • @GabrielStaples: For the general case, not a simply incrementing counter, you can use a SeqLock: [Implementing 64 bit atomic counter with 32 bit atomics](https://stackoverflow.com/q/54611003) You have a 3rd word in memory that you read before/after the payload to detect tearing and retry the read, so readers are read-only. Brendan's suggestion is also read/retry, but has the advantage of being lock-free: there's no point at which a writer can sleep or be interrupts that leaves the data in an unreadable state. As well as cheaper for writers and readers, and much less likely to need a retry. – Peter Cordes Aug 24 '22 at 19:17
  • @GabrielStaples: Might want to delete some of your earlier comments where you say Brendan's idea is broken. It's not, just limited to monotonic counters with small increments. – Peter Cordes Aug 24 '22 at 19:20
-1

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
  • 71
  • 6
  • 1
    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
  • 2
    You've almost re-invented the SeqLock, but a 1-bit counter isn't enough; it can wrap around too easily. ([Implementing 64 bit atomic counter with 32 bit atomics](https://stackoverflow.com/q/54611003)). Also, none of this is `volatile` so you're not forcing the compiler to make asm that reads it twice. – Peter Cordes Aug 24 '22 at 19:52