An implementation might define a one-to-one correspondence between abstract and actual semantics: at every sequence point, the values of the actual objects would agree with those specified by the abstract semantics. The keyword volatile would then be redundant.
然而现实的情况是:Abstract semantics和Actual semantics并非总是一致的,这就是为什么volatile存在的理由。
这句话其实也可以这么说:如果编译器天生就不会缓存、不会假设寄存器存储的值不变、不会优化外部访问,那我们就根本不需要volatile,因为写不写volatile,效果都差不多。然而就像上面说的,现实并不是这样的,因为现实中的编译器为了性能会做激进优化,而嵌入式程序必须防止这些优化破坏硬件语义。
为了进一步理解,我们来看看以下代码:
// main.c
/* 正确的写法是:volatile uint32_t counter; */
uint32_t counter;
extern void task();
extern void SysTick_Init();
int main()
{
counter = 10;
SysTick_Init();
while(counter != 0);
// 继续
while(1);
}
这里还看不出什么,所以我们来看看下面的汇编,读者们就大致知道区别了:
;如果counter不被声明volatile
__main PROC
LDR r1, =counter ;将counter的地址加载到r1
MOV r0, #10 ;r0 = 10
STR r0, [r1] ;将10存入counter
BL SysTick_Init ;初始化SysTick定时器
wait CMP r0, #0 ;比较当前r0是否等于0,然而r0是固定值10,而非从内存读取的counter
BNE wait ;因此产生死循环
stop B stop
ENDP
问题来了,wait CMP r0, #0里的r0存储的是什么?
答案是存储的常量10。
为什么?因为编译器认为,既然刚刚把10存入进counter,并且在wait循环里没有任何C代码修改counter,那么counter肯定一直等于10。所以编译器认为自己没必要再去从内存读取它,直接用我现成的r0(也就是寄存器缓存的值)来进行比较就好了。因此才构成了死循环。
;如果counter声明volatile
__main PROC
LDR r1, =counter ;将counter的地址加载到r1
MOV r0, #10 ;r0 = 10
STR r0, [r1] ;将10存入counter
BL SysTick_Init ;初始化SysTick定时器
wait LDR r1, =counter ;重新加载counter的地址到r1
LDR r0, [r1] ;从内存读取counter的最新值到r0
CMP r0, #0 ;比较当前counter是否等于0
BNE wait ;如果不为0,继续循环
stop B stop
ENDP
我们都知道,硬件寄存器并不是普通内存。它们的值可能在中断中改变、被DMA修改,或者由外设硬件自行更新,而这些变化对编译器而言是不可见的。编译器也不会、也无法主动推断这种外部变化。这正是volatile存在的原因。
volatile的作用并不是改变程序逻辑,而是强制编译器尊重硬件的真实变化。
看到两个汇编示例之后,自然我们就知道什么是volatile了,对吧?
volatile就是强制编译器每次访问这个变量的时候,都得通过这个内存地址去读取变量最新的值,而不是使用寄存器缓存的值,并且每次修改后都会写回给内存,防止编译器的小聪明。
当然啦,以上这些知识点都会在面试的时候,常常被提问到的。
网硕互联帮助中心


评论前必须登录!
注册