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