我在開發(fā)中也常常遇到這個(gè)問題,發(fā)現(xiàn)通常用在兩個(gè)方面,一方面是對(duì)硬件寄存器或固定內(nèi)存的訪問,一般要用到,這就是我們常常在寄存器的頭文件常??吹降?,另一個(gè)就是在多線程,或主程序和中斷共享,全局變量常常用到。言歸正傳,看看老外是怎么說的
認(rèn)識(shí)關(guān)鍵字Volatile
很多程序員對(duì)于volatile的用法都不是很熟悉。這并不奇怪,很多介紹C語(yǔ)言的書籍對(duì)于他的用法都閃爍其辭。
在你們使用C/C++語(yǔ)言開發(fā)嵌入式系統(tǒng)的時(shí)候,遇到過以下的情況么?
? 一打開編譯器的編譯優(yōu)化選項(xiàng),代碼就不再正常工作了;
? 中斷似乎總是程序異常的元兇;
? 硬件驅(qū)動(dòng)工作不穩(wěn)定;
? 多任務(wù)系統(tǒng)中,單個(gè)任務(wù)工作正常,加入任何其他任務(wù)以后,系統(tǒng)就崩潰了。
如果你曾經(jīng)向別人請(qǐng)教過和以上類似的問題,至少說明,你還沒有接觸過C語(yǔ)言關(guān)鍵字volatile的用法。這種情況,你不是第一個(gè)遇到。很多程序員對(duì)于volatile都幾乎一無所知。大部分介紹C語(yǔ)言的文獻(xiàn)對(duì)于它都閃爍其辭。
Volatile是一個(gè)變量聲明限定詞。它告訴編譯器,它所修飾的變量的值可能會(huì)在任何時(shí)刻被意外的更新,即便與該變量相關(guān)的上下文沒有任何對(duì)其進(jìn)行修改的語(yǔ)句。造成這種“意外更新”的原因相當(dāng)復(fù)雜。在我們分析這些原因之前,我們先回顧一下與其相關(guān)的語(yǔ)法。
語(yǔ)法
要想給一個(gè)變量加上volatile限定,只需要在變量類型聲明附之前/后加入一個(gè)volatile關(guān)鍵字就可以了。下面的兩個(gè)實(shí)例是等效的,它們都是將foo聲明為一個(gè)“需要被實(shí)時(shí)更新”的int型變量。
volatile int foo;
int volatile foo;
同樣,聲明一個(gè)指向volatile型變量的指針也是非常類似的。下面的兩個(gè)聲明都是將foo定義為一個(gè)指向volatile integer型變量的指針。
volatile int * foo;
int volatile * foo;
一個(gè)Volatile型的指針指向一個(gè)非volatile型變量的情況非常少見(我想,我可能使用過一次),盡管如此,我還是要給出他的語(yǔ)法:
int * volatile foo;
最后一種形式,針對(duì)你真的需要一個(gè)volatile型的指針指向一個(gè)volatile型的情形:
int volatile * volatile foo;
最后,如果你將volatile應(yīng)用在結(jié)構(gòu)體或者是公用體上,那么該結(jié)構(gòu)體/公用體內(nèi)的所有內(nèi)容就都帶有volatile屬性了。如果你并不想這樣(牽一發(fā)而動(dòng)全身),你可以僅僅在結(jié)構(gòu)體/公用體中的某一個(gè)成員上單獨(dú)使用該限定。
使用
當(dāng)一個(gè)變量的內(nèi)容可能會(huì)被意想不到的更新時(shí),一定要使用volatile來聲明該變量。通常,只有三種類型的變量會(huì)發(fā)生這種“意外”:
? 在內(nèi)存中進(jìn)行地址映射的設(shè)備寄存器;
? 在中斷處理程序中可能被修改的全局變量;
? 多線程應(yīng)用程序中的全局變量;
設(shè)備寄存器
嵌入式系統(tǒng)的硬件實(shí)體中,通常包含一些復(fù)雜的外圍設(shè)備。這些設(shè)備中包含的寄存器,其值往往隨著程序的流程同步的進(jìn)行改變。在一個(gè)非常簡(jiǎn)單的例子中,假設(shè)我們有一個(gè)8位的狀態(tài)寄存器映射在地址0x1234上。系統(tǒng)需要我們一直監(jiān)測(cè)狀態(tài)寄存器的值,直到它的值不為0為止。通常錯(cuò)誤的實(shí)現(xiàn)方法是:
UINT1 * ptr = (UINT1 *) 0x1234;
// Wait for register to become non-zero.等待寄存器為非0值
while (*ptr == 0);
// Do something else.作其他事情
一旦你打開了優(yōu)化選項(xiàng),這種寫法肯定會(huì)失敗,編譯器就會(huì)生成類似如下的匯編代碼:
mov ptr, #0x1234 mov a, @ptr loop bz loop
優(yōu)化的工作原理非常簡(jiǎn)單:一旦我們我們將一個(gè)變量讀入寄存器中(參照代碼的第二行),如果(從變量相關(guān)的上下文看)變量的值總是不變的,那么就沒有必要(從內(nèi)存中)從新讀取他。在代碼的第三行中,我們使用一個(gè)無限循環(huán)來結(jié)束。為了強(qiáng)迫編譯器按照我們的意愿進(jìn)行編譯,我們修改指針的聲明為:
UINT1 volatile * ptr =
(UINT1 volatile *) 0x1234;
對(duì)應(yīng)的匯編代碼為:
mov ptr, #0x1234
loop mov a, @ptr
bz loop
我們需要的功能實(shí)現(xiàn)了!
對(duì)于一些較為特殊的寄存器,(我們上面提到的方法)會(huì)導(dǎo)致一些難以想象的錯(cuò)誤。事實(shí)上,很多設(shè)備寄存器在讀取一次以后就會(huì)被清除。這種情況下,多余的讀取操作會(huì)導(dǎo)致意想不到的錯(cuò)誤。
中斷處理程序
中斷處理程序經(jīng)常負(fù)責(zé)更新一些在主程序中被查詢的變量的值。例如,一個(gè)串行通訊中斷會(huì)檢測(cè)接收到的每一個(gè)字節(jié)是否為ETX信號(hào)(以便來確認(rèn)一個(gè)消息幀的結(jié)束標(biāo)志)。如果其中的一個(gè)字節(jié)為ETX,中斷處理程序就是修改一個(gè)全局標(biāo)志。一個(gè)錯(cuò)誤的實(shí)現(xiàn)方法可能為:
int etx_rcvd = FALSE;
void main()
{
...
while (!ext_rcvd)
{
// Wait
}
...
}
interrupt void rx_isr(void)
{
...
if (ETX == rx_char)
{
etx_rcvd = TRUE;
}
...
}
在編譯優(yōu)化選項(xiàng)關(guān)閉的時(shí)候,代碼可能會(huì)工作的很好。但是,即便是任何半吊子的優(yōu)化,也會(huì)“破壞”這個(gè)代碼的意圖。問題就在于,編譯器并不知道 etx_rcvd會(huì)在中斷處理程序中被更新。在編譯器可以檢測(cè)的上下文內(nèi),表達(dá)式!ext_rcvd總是為真,所以,你就永遠(yuǎn)無法從循環(huán)中跳出。因此,該循環(huán)后面的代碼會(huì)被當(dāng)作“不可達(dá)到 ”的內(nèi)容而被編譯器的優(yōu)化選項(xiàng)簡(jiǎn)單的刪除掉。如果你比較幸運(yùn),你的編譯器也許會(huì)給你一個(gè)相關(guān)的警告;如果你沒有那么幸運(yùn)(或者你沒有注意到這些警告),你的代碼就會(huì)導(dǎo)致嚴(yán)重的錯(cuò)誤。通常,就會(huì)有人抱怨“該死的優(yōu)化選項(xiàng)”。
解決這個(gè)問題的方法很簡(jiǎn)單:將變量etx_rcvd聲明為volatile。然后,所有的(至少是一部分癥狀)那些錯(cuò)誤癥狀就會(huì)消失。
多線程應(yīng)用程序
在實(shí)時(shí)操作系統(tǒng)中,除去隊(duì)列、管道以及其他調(diào)度相關(guān)的通訊結(jié)構(gòu),在兩個(gè)任務(wù)之間采用共享的內(nèi)存空間(就是全局共享)實(shí)現(xiàn)數(shù)據(jù)的交換仍然是相當(dāng)常見的方法。當(dāng)你將一個(gè)優(yōu)先權(quán)調(diào)度器應(yīng)用于你的代碼時(shí),編譯器仍然不知道某一程序段分支選擇的實(shí)際工作方式以及什么時(shí)候某一分支情況會(huì)發(fā)生。這是因?yàn)?,另外一個(gè)任務(wù)修改一個(gè)共享的全局變量在概念上通常和前面中斷處理程序中提到的情形是一樣的。所以,(這種情況下)所有共享的全局變量都要被聲明為 volatile。例如:
int cntr;
void task1(void)
{
cntr = 0;
while (cntr == 0)
{
sleep(1);
}
...
}
void task2(void)
{
...
cntr++;
sleep(10);
...
}
一旦編譯器的優(yōu)化選項(xiàng)被打開,這段代碼的執(zhí)行通常會(huì)失敗。將cntr聲明為volatile是解決問題的好辦法。
反思
一些編譯器允許我們隱含的聲明所有的變量為volatile。最好抵制這種便利的誘惑,因?yàn)樗苋菀鬃屛覀儭安粍?dòng)腦子”,而且,這也常常會(huì)產(chǎn)生一個(gè)效率相對(duì)較低的代碼。
所以,我們又詛咒編譯優(yōu)化或者簡(jiǎn)單的關(guān)掉這一選項(xiàng)來抵制這些誘惑。現(xiàn)在的編譯優(yōu)化已經(jīng)相當(dāng)聰明,我不記得在編譯優(yōu)化中找到過什么錯(cuò)誤。與之相比,為了解決一些錯(cuò)誤,我卻常常使用瘋狂數(shù)量的volatile。
如果你恰巧有一段代碼需要去修正,先搜索一下有沒有volatile關(guān)鍵字。如果找不到volatile,那么這個(gè)代碼很可能會(huì)是一個(gè)很好的實(shí)例來檢測(cè)前面提到過的各種錯(cuò)誤。
volatile的本意是一般有兩種說法--1.“暫態(tài)的”;2.“易變的”。這兩種說法都有可行。但是究竟volatile是什么意思,現(xiàn)舉例說明(以Keil-c與a51為例),看完例子后你應(yīng)該明白volatile的意思了
例1:
void main (void)
{
volatile int i;
int j;
i = 1;? //1? 不被優(yōu)化 i=1
i = 2;? //2? 不被優(yōu)化 i=1
i = 3;? //3? 不被優(yōu)化 i=1
j = 1;? //4? 被優(yōu)化
j = 2;? //5? 被優(yōu)化
j = 3;? //6? j = 3
}
例2:
函數(shù):
void func (void)
{
unsigned char xdata xdata_junk;
unsigned char xdata *p = &xdata_junk;
unsigned char t1, t2;
t1 = *p;
t2 = *p;
}
編譯的匯編為:
0000 7E00??? R???? MOV???? R6,#HIGH xdata_junk
0002 7F00??? R???? MOV???? R7,#LOW xdata_junk
;---- Variable 'p' assigned to Register 'R6/R7' ----
0004 8F82????????? MOV???? DPL,R7
0006 8E83????????? MOV???? DPH,R6
;!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 注意
0008 E0??????????? MOVX??? A,@DPTR
0009 F500??? R???? MOV???? t1,A
000B F500??? R???? MOV???? t2,A
;!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
000D 22??????????? RET?
將函數(shù)變?yōu)椋?/p>
void func (void)
{
volatile unsigned char xdata xdata_junk;
volatile unsigned char xdata *p = &xdata_junk;
unsigned char t1, t2;
t1 = *p;
t2 = *p;
}
編譯的匯編為:
0000 7E00??? R???? MOV???? R6,#HIGH xdata_junk
0002 7F00??? R???? MOV???? R7,#LOW xdata_junk
;---- Variable 'p' assigned to Register 'R6/R7' ----
0004 8F82????????? MOV???? DPL,R7
0006 8E83????????? MOV???? DPH,R6
;!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
0008 E0??????????? MOVX??? A,@DPTR
0009 F500??? R???? MOV???? t1,A??????? a處
000B E0??????????? MOVX??? A,@DPTR
000C F500??? R???? MOV???? t2,A
;!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
000E 22??????????? RET????
比較結(jié)果可以看出來,未用volatile關(guān)鍵字時(shí),只從*p所指的地址讀一次
如在a處*p的內(nèi)容有變化,則t2得到的則不是真正*p的內(nèi)容。
評(píng)論
查看更多