原创 volatile的用法

2011-6-14 17:49 1364 9 9 分类: 测试测量

volatile的本意是“易变的”

由于访问寄存器的速度要快过RAM,所以编译器一般都会作减少存取外部RAM的优化。比如:

static int i=0;

int main(void)
{
...
while (1)
{
if (i) dosomething();
}
}

/* Interrupt service routine. */
void ISR_2(void)
{
i=1;
}

程序的本意是希望ISR_2中断产生时,在main当中调用dosomething函数,但是,由于编译器判断在main函数里面没有修改过i,因此可能只执行一次对从i到某寄存器的读操作,然后每次if判断都只使用这个寄存器里面的“i副本”,导致dosomething永远也不会被调用。如果将将变量加上volatile修饰,则编译器保证对此变量的读写操作都不会被优化(肯定执行)。此例中i也应该如此说明。

一般说来,volatile用在如下的几个地方:

1、中断服务程序中修改的供其它程序检测的变量需要加volatile;

2、多任务环境下各任务间共享的标志应该加volatile;

3、存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能由不同意义;

另外,以上这几种情况经常还要同时考虑数据的完整性(相互关联的几个标志读了一半被打断了重写),在1中可以通过关中断来实现,2中可以禁止任务调度,3中则只能依靠硬件的良好设计了。

===============================================

volatile 的字面含义是易变的,那么将一个变量指示为 volatile是什么意思呢?是告诉编
译器这个变量是易变的?事实上也是如此。在多任务、中断等环境下,变量可能被其他的任
务改变,而编译器无法发现,volatile 就是告诉编译器这个变量在其它任务(或中断)中可
能要修改。  
使用 volatile是与编译器优化有关系的。先看看下面的例子:

#include <avr/io.h>

unsigned char g_Flag=0;

int main(void)
{
g_Flag=1;
g_Flag=0;
while(1);
}

main程序中对g_Flag先赋值1,后赋值0,  显然g_Flag=1是没有意义的,聪明的编译器发
现这个后就产生了如下的代码:

int main(void)
{
  5c:  cf e5         ldi  r28, 0x5F ; 95
  5e:  d4 e0         ldi  r29, 0x04 ; 4
  60:  de bf         out  0x3e, r29 ; 62
  62:  cd bf         out  0x3d, r28 ; 61
g_Flag=1;
g_Flag=0;
  64:  10 92 60 00   sts  0x0060, r1
while(1);
  68:  ff cf         rjmp  .-2        ; 0x68 <main+0xc>

编译器没有生成 g_Flag=1 的机器码, 在优化过程中 g_Flag=1被忽略了。在一般的 C程
序中这是没有问题的,但是如果 g_Flag 是个单片机 I/O口变量那就麻烦了,本来想从 I/O口
输出一逻辑正脉冲的程序就这样被编译器优化掉了,显然不是我们所希望的。这时候该怎么
办呢?请拿出利剑 volatile,他能处理我们所遇到的麻烦。
将 unsigned char g_Flag=0; 该为  volatile unsigned char g_Flag=0;后编译所产生的汇编代
码如下:

int main(void)
{
  5c:  cf e5         ldi  r28, 0x5F ; 95
5e:  d4 e0         ldi  r29, 0x04 ; 4
  60:  de bf         out  0x3e, r29 ; 62
  62:  cd bf         out  0x3d, r28 ; 61
g_Flag=1;
  64:  81 e0         ldi  r24, 0x01 ; 1
  66:  80 93 60 00   sts  0x0060, r24
g_Flag=0;
  6a:  10 92 60 00   sts  0x0060, r1
while(1);
  6e:  ff cf         rjmp  .-2        ; 0x6e <main+0x12>

在 《AVR 单片机 GCC 程序设计》 第二章中我们展开了 PORTB 这个宏, 对于 AT90S2313
它等同于  *(volatile unsigned char *)(0x38)  ,现在你一定相信这里的 volatile是必不可少的。
尽管如此,有人会认为 “在一般的程序中不需要定义端口宏,因为 avr-libc 都为我们定义好
了,我只要包含 io.h 就可以了,在用户程序中 volatile 是无关紧要的”  ,那么让我们再来看
一下 volatile必不可少的另一种情况。

#include <avr/io.h>
#include <avr/interrupt.h>

unsigned char g_Flag=0;//串口接收标记

ISR(SIG_UART_RECV)
{
... ...
g_Flag=1;
}

int main(void)
{
  ...  ...
while(!g_Flag);//等待串口接收到数据
  
... ...
}

  上面的程序看似没有任何问题,实际上 main 函数是永远也发现不了串口接收数据的。
由于编译器的优化,while(!g_Flag)将生成一个非常有意思的代码,它首先从 g_Flag 对应的
内存读一次数据到一个寄存器中,之后不停的测试此寄存器是否为非零,即使中断程序中已
经改变了 g_Flag 对应内存的值,它还是始终检查一个不再更新的寄存器。那么如何让 while
循环每次都要从内存读取后再测试它是否为零呢?你应该猜到了,就是将 g_Flag 变量指示
volatile,告诉编译器 g_Flag 是易变的,要对它进行保守处理。

volatile 是一种优化指示,编译器优化操作使用一种技术叫做数据流分析,分析程序中
的变量在哪里赋值、在哪里使用、在哪里失效,分析结果可以用于常量合并,常量传播等优
化,从而可以消除死代码。但有时这优化不是程序所希望的,这时可以用 volatile 关键字关
闭这个优化开关。

最后提醒读者,没有必要将所有的变量都指示为 volatile  ,那样做的后果将是代码膨胀
和执行效率降低。如果到现在还是不能确定自己哪些变量应指示为 volatile,请记住一个原
则:将那些中断(或操作系统中其它任务)中改变,主程序中循环检查的状态变量指示为
volatile

===============================================

一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。下面是volatile变量的几个例子:

  1). 并行设备的硬件寄存器(如:状态寄存器)

  2). 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)

  3). 多线程应用中被几个任务共享的变量

  回答不出这个问题的人是不会被雇佣的。我认为这是区分C程序员和嵌入式系统程序员的最基本的问题。嵌入式系统程序员经常同硬件、中断、RTOS等等打交道,所用这些都要求volatile变量。不懂得volatile内容将会带来灾难。

  假设被面试者正确地回答了这是问题(嗯,怀疑这否会是这样),我将稍微深究一下,看一下这家伙是不是直正懂得volatile完全的重要性。

  1). 一个参数既可以是const还可以是volatile吗?解释为什么。

  2). 一个指针可以是volatile 吗?解释为什么。

  3). 下面的函数有什么错误:

  int square(volatile int *ptr)

  {

  return *ptr * *ptr;

  }

  下面是答案:

  1). 是的。一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。

  2). 是的。尽管这并不很常见。一个例子是当一个中服务子程序修该一个指向一个buffer的指针时。

  3). 这段代码的有个恶作剧。这段代码的目的是用来返指针*ptr指向值的平方,但是,由于*ptr指向一个volatile型参数,编译器将产生类似下面的代码:

  int square(volatile int *ptr)

  {

  int a,b;

  a = *ptr;

  b = *ptr;

  return a * b;

  }

  由于*ptr的值可能被意想不到地该变,因此a和b可能是不同的。结果,这段代码可能返不是你所期望的平方值!正确的代码如下:

  long square(volatile int *ptr)

  {

  int a;

  a = *ptr;

  return a * a;

  }

  volatile的本意是“易变的” 由于访问寄存器的速度要快过RAM,所以编译器一般都会作减少存取外部RAM的优化。比如:

  static int i=0;

  int main(void)

  {

  ...

  while (1)

  {

  if (i) dosomething();

  }

  }

  /* Interrupt service routine. */

  void ISR_2(void)

  {

  i=1;

  }

  程序的本意是希望ISR_2中断产生时,在main当中调用dosomething函数,但是,由于编译器判断在main函数里面没有修改过i,因此 可能只执行一次对从i到某寄存器的读操作,然后每次if判断都只使用这个寄存器里面的“i副本”,导致dosomething永远也不会被调用。如果将将变量加上volatile修饰,则编译器保证对此变量的读写操作都不会被优化(肯定执行)。此例中i也应该如此说明。

一般说来,volatile用在如下的几个地方:

  1、中断服务程序中修改的供其它程序检测的变量需要加volatile;

  2、多任务环境下各任务间共享的标志应该加volatile;

  3、存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能由不同意义;

  另外,以上这几种情况经常还要同时考虑数据的完整性(相互关联的几个标志读了一半被打断了重写),在1中可以通过关中断来实

  现,2中可以禁止任务调度,3中则只能依靠硬件的良好设计了。

  关键在于两个地方:

  1. 编译器的优化 (请高手帮我看看下面的理解)

  在本次线程内, 当读取一个变量时,为提高存取速度,编译器优化时有时会先把变量读取到一个寄存器中;以后,再取变量值时,就直接从寄存器中取值;

  当变量值在本线程里改变时,会同时把变量的新值copy到该寄存器中,以便保持一致

  当变量在因别的线程等而改变了值,该寄存器的值不会相应改变,从而造成应用程序读取的值和实际的变量值不一致

  当该寄存器在因别的线程等而改变了值,原变量的值不会改变,从而造成应用程序读取的值和实际的变量值不一致

  举一个不太准确的例子:

  发薪资时,会计每次都把员工叫来登记他们的*号;一次会计为了省事,没有即时登记,用了以前登记的*号;刚好一个员工的*丢了,已挂失该*号;从而造成该员工领不到*

  员工 -- 原始变量地址

  *号 -- 原始变量在寄存器的备份

  2. 在什么情况下会出现(如1楼所说)

  1). 并行设备的硬件寄存器(如:状态寄存器)

  2). 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)

  3). 多线程应用中被几个任务共享的变量

  补充: volatile应该解释为“直接存取原始内存地址”比较合适,“易变的”这种解释简直有点误导人;

  “易变”是因为外在因素引起的,象多线程,中断等,并不是因为用volatile修饰了的变量就是“易变”了,假如没有外因,即使用volatile定义,它也不会变化;

  而用volatile定义之后,其实这个变量就不会因外因而变化了,可以放心使用了; 大家看看前面那种解释(易变的)是不是在误导人

  ------------简明示例如下:------------------

  volatile关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。

使用该关键字的例子如下:

  int volatile nVint;

  >>>>当要求使用volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存。

  例如:

  volatile int i=10;

  int a = i;

  ...

  //其他代码,并未明确告诉编译器,对i进行过操作

  int b = i;

  >>>>volatile 指出 i是随时可能发生变化的,每次使用它的时候必须从i的地址中读取,因而编译器生成的汇编代码会重新从i的地址读取数据放在b中。而优化做法是,由于编译器发现两次从i读数据的代码之间的代码没有对i进行过操作,它会自动把上次读的数据放在b中。而不是重新从i里面读。这样以来,如果i是一个寄存器变量或者表示一个端口数据就容易出错,所以说volatile可以保证对特殊地址的稳定访问。

  >>>>注意,在vc6中,一般调试模式没有进行代码优化,所以这个关键字的作用看不出来。下面通过插入汇编代码,测试有无volatile关键字,对程序最终代码的影响:

  >>>>首先,用classwizard建一个win32 console工程,插入一个voltest.cpp文件,输入下面的代码:

  >>

  #include

  void main()

  {

  int i=10;

  int a = i;

  printf("i= %d",a);

  //下面汇编语句的作用就是改变内存中i的值,但是又不让编译器知道

  __asm {

  mov dword ptr [ebp-4], 20h

  }

  int b = i;

  printf("i= %d",b);

  }

  然后,在调试版本模式运行程序,输出结果如下:

  i = 10

  i = 32

  然后,在release版本模式运行程序,输出结果如下:

  i = 10

  i = 10

  输出的结果明显表明,release模式下,编译器对代码进行了优化,第二次没有输出正确的i值。下面,我们把 i的声明加上volatile关键字,看看有什么变化:

  #include

  void main()

  {

  volatile int i=10;

  int a = i;

  printf("i= %d",a);

  __asm {

  mov dword ptr [ebp-4], 20h

  }

  int b = i;

  printf("i= %d",b);

  }

  分别在调试版本和release版本运行程序,输出都是:

  i = 10

  i = 32

  这说明这个关键字发挥了它的作用!

  ------------------------------------

  volatile对应的变量可能在你的程序本身不知道的情况下发生改变

  比如多线程的程序,共同访问的内存当中,多个程序都可以操纵这个变量

  你自己的程序,是无法判定合适这个变量会发生变化

  还比如,他和一个外部设备的某个状态对应,当外部设备发生操作的时候,通过驱动程序和中断事件,系统改变了这个变量的数值,而你的程序并不知道。

  对于volatile类型的变量,系统每次用到他的时候都是直接从对应的内存当中提取,而不会利用cache当中的原有数值,以适应它的未知何时会发生的变化,系统对这种变量的处理不会做优化——显然也是因为它的数值随时都可能变化的情况。

  -----------------------------

  典型的例子

  for ( int i=0; i<100000; i++);

  这个语句用来测试空循环的速度的

  但是编译器肯定要把它优化掉,根本就不执行

  如果你写成

  for ( volatile int i=0; i<100000; i++);

  它就会执行了

==========================================================================

volatile的本意是一般有两种说法--1.“暂态的”;2.“易变的”。
这两种说法都有可行。但是究竟volatile是什么意思,现举例说明(以Keil-c与a51为例
例子来自Keil FQA),看完例子后你应该明白volatile的意思了,如果还不明白,那只好
再看一遍了。


例1.

void main (void)
{
volatile int i;
int j;

i = 1;  //1  不被优化 i=1
i = 2;  //2  不被优化 i=1
i = 3;  //3  不被优化 i=1

j = 1;  //4  被优化
j = 2;  //5  被优化
j = 3;  //6  j = 3
}
---------------------------------------------------------------------
例2.

函数:

void func (void)
{
unsigned char xdata xdata_junk;
unsigned char xdata *p = &xdata_junk;
unsigned char t1, t2;

t1 = *p;
t2 = *p;
}

编译的汇编为:

0000 7E00    R     MOV     R6,#HIGH xdata_junk
0002 7F00    R     MOV     R7,#LOW xdata_junk
;---- Variable 'p' assigned to Register 'R6/R7' ----

0004 8F82          MOV     DPL,R7
0006 8E83          MOV     DPH,R6

;!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 注意
0008 E0            MOVX    A,@DPTR
0009 F500    R     MOV     t1,A

000B F500    R     MOV     t2,A
;!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
000D 22            RET     

将函数变为:
void func (void)
{
volatile unsigned char xdata xdata_junk;
volatile unsigned char xdata *p = &xdata_junk;
unsigned char t1, t2;

t1 = *p;
t2 = *p;
}

编译的汇编为:
0000 7E00    R     MOV     R6,#HIGH xdata_junk
0002 7F00    R     MOV     R7,#LOW xdata_junk
;---- Variable 'p' assigned to Register 'R6/R7' ----

0004 8F82          MOV     DPL,R7
0006 8E83          MOV     DPH,R6

;!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
0008 E0            MOVX    A,@DPTR
0009 F500    R     MOV     t1,A        a处

000B E0            MOVX    A,@DPTR
000C F500    R     MOV     t2,A
;!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

000E 22            RET     


比较结果可以看出来,未用volatile关键字时,只从*p所指的地址读一次
如在a处*p的内容有变化,则t2得到的则不是真正*p的内容。

---------------------------------------------------------------------
例3


volatile unsigned char bdata var;  // use volatile keyword here
sbit var_0 = var^0;
sbit var_1 = var^1;
unsigned char xdata values[10];

void main (void)  {
  unsigned char i;

  for (i = 0; i < sizeof (values); i++)  {
    var = values;
    if (var_0)  {
      var_1 = 1; //a处
       
      values = var;  // without the volatile keyword, the compiler
                        // assumes that 'var' is unmodified and does not
                        // reload the variable content.
    }
  }
}


在此例中,如在a处到下一句运行前,var如有变化则不会,如var=0xff; 则在
values = var;得到的还是values = 1;

---------------------------------------------------------------------
应用举例:

例1.
#define DBYTE ((unsigned char volatile data  *) 0)

说明:此处不用volatile关键字,可能得不到真正的内容。
---------------------------------------------------------------------

例2.


#define TEST_VOLATILE_C

//***************************************************************
// verwendete Include Dateien
//***************************************************************
#if __C51__ < 600
  #error: !! Keil 版本不正确
#endif

//***************************************************************
// 函数 void v_IntOccured(void)
//***************************************************************
extern void v_IntOccured(void);

//***************************************************************
// 变量定义
//***************************************************************
char xdata cvalue1;          //全局xdata
char volatile xdata cvalue2; //全局xdata

//***************************************************************
// 函数: v_ExtInt0()
// 版本:
// 参数:
// 用途:cvalue1++,cvalue2++
//***************************************************************
void v_ExtInt0(void) interrupt 0 {
  cvalue1++;
  cvalue2++;
}

//***************************************************************
// 函数: main()
// 版本:
// 参数:
// 用途:测试volatile
//***************************************************************

void main() {
char cErg;

//1. 使cErg=cvalue1;
cErg = cvalue1;

//2. 在此处仿真时手动产生中断INT0,使cvalue1++; cvalue2++
if (cvalue1 != cErg)
  v_IntOccured();

//3. 使cErg=cvalue2;
cErg = cvalue2;

//4. 在此处仿真时手动产生中断INT0,使cvalue1++; cvalue2++
if (cvalue2 != cErg)
  v_IntOccured();
  
//5. 完成
  while (1);
}

//***************************************************************
// 函数: v_IntOccured()
// 版本:
// 参数:
// 用途: 死循环
//***************************************************************
void v_IntOccured() {
  while(1);
}


仿真可以看出,在没有用volatile时,即2处,程序不能进入v_IntOccured();
但在4处可以进入v_IntOccured();

文章评论0条评论)

登录后参与讨论
我要评论
0
9
关闭 站长推荐上一条 /2 下一条