使用C的易失性關鍵字volatile

C的volatile關鍵字是一個限定符,在聲明變量時將其應用於該變量。它告訴編譯器變量的值可以隨時更改-編譯器在附近找到的代碼不會採取任何操作。這意味着非常嚴重的。但是,在檢查它們之前,讓我們看一下語法。

C的易失關鍵字的語法

要聲明變量volatile,請在變量定義的數據類型之前或之後包含關鍵字volatile。例如,這兩個聲明都將一個無符號的16位整數變量聲明爲易失性整數:

volatile uint16_t x;
uint16_t volatile y;
現在,事實證明,指向易失性變量的指針非常普遍,尤其是在內存映射的I / O寄存器中。這兩個聲明都將p_reg聲明爲一個易失的無符號8位整數的指針:

volatile uint8_t * p_reg; 
uint8_t volatile * p_reg;

非易失性數據的易失性指針非常少見(我想我曾經使用過它們),但是我最好繼續講一下語法:

uint16_t * volatile p_x;

僅出於完整性考慮,如果您確實必須具有指向volatile變量的volatile指針,則可以這樣編寫:

uint16_t volatile * volatile p_y;

順便說一句,有關如何選擇在何處放置volatile以及爲什麼應將其放置在數據類型之後的詳細說明(例如,int volatile * foo),請閱讀Dan Sak的“函數中的頂級cv-Qualifiers”列參數”(嵌入式系統編程,2000年2月,第63頁)。

最後,如果將volatile應用於結構或聯合,則該結構或聯合的全部內容都是易失的。如果您不希望出現這種情況,則可以將volatile限定符應用於結構或聯合的各個成員。

正確使用C的易失性關鍵字

只要變量的值可能意外更改,就應將其聲明爲volatile。實際上,只有三種類型的變量可以更改:

  • 1.內存映射外設寄存器
  • 2.由中斷服務程序修改的全局變量
  • 3.由多線程應用程序中的多個任務訪問的全局變量

我們將在以下各節中討論每種情況。

外圍寄存器

嵌入式系統包含真實的硬件,通常帶有複雜的外圍設備。這些外設包含其值可能與程序流異步更改的寄存器。舉一個非常簡單的例子,考慮一個8位狀態寄存器,該寄存器被映射到地址0x1234。要求您輪詢狀態寄存器,直到狀態寄存器變爲非零爲止。天真的和不正確的實現如下:

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

因此實現了期望的行爲。

當訪問具有特殊屬性的寄存器而不使用易失性聲明時,會出現一些微妙的錯誤。例如,許多外設都包含寄存器,只需通過讀取即可清除它們。在這些情況下,超出預期的額外(或更少)讀取可能會導致非常意外的行爲。

中斷服務程序

中斷服務例程通常設置在主線代碼中測試的變量。例如,串行端口中斷可以測試每個接收到的字符,以查看它是否是ETX字符(大概表示消息的結尾)。如果字符是ETX,則ISR可能會設置一個全局標誌。一個不正確的實現可能是:

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;
    } 
    ...
}

[注意:我們不提倡使用全局變量;此代碼使用一個使示例簡短/清晰。]

關閉編譯器優化後,此程序可能會運行。但是,任何一半不錯的優化器都會“破壞”程序。問題是編譯器不知道可以在ISR函數中更改gb_etx_found,這似乎從未被調用過。

就編譯器而言,表達式!gb_ext_found在每次循環中都將具有相同的結果,因此,您永遠都不想退出while循環。因此,while循環之後的所有代碼都可以被優化器簡單地刪除。如果幸運的話,編譯器將警告您。如果您很不幸(或者您還沒有學會認真對待編譯器警告),您的代碼將慘遭失敗。自然,責任歸咎於“糟糕的優化器”。

解決方案是將變量gb_etx_found聲明爲volatile。之後,該程序將按預期工作。

多線程應用

儘管實時操作系統中存在隊列,管道和其他可感知調度程序的通信機制,但RTOS任務仍可能會通過共享內存位置(即全局存儲)交換信息。當您向代碼中添加搶佔式調度程序時,編譯器不知道什麼是上下文切換或何時發生切換。因此,異步修改共享全局的任務在概念上與上面討論的ISR場景相同。因此,所有共享的全局對象(變量,內存緩衝區,硬件寄存器等)也必須聲明爲volatile,以防止編譯器優化引入意外行爲。例如,下面的代碼詢問麻煩:

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++;
        ...
    }
}

啓用編譯器的優化器後,該程序可能會失敗。使用volatile聲明gn_bluetask_runs是解決問題的正確方法。

[注意:我們不提倡使用全局變量;該代碼使用了全局變量,因爲它正在解釋易失性變量和全局變量之間的關係。]

[警告:還需要保護任務和ISR共享的全局變量不受競爭條件的影響,例如通過互斥。]

最後的想法

一些編譯器允許您隱式地將所有變量聲明爲volatile。抵制這種誘惑,因爲它本質上是思想的替代品。這也可能導致代碼效率降低。

另外,當遇到意外的程序行爲時,請不要試圖怪罪於優化器或將其關閉。現代的C / C ++優化器是如此出色,以至於我不記得上次遇到優化錯誤了。相反,我經常遇到程序員使用volatile的失敗。

如果爲您提供了一些易懂的代碼來“修復”,請對volatile執行grep。如果grep空了,這裏給出的示例可能是開始查找問題的好地方。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章