C語言之volatile【整理】

根據c/c++語法,const能夠出現的地方,volatile幾乎也均可以出現。可是,const修飾的對象其值不能改變,而volatile修飾的對象其值能夠隨意地改變,也就是說,volatile對象值可能會改變,即便沒有任何代碼去改變它。在這一點上,最典型的例子就是內存映射的設備寄存器和多線程中的共享對象。懂得使用volatile也是一門小小的藝術。使用volatile約束符能夠阻止編譯器對代碼過度優化防止出現一些你意想不到的狀況,達不到預期的結果;過頻地使用volatile極可能會增長代碼尺寸和下降性能。下面舉個例子來講明volatile在優化中的微妙做用。 
1.阻止編譯器優化
  ARM Evaluator-7T模擬單機板使用基於內存映射的設備寄存器叫特殊寄存器,用來
控制和交互外圍設備。CPU對內存的操做能夠作到按位進行,而特殊寄存器是4字節對齊並佔四個字節。你能夠象unsigned int變量同樣操做特殊寄存器(有些人可能更喜歡uint32_t,認爲這樣體現寄存器佔用4個字節的特色。uint32_t在C99 頭文件<stdint.h>中有定義)。而這裏,爲了體現寄存器自己做爲寄存器的含義而非它的物理意義的,咱們作以下定義:
typedef uint32_t special_register;
  Evaluator-7T板子上有一個按鈕(能夠認爲是外設之一)。按下該按鈕能夠對IOPDATA寄存器第8位置1,相反,釋放按鈕會將該位從新清0。咱們使用枚舉方法爲IOPDATA寄存器的第8位置定義一個掩碼mask:
enum { button = 0x100 };
IOPDATA寄存器對應的地址爲0x3FF5008,咱們能夠用宏形象地定義IOPDATA:
#define IOPDATA (*(special_register *)0x03FF5008)
有了這個定義,咱們執行下面的循環就可使CPU一直等待該按鈕被按下:
while ((IOPDATA & button) == 0)
    ;
  然而這個指望必須創建在編譯器不對代碼進行優化的前提假設之上。若是編譯器優化這段代碼,那麼它會認爲在這個循環中沒有什麼會改變IOPDATA並且認爲條件判斷結果老是真或假,最終優化的結果是隻對(IOPDATA & button)==0判斷一次,以後的循環都不在對其進行判斷,其等同於:
if ((IOPDATA & button) == 0)
    for (;;)
        ;
  顯然,若是條件判斷結果爲真(那麼以後都會認爲是真),那麼這段代碼將會陷入死循環。若是判斷爲假,那麼循環就此結束。能夠看出,優化的代碼效率更高,由於每次循環相比原來的執行時間要短。不幸的是,這段優化代碼使得它根本就不能響應按鈕的每次動做。那麼,如何解決這個問題呢?解決的關鍵就是不要讓編譯器優化這段代碼,使用volatile就能夠辦到這一點。咱們修改前面關於IOPDATA的宏定義:
#define IOPDATA (*(special_register volatile *)0x03FF5008)
這個定義將IOPDATA 定義爲volatile類型的寄存器。volatile隱含地告訴編譯器特殊寄存器可能會改變內容,即便沒有任何顯式地代碼去改變它的內容。這樣一來,編譯器就不對IOPDATA做優化,而是每次都去訪問IOPDATA,這其實正是咱們所指望的。
2.無心中下降了效率
  有時候,若是不注意的話,使用volatile會無心中下降代碼效率。舉個例子。Evaluator-7T有一個七段數碼顯示器。
  在IOPDATA 寄存器中第10到16位用來控制顯示器的每一段。好比第10位就是用來控制頂部的那段顯示,置1則點亮它,清0則熄滅它。咱們能夠定義一個掩碼mask來覆蓋從第10到16的全部位:
enum { display = 0x1FC00 };
假設變量b用來控制這7段顯示器的每一段顯示,而且b的值已經你想要設置值(準備用來顯示哪幾段和熄滅哪幾段,其它無關的位均爲0)。那麼你想要改變設置新的顯示方式的操做就是:
IOPDATA = b;
可是這種賦值可能會改變第10到16位以外的其它位,這是咱們不指望的。因此,採用下面的方法更好:
IOPDATA |= b
可是,使用 |= 並不能熄滅那些已經點亮的顯示段(1 | 0 -> 1),因此咱們能夠用下面的函數達到目的:
void display_put(uint32_t b)
{
    IOPDATA &= ~display;    /*熄滅全部的段*/
    IOPDATA |= b;        /*點亮想要的段*/
}
  不過,可能沒想到的是這樣的操做在無心中下降了代碼效率。由於咱們定義IOPDATA爲
volatile類型,它阻止了編譯器對代碼的優化,要求任何讀寫IOPDATA的操做都死死板板地進行。IOPDATA &= ~display的等價表現爲IOPDATA = IOPDATA & ~display,也就是先從IOPDATA讀出內容而後與上~display,最後又回寫IOPDATA。同理,IOPDATA |=b也有類似的過程。整個過程分別有2次讀IOPDATA和2次寫IOPDATA的操做。若是IOPDATA不使用volatile,那麼編譯器會要求將IOPDATA & ~display的結果放在CPU寄存器中,直到完成IOPDATA |= b操做才寫回特殊寄存器IOPDATA。顯而後者較以前者分別省掉了1次讀IOPDATA和1次I寫OPDATA的耗時操做(外設操做是最耗時的),效率要高不少。若是你想使用volatile但又能使能優化功能,你能夠將函數做以下的修改:
void display_put(uint32_t b)
{
    register uint32_t temp = IOPDATA;/*定義局部變量*/
    temp &= ~display;         /*讀取IOPDATA內容到temp*/
    temp |= b;              /*將temp內容或上b*/
    IOPDATA = temp;          /*將結果寫回IOPDATA*/
}
這樣作有點煩瑣,下面的等效方法更簡單:
void display_put(uint32_t b)
{
    IOPDATA = (IOPDATA & ~display) | b;
}
結論:從該例子看出,它並不鼓勵使用volatile,即便要用也要很當心,由於volatile可能在無心中下降了代碼效率,而你卻沒法察覺。可是,咱們說,不鼓勵並非說就不能或不要用,而是要懂得什麼時候用,怎麼用好它。其所謂智用了。
在上文中提到,volatile定義的對象其內容可能會突然的變化。換句話講,若是你定義了一個volatile對象,就等於你告訴編譯器該對象的內容可能會改變,即便代碼中沒有任何語句去改變該對象。編譯器訪問非volatile對象和volatile對象的方式很不同。對於前者(經優化後),它先將非volatile對象的內容讀到CPU寄存器中,等操做CPU寄存器一段時間後,才最終將CPU寄存器的內容寫回volatile對象。然而,對於volatile對象就沒有這種優化操做。這時候編譯器有些「笨」,代碼要求它讀取或寫入volatile,它就立刻如實地去作。前一篇《慎重使用》主要講述如何明智地正確使用volatile,本篇文章經過一些實際應用進一步闡述volatile在解決嵌入式問題中的一些微妙做用並繼續深刻探討使用volatile要注意的一些細節問題。 
 
1.構造內存映射的設備寄存器
  許多處理器使用內存映射的I/O設備。這些設備將寄存器映射到普通內存空間的某些固定地址。這些基於內存映射的設備寄存器看起來與通常的數據對象沒啥兩樣。在《慎重使用》中提到ARM Evaluator-7T 的特殊寄存器的定義爲:
typedef uint32_t special_register;
在嵌入式應用中,許多設備有時候不只僅與一個寄存器打交道,有時可能與多個寄存器的集合同時打交道。在Evaluator-7T板子上,串口UART就是一個很好的例子。在這個板子上有兩個UART,UART0和UART1。每一個UART都由6個特殊寄存器控制。咱們能夠經過一個數據結構來表示這些寄存器的集合:
 
注意:數據結構UART和標識符UART的不一樣使用方法和位置。
 
typedef struct UART UART; 
struct UART
{
  special_register ULCON; 
  special_register UCON;    /*控制*/
  special_register USTAT;     /*狀態*/
  special_register UTXBUF;    /*發送緩衝*/
  special_register URXBUF;    /*接收緩衝*/
  special_register UBRDIV;    
};
UART0對應的特殊寄存器被映射到0x03FFD000。咱們有兩種方法來訪問該寄存器,一種是《智用篇》中提到過的宏定義方法:
#define UART0 ((UART *)0x03FFD000)
另外一種是經過常量指針:
UART *const UART0  = (UART *) 0x03FFD000;
 
2.使用volatile
  《慎重使用》提到,若是你不但願編譯器對你的代碼做優化以防止出現你預想不到的狀況,那麼使用volatile是不二之選。顯然,要訪問串口的設備寄存器,咱們必需要關掉編譯器優化。如今,volatile能夠大顯身手了。咱們修改前面的定義爲:
#define UART0 ((UART volatile *) 0x03FFD000)
或:
UART volatile *const UART0  = (UART *) 0x03FFD000;
若是使用後者(常量指針),就建議作強制轉化:
UART volatile *const UART0  = (UART volatile *)0x03FFD000;
但這並非必須。對於任意類型T,c編譯器提供T指針到volatile T指針的標準內置轉化,就如同T指針到const T指針的轉化,整個過程自動完成。另外,將一個對象定義爲volatile類型,那麼該對象中的全部成員也將成爲volatile類型。顯然,在UART0前面加volatile類型,不可避免在其它地方也必需要加上volatile。
好比,咱們有下面的函數實現串口的字符串輸出:
void put(char const *s, UART *u);
若是UART0是指向UART對象的volatile指針,那麼以下調用會有什麼問題呢:
put("hello, world/n", UART0);   
編譯出錯通不過!由於編譯器不會將volatile UART指針轉化爲UART指針,因此咱們能作的就是將其強制轉化:/*UART == struct UART*/
put("hello, world/n", (UART *)UART0);/*volatile UART -> UART*/
這個強制轉化雖然騙過了編譯器,但在運行態(run time)可能會出問題。由於這時編譯器將volatile類型UART0當作非volatile類型使用。爲了不這個缺陷,能夠這樣聲明:
void put(char const *s, UART volatile *u);
注意:在這裏加了volatile以後,在其它相關的地方別忘了也要加上volatile!
 
2.準確地構造寄存器
  先看下面對UART0的聲明:UART volatile*const UART0 = ...;
這種添加volatile的同時還添加const的作法有下面微妙的隱含功能:UART結構自己並非volatile的,這個聲明使得UART0指向一個volatile類型的UART常量對象。然而,其它的串口好比UART1有可能不是定義成volatile類型(有可能將UART1定義成UART類型)。除非系統確實有這樣區分的須要,不然這種不一致並非值得提倡的編程風格。解決這種問題的方法之一就是將volatile引入到UART類型:
typedef struct UART volatile UART;
有些人可能更願意這麼定義:
typedef volatile struct UART UART;
但我本人推薦將const/volatile放到類型右側的定義風格(即前者的風格)。使用上面的定義,咱們不用擔憂哪裏是否遺漏了volatile。另外,UART0的定義也修正爲:
#define UART0 ((UART *)0x03FFD000)
或
UART *const UART0  = (UART *) 0x03FFD000;
而put函數也修正爲:
void put(char const *s, UART *u);
這時的UART已經是volatile類型。若是UART1定義成UART類型,那麼顯然它也是volatile類型。先打住,假若有人將UART1定義爲struct UART呢?:
struct UART *const UART1  = (struct UART *) 0x03FF...;
哎呀,沒錯!咱們遺漏了有人可能用struct UART 定義UART1的可能,這種定義使得
對UART1的訪問仍是非volatile方式。到此,咱們能夠看出將UART 定義爲 volatile struct UART 並不能防止有人作出不恰當或不統一的定義。因此,想從根本上解決這種不一致的問題,就只能這麼定義:
typedef struct   
{   
  special_register ULCON; 
  special_register UCON;     /*控制*/
  special_register USTAT;      /*狀態*/
  special_register UTXBUF;     /*發送緩衝*/
  special_register URXBUF;     /*接收緩衝*/
  special_register UBRDIV;
} volatile UART;
這樣使得任何使用UART的地方都是volatile類型的。
或:
struct UART
{    
  special_register volatile ULCON;
  special_register volatile UCON;    /*控制*/
  special_register volatile USTAT;     /*狀態*/
  special_register volatile UTXBUF;   /*發送緩衝*/
  special_register volatile URXBUF;    /*接收緩衝*/
  special_register volatile UBRDIV;
};/*UART結構每一個成員都是volatile 類型*/
雖然咱們用上面的方法解決UART結構類型和struct UART類型的不統一,但能夠看出special_register不是固有的volatile類型,因此在別的地方,特殊積存器可能不是volatile類型(有人可能不須要用UART來定義寄存器組,他要的只是用special_register定義單個寄存器)。爲了從根本上完全解決這種潛在問題,須要將special_register
做以下定義:
typedef uint32_t volatile special_register;
 
這樣一來,不論你定義寄存器組仍是單個寄存器都是volatile類型的!
 
總結:本篇文章始終圍繞設備寄存器定義,就volatile到底該用在什麼地方,該用在什麼位置展開深刻的分析討論,最終獲得將special_register定義爲volatile類型是嵌入式應用中最理想的設計。
上文主要探討關於volatile在定義設備寄存器時應該放到什麼位置最合適的問題。另外,在文章中也提到下面兩個觀點:
*對任意數據類型T,C提供一種標準內置的轉換。這個轉化能夠完成從T指針到volatile T指針的轉換,並規定其逆過程即volatile T指針向T指針轉換爲非法。
*const指針和volatile指針在轉換規則方面具備類似性。
本篇文章就後一個觀點繼續深刻探討。
本人認爲const指針的轉換規則與const指針的基本一致,所以只要咱們懂得其中的一種規則,那麼另外的一種就能夠不攻自破。關鍵就是要懂得其中的共同規律,而不是去死記硬背一些具體應用。 
1.自相矛盾
T *p;
...
void f(T const *qc);
若是調用f(p)將p傳入函數,T指針將會轉換成const T指針。整個轉換過程是自動完成的,沒必要人爲地強制轉換T指針。這是一條潛規則。相反,在下面狀況下,若是調用g(pc),就會產生編譯錯誤:
T const *pc;
...
void g(T *q);
由於編譯器拒絕將const T指針轉換成T指針。這也是一條潛規則。
讓記住下面的推斷:若是你許諾你使用const是不讓其它程序改變const對象的內容,那麼你本身在後面編寫const相關代碼時必需要遵照這個許諾。就象一些作官的,表面是一套,背後又是另外一套,最後對本身的所作所爲不能自圓其說!
下面舉個簡單的例子來講明諾言是怎麼許下,又是怎麼被打破的。
假設有人寫了下面的代碼:
int const *p;
顯然,他但願經過const阻止任何有意圖去修改const對象的內容的行爲,可他又繼續寫下了"挨扁"的代碼:
*p += 3; /*改變p指向的內容*/
++(*p);
由於,他本身又去修改p指針指向的內容,自相矛盾啊!!!
那讓咱們回頭看原先的代碼:
T const *pc;
...
void g(T *q);
當你定義const類型的指針pc,等價於你對編譯器許諾說我決不容許有代碼直接地或間接地甚至潛在地去修改pc指向的內容。固然,咱們的編譯器是「大好人」,確定會爽快地答應。接着,你又對編譯器許諾說g函數能夠修改經過q傳入的任何指針的內容。最後你試着調用g(pc)將p經過pc傳入g。這時編譯器確定看不過去了,必定會這樣地質問你:
你爲什麼將const指針pc傳入可能會改變pc指向內容的g函數呢,你不是決不容許其它代碼直接地或間接地甚至潛在地去修改pc指向的內容嗎,你如今將pc傳入g函數不是本身打本身嘴巴嗎?嘿嘿,啞口無言了吧!因此,既然作出了許諾,就要堅持到底
繼續下面的代碼:
T *p;
...
void f(T const *qc);
顯然,你許諾編譯器說任何代碼均可以改變p指向的內容而且你編寫的f函數不會改變經過qc傳入的其它指針指向的內容。編譯器又一次爽快地答應了你。最後你調用了f(p)。此次,編譯器只是對你笑笑,心理暗自道:小樣你可別讓我逮到在f函數中調用諸如g之類可能會改變p指向的代碼哦!
2.Const vs Volatile
前面提過,const指針的轉換規則與const指針的基本一致。不一樣的是const是你答應編譯器不會編寫可能改變const對象指向的內容的代碼,而volatile則是編譯器答應你不會對相關代碼進行優化。
看下面的代碼:
T volatile *pv;
...
void g(T *q);
對比const能夠知道,調用g(pv)確定會出現編譯錯誤。由於你跟編譯器說不要間接或直接地甚至潛在地優化pv相關的代碼,同時你又有跟編譯器說它能夠優化經過q傳入的指針的相關代碼。若是你調用g(pv),將不能優化的pv傳入可能會優化pv的g函數,顯然也是危險而且自相矛盾的作法。
再看:
T *p;
...
void h(T volatile *qv);
對比const能夠知道,調用h(p)不會有事,由於編譯履行了它的諾言,不在h函數中優化經過qv傳入的任何指針相關的代碼。
結論:const指針的轉換規則與const指針的基本一致,主要的不一樣在於誰許下了諾言。對於const,諾言的主體是咱們本身,而對於volatile則是編譯器。不論誰許了諾,都必須遵照並兌現它。
相關文章
相關標籤/搜索