如何使用C的volatile關鍵字

2021-10-06 22:57:06 字數 3830 閱讀 5127

1.其實bug出現了,但是難以復現,所以被你忽略了

2.現在的優化器足夠智慧型,即使開啟了優化,也能避免這些bug的出現

許多程式設計師對c的volatile關鍵字了解得很少。這並不奇怪,因為大多數c文章對它都是一兩句話避而不談。這篇文章將教你正確的使用它。

在你的c/c++嵌入式**中你是否經歷過以下情況?

**正常執行–直到你使能了編譯器優化

**正常執行–直到你使能了中斷

奇怪的硬體驅動程式

rtos任務工作正常–直到你新增了其它任務

如果你對上面任意乙個的回答為"是",那麼意味著你沒有使用volatile關鍵字。許多程式設計師和你一樣對volatile關鍵字了解得很少。遺憾的是,大多數關於c程式語言的書只用一兩句話就避開它。

【正確的使用volatile是消除bug的一部分embedded c coding standard

c關鍵字volatile是乙個限定符,在變數宣告的時候應用它。它告訴編譯器這個變數的值可能隨時被改變–編譯器不要去優化它。影響是非常嚴重的。但是,在剖析它之前,我們先看一下它的語法。

宣告乙個變數為volatile,就在這個變數宣告的資料型別前面或者後面加乙個volatile關鍵字。下面兩個例項都宣告乙個無符號16位整型變數為volatile整型:

volatile uint16_t x; 

uint16_t volatile y;

現在,實際證明指向volatile變數的指標也是非常普遍的,特別是記憶體對映i/o暫存器。下面兩個宣告都將p_reg宣告為乙個volatile的無符號8位整型指標:

volatile uint8_t * p_reg; 

uint8_t volatile * p_reg;

指向非volatile資料的volatile指標是非常少見的,但是我最好還是講一下語法:

uint16_t * volatile p_x;
並且,為了完整性,如果你真的需要乙個指向volatile資料的volatile指標,你可以這樣寫:

uint16_t volatile * volatile p_y;
順便提一句,如果你想得到乙個更好的解釋對於如何選擇在哪兒放置volatile以及為什麼要放在資料型別的後面(例如,int volatile * foo),可以閱讀dan sak』s的欄目,「top-level cv-qualifiers in function parameters」 (embedded systems programming, february 2000, p. 63)。

最後,如果你將volatile應用於結構體或者共用體,那麼整個結構體或者共用體就都是volatile的。如果你並不是想這樣,你可以對結構體或者共用體中需要的成員單獨的新增volatile限定符。

如果乙個變數的值會被意想不到的修改那它應該被volatile修飾,實際上,只有三種型別的變數可以被修改:

1.記憶體對映外設暫存器

2.被中斷服務程式修改的全域性變數

3.多執行緒內部的多工訪問的全域性變數

我們將在下面的章節討論每一種情況。

嵌入式系統包含真正的硬體,通常帶有複雜的外設。這些外設包含可能被程式流非同步更改的暫存器。在乙個非常簡單的程式中,包含乙個8位的狀態暫存器,它的記憶體位址被對映到0x1234。需要你輪詢這個狀態暫存器直到它的值變為非0。不正確的實現如下:

uint8_t * p_reg = (uint8_t *) 0x1234;

// wait for register to read non-zero

do while (0 == *p_reg)

一旦你開啟編譯器優化,這段**幾乎肯定會失敗。這是因為編譯器將生成如下的組合語言(這裡以16位x86機器為例):

mov p_reg, #0x1234

mov a, @p_reg

loop:

...bz loop

優化器的理由很簡單:它已經把變數的值讀取到了累加器中(對應彙編**第二行),後面就不需要再重複讀取了,這樣的話這個值總是相同的。因此,從彙編**的第三行開始就進入了乙個死迴圈。要強制編譯器如我們想的那樣做,我們需要修改宣告如下:

uint8_t volatile * p_reg = (uint8_t volatile *) 0x1234;
彙編**現在看起來就像這樣:

mov p_reg, #0x1234

loop:

...mov a, @p_reg

bz loop

因此實現了我們想要的行為。

當具有特殊屬性的暫存器操作沒有volatile宣告時就會產生一些微妙的bug。例如,許多外設具有通過簡單的讀取就能清除它們的暫存器。在這種情況下,額外的讀取可能會導致超出預期的行為。

中斷服務程式通常設定在主線**中被測試的變數。例如乙個串列埠中斷程式也許測試每個收到的字元是否是乙個etx字元(用以表示訊息的結尾),如果這個字元是etx,中斷服務程式也許設定乙個全域性標誌。乙個不正確的實現可能如下:

bool gb_etx_found = false;

void main()

...}interrupt void rx_isr(void)

...}

【注意:我們不提倡使用全域性變數;這段**使用僅為了讓例程簡短/清晰。】

在編譯器優化關閉的情況下,這段程式也許正常的工作。然而,任何一半像樣的優化器都會"破壞"這段程式。問題是編譯器不知道這個變數gb_etx_found可以在中斷服務程式中被更改,這似乎從來沒有被呼叫過。

就編譯器而言,表示式!gb_ext_found在迴圈中每次都是一樣的結果,因此,你不要想那能夠退出迴圈。因而,所有在while迴圈之後的**都可能被優化器簡單的移除。如果你夠幸運,編譯器將警告你。如果你不夠幸運(或者你還沒有學會認真對待編譯器警告),你的**將不幸地失敗。自然,這責任將歸咎於"糟糕的優化器"。

解決方案是使用volatile宣告變數gb_etx_found。這樣,程式就會按照你的預期正常工作。

在實時作業系統中儘管存在佇列,管道,及其它排程感知的通訊機制,但rtos任務任然可能通過一段共享記憶體來交換資訊。當你新增乙個搶占式排程器到你的**中時,你的編譯器並不知道什麼是上下文切換或者它何時發生,因此,乙個任務非同步修改乙個共享的全域性內容就和中斷服務程式討論的情況差不多。因此所有全域性物件(變數,記憶體緩衝區,硬體暫存器等等)都必須宣告為volatile以防止編譯器優化而引入的不可預料的行為。例如,下面的**詢問問題:

uint8_t gn_bluetask_runs = 0;

void red_task (void)

// exit after 4 iterations of blue_task.

}void blue_task (void)

}

這段**將失敗一旦編譯器優化被使能。使用volatile宣告gn_bluetask_runs是解決這個問題的正確方式。

【注意:我們不提倡使用全域性變數,這段**使用全域性變數僅僅是因為它正在說明volatile和全域性變數的關係。】

【警告:被任務及中斷共享的全域性變數還應該被保護以防止競爭,比如通過互斥量。】

一些編譯器允許你隱式的宣告所有變數為volatile,抵制這種**,因為它本質上是思想的替代品。這也潛在的導致**效率降低。

另外,當你的程式出現非預期的行為時,不要責備優化器或者關掉它。現代c/c++優化器是如此出色,以至於我不記得上次遇到了優化bug。相反,我經常遇到程式設計師使用volatile失敗。如果給你乙份行為怪異的**去"修復",請對volatile執行grep。如果grep為空,這裡給出的示例可能是開始查詢問題的好地方。

C 中volatile關鍵字的使用

有些變數是用volatile關鍵字宣告的。當兩個執行緒都要用到某乙個變數且該變數的值會被改變時,應該用volatile宣告,該關鍵字的作用是防止優化編譯器把變數從記憶體裝入cpu暫存器中。如果變數被裝入暫存器,那麼兩個執行緒有可能乙個使用記憶體中的變數,乙個使用暫存器中的變數,這會造成程式的錯誤執行...

volatile的使用及分析

vlolatile修飾的變數可以做為共享變數,在多執行緒中可見。意思是,在乙個執行緒中volatile修飾的變數修改了,其他呼叫此變數的執行緒中同樣可以看到修改。兩個執行緒a b,b在a中,a修改了b中的變數值,b中可以看到修改。public class usevolatile extends th...

volatile的使用場景

單詞解釋 亂序執行 指cpu對 的執行順序進行亂序優化,但保證各執行 單元的順序按指令順序排列。以達到充分利用處理器的各處理單元的目的。可以理解成 乙個任務有不同的執行單元,這些單元之間有一定的執行順序,但部分執行單元可提前工作,亂序執行就是讓這部分執行單元提前一段時間執行,從而提高整體的效率,減少...