volatile(不稳定)
C语言中volatile
关键字很多人都掌握好,很多C书籍也是一两行概括。我将会教你一个合适的方式,理解volatile
。
首先,在嵌入式C或C++代码中,你是否遇到过下面几个情形?
- 只要不开启编译器优化,代码工作得很好。
- 只要中断没有使能,代码工作得很好-
- 片状硬件驱动
- 没有其他进程,RTOS(实时操作系统)任务单独运行将会很好。
只要你认为其中一个是这样的,说明你还不了解volatile
关键字。C中volatile
在变量声明的时候一个修饰符。它将会告诉编译器这个变量将会随时改变,编译器无论怎么样都不会在该代码附近找到这个变量值。这个声明是相当严格的。在验证他们之前,我们先看下语法。
c volatile关键字语法
在变量数据前面或后面加volatile
关键字,来声明一个volatile
变量。例如,一个整型变量foo,的两种方式。
volatile int foo;
int volatile foo;
现在,volatile
通常见于指针的应用中,尤其是在内存映射I/O寄存器中。下面都是申明一个pReg指向一个volatile
无符号8位整型。
volatile uint8_t * pReg;
uint8_t volatile * pReg;
volatile
指向一个不是常变的数据很少见(我认为我用过),但是我还是将继续给出使用的语法:
int *volatile p;
下面只是为了完整性,如果你非要声明一个volatile
指针指向一个volatile
变量,应该写成下面形式:
int volatile*volatile p;
最后,如果你将volatile
修饰一个结构体或者两合体,那么他们所有的内容都是volatile
。也可以在内部成员进行单独声明。
合理使用volatile关键字
一个变量无论在何时都可能有不可预期的变化,应该将其声明为volatile
型变量。实际上,只有三种类型变量能够这样的改变:
- 内存映射下外围设备寄存器
- 中断服务例程修改的全局变量
- 含有多线程的所任务能够访问的全局变量。
我们将会讨论每种情况。
外围设备寄存器
嵌入式系统包含实时硬件,通常有复杂的外围设备。这些外围设备包含改变值的寄存器,寄存器的值和程序流程是异步的。举个简单例子,一个8位状态寄存器,并且映射内存地址是0x1234。在非零之前要获取寄存器的状态。天真,错误的方法如下:
//错误的方法
uint8_t *pReg=(uint8_t*)0x1234;
//wait for register to become non-zero
while(*pReg==0){
//do something else
}
只要你打开编译器的优化,这个几乎可认为是错误的,由于编译器将会产生先下面的汇编代码:
mov ptr,#0x1234
mov a,@ptr
loop:
bz loop
编译器优化的原理很简单:已经读到变量值到累加器(第二行),不再重复读它,因为这个值通常是一样的。因此,第三行将会无限的进入循环中。为了让编译器做我们想做的方式,应该修改成这样:
//正确的申明
uint8_t volatile*pReg=(uint8_t volatile*)0x1234;
while(*pReg==0){
//do something else
}
这个将会产生这样的汇编:
mov ptr,#0x1234
loop:
mov a,@ptr
bz loop
这就是我们完成我们想要的方式。
具有特殊属性寄存器将会引起更微妙的问题。例如,很多外围设备只是简单读取包含的寄存器,比你想要的多或少的读将会导致不可预期的结果。
中断服务例程
中断服务例程通常设置尝试变量再主线代码中。例如,一些列端口中断将会测试接收到的字符看是否是一个ETX字符(认为标注消息结束)。如果这个字符是ETX,ISR(中断服务程序)将会设备一个全局标志位。一个不正确的例子,可能是这样:
//错误的程序
int ext_rcvd=FALSE;
void main(){
...
while(!ext_rcvd){
//wait
}
...
}
interrupt void rx_isr(void){
...
if(EXT==rx_char){
etx_rcvd=TRUE;
}
...
}
当编译器优化关闭,这个代码可能正常。然而只要50%优化将会导致代码错误。问题在于编译器不知道ext_rcvd
将会在ISR改变。只要将变量声明为volatile
将会解决这个问题。
bool volatile ext_rcvd=FALSE;
void main(){
...
while(!ext_rcvd){
//wait
}
...
}
interrupt void rx_isr(void){
...
if(EXT==rx_char){
etx_rcvd=TRUE;
}
...
}
应用多线程
尽管有队列,管道和其他调度机制在实时系统中,相对常见的两个进程通过共享内存(也就是一个全局的)去两个进程通讯。即使你添加抢占式调度方法,你的编译器还是不知道切换到什么样的上下文或什么时候切换。因此,另一个进程修改一个共享全局,和中断服务程序相类似。所以,所有全局变量应该声明为valatile
变量。.应当这样声明:
int volatile cntr;
void stask(void){
cntr=0;
while(cntr==0){
sleep(1);
}
...
}
void task2(void){
...
cntr++;
sleep(10);
...
}
最后思考
一些编译器允许你将所有变量隐式申明为volatile
。.抵住这样的诱惑,因为它本质是一种思想的替换。也会导致一些可能效率问题。
当然,抵住这个诱惑去怪编译器或关闭。现在的编译器是很好了,我都忘了上次遇到编译器优化问题是在什么时候了。相反的,我遇到编程者过度使用volatile
失败的案例。
如果你给一些代码片区修改,用grep
关键字volatile
,居然没有,这个例子是很好去找问题的地方。