volatile的用法与陷阱

基本用法

一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。下面是volatile变量的几个例子:
1). 并行设备的硬件寄存器(如:状态寄存器)
2). 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)
3). 多线程应用中被几个任务共享的变量

如果变量定义的时候用了关键字volatile修饰,但是在其他文件引用时不加volatile变量修饰,同样会被编译器优化掉。反过来想想,原因还是很简单的,MDK编译多个文件时是分别编译,最后再用链接器链接。当编译的时候一个模块引用另外一个模块的变量时,完全是靠的变量声明。如果声明都不加volatile,那么引用的模块肯定会把变量当成普通变量。再反推一下,如果原变量没有加volatile,但是声明的时候加了volatile,是不是引用的模块会将这个变量当成volatile型变量呢? 

  C编译器是以每个C文件作为基本编译单元的,称为模块,被编译为obj;而模块之间的函数或变量访问都是通过标号来实现的,标号本身没有任何属性,只是提供给链接器使用的一个符号名称而已,标号的属性完全就靠调用的地方的原型声明来决定的!因此,在一个.C模块中定义为volatile,仅仅是在.C模块中告诉编译器不要优化而已,在另外的模块内使用了这个变量,而它们是不知道该变量是什么属性的,所以只有靠原型声明来告诉编译器这些信息了。

陷阱

引用博客地址: volatile的陷阱

对于volatile关键字,大部分C语言的教程都是一笔带过,并没有做太深入的分析,所以这里简单的整理了一些关于volatile的使用注意事项。实际上从语法上来看volatile和const是一样的,但是如果const用错,几乎不会有什么问题,而volatile用错,后果可能很严重。所以在volatile的使用上建议大家还是尽量求稳,少用一下没有切实把握的技巧。

首先看下面两个定义的区别:

unsigned char *volatile reg;

  这行代码里reg是指针类型,存储一个指向unsigned char 类型的指针。volatile修饰的是reg这个变量即为指针变量是volatile的,但是指针指向的unsigne char 内容不是volatile。在实际使用的时候编译器对代码中指针变量reg本身的操作不会优化,但是对reg所指的内容 "*reg"却会作为no-volatile内容处理,对“*reg”的操作还是会被优化。通常这种写法一般用在对共享的指针变量上,即这个指针变量有可能会被中断函数修改,将其定义为volatile后编译器每次取指针变量值的时候都会从内存载入,这样即使这个变量已经被别的程序修改了,当前函数用的时候也能得到修改后的值(否则通常只在函数开始取一次放在寄存器里,以后就一直使用寄存器内的副本)。

volatile unsigned char *reg;

这行代码里volatile修饰的是指针所指的内容,所以这里定义了一个unsigned char类型的指针,并且这个指针指向的是一个volatile的对象,但是指针变量本身不是volatile。如果对指针变量reg本身进行计算或赋值等操作,是可能被编编译器优化的。但是对reg所指的内容*reg的引用却禁止编译器优化。因为这个指针所指的是一个volatile的对象,所以编译器必须保证对*reg的操作都不被优化。通常在驱动程序的开发中,对硬件寄存器指针的定义,都应该采用这种形式。

volatile unsigned char *volatile reg;

这样定义出来的指针本身就是个volatile变量,又指向了volatile的数据内容。

---------------------------------------------【分割线】----------------------------------------------------

volatile 和 const 的合用

  从字面上看volatile 和 const 似乎是一个对象的两个对立属性,是互斥的。但实际上,两者是有可能一起修饰同一个对象的。看看下面的这行声明:

extern const volatile unsigned int rt_clock;

这个是在RTOS系统内核中常见的一种声明:rt_clock通常是指系统时钟,他经常被时钟中断进行更新,所以他是volatile的,易变的。因此在用的时候要让编译器每次从内存里面取值。而rt_clock通常只有一个写者(时钟中断),其他地方的使用通常是只读的,所以将其声明为const,表示这里不应该修改这个变量。所以volatile和const是两个不矛盾的东西,并且一个对象同时具备这两种属性也是具有实际意义的。

  注意:上面这个例子里要注意声明和定义是const的使用,在需要读写rt_clock变量的中断函数里应该如下定义

//在需要读写rt_clock变量的中断函数里如下定义
unsigned char *volatile reg;
//在提供给用户的声明头文件中可以如此申明
external const volatile unsigned int rt_clock;

再看一个例子

volatile struct deveregs *const dvp = DEVADDR;

   这里的volatile和const实际上是分别修饰了两个不同的对象:volatile修饰的是指针dvp所指向的类型为struct devregs的数据结构,这个结构对应设备的硬件寄存器,所以是易变的,不能被优化的;而后面的const修饰的是指针变量dvp。应为硬件寄存器地址是一个常量,所以这个指针变量定义成const的不能被修改。

---------------------------------------------【分割线】----------------------------------------------------

危险的volatile用法

例:定义为volatile的结构体成员

考察下面对一个设备硬件寄存器的结构类型定义:

struct deveregs
{
   unsigned short volatile csr;
   unsigned short const volatile data; 
};

   我们的原意是希望声明一个设备的硬件寄存器组,其中有一个16bit的CSR寄存器,这个寄存器可以由程序向设备写入控制字,也可以由硬件设备设置反应其工作状态。另外还有一个16bit的data数据寄存器,这个寄存器只会有硬件进行设置由程序进行读入。

  看起来这个结构的定义没有什么问题,也符合实际情况。但是如果执行下面这样的代码时,会发生什么情况呢。

struct deveregs *const dvp = DEVADDR;

while((dvp->csr & (READY | ERROR)) == 0)
{
     //NULL wait till done
    ;   
}

通过一个non-volatile的结构体指针,去访问被定义为volatile的结构体成员,编译器将如何处理?答案是:undefined!C99标准没有对编译器在这种情况下的行为做规定,随意编译器有可能正确的将dvp->car作为volatile的变量来处理,使程序运行正常;也有可能就将dvp->csr作为普通的non-volatile变量来处理,在while当中优化为只有开始的时候取值一次,以后每次循环始终使用第一次取来的值而不再从硬件寄存器里读取,这样上面的代码就有可能死循环。

注:Keil MDK编译器能正确的将dvp->car作为volatile的变量来处理。

  如果你使用一个volatile的指针来指向一个非volatile的对象,比如将一个non-volatile的结构体赋给一个volatile的指针,这样对volatile指针所指向结构体的使用都会被编译器认为是volatile的,即便那个对象没有被声明volatile。然而反过来,如果将一个volatile对象的地址赋值给一个non_volatile的普通变量,通过这个指针访问volatile对象的结果是undefined,是危险的。

 所以对于本例中的代码,我们应该修改成这样:

struct devregs
{
    unsigned short csr;
    unsigned short data;
};

volatile struct devrges *const dvp = DEVADDE;

   这样我们才能保证通过dvp指针去访问结构体成员的时候都是volatile来处理的。

例:定义为volatile的结构体类型

考察如下代码:

volatile struct devregs
{
    .....
}dev1;

struct devregs dev2;

作者目的也许是希望定义一个volatile的结构体类型,然后顺便定义一个这样的volatile结构体变量,因此定义了一个dev2。然而第二次所定义的dev2变量实际上是non-volatile的!因为实际在定义结构体类型时那个关键字volatile修饰的是dev1这个变量而不是struct devregss类型的结构体,所以这个代码应该写成这样:

typedef volatile struct devres
{
 .....
}devregs_t;

devregs_t dev1;
devregs_t dev2;

这样我们才能得到两个volatile的结构体变量。

 例:多次的间接指针引用

考察如下代码:

struct bd
{
    unsigned int state ;
    unsigned char *data_buff;
};

struct devregs
{
    unsigned int csr;
    struct bd *tx_bd;
    struct bd *rx_bd;
};

volatile struct devregs *const dvp = DEVADDR;

dvp->tx_bd->state = READY;

while((dvp->tx_bd->state & EMPTY | ERROR) == 0)
{
  ;
}

 

这样的代码常用在对一些DMA设备的发送buffer上。通常这些buffer descriptor(BD)当中的状态会由硬件进行设置已经告诉软件buffer是否完成发送或接收。但是请注意上面的代码中对dvp->ta_bd->state的操作实际上是non-volatile的,这样的操作有可能因为编译器对其读取的优化而导致后面陷入死循环。

  因为虽然dvp已经定义volatile的指针了,但是也只有向其指向的deVregs结构才属于volatile object的范围,也就是说将dvp声明为指向volatile数据的指针可以保障其所指的volatile object之内的tx_bd这个结构体成员自身是volatile变量,但是并不能保证这个指针变量所指的数据也是volatile的(因为这个指针并没有声明为指向volatile的指针)。

  要让上面的代码工作如下定义

struct devregs
{
    unsigned int csr;
    volatile struct bd *tx_bd;
    volatile sturct bd *rx_bd;
};

 这样可以保障对state成员的处理是volatile的。不过最为稳妥和清晰的办法是这样

 

volatile struct devregs *const dvp = DEVADDR;
volatile struct bd *tx_bd = dvp->tx_bd;

tx_bd->state = READY;
whlie((tx_bd->state &(EMPTY | ERROR)) == 0) 
{
   ;
}

 

这样在代码里能绝对保障数据结构的易变性,即使数据结构里面没有定义好也没有关系。而且对于日后的维护也有好处:因为这样从代码里一眼就能看出那些数据的访问是必须保证volatile的。

例:到底哪个volatile可能无效

就在前面的几个例子,感觉自己可能都已经弄明白了的时候,请最好看这个例子

struct hw_bd
{
    .......
    volatile unsigned char *volatile buffer;
};

struct hw_bd *bdp;

bdp->buffer = ...; //1
bdp->buffer = ...; //2

 

 请问上面标记了1和2的代码哪个是确实在访问volatile对象,而哪个是undefined的结果。

答案是2是volatile的1是undefined的。来看本例数据结构示意图

        (non-volatile)

bdp->+---------+

    |              |   

    |              |   

    |              |   

          +---------+    (volatile)

          |   buffer   |-->+----------+

           +---------+     /               /

                                /               /

                                /               /

                              +----------+

                              /   buffer    /

                              +----------+

                               /               /

                              /               /

                              +----------+

buffer成员本身是通过一个non-volatile的时钟bdp访问的,按照C99标准的定义,这就属于undefined的情况,因此对bdp->buffer的访问编译器不一定能保证是volatile的;虽然buffer本身可能不是volatile的变量,但是buffer成员是一个指向volatile对象的指针。因此对buffer成员所指向对象的访问编译器可以保证是volatile的,所以bdp->buffer是volatile的。所以,看似简单的volatile的关键字,用起来还是有很多讲究的。

  • 3
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值