首先聲明本文譯自國外網(wǎng)站的一篇文章,原文鏈接如下:
https://barrgroup.com/embedded-systems/how-to/c-volatile-keyword
建議有條件的直接閱讀英文原版??赡茏x了這篇文章后,你會有所懷疑,因為你平時可能遇到過下面出現(xiàn)的情況,但是你并沒有添加volatile關(guān)鍵字,程序任然正常的運行,個人覺得可能有以下的原因:
1.其實BUG出現(xiàn)了,但是難以復(fù)現(xiàn),所以被你忽略了
2.現(xiàn)在的優(yōu)化器足夠智能,即使打開了優(yōu)化,也能避免這些BUG的出現(xiàn)
正文開始
許多程序員對C的volatile關(guān)鍵字了解得很少。這并不奇怪,因為大多數(shù)C文章對它都是一兩句話避而不談。這篇文章將教你正確的使用它。
在你的C/C++嵌入式代碼中你是否經(jīng)歷過以下情況?
代碼正常運行--直到你使能了編譯器優(yōu)化
代碼正常運行--直到你使能了中斷
奇怪的硬件驅(qū)動程序
RTOS任務(wù)工作正常--直到你添加了其它任務(wù)
如果你對上面任意一個的回答為"是",那么意味著你沒有使用volatile關(guān)鍵字。許多程序員和你一樣對volatile關(guān)鍵字了解得很少。遺憾的是,大多數(shù)關(guān)于C編程語言的書只用一兩句話就避開它。
【正確的使用volatile是消除BUG的一部分 Embedded C Coding Standard】
C關(guān)鍵字volatile是一個限定符,在變量聲明的時候應(yīng)用它。它告訴編譯器這個變量的值可能隨時被改變--編譯器不要去優(yōu)化它。影響是非常嚴重的。但是,在剖析它之前,我們先看一下它的語法。
C關(guān)鍵字volatile的語法
聲明一個變量為volatile,就在這個變量聲明的數(shù)據(jù)類型前面或者后面加一個volatile關(guān)鍵字。下面兩個實例都聲明一個無符號16位整型變量為volatile整型:
volatile uint16_t x;
uint16_t volatile y;
現(xiàn)在,實際證明指向volatile變量的指針也是非常普遍的,特別是內(nèi)存映射I/O寄存器。下面兩個聲明都將p_reg聲明為一個volatile的無符號8位整型指針:
volatile uint8_t * p_reg;
uint8_t volatile * p_reg;
指向非volatile數(shù)據(jù)的volatile指針是非常少見的,但是我最好還是講一下語法:
uint16_t * volatile p_x;
并且,為了完整性,如果你真的需要一個指向volatile數(shù)據(jù)的volatile指針,你可以這樣寫:
uint16_t volatile * volatile p_y;
順便提一句,如果你想得到一個更好的解釋對于如何選擇在哪兒放置volatile以及為什么要放在數(shù)據(jù)類型的后面(例如,int volatile * foo),可以閱讀Dan Sak's的欄目,"Top-Level cv-Qualifiers in Function Parameters" (Embedded Systems Programming, February 2000, p. 63)。
最后,如果你將volatile應(yīng)用于結(jié)構(gòu)體或者共用體,那么整個結(jié)構(gòu)體或者共用體就都是volatile的。如果你并不是想這樣,你可以對結(jié)構(gòu)體或者共用體中需要的成員單獨的添加volatile限定符。
正確的使用C的volatile關(guān)鍵字
如果一個變量的值會被意想不到的修改那它應(yīng)該被volatile修飾,實際上,只有三種類型的變量可以被修改:
1.內(nèi)存映射外設(shè)寄存器
2.被中斷服務(wù)程序修改的全局變量
3.多線程內(nèi)部的多任務(wù)訪問的全局變量
我們將在下面的章節(jié)討論每一種情況。
外設(shè)寄存器
嵌入式系統(tǒng)包含真正的硬件,通常帶有復(fù)雜的外設(shè)。這些外設(shè)包含可能被程序流異步更改的寄存器。在一個非常簡單的程序中,包含一個8位的狀態(tài)寄存器,它的內(nèi)存地址被映射到0x1234。需要你輪詢這個狀態(tài)寄存器直到它的值變?yōu)榉?。不正確的實現(xiàn)如下:
uint8_t * p_reg = (uint8_t *) 0x1234;
// Wait for register to read non-zero
do { ... } while (0 == *p_reg)
一旦你打開編譯器優(yōu)化,這段代碼幾乎肯定會失敗。這是因為編譯器將生成如下的匯編語言(這里以16位x86機器為例):
mov p_reg, #0x1234
mov a, @p_reg
loop:
...
bz loop
優(yōu)化器的理由很簡單:它已經(jīng)把變量的值讀取到了累加器中(對應(yīng)匯編代碼第二行),后面就不需要再重復(fù)讀取了,這樣的話這個值總是相同的。因此,從匯編代碼的第三行開始就進入了一個死循環(huán)。要強制編譯器如我們想的那樣做,我們需要修改聲明如下:
uint8_t volatile * p_reg = (uint8_t volatile *) 0x1234;
匯編代碼現(xiàn)在看起來就像這樣:
mov p_reg, #0x1234
loop:
...
mov a, @p_reg
bz loop
因此實現(xiàn)了我們想要的行為。
當(dāng)具有特殊屬性的寄存器操作沒有volatile聲明時就會產(chǎn)生一些微妙的BUG。例如,許多外設(shè)具有通過簡單的讀取就能清除它們的寄存器。在這種情況下,額外的讀取可能會導(dǎo)致超出預(yù)期的行為。
中斷服務(wù)程序
中斷服務(wù)程序通常設(shè)置在主線代碼中被測試的變量。例如一個串口中斷程序也許測試每個收到的字符是否是一個ETX字符(用以表示消息的結(jié)尾),如果這個字符是ETX,中斷服務(wù)程序也許設(shè)置一個全局標志。一個不正確的實現(xiàn)可能如下:
bool gb_etx_found = false;
void main()
{
...
while (!gb_etx_found)
{
// Wait
}
...
}
interrupt void rx_isr(void)
{
...
if (ETX == rx_char)
{
gb_etx_found = true;
}
...
}
【注意:我們不提倡使用全局變量;這段代碼使用僅為了讓例程簡短/清晰。】
在編譯器優(yōu)化關(guān)閉的情況下,這段程序也許正常的工作。然而,任何一半像樣的優(yōu)化器都會"破壞"這段程序。問題是編譯器不知道這個變量gb_etx_found可以在中斷服務(wù)程序中被更改,這似乎從來沒有被調(diào)用過。
就編譯器而言,表達式!gb_ext_found在循環(huán)中每次都是一樣的結(jié)果,因此,你不要想那能夠退出循環(huán)。因而,所有在while循環(huán)之后的代碼都可能被優(yōu)化器簡單的移除。如果你夠幸運,編譯器將警告你。如果你不夠幸運(或者你還沒有學(xué)會認真對待編譯器警告),你的代碼將不幸地失敗。自然,這責(zé)任將歸咎于"糟糕的優(yōu)化器"。
解決方案是使用volatile聲明變量gb_etx_found。這樣,程序就會按照你的預(yù)期正常工作。
多線程應(yīng)用
在實時操作系統(tǒng)中盡管存在隊列,管道,及其它調(diào)度感知的通信機制,但RTOS任務(wù)任然可能通過一段共享內(nèi)存來交換信息。當(dāng)你添加一個搶占式調(diào)度器到你的代碼中時,你的編譯器并不知道什么是上下文切換或者它何時發(fā)生,因此,一個任務(wù)異步修改一個共享的全局內(nèi)容就和中斷服務(wù)程序討論的情況差不多。因此所有全局對象(變量,內(nèi)存緩沖區(qū),硬件寄存器等等)都必須聲明為volatile以防止編譯器優(yōu)化而引入的不可預(yù)料的行為。例如,下面的代碼詢問問題:
uint8_t gn_bluetask_runs = 0;
void red_task (void)
{
while (4 < gn_bluetask_runs)
{
...
}
// Exit after 4 iterations of blue_task.
}
void blue_task (void)
{
for (;;)
{
...
gn_bluetask_runs++;
...
}
}
這段代碼將失敗一旦編譯器優(yōu)化被使能。使用volatile聲明gn_bluetask_runs是解決這個問題的正確方式。
【注意:我們不提倡使用全局變量,這段代碼使用全局變量僅僅是因為它正在說明volatile和全局變量的關(guān)系?!?/p>
【警告:被任務(wù)及中斷共享的全局變量還應(yīng)該被保護以防止競爭,比如通過互斥量?!?/p>
最后的想法
一些編譯器允許你隱式的聲明所有變量為volatile,抵制這種誘惑,因為它本質(zhì)上是思想的替代品。這也潛在的導(dǎo)致代碼效率降低。
另外,當(dāng)你的程序出現(xiàn)非預(yù)期的行為時,不要責(zé)備優(yōu)化器或者關(guān)掉它?,F(xiàn)代C/C++優(yōu)化器是如此出色,以至于我不記得上次遇到了優(yōu)化BUG。相反,我經(jīng)常遇到程序員使用volatile失敗。如果給你一份行為怪異的代碼去"修復(fù)",請對volatile執(zhí)行g(shù)rep。如果grep為空,這里給出的示例可能是開始查找問題的好地方。
下面是我的公眾號二維碼,歡迎關(guān)注。