在stm32中,为什么i++是原子操作,他是为什么服务的,为什么会出现这种编程方式。
这是一个非常好的问题,它触及了嵌入式编程中一个常见且重要的误解。让我们来彻底澄清一下。
核心结论:一个普遍的误解
首先,也是最关键的一点:在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++ 操作。
-
主程序执行了前3步:将 i (0) 加载到 R0,加1变为1,正准备存回内存。
-
此时,一个中断发生,CPU跳转到中断服务程序。
-
ISR 完整地执行了
i++:它从内存中读取 i (仍然是0),加1变为1,然后将其存回内存。现在内存中的 i 是 1。 -
中断返回,主程序从被打断的地方继续执行。
-
主程序执行第4步:将它之前计算好的值 (1) 存回内存中的 i。
结果: 尽管 i 被加了两次,但最终的值是 1,而不是预期的 2。一次增加操作被“丢失”了。
2. “原子操作”是为谁服务的?为什么需要它?
原子操作服务于需要 “不可分割” 的代码段。
核心服务对象:
-
共享资源的访问:多个执行流(主循环 + 中断,或多个线程在RTOS中)共同访问的全局变量、缓冲区、状态标志等。
-
外设寄存器配置:对某个外设的多个寄存器进行配置时,需要确保配置过程不被打断,以免外设进入非法状态。
为什么需要它?
为了防止上面描述的 “竞态条件”。当一个执行流正在修改共享数据的过程中,被另一个执行流打断并访问或修改了同一份数据,就会导致数据损坏、逻辑错误或程序崩溃。
3. 为什么会流行“i++是原子操作”这种误解?
这种误解通常源于以下几个原因:
-
8位/简单单片机的遗留印象:在古老的8位单片机(如8051、AVR的某些简单应用)上,对单个字节的读写指令本身就是原子的,因为数据总线和CPU寄存器是8位的,一次就能完成操作。很多人将这种经验错误地带到了32位世界。
-
在单线程、无中断环境中测试:如果代码只在主循环中运行,且所有中断都不修改
i,那么i++看起来是“正常”工作的。但这是一种假象,一旦在中断中使用,bug就会随机出现。 -
对编译器和硬件架构不了解:没有意识到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++是原子操作”的观念。在任何可能被中断或其它任务访问的共享变量上,都必须使用正确的同步机制来保护它。
更多推荐
所有评论(0)