这是一个非常好的问题,它触及了嵌入式编程中一个常见且重要的误解。让我们来彻底澄清一下。

核心结论:一个普遍的误解

首先,也是最关键的一点:在STM32(以及绝大多数现代微控制器)中,i++ 通常并不是原子操作。

认为它是原子操作是一个常见的误区,基于这个假设编写代码可能会导致非常隐蔽且难以调试的bug。


1. 为什么 i++ 不是原子操作?

要理解这一点,我们需要看看编译器是如何将C代码 i++ 翻译成ARM汇编指令的。

假设 i 是一个全局变量,存储在内存中。

C代码:

i++;

可能的ARM汇编指令序列:

    LDR     R1, =i        ; 步骤1: 将变量 i 的内存地址加载到寄存器 R1
    LDR     R0, [R1]      ; 步骤2: 从内存地址 R1 处,将 i 的值加载到寄存器 R0
    ADD     R0, R0, #1    ; 步骤3: 在寄存器 R0 中对值进行加1操作
    STR     R0, [R1]      ; 步骤4: 将寄存器 R0 中的新值存回变量 i 的内存地址

从这个汇编代码可以清晰地看到,一个简单的 i++ 在底层被分解为了 4个步骤加载地址 -> 加载值 -> 修改值 -> 存储值

“非原子性”的危险场景(中断/多线程):

想象一下 i 的初始值为 0,一个主循环和一个中断服务程序都对 i 进行 i++ 操作。

  1. 主程序执行了前3步:将 i (0) 加载到 R0,加1变为1,正准备存回内存。

  2. 此时,一个中断发生,CPU跳转到中断服务程序

  3. ISR 完整地执行了 i++:它从内存中读取 i (仍然是0),加1变为1,然后将其存回内存。现在内存中的 i 是 1。

  4. 中断返回,主程序从被打断的地方继续执行。

  5. 主程序执行第4步:将它之前计算好的值 (1) 存回内存中的 i。

结果: 尽管 i 被加了两次,但最终的值是 1,而不是预期的 2。一次增加操作被“丢失”了。


2. “原子操作”是为谁服务的?为什么需要它?

原子操作服务于需要 “不可分割” 的代码段。

核心服务对象:

  1. 共享资源的访问:多个执行流(主循环 + 中断,或多个线程在RTOS中)共同访问的全局变量、缓冲区、状态标志等。

  2. 外设寄存器配置:对某个外设的多个寄存器进行配置时,需要确保配置过程不被打断,以免外设进入非法状态。

为什么需要它?
为了防止上面描述的 “竞态条件”。当一个执行流正在修改共享数据的过程中,被另一个执行流打断并访问或修改了同一份数据,就会导致数据损坏、逻辑错误或程序崩溃。


3. 为什么会流行“i++是原子操作”这种误解?

这种误解通常源于以下几个原因:

  1. 8位/简单单片机的遗留印象:在古老的8位单片机(如8051、AVR的某些简单应用)上,对单个字节的读写指令本身就是原子的,因为数据总线和CPU寄存器是8位的,一次就能完成操作。很多人将这种经验错误地带到了32位世界。

  2. 在单线程、无中断环境中测试:如果代码只在主循环中运行,且所有中断都不修改 i,那么 i++ 看起来是“正常”工作的。但这是一种假象,一旦在中断中使用,bug就会随机出现。

  3. 对编译器和硬件架构不了解:没有意识到C语言的一条语句会被编译成多条机器指令,而中断可以发生在任何两条机器指令之间。


4. 在STM32中如何实现真正的原子操作?

既然 i++ 不是原子的,我们如何保证对 i 的操作是安全的呢?

方法一:关闭中断(对于基于中断的并发)

在访问共享资源之前关闭中断,操作完成后再打开。

__disable_irq();   // 关闭所有中断
i++;
__enable_irq();    // 重新开启中断

优点:简单、可靠。
缺点:会增加中断延迟,影响系统实时性。

方法二:使用信号量/互斥锁(对于RTOS多任务)

如果使用FreeRTOS、μC/OS等操作系统,应该使用其提供的同步机制。

xSemaphoreTake(xMutex, portMAX_DELAY); // 获取互斥锁
i++;
xSemaphoreGive(xMutex);                // 释放互斥锁
方法三:使用C11原子类型(如果编译器支持)

C11标准引入了原子操作库 <stdatomic.h>

#include <stdatomic.h>

atomic_int i = ATOMIC_VAR_INIT(0);

// 原子地增加 i
atomic_fetch_add(&i, 1);

优点:可移植、语法清晰、编译器保证原子性。

方法四:使用硬件提供的原子指令(LDREX/STREX)

ARM Cortex-M内核提供了 LDREX 和 STREX 指令,用于实现“独占访问”,可以构建出自等的锁和无锁数据结构。这通常是上面方法二和方法三在底层实现的方式,但一般由操作系统或库函数封装好了,用户无需直接使用。

总结

特性说明
i++ 的本质非原子操作,包含加载、修改、存储多个步骤。
原子操作的服务对象保护共享资源多执行流环境下的数据完整性。
误解来源8位机经验、单线程测试、对底层不了解。
正确实现方式关闭中断、使用RTOS信号量、使用C11原子操作。

结论: 在STM32编程中,请务必放弃“i++是原子操作”的观念。在任何可能被中断或其它任务访问的共享变量上,都必须使用正确的同步机制来保护它。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐