做嵌入式C開發的相信都使用過一個關鍵字volatile,特別是做底層開發的。假設一個GPIO的數據寄存器地址是0x50000004,我們一般會定義一個這樣的宏:
#define GDATA *((volatile unsigned int*)0x50000004)
在面試的時候也會被問到過volatile關鍵字起什麼作用?
網絡上的回答一般是防止被編譯器優化,或者還會加一點就是訪問被volatile修飾的變量時,強制訪問內存中的值,而不是緩存中的。
我對上面的回答一直存在誤解,以爲是:因爲被編譯器優化,所以導致存取變量時是存取變量在cache中的緩存。
volatile官方說明
volatile
Indicates that a variable can be changed by a background routine.
Keyword volatile is an extreme opposite of const. It indicates that a variable may be changed in a way which is absolutely unpredictable by analysing the normal program flow (for example, a variable which may be changed by an interrupt handler). This keyword uses the following syntax:
volatile data-definition;
Every reference to the variable will reload the contents from memory rather than take advantage of situations where a copy can be in a register.
volatile 表明變量能被後臺程序修改
關鍵字volatile和const是完全相反的。它表明變量可能會通過某種方式發生改變,而這種方式是你通過分析正常的程序流程完全預測不出來的。(例如,一個變量可能被中斷處理程序修改)。關鍵字使用語法如下:
volatile data-definition;
每次對變量內容的引用會重新從內存中加載而不是從變量在寄存器裏面的拷貝加載。
我的理解:以中斷處理程序修改變量解釋可能不太合適,以GPIO爲例最合適。首先什麼是變量?變量就是一塊編了地址的內存區域。GPIO的數據寄存器有一個地址,大小一般爲32bit,所以這個數據寄存器可以認爲就是一個變量,我們可以讀寫它。如果GPIO設置爲輸入,修改GPIO數據寄存器這個變量的就是這個GPIO的引腳,不管你如何分析你的程序,你不可能知道這個GPIO數據寄存器裏面值是多少,你得讀它。你此刻讀到數據和下一刻讀到的完全可能是不一樣的。簡單的說就是你要的數據不同步。使用volatile修飾後,會強制你每次引用GPIO寄存器對應的變量時都會去它的寄存器裏面讀。
爲了搞清楚volatile到底是怎麼影響編譯器行爲的,需要以具體的例子來說明:
防止編譯器優化掉操作變量的語句
下面是a.c文件:
int main(void)
{
volatile char a;
a = 5;
a = 7;
return 0;
}
下面是b.c文件:
int main(void)
{
char a;
a = 5;
a = 7;
return 0;
}
進行編譯,分別得到它們對應的彙編文件:
$ make
arm-linux-gcc -S a.c -o a.s
arm-linux-gcc -S b.c -o b.s
對比a.s、b.s:
vimdiff a.s b.s
可以看到無任何差異,爲什麼呢?因爲我們gcc的優化等級是默認等級,現在把優化等級調至-O3。再看對比
$ make
arm-linux-gcc -O3 -S a.c -o a.s
arm-linux-gcc -O3 -S b.c -o b.s
可以看到未加volatile修飾的文件b.c,在優化後,彙編對應的a=5;a=7;
這兩個語句直接優化沒了。a=1;a=0;
假設a是控制GPIO的語句,原來打算是讓GPIO先拉高,再拉低,實現某種時序,結果優化一開,這兩句直接廢了。這讓你在調試硬件的時候會感到莫名其妙。所以這種情況得像a.c那樣用volatile來修飾。
這裏是防止編譯器優化掉相關語句,而不是優化變量的存取對象(memory or register)。
防止編譯器優化變量的存取對象(memory or register)
a.c
int main(void)
{
int b;
int c;
volatile int* a = (int*)0x30000000;
b = *a;
c = *a;
return c + b;
}
b.c
int main(void)
{
int b;
int c;
int* a = (int*)0x30000000;
b = *a;
c = *a;
return c + b;
}
a.s
mov r3, #805306368
ldr r2, [r3] @ b = *a;
ldr r0, [r3] @ c = *a;
add r0, r0, r2 @ b + c;
bx lr
b.s
mov r3, #805306368
ldr r0, [r3] @ b = *a;
mov r0, r0, asl #1 @ b << 2; 也就是 b * 2;也就是 b + b;也就是 add r0, r0, r0(可能這句彙編不合法)
bx lr
可以看到b.s被優化後,第一次取*a
的值時,是從地址0x30000000取出來的(ldr r0, [r3]
),第二次就直接沒取了,是直接使用了r0的值。這裏的r0就是*a
的緩存。我之前在這裏的理解存在很大的錯誤:
訪問被volatile修飾的變量時,強制訪問內存中的值,而不是緩存中的。
以爲這裏的緩存指cache。
畢竟從彙編指令優化着手怎麼可能控制變量的一定從cache裏面存取。
你不能假定一個共享(GPIO引腳和你的程序共享了GPIO數據寄存器)的變量只會被你修改。就好比你讀一個文件的內容,過10秒後,你不能假定文件內容沒變,必須要重新讀取文件內容。
注意事項
volatile關鍵詞影響編譯器編譯的結果,用volatile聲明的變量表示該變量隨時可能發生變化,與該變量有關的運算,不要進行編譯優化,以免出錯
一個例子:
int main(void)
{
int b;
volatile int* a = (int*)0x30000000;
b = (*a) * (*a);
return b;
}
彙編後
mov r3, #805306368
ldr r2, [r3] ①
ldr r0, [r3] ②
mul r0, r2, r0
bx lr
程序本意是要計算平方。如果這段代碼在運行至①這行彙編時,被調度開了,過了一陣調度回來繼續運行②行,此時完全有可能 R2 != R0。那麼計算出來的結果R0必然不等於那個平方值。