嵌入式C中的volatile關鍵字

volatile,譯爲"易失的",也就說該關鍵字用來申明一個易變的變量,該變量是會在程序運行時會被不經意地改變,因此該關鍵字的做用就是告訴編譯器每次都必須從新當心的讀取該變量的值,而不是簡單地拷貝該變量保存在副本里面的值,同時也不容許編譯器對該變量進行優化。安全

咱們先來看看常見的volatile申明的變量有哪些?多線程

一、定義易失型變量異步

//定義整型變量 
volatile int Data;
int volatile Data; //兩種申明等價

二、定義指針函數

//定義指針
volatile int* pData;  //指針是整型的,可是指針指向的值是易失的 
int volatile* pData;  //指針是volatile型的,可是指向的值是非易失的

//正確的用法 
volatile int Value; 
volatile int *pData = &Value;

//錯誤的用法
volatile int Value; 
int* pData = &Value;

//定義易失指針而且指向的值也是易失的
//此時pData自己和指向的值都是不會被優化的 
volatile intvolatile pData;

三、定義結構體性能

//定義結構體用法1 
typedef struct {
  int Data;
  int Next;
}List;

List volatile Node; //此時Node中的結構體成員Data、Next是易失的,而List原有的結構體成員變量是非易失的

//定義結構體用法2 
typedef volatile struct {
  int Data;
  int Next;
}List;

List Node;  //此時List定義的全部結構體成員變量是易失型的

//定義結構體用法3
typedef volatile struct {
  int volatile Data;
  int Next;
}List;

List Node;  //此時List定義的結構體成員變量Data都是易失型的

//定義結構體用法4等價於用法5,不等同於用法6 
typedef struct {
  int  Data;
  int* Next;
}List;

volatile List Node;

//定義結構體用法5 
typedef struct {
  int volatile  Data;
  intvolatile Next;
}List;

List Node;

//定義結構體用法6
typedef struct {
  volatile int   Data;
  volatile int*  Next;
}List;

List Node;

再來看看volatile申明的變量的用途!優化

在如下狀況中,可能就須要volatile關鍵字來申明變量了ui

一、程序運行良好,可是當提升編譯器的優化級別時,其行爲將發生變化,而且沒法按需運行;spa

二、一切都很好,可是一旦啓用中斷,代碼行爲就會發生變化,而且沒法按預期運行;線程

三、不穩定的硬件驅動程序;指針

四、孤立執行的任務正常,但啓用另外一個任務後崩潰。

下面來看幾個代碼塊,看看定義易失型變量的好處,你們能夠看一下代碼塊1和代碼塊2的區別,區別是對於FLAG_REG的定義,一個是無符號char變量,一個是無符號易失型變量,那你們想一想哪個代碼塊會運行正常呢?答案是代碼塊2,在編譯器優化時,爲了得到更好的性能,會將FLAG_REG值加載到寄存器中,儘管FLAG_REG的值由硬件更改,但不會再次讀取。代碼塊1中定義的FLAG_REG變量永遠只會被取到它的副本值,不會從新從寄存器中讀該變量的值,若是該變量的值被改變爲了0x01的話,在代碼塊1中讀回的值仍是0x00,那麼代碼塊1將不會按照預期的意圖去運行了,而代碼塊2則不一樣。

//Code Block 1
unsigned char FLAG_REG;   // Hardware flag register

void func (void) {
  while (FLAG_REG & 0x01)   // Repeat while bit 0 is set
    {
      //do something 
    }
}

//Code Block 2
unsigned volatile char FLAG_REG;   //qualify from the volatile

void func (void) {
  while (FLAG_REG & 0x01)   // Repeat while bit 0 is set
    {
      //do something 
    }
}

關鍵字volatile主要使用的場合有哪些?

一、訪問內存映射的外設寄存器。

二、在多個線程之間共享全局變量或緩衝區。

三、在中斷例程或信號處理程序中訪問全局變量。

在這些場合下,都須要將變量設置爲易失型的,纔可能保證代碼的正常運行。

外設寄存器

在嵌入式系統中,全部外圍設備均位於特定的存儲器地址。外設具備寄存器,這些寄存器的值能夠異步地更改成代碼流。在程序中,爲了方便地訪問外設寄存器,咱們必須將外設寄存器映射爲c變量,而後使用指針訪問此變量。

注意:在映射中,不只要關心寄存器的大小和地址,還須要關心其在內存中的對齊方式。

來上一個例子,這裏是一個位於地址0x40000000的32位標誌狀態寄存器,咱們必須監視它的第一位並在循環中等待,直到其第一位爲1。

#define  STATUS_REG    (unsigned volatile int*)(0x40000000)

unsigned volatile int* puiData = STATUS_REG;
while((*puiData) & 0x01// Wait until first bit is set
{ 
  //do something
}

當咱們使用volatile關鍵字對狀態寄存器地址進行限定時,編譯器就會知道該地址的值能夠隨時更改,而無需任何先驗信息。所以,編譯器不會對此變量執行任何優化,並始終從地址中從新讀取該值。

中斷服務函數

對於中斷服務程序,ISR()和main()函數之間一般共享一個全局變量。在這種狀況下,設置全局變量的值或經過ISR或main函數進行檢查,爲更好地理解,下面來舉個例子。

int Timer_Flag = 0;

Timer_ISR(void) 
{ 
    Timer_Flag = 1; 
}

int main(void)  {
    while(!Timer_Flag)
    {
        //do some work
    }

    return 0;
}

在上面的代碼中,全局標誌是main函數中的監視器,而且main函數執行其餘一些任務,直到全局標誌值爲零爲止。當咱們打開編譯器的優化級別時,此代碼可能工做正常,由於編譯器沒有意識到ISR中全局變量的值更改,所以它假定while循環始終爲真,而且永遠不會退出循環 。

解決此問題的方法是使全局標誌易變,此技術可防止編譯器對全局標誌進行任何優化,並告訴編譯器該標誌的值能夠隨時由外部事件更改,而無需代碼採起任何措施。

volatile int Timer_Flag = 0;

Timer_ISR(void) 
{ 
    Timer_Flag = 1; 
}

int main(void)  {
    while (!Timer_Flag)
    {
        //do some work
    }

    return 0;
}

多線程應用

在多線程應用程序中,兩個線程使用管道或消息隊列相互通訊,除了此種方式實現通訊外,還有另外一種技術可使線程彼此通訊,該技術是共享地址(共享緩衝區或全局變量)。

一般,線程以異步方式執行。若是咱們不使用volatile聲明這些共享位置,而是提升編譯器的優化級別,則編譯器會將這些值存儲在線程上下文的局部變量中,並始終從這些局部變量中讀取值。所以,對於所需的操做,咱們必須將共享緩衝區或全局變量聲明爲volatile,能夠防止當提升編譯器的優化級別時線程運行失敗。

volatile int gValue;

void Task_1(void)  {
    gValue = 0;

    while (gValue == 0) 
    {
        sleep(1);
    } 
    ...
}

void Task_2(void)  {
    ...
    gValue++; 
    sleep(10); 
    ...
}

若是關鍵字const和volatile同時申明一個變量,那該變量是作什麼用途的呢?

一塊兒使用volatile和const關鍵字很是有趣,由於volatile(「隨時間變化的」)和const(「只讀」)的質量彷佛相反,可是有時同時用這兩個關鍵字定義變量會有意想不到的好處。

GPIO寄存器的常量地址

限定符const和volatile在訪問GPIO寄存器或任何硬件寄存器的場景中常用,要訪問這些寄存器,咱們須要聲明一個指向volatile寄存器的const指針。

unsigned int volatile * const pLcdReg = (unsigned int volatile *) 0x00020000;

*pLcdReg = WRITE_DATA; // to write data on LCD
READ_DATA = *pLcdReg; //to read data from the LCD

理解上述複雜聲明的正確方法是:pLcdReg是指向易失性無符號整數的常量指針。在此聲明中,惟一的指針是常量(即固定地址0x00020000),這意味着在整個程序中,其值不會改變。關鍵字volatile僅修改無符號整數的行爲。這是訪問GPIO或硬件寄存器的指針的最有用且最安全的聲明。

只讀共享內存位置

當const和volatile一塊兒使用時,這種狀況也很重要。若是兩個處理器使用共享位置相互通訊,而且處理器僅將該位置用於讀取,那麼咱們必須使用const關鍵字將這些位置設置爲只讀類型,咱們能夠經過如下方式聲明共享內存的位置。

unsigned int const volatile gSharedFlag;

unsigned char const volatile acSharedBuffer[BUFFER_SIZE];

只讀狀態寄存器

如下硬件寄存器表明了硬件不一樣階段的狀態,該寄存器是隻讀類型,要訪問這些寄存器,咱們須要聲明一個指向常量易失性寄存器的常量指針。

理解此複雜聲明的正確方法是pStatusFlagReg是指向易失性常量無符號整數的常量指針。

unsigned int const volatile * const pStatusFlagReg = (uint8_t *) 0x20000000;

READ_DATA = * pStatusFlagReg; //to read status from the status register

* pStatusFlagReg = WRITE_DATA // Not possible because address qualify by const keyword

由上面的三個應用場景,關鍵字const和volatile同時申明一個變量是很是有用的!

參考:

https://aticleworld.com/understanding-volatile-qualifier-in-c/

相關文章
相關標籤/搜索