原创 Volatile关键字的说明

2013-1-21 11:06 868 13 13 分类: MCU/ 嵌入式 文集: MCU

 

今天用KEIL调试一段PID的程序发现了下面的现象,
24343357_1346246161emMg.jpg
24343357_13462461635t54.jpg
24343357_1346246165tumv.jpg
 
 
 

PC指针运行到第一个函数的时候(rIn = sensor ();),rIn=99;rout=2.5;

PC指针运行到第二个函数的时候(  rOut = PIDCalc (&sPID,rIn );)rIn=99;rout=99;

PC指针运行到第三个函数的时候(rOut=actuator ( rOut );)rIn=99;rout=2;

在中间时刻rout值出现很大的误差这是为啥呢?

第一反应是KEIL代码优化的结果。KEIL默认优化等级是8级,我们选择0级,也就是不优化。

24343357_1346246168H75D.jpg24343357_13462461706iVb.jpg

 

看图片,在第二个函数的时候,rout没有出现99的值,这才是对的。

24343357_1346246174xZf1.jpg

你也可以在rout变量定义前加入Volatile关键字,告诉编译器,每次重新从变量地址中读取数值,而不是数值副本,或者说是数据的寄存器,也就是让编译器不要优化这段代码。我在程序添加了t=1,2,3,这几句,你可以自己试试看看没有Volatile关键字,单步执行,会出现什么情况。

24343357_134624617801IG.jpg

你也可以使用全局变量,这样编译器也不会优化rout 了。

 

24343357_1346246182miHX.jpg

 

具体原因请看下面分析,我摘抄了几段网上文章,我认为你可以看懂的啊。

 

http://www.cnblogs.com/yc_sunniwell/archive/2010/06/24/1764231.html

volatile提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。

下面举例说明。在DSP开发中,经常需要等待某个事件的触发,所以经常会写出这样的程序:
short flag;
void test()
{
do1();
while(flag==0);
do2();
}

    这段程序等待内存变量flag的值变为1(怀疑此处是0,有点疑问,)之后才运行do2()。变量flag的值由别的程序更改,这个程序可能是某个硬件中断服务程序。例如:如果某个按钮按下的话,就会对DSP产生中断,在按键中断程序中修改flag为1,这样上面的程序就能够得以继续运行。但是,编译器并不知道flag的值会被别的程序修改,因此在它进行优化的时候,可能会把flag的值先读入某个寄存器,然后等待那个寄存器变为1。如果不幸进行了这样的优化,那么while循环就变成了死循环,因为寄存器的内容不可能被中断服务程序修改。为了让程序每次都读取真正flag变量的值,就需要定义为如下形式:
volatile short flag;
    需要注意的是,没有volatile也可能能正常运行,但是可能修改了编译器的优化级别之后就又不能正常运行了。因此经常会出现debug版本正常,但是release版本却不能正常的问题。所以为了安全起见,只要是等待别的程序修改某个变量的话,就加上volatile关键字。

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

比如:
static int i=0;
int main(void)
{
...
while (1)
{
if (i) do_something();
}
}
/* Interrupt service routine. */
void ISR_2(void)
{
i=1;
}
    程序的本意是希望ISR_2中断产生时,在main当中调用do_something函数,但是,由于编译器判断在main函数里面没有修改过i,因此可能只执行一次对从i到某寄存器的读操作,然后每次if判断都只使用这个寄存器里面的“i副本”,导致do_something永远也不会被调用。如果变量加上volatile修饰,则编译器保证对此变量的读写操作都不会被优化(肯定执行)。此例中i也应该如此说明。
    一般说来,volatile用在如下的几个地方:
1、中断服务程序中修改的供其它程序检测的变量需要加volatile;
2、多任务环境下各任务间共享的标志应该加volatile;
3、存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能由不同意义;
另外,以上这几种情况经常还要同时考虑数据的完整性(相互关联的几个标志读了一半被打断了重写),在1中可以通过关中断来实现,2中可以禁止任务调度,3中则只能依靠硬件的良好设计了。

 

http://www.chsi.com.cn/xy/com/200906/20090610/25512445.html

volatile关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改。

 

用volatile关键字声明的变量i每一次被访问时,执行部件都会从i相应的内存单元中取出i的值。 


没有用volatile关键字声明的变量i在被访问的时候可能直接从cpu的寄存器中取值(因为之前i被访问过,也就是说之前就从内存中取出i的值保存到某个寄存器中),之所以直接从寄存器中取值,而不去内存中取值,是因为编译器优化代码的结果(访问cpu寄存器比访问ram快的多)。

 
以上两种情况的区别在于被编译成汇编代码之后,两者是不一样的。之所以这样做是因为变量i可能会经常变化,保证对特殊地址的稳定访问。 
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文件,输入下面的代码: 
#i nclude <stdio.h> 
void main() 

  int i=10; 
  int a = i; 
  printf("i= %dn",a); 
  //下面汇编语句的作用就是改变内存中i的值,但是又不让编译器知道 
  __asm { 
  mov dword ptr [ebp-4], 20h 
   } 
  int b = i; 
  printf("i= %dn",b); 

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

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

#i nclude <stdio.h> 
void main() 
 { 
  volatile int i=10; 
  int a = i; 
  printf("i= %dn",a); 
  __asm { 
  mov dword ptr [ebp-4], 20h 
  } 
  int b = i; 
  printf("i= %dn",b); 
 } 
分别在调试版本和release版本运行程序,输出都是: 
  i = 10 
  i = 32 
这说明这个关键字发挥了它的作用!

 

 

http://hi.baidu.com/chcwdpwnofeqtze/item/5685770d6c544131a3332a93,也是一篇很好的文章,读者自己看,

如果链接不可以打开,你google,按照下图片。

 

24343357_1346298951Fa6p.jpg

 

 

最后附上KEIL中优化级别的意思。

 

Keil C51中的优化级别及优化作用

级别 说明
0   常数合并:编译器预先计算结果,尽可能用常数代替表达式。包括运行地址计算。
    优化简单访问:编译器优化访问8051系统的内部数据和位地址。
    跳转优化:编译器总是扩展跳转到最终目标,多级跳转指令被删除。

1   死代码删除:没用的代码段被删除。
    拒绝跳转:严密的检查条件跳转,以确定是否可以倒置测试逻辑来改进或删除。

2   数据覆盖:适合静态覆盖的数据和位段被确定,并内部标识。BL51连接/定位器可以通过

    全局数据流分析,选择可被覆盖的段。

3   窥孔优化:清除多余的MOV指令。这包括不必要的从存储区加载和常数加载操作。

    当存储空间或执行时间可节省时,用简单操作代替复杂操作。

4   寄存器变量:如有可能,自动变量和函数参数分配到寄存器上。为这些变量保留的存储区     就省略了。
    优化扩展访问:IDATA、XDATA、PDATA和CODE的变量直接包含在操作中。在多数时间没必     要使用中间寄存器。
    局部公共子表达式删除:如果用一个表达式重复进行相同的计算,则保存第一次计算结       果,后面有可能就用这结果。多余的计算就被删除。
    Case/Switch优化:包含SWITCH和CASE的代码优化为跳转表或跳转队列。

5   全局公共子表达式删除:一个函数内相同的子表达式有可能就只计算一次。中间结果保存     在寄存器中,在一个新的计算中使用。
    简单循环优化:用一个常数填充存储区的循环程序被修改和优化。

6   循环优化:如果结果程序代码更快和有效则程序对循环进行优化。

7   扩展索引访问优化:适当时对寄存器变量用DPTR。对指针和数组访问进行执行速度和代       码大小优化。

8   公共尾部合并:当一个函数有多个调用,一些设置代码可以复用,因此减少程序大小 。

9   公共块子程序:检测循环指令序列,并转换成子程序。Cx51甚至重排代码以得到更大的循     环序列。

 

文章评论0条评论)

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