C语言中关键词volatile的用法(二)

volatile用于声明变量时的使用的限定符。它告诉编译器该变量值可能随时发生变化,且这种变化并不是代码引起的。给编译器这个暗示是很重要的。
volatile的声明:
1.声明一个变量为volatile,可以在数据类型之前或之后加上关键字volatile。下面的语句,把变量abc声明一个volatile的整型变量;
volatile int abc;
int volatile abc;
2.把指针指向的变量声明为volatile很常见,尤其是I/O寄存器的地址映射。下面的语句,把pReg声明为一个指向8-bit无符号指针,指针指向的内容为volatile。
volatile uint8_t * pReg;
uint8_t volatile * pReg;
3.volatile的指针指向非volatile的变量很少见(我只使用过一次),但我还是给出相应的语法。
int * volatile p;
4.最后,如果你再struct或者union前使用volatile关键字,表明struct或者union的所有内容都是volatile。当然也可以在struct或者union成员上使用volatile关键字。

volatile的使用方法:
只要变量可能被意外的修改,就需要把该变量声明为volatile。在实际应用中,只有三种类型数据可能被修改:
1.外设寄存器地址映射;
2.在中断服务程序中修改全局变量;
3.在多线程、多任务应用中,全局变量被多个任务读写。

外设寄存器
嵌入式系统包含真正的硬件,通常会有复杂的外设。这些外设寄存器的值可能被异步的修改。举个简单的例子,我们要把一个8-bit状态寄存器的地址映射到0x1234。在程序中循环查看该状态寄存器的值是否变为非0。
下面是最容易想到,但错误的实现方法:
uint8_t * pReg = (uint8_t *)0x1234;
while(*pReg == 0) {printf("found *pReg == 0");}
当你打开编译器优化时,程序总是执行失败。因为编译器会生成下面的汇编代码:
mov ptr, #0x1234

mov a, @ptr
loop:
bz loop
程序被优化的原因很简单,既然已经把变量的值读入累加器,就没有必要重新一遍,编译器认为值是不会变化的。就这样,在第三行,程序进入了无限死循环。为了告诉编译器我们的真正意图,我们需要修改函数的声明:
uint8_t volatile * pReg = (uint8_T volatile *)0x1234
编译器生成的汇编代码:
mov ptr, #0x1234

loop:
mov a, @ptr
bz loop

中断服务程序
在中断服务程序中,经常会修改一些全局变量值,来作为主程序中的判断条件。例如,在串口中断服务程序中,可能会检测是否接收到了ETX(假如是消息的结束标识符)字符。如果接收到了ETX,ISR设置一个全局标志位。
错误的做法:

int etx_rcvd = FALSE;
void main()
{
  ...
  while(!etx_rvcd)
  {
    //wait
  }
  ...
interrupt void rx_isr(void)
{
  ...
  if(ETX == rx_char)
  {
    etx = TRUE;
  }
  ...
}
}

在关闭编译器优化的情况下,程序可能执行正常。然而,任何像样点而优化都会“break”这段程序。问题是编译器并不知道etx_rcvd可能被ISR中被修改。编译器只知道,表达式!ext_rcvd始终为真,你讲用于无法退出循环。结果,循环后面的代码可能被编译器优化掉。
幸运的话,你的编译器可能会发出警告;不幸的话,(或者你不认真的查看编译器警告),你的程序无法正常执行。当然,你可以责怪编译器执行了“糟糕的优化”。
解决方式是,将变量etx_rcvd声明为volatile,所有问题(当然,也可能是部分问题)就消失了。

多线程应用
在实时系统中,尽管有想queues,pipes等这些同步机制,使用全局变量实现两个任务共享信息的做法依然很常见。即使在你的程序中加入了抢占式调度器,你的编译器依然无法知道什么是上下文切换,或何时发生上下文切换。因此从概念上讲,多任务修改全局变量的的做法与中断服务程序中修改全局变量的做法是相同的。因此,所有这类全局变量都应该声明为volatile。

在多线程、多任务应用中,全局变量被多个任务读写
在C中,有volatile关键字,它的作用就是在多线程时保证变量的内存可见性,但是具体怎么理解呢?
比如对于一个四核三级缓存的CPU,它的缓存结构是这样的。

我们可以看到L3是四个核共有的,但是L2,L1其实是每个核私有的,如果我有一个变量var,它会被两个线程同时读取,这两个线程在两个核上并行执行,因为我们的缓存原理,这个var可能分别在两个核的 L2或L1缓存,这样读取速度最快,但是该var值可能就分别被这两个核分别修改成不同的值, 最后将值回写到L3或L4主存,此时就会发生bug了。
所以volatile关键字就是预防这种情况,对于被volatile修饰的的变量,每次CPU需要读取时,都至少要从L3读取,并且CPU计算结束后,也立刻回写到L3中,这样读写速度虽然减慢了一些,但是避免了该值在每个core的私有缓存中单独操作而其他核不知道。

Guess you like

Origin blog.csdn.net/Michael177/article/details/121345597