1.简单介绍一个定义为volatile的变量,意思是这个变量可能会被意外改变,这样编译器就不会假设这个变量的值了。准确地说,优化器每次使用它时都必须仔细地重新读取这个变量的值,而不是使用存储在寄存器中的备份。如果没有volatile关键字,编译器可能会优化读取和存储,可能会暂时使用寄存器中的值。如果这个变量被其他程序更新,就会出现不一致。简单的说,如果这个变量很重要,你不想让它被编译器优化,就用volatile来修饰它。2.对编译器优化有用如果你在编译器中启用了优化,那就要小心了。以下代码使用IAR7.20,优化级别为High,选择Balanced。main函数的主循环如下:while(1){Delay_ms(500);LCD_refresh_flg=0;延迟_ms(500);如果(LCD_refresh_flg){LCD_refresh_flg=0;LCD_ShowString(0,13,"接收数据");}}其中LCD_refresh_flg变量在串口中断中voidUSART1_IRQHandler(void){if(USART_GetFlagStatus(USART1,USART_FLAG_TC)){USART_ClearFlag(USART1,USART_FLAG_TC);}if(USART_GetFlagStatus(USART1,USART_FLAG_RXNE)){LCD_refresh_flg=1;USART_ClearFlag(USART1,USART_FLAG_RXNE);}}其中延迟时间函数如下:/***@brief插入延迟时间。*@paramnTime:指定延迟时间长度,单位为10ms。*@retval无*/voidDelay_ms(uint32_tnTime){TimingDelay=nTime;while(TimingDelay!=0);}/***@brief递减TimingDelay变量。*@paramNone*@retvalNone*/voidTimingDelay_Decrement(void){if(TimingDelay!=0x00){TimingDelay--;}}/***@brief这个函数处理SysTickHandler。*@paramNone*@retvalNone*/voidSysTick_Handler(void){蒂姆ingDelay_Decrement();}这是一个简单的示例代码(为了说明问题,实际项目开发中应该没有类似的代码),代码设计的意图是串口接收数据,LCD显示string,但是debug如下:执行单步时,直接跳过85行的代码,85行没有断点。查看反汇编代码,确认第85、87、88、89行代码直接由编译器优化。可能编译器认为LCD_refresh_flg变量在85行被清除,86行代码量不大,所以认为87行的if条件不成立,这一切都被优化掉了。编译器没想到我们的延迟竟然长达500ms,并且在串口中断中会把LCD_refresh_flg设置为1,这完全违背了我们的设计意图。如果我们用volatile修改LCD_refresh_flg,volatileuint8_tLCD_refresh_flg;当编译器和优化级别不变时,操作如下:虽然编译器和优化级别相同,但此时不再优化LCD_refresh_flg变量。当然,这是编译优化的结果。并不是说在main函数的主循环中会把变量清零,然后再由编译器对变量进行优化。在下面的代码中,LCD_refresh_flg没有用volatile修改。此时LCD_ShowString函数是一个代码量较大的函数。这时候编译器就不敢轻易优化LCD_refresh_flg这个变量了。希望大家能从这个例子中了解到volatile对编译优化的影响。访问硬件内存映射的硬件寄存器通常也会加上voliate,因为每次对它的读写可能有不同的含义。例如:假设要初始化一个设备,这个设备的某个寄存器是0xff800000。(例子只是为了说明问题,没有实际意义)int*output=(unsignedint*)0xff800000;//定义一个IO口;int初始化(无效){inti;for(i=0;i<10;i++){*output=i;}}经过编译器的优化,编译器认为之前的循环很长一段时间都是废话,对最后的结果没有影响,因为最后只是把输出指针赋给了9,所以编译器最后给你编译编译代码的结果相当于:intinit(void){*output=9;},这显然不符合我们的设计意图。不仅是写操作,还有读操作。如果需要重复读取一个寄存器的值,那么编译器在优化时只读取1次。这时候就应该用volatile来通知编译器遇到这个变量不要优化。这里我再说一件事。我们在这里谈论的是编译器,它们是嵌入式的。不同的编译器会有不同的结果,所以上面的例子不一定会出现在不同的编译器中。但是在嵌入式领域要反复访问硬件寄存器需要加上volatile。中断服务程序中为其他程序检测到的被中断服务程序修改的变量需要加上volatile,中断会突然发生。当在触发中断的程序中修改了变量,但是编译器判断main函数中没有修改变量,因此可以只执行一次从内存到某个寄存器的读操作,然后只读一个每次从寄存器中拷贝变量,使中断程序的运行短路。使用volatile修饰符,内核每次都会仔细地从变量存储的内存中重新读取数据,而不是从已经加载到内核寄存器中的值中读取。在RTOS系统下,需要注意的是这个问题和上一个问题本质上是一样的。它是为了防止内核从寄存器中读取变量的值,而不是从存储变量的内存中读取。尤其是具有抢占属性的RTOS,本质上是中断,紧急任务立即打断正在执行的任务,与中断“类似”。如果有同学了解RTOS,可以想一下RTOS临界区的概念,就是保护变量。此处将不详细描述RTOS。如果有同学懂linux就更好了。volatile的作用和linux的原子操作没什么区别。这里就不细说了,发散一下思路。3.易变的问题易变的问题在面试中很容易看到。总结一下常见的:1.参数可以是const还是volatile?是的,例如只读状态寄存器。它是易变的,因为它可以意外更改。它是const是因为程序不应该尝试修改它。2.指针可以是volatile吗?是的,当服务子程序修改指向缓冲区的指针时。3、下面这个函数有什么问题?intsquare(volatileint*ptr){return*ptr**ptr;}这段程序的目的是返回指针*ptr指向的值的平方,但是由于*ptr指向的是volatile类型的参数,所以编译器将生成类似于以下的代码:intsquare(volatileint*ptr){inta,b;a=*指针;b=*指针;returna*b;}因为*ptr的值可能是有意的意外改变,所以a和b可能不同。因此,此代码可能不会返回您期望的平方值!正确的代码如下:longsquare(volatileint*ptr){inta;a=*指针;returna*a;}注意:频繁使用volatile是很危险的。可能会增加可执行文件的大小,降低性能,所以合理使用volatile,不能滥用volatile。4.总结volatile关键字是一种类型修饰符。其总结如下:用volatile关键字修饰的变量可以避免编译器优化。不再优化以提供对特殊地址的稳定访问。用volatile关键字修饰的变量,每次都重新读取内存中的值,而不是使用寄存器中保存的值;它通常用于中断、RTOS和硬件访问。频繁使用volatile很可能会增加可执行文件的体积,降低性能,所以合理使用volatile,不要滥用volatile。嵌入式系统程序员经常和硬件、中断、RTOS等打交道,这是区分C程序员和嵌入式系统程序员的最基本的问题,所以这些都需要在嵌入式应用中使用volatile变量。如果不了解volatile,可能会带来灾难。
