C语言之volatile【整理】

根据c/c++语法,const能够出现的地方,volatile几乎也均可以出现。可是,const修饰的对象其值不能改变,而volatile修饰的对象其值能够随意地改变,也就是说,volatile对象值可能会改变,即便没有任何代码去改变它。在这一点上,最典型的例子就是内存映射的设备寄存器和多线程中的共享对象。懂得使用volatile也是一门小小的艺术。使用volatile约束符能够阻止编译器对代码过度优化防止出现一些你意想不到的状况,达不到预期的结果;过频地使用volatile极可能会增长代码尺寸和下降性能。下面举个例子来讲明volatile在优化中的微妙做用。 
1.阻止编译器优化
  ARM Evaluator-7T模拟单机板使用基于内存映射的设备寄存器叫特殊寄存器,用来
控制和交互外围设备。CPU对内存的操做能够作到按位进行,而特殊寄存器是4字节对齐并占四个字节。你能够象unsigned int变量同样操做特殊寄存器(有些人可能更喜欢uint32_t,认为这样体现寄存器占用4个字节的特色。uint32_t在C99 头文件<stdint.h>中有定义)。而这里,为了体现寄存器自己做为寄存器的含义而非它的物理意义的,咱们作以下定义:
typedef uint32_t special_register;
  Evaluator-7T板子上有一个按钮(能够认为是外设之一)。按下该按钮能够对IOPDATA寄存器第8位置1,相反,释放按钮会将该位从新清0。咱们使用枚举方法为IOPDATA寄存器的第8位置定义一个掩码mask:
enum { button = 0x100 };
IOPDATA寄存器对应的地址为0x3FF5008,咱们能够用宏形象地定义IOPDATA:
#define IOPDATA (*(special_register *)0x03FF5008)
有了这个定义,咱们执行下面的循环就可使CPU一直等待该按钮被按下:
while ((IOPDATA & button) == 0)
    ;
  然而这个指望必须创建在编译器不对代码进行优化的前提假设之上。若是编译器优化这段代码,那么它会认为在这个循环中没有什么会改变IOPDATA并且认为条件判断结果老是真或假,最终优化的结果是只对(IOPDATA & button)==0判断一次,以后的循环都不在对其进行判断,其等同于:
if ((IOPDATA & button) == 0)
    for (;;)
        ;
  显然,若是条件判断结果为真(那么以后都会认为是真),那么这段代码将会陷入死循环。若是判断为假,那么循环就此结束。能够看出,优化的代码效率更高,由于每次循环相比原来的执行时间要短。不幸的是,这段优化代码使得它根本就不能响应按钮的每次动做。那么,如何解决这个问题呢?解决的关键就是不要让编译器优化这段代码,使用volatile就能够办到这一点。咱们修改前面关于IOPDATA的宏定义:
#define IOPDATA (*(special_register volatile *)0x03FF5008)
这个定义将IOPDATA 定义为volatile类型的寄存器。volatile隐含地告诉编译器特殊寄存器可能会改变内容,即便没有任何显式地代码去改变它的内容。这样一来,编译器就不对IOPDATA做优化,而是每次都去访问IOPDATA,这其实正是咱们所指望的。
2.无心中下降了效率
  有时候,若是不注意的话,使用volatile会无心中下降代码效率。举个例子。Evaluator-7T有一个七段数码显示器。
  在IOPDATA 寄存器中第10到16位用来控制显示器的每一段。好比第10位就是用来控制顶部的那段显示,置1则点亮它,清0则熄灭它。咱们能够定义一个掩码mask来覆盖从第10到16的全部位:
enum { display = 0x1FC00 };
假设变量b用来控制这7段显示器的每一段显示,而且b的值已经你想要设置值(准备用来显示哪几段和熄灭哪几段,其它无关的位均为0)。那么你想要改变设置新的显示方式的操做就是:
IOPDATA = b;
可是这种赋值可能会改变第10到16位以外的其它位,这是咱们不指望的。因此,采用下面的方法更好:
IOPDATA |= b
可是,使用 |= 并不能熄灭那些已经点亮的显示段(1 | 0 -> 1),因此咱们能够用下面的函数达到目的:
void display_put(uint32_t b)
{
    IOPDATA &= ~display;    /*熄灭全部的段*/
    IOPDATA |= b;        /*点亮想要的段*/
}
  不过,可能没想到的是这样的操做在无心中下降了代码效率。由于咱们定义IOPDATA为
volatile类型,它阻止了编译器对代码的优化,要求任何读写IOPDATA的操做都死死板板地进行。IOPDATA &= ~display的等价表现为IOPDATA = IOPDATA & ~display,也就是先从IOPDATA读出内容而后与上~display,最后又回写IOPDATA。同理,IOPDATA |=b也有类似的过程。整个过程分别有2次读IOPDATA和2次写IOPDATA的操做。若是IOPDATA不使用volatile,那么编译器会要求将IOPDATA & ~display的结果放在CPU寄存器中,直到完成IOPDATA |= b操做才写回特殊寄存器IOPDATA。显而后者较以前者分别省掉了1次读IOPDATA和1次I写OPDATA的耗时操做(外设操做是最耗时的),效率要高不少。若是你想使用volatile但又能使能优化功能,你能够将函数做以下的修改:
void display_put(uint32_t b)
{
    register uint32_t temp = IOPDATA;/*定义局部变量*/
    temp &= ~display;         /*读取IOPDATA内容到temp*/
    temp |= b;              /*将temp内容或上b*/
    IOPDATA = temp;          /*将结果写回IOPDATA*/
}
这样作有点烦琐,下面的等效方法更简单:
void display_put(uint32_t b)
{
    IOPDATA = (IOPDATA & ~display) | b;
}
结论:从该例子看出,它并不鼓励使用volatile,即便要用也要很当心,由于volatile可能在无心中下降了代码效率,而你却没法察觉。可是,咱们说,不鼓励并非说就不能或不要用,而是要懂得什么时候用,怎么用好它。其所谓智用了。
在上文中提到,volatile定义的对象其内容可能会突然的变化。换句话讲,若是你定义了一个volatile对象,就等于你告诉编译器该对象的内容可能会改变,即便代码中没有任何语句去改变该对象。编译器访问非volatile对象和volatile对象的方式很不同。对于前者(经优化后),它先将非volatile对象的内容读到CPU寄存器中,等操做CPU寄存器一段时间后,才最终将CPU寄存器的内容写回volatile对象。然而,对于volatile对象就没有这种优化操做。这时候编译器有些“笨”,代码要求它读取或写入volatile,它就立刻如实地去作。前一篇《慎重使用》主要讲述如何明智地正确使用volatile,本篇文章经过一些实际应用进一步阐述volatile在解决嵌入式问题中的一些微妙做用并继续深刻探讨使用volatile要注意的一些细节问题。 
 
1.构造内存映射的设备寄存器
  许多处理器使用内存映射的I/O设备。这些设备将寄存器映射到普通内存空间的某些固定地址。这些基于内存映射的设备寄存器看起来与通常的数据对象没啥两样。在《慎重使用》中提到ARM Evaluator-7T 的特殊寄存器的定义为:
typedef uint32_t special_register;
在嵌入式应用中,许多设备有时候不只仅与一个寄存器打交道,有时可能与多个寄存器的集合同时打交道。在Evaluator-7T板子上,串口UART就是一个很好的例子。在这个板子上有两个UART,UART0和UART1。每一个UART都由6个特殊寄存器控制。咱们能够经过一个数据结构来表示这些寄存器的集合:
 
注意:数据结构UART和标识符UART的不一样使用方法和位置。
 
typedef struct UART UART; 
struct UART
{
  special_register ULCON; 
  special_register UCON;    /*控制*/
  special_register USTAT;     /*状态*/
  special_register UTXBUF;    /*发送缓冲*/
  special_register URXBUF;    /*接收缓冲*/
  special_register UBRDIV;    
};
UART0对应的特殊寄存器被映射到0x03FFD000。咱们有两种方法来访问该寄存器,一种是《智用篇》中提到过的宏定义方法:
#define UART0 ((UART *)0x03FFD000)
另外一种是经过常量指针:
UART *const UART0  = (UART *) 0x03FFD000;
 
2.使用volatile
  《慎重使用》提到,若是你不但愿编译器对你的代码做优化以防止出现你预想不到的状况,那么使用volatile是不二之选。显然,要访问串口的设备寄存器,咱们必需要关掉编译器优化。如今,volatile能够大显身手了。咱们修改前面的定义为:
#define UART0 ((UART volatile *) 0x03FFD000)
或:
UART volatile *const UART0  = (UART *) 0x03FFD000;
若是使用后者(常量指针),就建议作强制转化:
UART volatile *const UART0  = (UART volatile *)0x03FFD000;
但这并非必须。对于任意类型T,c编译器提供T指针到volatile T指针的标准内置转化,就如同T指针到const T指针的转化,整个过程自动完成。另外,将一个对象定义为volatile类型,那么该对象中的全部成员也将成为volatile类型。显然,在UART0前面加volatile类型,不可避免在其它地方也必需要加上volatile。
好比,咱们有下面的函数实现串口的字符串输出:
void put(char const *s, UART *u);
若是UART0是指向UART对象的volatile指针,那么以下调用会有什么问题呢:
put("hello, world/n", UART0);   
编译出错通不过!由于编译器不会将volatile UART指针转化为UART指针,因此咱们能作的就是将其强制转化:/*UART == struct UART*/
put("hello, world/n", (UART *)UART0);/*volatile UART -> UART*/
这个强制转化虽然骗过了编译器,但在运行态(run time)可能会出问题。由于这时编译器将volatile类型UART0当作非volatile类型使用。为了不这个缺陷,能够这样声明:
void put(char const *s, UART volatile *u);
注意:在这里加了volatile以后,在其它相关的地方别忘了也要加上volatile!
 
2.准确地构造寄存器
  先看下面对UART0的声明:UART volatile*const UART0 = ...;
这种添加volatile的同时还添加const的作法有下面微妙的隐含功能:UART结构自己并非volatile的,这个声明使得UART0指向一个volatile类型的UART常量对象。然而,其它的串口好比UART1有可能不是定义成volatile类型(有可能将UART1定义成UART类型)。除非系统确实有这样区分的须要,不然这种不一致并非值得提倡的编程风格。解决这种问题的方法之一就是将volatile引入到UART类型:
typedef struct UART volatile UART;
有些人可能更愿意这么定义:
typedef volatile struct UART UART;
但我本人推荐将const/volatile放到类型右侧的定义风格(即前者的风格)。使用上面的定义,咱们不用担忧哪里是否遗漏了volatile。另外,UART0的定义也修正为:
#define UART0 ((UART *)0x03FFD000)
或
UART *const UART0  = (UART *) 0x03FFD000;
而put函数也修正为:
void put(char const *s, UART *u);
这时的UART已经是volatile类型。若是UART1定义成UART类型,那么显然它也是volatile类型。先打住,假若有人将UART1定义为struct UART呢?:
struct UART *const UART1  = (struct UART *) 0x03FF...;
哎呀,没错!咱们遗漏了有人可能用struct UART 定义UART1的可能,这种定义使得
对UART1的访问仍是非volatile方式。到此,咱们能够看出将UART 定义为 volatile struct UART 并不能防止有人作出不恰当或不统一的定义。因此,想从根本上解决这种不一致的问题,就只能这么定义:
typedef struct   
{   
  special_register ULCON; 
  special_register UCON;     /*控制*/
  special_register USTAT;      /*状态*/
  special_register UTXBUF;     /*发送缓冲*/
  special_register URXBUF;     /*接收缓冲*/
  special_register UBRDIV;
} volatile UART;
这样使得任何使用UART的地方都是volatile类型的。
或:
struct UART
{    
  special_register volatile ULCON;
  special_register volatile UCON;    /*控制*/
  special_register volatile USTAT;     /*状态*/
  special_register volatile UTXBUF;   /*发送缓冲*/
  special_register volatile URXBUF;    /*接收缓冲*/
  special_register volatile UBRDIV;
};/*UART结构每一个成员都是volatile 类型*/
虽然咱们用上面的方法解决UART结构类型和struct UART类型的不统一,但能够看出special_register不是固有的volatile类型,因此在别的地方,特殊积存器可能不是volatile类型(有人可能不须要用UART来定义寄存器组,他要的只是用special_register定义单个寄存器)。为了从根本上完全解决这种潜在问题,须要将special_register
做以下定义:
typedef uint32_t volatile special_register;
 
这样一来,不论你定义寄存器组仍是单个寄存器都是volatile类型的!
 
总结:本篇文章始终围绕设备寄存器定义,就volatile到底该用在什么地方,该用在什么位置展开深刻的分析讨论,最终获得将special_register定义为volatile类型是嵌入式应用中最理想的设计。
上文主要探讨关于volatile在定义设备寄存器时应该放到什么位置最合适的问题。另外,在文章中也提到下面两个观点:
*对任意数据类型T,C提供一种标准内置的转换。这个转化能够完成从T指针到volatile T指针的转换,并规定其逆过程即volatile T指针向T指针转换为非法。
*const指针和volatile指针在转换规则方面具备类似性。
本篇文章就后一个观点继续深刻探讨。
本人认为const指针的转换规则与const指针的基本一致,所以只要咱们懂得其中的一种规则,那么另外的一种就能够不攻自破。关键就是要懂得其中的共同规律,而不是去死记硬背一些具体应用。 
1.自相矛盾
T *p;
...
void f(T const *qc);
若是调用f(p)将p传入函数,T指针将会转换成const T指针。整个转换过程是自动完成的,没必要人为地强制转换T指针。这是一条潜规则。相反,在下面状况下,若是调用g(pc),就会产生编译错误:
T const *pc;
...
void g(T *q);
由于编译器拒绝将const T指针转换成T指针。这也是一条潜规则。
让记住下面的推断:若是你许诺你使用const是不让其它程序改变const对象的内容,那么你本身在后面编写const相关代码时必需要遵照这个许诺。就象一些作官的,表面是一套,背后又是另外一套,最后对本身的所作所为不能自圆其说!
下面举个简单的例子来讲明诺言是怎么许下,又是怎么被打破的。
假设有人写了下面的代码:
int const *p;
显然,他但愿经过const阻止任何有意图去修改const对象的内容的行为,可他又继续写下了"挨扁"的代码:
*p += 3; /*改变p指向的内容*/
++(*p);
由于,他本身又去修改p指针指向的内容,自相矛盾啊!!!
那让咱们回头看原先的代码:
T const *pc;
...
void g(T *q);
当你定义const类型的指针pc,等价于你对编译器许诺说我决不容许有代码直接地或间接地甚至潜在地去修改pc指向的内容。固然,咱们的编译器是“大好人”,确定会爽快地答应。接着,你又对编译器许诺说g函数能够修改经过q传入的任何指针的内容。最后你试着调用g(pc)将p经过pc传入g。这时编译器确定看不过去了,必定会这样地质问你:
你为什么将const指针pc传入可能会改变pc指向内容的g函数呢,你不是决不容许其它代码直接地或间接地甚至潜在地去修改pc指向的内容吗,你如今将pc传入g函数不是本身打本身嘴巴吗?嘿嘿,哑口无言了吧!因此,既然作出了许诺,就要坚持到底
继续下面的代码:
T *p;
...
void f(T const *qc);
显然,你许诺编译器说任何代码均可以改变p指向的内容而且你编写的f函数不会改变经过qc传入的其它指针指向的内容。编译器又一次爽快地答应了你。最后你调用了f(p)。此次,编译器只是对你笑笑,心理暗自道:小样你可别让我逮到在f函数中调用诸如g之类可能会改变p指向的代码哦!
2.Const vs Volatile
前面提过,const指针的转换规则与const指针的基本一致。不一样的是const是你答应编译器不会编写可能改变const对象指向的内容的代码,而volatile则是编译器答应你不会对相关代码进行优化。
看下面的代码:
T volatile *pv;
...
void g(T *q);
对比const能够知道,调用g(pv)确定会出现编译错误。由于你跟编译器说不要间接或直接地甚至潜在地优化pv相关的代码,同时你又有跟编译器说它能够优化经过q传入的指针的相关代码。若是你调用g(pv),将不能优化的pv传入可能会优化pv的g函数,显然也是危险而且自相矛盾的作法。
再看:
T *p;
...
void h(T volatile *qv);
对比const能够知道,调用h(p)不会有事,由于编译履行了它的诺言,不在h函数中优化经过qv传入的任何指针相关的代码。
结论:const指针的转换规则与const指针的基本一致,主要的不一样在于谁许下了诺言。对于const,诺言的主体是咱们本身,而对于volatile则是编译器。不论谁许了诺,都必须遵照并兑现它。
相关文章
相关标签/搜索