读取由 ISR 更新的 64 位变量

发布于 2025-01-17 03:58:30 字数 181 浏览 3 评论 0 原文

我没有找到太多关于非原子操作的材料。

假设我有一个 32 位处理器,并且我想在 64 位变量中保留微秒计数。中断将每微秒更新一次变量。调度程序是非抢占式的。将有一个函数用于清除变量,另一个函数用于读取变量。由于它是 32 位处理器,因此访问将是非原子的。是否有一种“标准”或惯用的方法来处理这个问题,以便读取器函数不会获得半更新的值?

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?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

时光磨忆 2025-01-24 03:58:30

是否有一种“标准”或惯用的方法来处理此问题,以便读取器函数不会获得半更新的值?

您需要做的是使用我所说的“原子访问防护”或“中断防护”。这是我感兴趣的领域,我花费了大量时间来学习和使用各种类型的微控制器。

@chux - 恢复莫妮卡,是正确的,但我想补充一些说明:

阅读

通过快速复制变量,然后在计算中使用副本,最大限度地缩短中断关闭的时间:

// ==========
// 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;

    }
}

对于写入易失性变量,快速写入:

通过在更新易失性变量时快速禁用中断,最大限度地缩短中断关闭的时间:

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

替代方案:通过重复读取进行无锁原子读取循环:doAtomicRead():确保原子读取而不关闭中断!

如上所示,使用原子访问防护的另一种方法是重复读取变量,直到它不再更改,这表明该变量在读取后的中间更新您只读取了其中的一些字节。

请注意,这适用于任何大小的内存块。下面的示例中使用的 uint64_t 类型甚至可以是几十个或几百个字节的 struct my_struct 。它不限于任何尺寸。 doAtomicRead() 仍然有效。

这是该方法。 @Brendan 和 @chux-ReinstateMonica 和我在 @chux-ReinstateMonica 的回答下讨论了一些想法。

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

如果您想更深入地了解,这里又是相同的 doAtomicRead() 函数,但这次带有大量解释性注释。我还展示了一个注释掉的细微变化,这在某些情况下可能会有所帮助,如评论中所述。

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

通过在循环开始之前添加一次额外的读取,可以优化上述内容,以仅获得 *val 的新读数每次迭代仅获得一次,而不是两次,并且在循环中只读取一次,如下所示:

[这是我最喜欢的版本:]

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

一般用法 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.
}

这要求编写器在以下方面具有原子性:任何读者,例如,对于单个作者而言,这是正确的写入该变量。例如,此写入可能发生在 ISR 内部。我们仅检测到撕裂的读取(由于读取器被中断)并重试。如果当该读取器运行时 64 位值在内存中已经处于撕裂写入状态,则读取器可能会错误地将其视为有效。

SeqLock 没有这个限制,因此对于多核情况很有用。但是,如果您不需要它(例如:您有一个单核微控制器),它的效率可能会较低,而 doAtomicRead() 技巧就可以正常工作。

对于单调递增计数器的特殊边缘情况(不适用于可以用任何值更新的变量,例如存储传感器读数的变量!),正如布伦丹在这里建议的您只需要重新读取 64 位值的最高有效一半并检查它是否没有 改变。因此,为了(可能)稍微提高上面的 doAtomicRead() 函数的效率,请对其进行更新。唯一可能的撕裂(除非您错过 2^32 计数)是当低半部分换行且高半部分递增时。这就像检查整个事情,但重试的频率会更低。

在 FreeRTOS 中使用 doAtomicRead()

  1. 单个 FreeRTOS 任务从所有传感器收集传感器数据(通过 SPI、I2C、串行 UART、ADC 读数等)。它写入封装在模块中的共享易失性全局变量,因此它们感觉不像全局变量。这是一种“共享内存”多线程模型。这些变量不需要是原子的。为所有变量提供了 setter 和 getter。只有这一项任务可以写入变量。 它的优先级必须高于读者任务才能正常工作。即:通过成为比消费者更高优先级的任务,它模拟处于受保护的 ISR 中,并且能够相对于读者/消费者以原子方式写入
  2. 较低优先级的读取器任务可以读取数据。他们使用我的无锁 doAtomicRead() 函数,如上所述。请注意,这适用于任何大小的内存块。我的示例中使用的 uint64_t 类型甚至可能是数十或数百字节的结构。它不限于任何尺寸。
  3. 就是这样!无论如何,不​​需要信号量、锁或互斥体。一项任务“产生”数据。所有其他任务都会消耗它,无锁。这是一个单生产者、多消费者模型。
  4. 它会丢弃数据,这对于大多数传感器数据来说是可以接受的。换句话说,生产者总是用最新数据覆盖值,而消费者只获取最新数据。如果需要执行任何移动平均值或过滤器而不丢失过滤器输入上的数据,则应由生产者任务在将数据存储到共享内存以供消费者使用之前执行此过滤。

进一步讨论原子访问防护、禁用中断等主题。

  1. 我的c/containers_ring_buffer_FIFO_GREAT.c 来自我的 eRCaGuy_hello_world 存储库的演示。此代码示例的描述来自我在该文件顶部的注释:

    <块引用>

    演示基本、高效的无锁 SPSC(单生产者单消费者)环形缓冲区 FIFO
    C 中的队列(也在 C++ 中运行)。

    此队列仅在 SPSC 上下文中无锁工作,例如在裸机上
    例如,ISR 需要将数据发送到主循环的微控制器。

  2. [Peter Cordes 对 SeqLock 的回答(“序列lock") 模式] 用 32 位实现 64 位原子计数器原子

  3. [我的答案] C++ 递减单字节(易失性)数组的元素不是原子的!为什么? (另外:如何在 Atmel AVR mcus/Arduino 中强制原子性)

  4. 我又长又详细的答案< /a> 哪些 Arduino 支持 ATOMIC_BLOCK? 以及:

    1. 如何使用 gcc 编译器在 C 语言中实现 ATOMIC_BLOCK 宏?在哪里可以看到它们的源代码?
    2. 如何使用 C++(而不是 avrlibc 的 gcc C 版本)在 Arduino 中实现 ATOMIC_BLOCK 功能?
    3. 我详细解释了这个非常聪明的原子访问保护宏如何通过 gcc 扩展在 C 中工作,以及如何轻松地在 C++ 中实现它:
      ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
      {
          my_var_copy = my_var;
      }
      

  5. [我的问答]哪些变量类型/大小在 STM32 微控制器上是原子的?

    1. 并非所有变量都需要原子访问保护来进行简单的读取和写入(对于增量/减量,它们总是这样做!--请参阅上面列表中的第一个链接!),因为某些变量具有对于给定的架构,>自然地原子读取和写入
      1. 对于 8 位 AVR 微控制器(例如 Arduino Uno 上的 ATmega328):8 位变量具有自然的原子读写
      2. 对于 32 位 STM32 微控制器,所有 32 位及以下的非结构(简单)类型都具有自然原子读取和写入有关详细信息,请参阅上面我的回答以及源文档和证明。
  6. 在 STM32 MCU 上禁用中断的技术:https://stm32f4-discovery.net/2015/06/how-to-properly-enabledisable-interrupts-in-arm-cortex-m/

  7. [我的答案] ISR 中未更新全局易失性变量:如何使用原子访问识别和修复 Arduino 中的竞争条件守卫:

  8. [我的答案] 禁用和重新启用中断的各种方法有哪些STM32微控制器为了实现原子访问防护?

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

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.

Note that this works on memory chunks of any size. The uint64_t type used in my examples below could instead be a struct my_struct of dozens or hundreds of bytes even. It is not limited to any size. doAtomicRead() still works.

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.

Using doAtomicRead() in FreeRTOS

  1. A single FreeRTOS task gathers sensor data from all sensors (via SPI, I2C, serial UART, ADC readings, etc). It writes into shared volatile global variables encapsulated in a module, so they don't feel like global variables. This is a "shared memory" multi-threaded model. These variables are not required to be atomic. Setters and getters are provided for all variables. ONLY this one task can write into the variables. It must be a higher-priority than the reader tasks for this to work properly. ie: by being a higher-priority task than the consumers, it emulates being in a protected ISR, and is able to write atomically with respect to the readers/consumers.
  2. Lower-priority reader tasks can read the data. They use my lockless doAtomicRead() function as described above. Note that this works on memory chunks of any size. The uint64_t type used in my example could be a struct of dozens or hundreds of bytes even. It is not limited to any size.
  3. That's it! No semaphores, locks, or mutexes are required, whatsoever. One task "produces" data. All other tasks consume it, lock-free. This is a single-producer, multi-consumer model.
  4. It drops data, which is okay for most sensor data. In other words, the producer is always overwriting the values with the latest data, and the consumers are only getting the latest data. If any moving averages or filters need to be conducted with no lost data on the input to the filters, this filtering should be conducted by the producer task prior to storing the data into the shared memory to be consumed by the consumers.

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

往日 2025-01-24 03:58:30

在 ISR 内,通常会阻止后续中断(除非优先级更高,但通常不会触及计数),因此只需 count_of_microseconds++;

在 ISR 外部,访问(读或写)< code>count_of_microseconds 你需要中断保护或原子访问。

atomic不可用*1但解释控制可用时:

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 

否则使用

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

请参阅如何在 C 中使用原子变量?

从 C89 开始,将 volatilecount_of_microseconds 一起使用。


[更新]

无论非 ISR 代码中使用哪种方法(此答案或其他)来读/写计数器,我建议将读/写代码包含在辅助函数中以隔离这组关键操作。


*1 自 C11 起可用,__STDC_NO_ATOMICS__ 定义。

#if __STDC_VERSION__ >= 201112
#ifndef __STDC_NO_ATOMICS__
#include <stdatomic.h>
#endif
#endif

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
轻拂→两袖风尘 2025-01-24 03:58:30

我很高兴听到读两次方法是可行的。我有疑问,不知道为什么。与此同时,我想出了这个:

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

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;
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文