volatile
正如您所说,您在 AVR 变量上使用关键字的原因是避免编译器优化对变量的访问。现在的问题是,这是怎么发生的呢?
一个变量有两个可以驻留的地方。在通用寄存器文件中或在 RAM 中的某个位置。考虑变量驻留在 RAM 中的情况。为了访问变量的最新值,编译器使用某种形式的ld
指令从 RAM 加载变量,比如lds r16, 0x000f
. 在这种情况下,变量存储在 RAM 位置0x000f
,程序在r16
. 现在,如果启用中断,事情就会变得有趣。假设加载变量后,发生以下情况inc r16
,然后触发中断并运行其相应的ISR。在 ISR 中,也使用该变量。然而,有一个问题。该变量存在于两个不同的版本中,一个在 RAM 中,一个在r16
. 理想情况下,编译器应该使用 中的版本r16
,但不保证这个版本存在,所以它改为从 RAM 加载它,现在,代码无法按需要运行。然后输入volatile
关键字。变量仍然存储在 RAM 中,但是,编译器必须确保变量在 RAM 中更新,然后再发生其他任何事情,因此可能会生成以下程序集:
cli
lds r16, 0x000f
inc r16
sei
sts 0x000f, r16
首先,中断被禁用。然后,将变量加载到 r16 中。增加变量,启用中断,然后存储变量。在将变量存储回 RAM 之前启用全局中断标志可能看起来令人困惑,但来自指令集手册:
SEI 之后的指令将在任何未决中断之前执行。
这意味着sts
指令将在任何中断再次触发之前执行,并且中断被禁用的时间尽可能短。
现在考虑变量绑定到寄存器的情况。对变量进行的任何操作都直接在寄存器上进行。这些操作与对 RAM 中的变量执行的操作不同,可以被认为是原子的,因为没有读 -> 修改 -> 写周期可言。如果在变量更新后触发中断,它将获取变量的新值,因为它将从绑定的寄存器中读取变量。
此外,由于变量绑定到寄存器,因此任何测试指令都将使用寄存器本身,并且不会因为编译器有“预感”而优化掉,这是一个静态值,因为寄存器本质上是易挥发的。
现在,根据经验,在 AVR 中使用中断时,我有时会注意到全局 volatile 变量永远不会影响 RAM。编译器一直将它们保存在寄存器上,完全绕过了读取->修改->写入循环。然而,这是由于编译器优化造成的,不应依赖它。不同的编译器可以自由地为同一段代码生成不同的程序集。您可以使用该avr-objdump
实用程序生成最终文件或任何特定目标文件的反汇编。
干杯。