在STM32微控制器上,哪些变量类型/大小是原子的?

25

以下是STM32微控制器上的数据类型:http://www.keil.com/support/man/docs/armcc/armcc_chr1359125009502.htm

这些微控制器使用32位ARM核心处理器。

哪些数据类型具有自动原子读取和原子写入访问权限?

我非常确定所有32位数据类型都可以(因为处理器是32位的),而所有64位数据类型都不行(因为读取或写入64位字需要至少2个处理器操作),但是bool(1字节)、uint16_t / int16_t(2字节)呢?

上下文:我在多个线程(单核,但多个线程,在FreeRTOS中称为“任务”)之间共享变量,并且需要知道是否需要通过关闭中断、使用互斥锁等来强制实现原子访问。

更新:

参考此示例代码:

volatile bool shared_bool;
volatile uint8_t shared u8;
volatile uint16_t shared_u16;
volatile uint32_t shared_u32;
volatile uint64_t shared_u64;
volatile float shared_f; // 32-bits
volatile double shared_d; // 64-bits

// Task (thread) 1
while (true)
{
    // Write to the values in this thread.
    //
    // What I write to each variable will vary. Since other threads are reading
    // these values, I need to ensure my *writes* are atomic, or else I must
    // use a mutex to prevent another thread from reading a variable in the
    // middle of this thread's writing.
    shared_bool = true;
    shared_u8 = 129;
    shared_u16 = 10108;
    shared_u32 = 130890;
    shared_f = 1083.108;
    shared_d = 382.10830;
}

// Task (thread) 2
while (true)
{
    // Read from the values in this thread.
    //
    // What thread 1 writes into these values can change at any time, so I need
    // to ensure my *reads* are atomic, or else I'll need to use a mutex to
    // prevent the other thread from writing to a variable in the midst of
    // reading it in this thread.
    if (shared_bool == whatever)
    {
        // do something
    }
    if (shared_u8 == whatever)
    {
        // do something
    }
    if (shared_u16 == whatever)
    {
        // do something
    }
    if (shared_u32 == whatever)
    {
        // do something
    }
    if (shared_u64 == whatever)
    {
        // do something
    }
    if (shared_f == whatever)
    {
        // do something
    }
    if (shared_d == whatever)
    {
        // do something
    }
}

在上述代码中,哪些变量可以不使用互斥锁进行操作? 我的怀疑如下:

  1. volatile bool:安全 - 无需互斥锁
  2. volatile uint8_t:安全 - 无需互斥锁
  3. volatile uint16_t:安全 - 无需互斥锁
  4. volatile uint32_t:安全 - 无需互斥锁
  5. volatile uint64_t:不安全 - 必须使用临界区或互斥锁!
  6. volatile float:安全 - 无需互斥锁
  7. volatile double:不安全 - 必须使用临界区或互斥锁!

使用FreeRTOS的示例临界区:

相关但未回答我的问题:

  1. ARM中的原子操作
  2. ARM:从int读写是否是原子的?
  3. (我自己关于8位AVR [和Arduino]微控制器的原子性问题):https://stackoverflow.com/a/39693278/4561887
  4. https://stm32f4-discovery.net/2015/06/how-to-properly-enabledisable-interrupts-in-arm-cortex-m/

这就是针对您特定芯片的ARM指令集手册吗? - user268396
可能是ARM:写入/读取int是否原子化?的重复问题。 - zneak
你必须查看汇编代码。 - Fiddling Bits
1
你是在试图防止两个核心对同一数据进行操作,还是在写入中被打断以让同一核心上的另一个线程运行? - zneak
后者:在写入过程中被打断,以便让同一核心上的其他线程运行。 - Gabriel Staples
考虑在STM32F767ZI上运行代码,并使用FreeRTOS来处理多线程。 - Gabriel Staples
3个回答

26

要得到这个问题的最终确定答案,请直接跳到下面标题为“问题的最终确定答案”的部分。

更新 2018年10月30日:我无意中引用了(略微)错误的文档(但内容完全相同),因此在我的答案中进行了修正。请参见底部关于2018年10月30日更改的说明。

我确实不明白这里的每个单词,但是ARM v7-M架构参考手册在线来源PDF文件直接下载)(不是技术参考手册[TRM],因为它不讨论原子性)验证了我的假设:

enter image description here

所以......我认为我在问题底部列出的7项假设都是正确的。 [2018年10月30日:是的,那是正确的。有关详细信息,请参见下面的说明。]


更新 2018年10月29日:

还有一个小提示:

Richard Barry,FreeRTOS创始人,专家和核心开发人员,在tasks.c中表示......

/* A critical section is not required because the variables are of type BaseType_t. */

...在STM32上读取“unsigned long”(4字节)易失变量时不需要关键部分。这意味着,至少他确信STM32的4字节读写是原子的。他没有提及更小的字节读取,但对于4字节读取,他可以得出确定的结论。我必须假设4字节变量作为本机处理器宽度,而且还按字对齐,这对于它的真实性非常重要。

例如,在FreeRTOS v9.0.0的tasks.c,第2173-2178行:

UBaseType_t uxTaskGetNumberOfTasks( void )
{
    /* A critical section is not required because the variables are of type
    BaseType_t. */
    return uxCurrentNumberOfTasks;
}

他在这个文件中的两个不同位置使用了这个确切的短语...

/* 由于变量是 BaseType_t 类型,因此不需要关键段。 */

...对我问题的最终答案是:小于等于4字节的所有类型(下面9行列表中加粗的类型)都是原子的。

此外,仔细查看我的屏幕截图上第141页的 TRM,我想指出的关键句子是:

在 ARMv7-M 中,单个副本的原子处理器访问是:
• 所有字节访问。
• 所有半字访问半字对齐的位置。
• 所有字访问字对齐的位置。

而且,根据 这个链接,对于“在 ARM C 和 C++ 中实现的基本数据类型”(即在 STM32 上):

  1. bool/_Bool 为“字节对齐”(1字节对齐)
  2. int8_t/uint8_t 为“字节对齐”(1字节对齐)
  3. int16_t/uint16_t 为“半字对齐”(2字节对齐)
  4. int32_t/uint32_t 为“字对齐”(4字节对齐)
  5. int64_t/uint64_t 为“双字对齐”(8字节对齐)<-- 不保证原子性
  6. float 为“字对齐”(4字节对齐)
  7. double 为“双字对齐”(8字节对齐)<-- 不保证原子性
  8. long double 为“双字对齐”(8字节对齐)<-- 不保证原子性
  9. 所有指针都是“字对齐”的(4字节对齐)

这意味着我现在有并理解了所需的证据,可以确切地说明上面加粗的行都具有自动原子读写访问(当然不包括增量/减量,这是多个操作)。 这是我的问题的最终答案。 唯一的例外情况可能是在打包结构中,在这种情况下,否则自然对齐的数据类型可能不是自然对齐的。

同时要注意,阅读技术参考手册时,“single-copy atomicity”实际上只是指“单核CPU原子性”,或者“单CPU核心架构的原子性”。这与“multi-copy atomicity”形成对比,后者指的是“多处理系统”或者多核CPU架构。维基百科表示,“多处理是在单个计算机系统中使用两个或多个中央处理器(CPU)”(https://en.wikipedia.org/wiki/Multiprocessing)。

我所涉及的架构STM32F767ZI(搭载ARM Cortex-M7内核)是单核架构,因此从我引用TRM中得知,“single-copy atomicity”适用。

进一步阅读:

关于2018年10月30日更改的说明:

要创建原子访问保护(通常通过关闭中断来实现非原子读写时),请参见:

  1. [我的Q&A]有哪些禁用和重新启用STM32微控制器中断的方法,以实现原子访问保护?
  2. 我的 doAtomicRead()函数可以在不关闭中断的情况下进行原子读取

1
在单核环境中,执行不能在指令的中间被打断,因此任何构建为一个指令的 C 结构都是原子性的。32 位 ARM 没有能够一次操作超过 32 位内存的单个指令,因此这就设定了明显的上限,即 64 位操作无法实现原子性。 - zneak
2
然而,即使操作32位或更少的C操作(例如a += 1int a)也会编译为多条指令,您需要小心处理这些操作。一个不太明显的例子是如果您使用具有未对齐字段的结构:您的编译器将需要生成至少两个加载和两个存储来处理读取/写入它们。在某些优化级别下,复制适合32位的结构可能也会使用多于一条指令。对于数字变量,通常都不是问题。 - zneak
1
@P__J__,请教我一些东西,并向我展示一个能够实现这个功能的架构。 - zneak
1
@P__J__,当除法被中断时会发生什么?您会得到损坏的状态,还是状态被回滚,以至于在中间被中断与刚刚被中断完全无法区分? - zneak
@old_timer,这可能是我迄今为止收到的最有用的反馈。 :) 感谢您指出这一点。我现在要看看能否找到正确的TRM。看起来您对我的问题进行了投票。请解释一下原因。 - Gabriel Staples
显示剩余22条评论

3

根据您所说的"原子性"是指什么。

如果不是像简单的加载或存储操作,例如

a += 1;

如果所有类型都不是原子的。

如果它是简单的存储或加载操作32位、16位和8位数据类型是原子的。如果寄存器中的值需要规范化,则8位和16位存储和加载可能不是原子的。

如果您的硬件支持位带操作,那么如果使用位带操作,则支持位带操作的内存区域中的位操作(设置和重置)是原子的。

注意。

如果您的代码不允许非对齐操作,则8位和16位操作可能不是原子的。


4
顺便提一下,a += 1 是两个操作,不是原子操作。 - zneak
1
同意。几年前,我通过对一个8位AVR处理器进行增量操作(非原子操作)来学习这一点,尽管该变量实际上是原子读写可行的。 - Gabriel Staples
@zneak 不,只有在RMW操作时才是非原子的,否则它是原子的。它可能不是一致的(缓存),但是是原子的。 - 0___________
1
如果该操作是原子的,多个核心同时尝试执行它将会成功。但事实并非如此,如果你有两个核心在循环中执行此操作,你肯定会失去一些增量。 - zneak
此外,在ARM上无法进行未对齐的8位访问。 - zneak
显示剩余6条评论

0

原子“算术”可以由CPU核心寄存器处理!

它可以是任何类型一个或四个字节,取决于架构和指令集

但是,对于位于内存中的任何变量的修改至少需要3个系统步骤:RMW = 读取内存到寄存器,修改寄存器并将寄存器写入内存。

因此,只有在控制使用CPU寄存器时才可能进行原子修改,这意味着需要使用纯汇编语言而不使用C或Cpp编译器。

当您使用C\Cpp编译器时,它会将全局或全局静态变量放置在内存中,因此C\Cpp不提供任何原子操作和类型

注意:例如,您可以使用“FPU寄存器”进行原子修改(如果您确实需要),但必须隐藏架构具有FPU的事实,以免被编译器和RTOS发现。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接