volatile的陷阱


     對於volatile關鍵字,大部分C語言的教程都是一筆帶過,並沒有做太深入的分析,所以這里簡單的整理了一些
關於volatile的使用注意事項。實際上從語法上來看volatile和const是一樣的,但是如果const用錯,幾乎不會有什
么問題,而volatile用錯,后果可能很嚴重。所以在volatile的使用上建議大家還是盡量求穩,少用一下沒有切實把
握的技巧。
 
  首先看下面兩個定義的區別:
1   unsigned char* volatile reg;

  這行代碼里reg是指針類型,存儲一個指向unsigned char 類型的指針。volatile修飾的是reg這個變量即為指針

變量是volatile的,但是指針指向的unsigne char 內容不是volatile。在實際使用的時候編譯器對代碼中指針變量reg

本身的操作不會優化,但是對reg所指的內容 "*reg"卻會作為no-volatile內容處理,對“*reg”的操作還是會被優化。

通常這種寫法一般用在對共享的指針變量上,即這個指針變量有可能會被中斷函數修改,將其定義為volatile后編譯器

每次取指針變量值的時候都會從內存載入,這樣即使這個變量已經被別的程序修改 了當前函數用的時候也能得到修改后

的值(否則通常只在函數開始取一次放在寄存器里,以后就一直使用寄存器內的副本)。

 

1     volatile unsigned char *reg;

  這行代碼里volatile修飾的是指針所指的內容,所以這里定義了一個unsigned char類型的指針,並且這個指針指向

的是一個volatile的對象,但是指針變量本身不是volatile。如果對指針變量reg本身進行計算或賦值等操作,是可能被編

編譯器優化的。但是對reg所指的內容*reg的引用卻禁止編譯器優化。因為這個指針所指的是一個volatile的對象,所以

編譯器必須保證對*reg的操作都不被優化。通常在驅動程序的開發中,對硬件寄存器指針的定義,都應該采用這種形式。

 

1     volatile unsigned char * volatile reg

 

   這樣定義出來的指針本身就是個volatile變量,又指向了volatile的數據內容。

 

---------------------------------------------【分割線】----------------------------------------------------

volatile 和 const 的合用

  從字面上看volatile 和 const 似乎是一個對象的兩個對立屬性,是互斥的。但實際上,兩者是有可能一起修飾同一個

對象的。看看下面的這行聲明。

    extern const volatile unsigned int rt_clock;

 

  這個是在RTOS系統內核中常見的一種聲明:rt_clock通常是指系統時鍾,他經常被時鍾中斷進行更新,所以他是volatile

的,易變的。因此在用的時候要讓編譯器每次從內存里面取值。而rt_clock通常只有一個寫者(時鍾中斷),其他地方的使用

通常是只讀的,所以將其聲明為const,表示這里不應該修改這個變量。所以volatile和const是兩個不矛盾的東西,並且一個

對象同時具備這兩種屬性也是具有實際意義的。

  注意:上面這個例子里要注意聲明和定義是const的使用,在需要讀寫rt_clock變量的中斷函數里應該如下定義

//在需要讀寫rt_clock變量的中斷函數里如下定義
unsigned char* volatile reg;
//在提供給用戶的聲明頭文件中可以如此申明
external const volatile unsigned int rt_clock;

 

  再看一個例子
volatile struct deveregs* const dvp = DEVADDR;

   這里的volatile和const實際上是分別修飾了兩個不同的對象:volatile修飾的是指針dvp所指向的類型為struct devregs

的數據結構,這個結構對應設備的硬件寄存器,所以是易變的,不能被優化的;而后面的const修飾的是指針變量dvp。應為硬

件寄存器地址是一個常量,所以這個指針變量定義成const的不能被修改。

 

---------------------------------------------【分割線】----------------------------------------------------

危險的volatile用法
 
例:定義為volatile的結構體成員
 
  考察下面對一個設備硬件寄存器的結構類型定義:
struct deveregs{
   unsigned short volatile csr;
   unsigned short const volatile data; 
};

   我們的原意是希望聲明一個設備的硬件寄存器組,其中有一個16bit的CSR寄存器,這個寄存器可以由程序向設備寫入

控制字,也可以由硬件設備設置反應其工作狀態。另外還有一個16bit的data數據寄存器,這個寄存器只會有硬件進行設置

由程序進行讀入。

  看起來這個結構的定義沒有什么問題,也符合實際情況。但是如果執行下面這樣的代碼時,會發生什么情況呢。

struct deveregs* const dvp = DEVADDR;
while((dvp->csr & (READY | ERROR)) == 0){
     //NULL wait till done
    ;   
}

   通過一個non-volatile的結構體指針,去訪問被定義為volatile的結構體成員,編譯器將如何處理?答案是:undefined!

C99標准沒有對編譯器在這種情況下的行為做規定,隨意編譯器有可能正確的將dvp->car作為volatile的變量來處理,使程序

運行正常;也有可能就將dvp->csr作為普通的non-volatile變量來處理,在while當中優化為只有開始的時候取值一次,以后

以后每次循環始終使用第一次取來的值而不再從硬件寄存器里讀取,這樣上面的代碼就有可能死循環。

 

  如果你使用一個volatile的指針來指向一個非volatile的對象,比如將一個non-volatile的結構體賦給一個volatile的指針,
這樣對volatile指針所指向結構體的使用都會被編譯器認為是volatile的,計算原本那個對象沒有被聲明volatile。然而反過來,
如果將一個volatile對象的地址賦值給一個non_volatile的普通變量,通過這個指針訪問volatile對象的結果是undefined,是
危險的。

 

 所以對於本例中的代碼,我們應該修改成這樣:

struct devregs{
    unsigned short csr;
    unsigned short data;
};

volatile struct devrges* const dvp = DEVADDE;

   這樣我們才能保證通過dvp指針去訪問結構體成員的時候都是volatile來處理的。

 

 例:定義為volatile的結構體類型

考察如下代碼:

volatile struct devregs{
    .....
}dev1;
.....
struct devregs dev2;

 

 作者目的也許是希望定義一個volatile的結構體類型,然后順便定義一個這樣的volatile結構體變量,

因此定義了一個dev2.然而第二次所定義的dev2變量實際上是non-volatile的!因為實際在定義結構

體類型時那個關鍵字volatile修飾的是dev1這個變量而不是struct devregss類型的結構體

所以這個代碼應該寫成這樣:

typedef volatile struct devres{
 。。。
}devregs_t;

devregs_t dev1;
..
devregs_t dev2;

 

 這樣我們才能得到兩個volatile的結構體變量。

 

 例:多次的間接指針引用

考察如下代碼:

struct bd{
     unsigned int state ;
   unsigned char* data_buff;
};
struct devregs{
    unsigned int csr;
    struct bd* tx_bd;
    struct bd* rx_bd;
};

volatile struct devregs* const dvp = DEVADDR;

dvp->tx_bd->state = READY;
while((dvp->tx_bd->state & EMPTY | ERROR) == 0){
  ;
}

 

  這樣的代碼常用在對一些DMA設備的發送buffer上。通常這些buffer descriptor(BD)當中的狀態會由硬件進行
設置已經告訴軟件buffer是否完成發送或接收。但是請注意上面的代碼中對dvp->ta_bd->state的操作實際上是non-volatile
的,這樣的操作有可能因為編譯器對其讀取的優化而導致后面陷入死循環。
 
  因為雖然dvp已經定義volatile的指針了,但是也只有向其指向的deVregs結構才屬於volatile object的范圍,也就
是說將dvp聲明為指向volatile數據的指針可以保障其所指的volatile object之內的tx_bd這個結構體成員自身是volatile
變量,但是並不能保證這個指針變量所指的數據也是volatile的(因為這個指針並沒有聲明為指向volatile的指針)。
  要讓上面的代碼工作如下定義
struct devregs{
    unsigned int csr;
    volatile struct bd* tx_bd;
    volatile sturct bd* rx_bd;
};

 這樣可以保障對state成員的處理是volatile的。不過最為穩妥和清晰的辦法是這樣

volatile struct devregs* const dvp = DEVADDR;
volatile struct bd* tx_bd = dvp->tx_bd;

tx_bd->state = READY;
whlie((tx_bd->state &(EMPTY | ERROR)) == 0) {
   ;
}

 

 這樣在代碼里能絕對保障數據結構的易變性,即使數據結構里面沒有定義好也沒有關系。而且對於日后的維護也有好處

:因為這樣從代碼里一眼就能看出那些數據的訪問是必須保證volatile的。

 

例:到底哪個volatile可能無效

就在前面的幾個例子,感覺自己可能都已經弄明白了的時候,請最好看這個例子

struct hw_bd{
    。。。。。
    volatile unsigned char* volatile buffer;
};

struct hw_bd* bdp;
......
bdp->buffer = ...; //1
bdp->buffer = ...; //2

 

 請問上面標記了1和2的代碼哪個是確實在訪問volatile對象,而哪個是undefined的結果。

答案是2是volatile的1是undefined的。來看本例數據結構示意圖

        (non-volatile)

bdp->+---------+

    |              |

        |  ...  ...    | 

        |              |

        +---------+ (volatile)

        |   buffer   |-->+----------+

        +---------+     /               /

                              /               /

                              /               /

                              +----------+

                              /   buffer    /

                              +----------+

                               /               /

                              /               /

                              +----------+

buffer成員本身是通過一個non-volatile的時鍾bdp訪問的,按照C99標准的定義,這就屬於

undefined的情況,因此對bdp->buffer的訪問編譯器不一定能保證是volatile的;雖然buffer

本身可能不是volatile的變量,但是buffer成員是一個指向volatile對象的指針。因此對buffer

成員所指向對象的訪問編譯器可以保證是volatile的,所以bdp->buffer是volatile的。

所以,看似簡單的volatile的關鍵字,用起來還是有很多講究的。

 

 

 

 

 

 

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM