題:
在嵌入式C開發中使用volatile
Pryda
2018-11-29 18:30:25 UTC
view on stackexchange narkive permalink

我已經閱讀了一些文章,並在Stack Exchange的答案中找到了有關使用 volatile 關鍵字來防止編譯器對可能以編譯器無法確定的方式更改的對象進行任何優化的方法。 >

如果我正在從ADC讀取數據(我們將其稱為變量 adcValue ),並且我將該變量聲明為全局變量,那麼在這種情況下應該使用關鍵字 volatile

  1. 不使用 volatile 關鍵字

      //包括
    #include“ adcDriver.h”
    
    //全局變量
    uint16_t adcValue;
    
    //一些代碼
    void readFromADC(void)
    {
       adcValue = readADC();
    }
     
  2. 使用 volatile 關鍵字

      //包括
    #include“ adcDriver.h”
    
    //全局變量
    volatile uint16_t adcValue;
    
    //一些代碼
    void readFromADC(void)
    {
       adcValue = readADC();
    }
     
  3. ol>

    我之所以問這個問題,是因為在調試時,儘管最佳實踐表明(在我的情況下(直接從硬件改變的全局變量),然後使用 volatile 是必填項。

許多調試環境(一定是gcc)沒有進行優化。生產版本通常會(取決於您的選擇)。這可能導致版本之間的“有趣”差異。查看鏈接器輸出映射是有用的。
根據您設置構建環境的方式,執行調試構建將導致構建沒有優化,因此您不會發現任何差異。您可能想要構建具有優化的發行版本,但打開調試信息以查看隨後實際生成的信息。
“在我的情況下(直接從硬件更改的全局變量)”-全局變量*不會*被硬件*更改,而是僅由編譯器知道的C代碼更改。-ADC提供其結果的硬件寄存器必須是易失的,因為編譯器無法知道其值是否/何時改變(如果/當ADC硬件完成轉換時,它就會改變)。
您是否比較了兩個版本生成的彙編程序?那應該告訴你引擎蓋下發生了什麼
BIOS應將硬件空間標記為不可緩存。
@stark: BIOS?在微控制器上?通過緩存規則和內存映射之間的設計一致性,內存映射的I / O空間將是不可緩存的(如果體系結構甚至首先具有數據緩存,則無法保證)。但是,volatile與內存控制器緩存無關。
值得注意的是,語言標準本身幾乎沒有說明“ volatile”的含義。按照慣例,它通常用於內存映射的I / O和其他一些事情,例如不應優化的延遲循環。但是您需要檢查它在您使用的特定編譯器上的實際作用。
@Davislor語言標準不需要一般地說。讀易失性對象將執行實際加載(即使編譯器最近做了一次加載並且通常會知道該值是什麼),而對此類對象的寫操作將執行真實存儲(即使從對像中讀取了相同的值))。因此,在`if(x == 1)x = 1;`中,寫操作可以針對非易失性`x`進行優化,如果`x`為易失性則無法優化。OTOH如果需要特殊的指令來訪問外部設備,則由您自己決定添加這些設備(例如,如果需要將存儲範圍寫通)。
@PeterSmith GCC當然可以在調試(-g)模式下進行優化,但是並非所有本地變量都可以被調試器訪問。
@curiousguy這裡的文化似乎與SO這樣的語言諮詢問題截然相反。事實是,儘管GCC將“ volatile”解釋為總是從內存中加載變量,但其他編譯器可能不會。因此,查找您的工作。
九 答案:
JimmyB
2018-11-29 21:07:37 UTC
view on stackexchange narkive permalink

volatile

的定義

volatile 告訴編譯器,變量的值可能會在編譯器不知道的情況下發生變化。因此,編譯器不能僅僅因為C程序似乎沒有更改值就不能假定值沒有更改。

另一方面,這意味著可能需要在編譯器不知道的其他地方讀取(讀取)變量的值,因此必須確保對變量的每個賦值實際上都是作為寫操作執行的。 / p>

用例

時需要

volatile

  • 將硬件寄存器(或內存映射的I / O)表示為變量-即使永遠不會讀取寄存器,編譯器也不能只是跳過寫操作,認為“愚蠢的程序員。試圖將值存儲在變量中,他/她將永遠不會回讀。他/她甚至不會注意到我們是否忽略了文字。”相反,即使程序從不向變量寫入值,硬件也可能會更改其值。
  • 在執行上下文之間共享變量(例如ISR /主程序)(請參閱@kkramo的答案)

volatile

的效果

在將變量聲明為 volatile 時,編譯器必須確保在程序代碼中對其進行的每個賦值均反映在實際的寫操作中,並且每次在程序代碼中進行的讀取均從(映射) )的內存。

對於非易失性變量,編譯器假定它知道是否/何時改變了變量的值,並可以以不同的方式優化代碼。

其中之一是,編譯器可以通過將值保留在CPU寄存器中來減少對內存的讀取/寫入次數。

示例:

  void uint8_t計算(uint8_t輸入){
  uint8_t結果=輸入+ 2;
  結果=結果* 2;
  如果(結果> 100){
    結果-= 100;
  }
  返回結果;
}
 

在這裡,編譯器可能甚至不會為 result 變量分配RAM,並且永遠不會將中間值存儲在任何位置,而是存儲在CPU寄存器中。

如果 result 是易失性的,則C代碼中每次出現 result 都將要求編譯器執行對RAM(或I / O端口)的訪問,從而導致降低性能。

第二,編譯器可能會針對性能和/或代碼大小對非易失性變量進行重新排序。簡單的例子:

  int a = 99;
int b = 1;
整數c = 99;
 

可以重新排序為

  int a = 99;
整數c = 99;
int b = 1;
 

這可以節省彙編程序指令,因為值 99 不必兩次加載。

如果 a b c 易失,則編譯器將必鬚髮出指令,以正確的順序分配值在程序中給出。

另一個經典示例如下:

  volatile uint8_t信號;

void waitForSignal(){
  而(信號== 0){
    // 沒做什麼。
  }
}
 

如果在這種情況下 signal 不是 volatile ,則編譯器會“認為” while(signal == 0)可能是一個無限循環(因為 signal 永遠不會在循環內被代碼更改),並且可能會生成等價於

  void waitForSignal(){
  如果(信號!= 0){
    返回;
  }其他{
    while(true){// <--無止境的循環!
      // 沒做什麼。
    }
  }
}
 

考慮處理 volatile

如上所述,當 volatile 變量被訪問的次數比實際需要的次數多時,可能會導致性能下降。為緩解此問題,您可以通過分配給非易失性變量(如

  volatile uint32_t sysTickCount;

void doSysTick(){
uint32_t ticks = sysTickCount; //對sysTickCount的單次讀取訪問

  刻度=刻度+ 1;

  setLEDState(打勾< 500000L);

  if(tick > = 1000000L){
    刻度= 0;
  }
  sysTickCount =滴答聲; //對易失sysTickCount的單個寫訪問
}
 

這可能在ISR中特別有用,在ISR中,您希望盡可能快地在知道不需要使用相同的硬件或內存時不會多次訪問同一硬件或內存,因為在您運行ISR正在運行。當ISR是變量值的“生產者”時,例如在上例中的 sysTickCount 中,這是很常見的。在AVR上,讓函數 doSysTick()訪問內存中相同的四個字節(四個指令=每次訪問 sysTickCount 的8個CPU週期)會特別痛苦。六次而不是兩次,因為程序員確實知道在他/她的 doSysTick()運行時,其他代碼不會更改該值。

使用此技巧,您基本上可以對非易失性變量執行與編譯器相同的操作,即僅在必須時才從內存中讀取它們,將值保留在寄存器中一段時間,然後僅在發生時才寫回內存它必須但是這一次,比(如果)讀/寫必須的時候要了解編譯器,因此您可以使編譯器從此優化任務中解脫出來,自己動手。

volatile

的限制

非原子訪問

volatile 是否not提供對多字變量的原子訪問。對於這些情況,除了使用 volatile 之外,您還需要通過 其他方式提供互斥。在AVR上,您可以使用 <util / atomic.h> 或簡單的 cli();中的 ATOMIC_BLOCK 。 ... sei(); 調用。各個宏也充當內存屏障,這對於訪問順序很重要:

執行順序

volatile 僅對其他volatile變量施加嚴格的執行順序。這意味著,例如

  volatile int i;
volatile int j;
詮釋

...

i = 1;
a = 99;
j = 2;
 
保證

首先 將1分配給 i ,然後然後將2分配給 j 。但是,不能保證在它們之間分配 a ;編譯器可以在代碼段之前或之後進行分配,基本上可以在任何時候進行,直到 a 的第一次(可見)讀取。

如果不是為了上述宏的存儲障礙,則允許編譯器進行翻譯

  uint32_t x;

cli();
x = volatileVar;
sei();
 

  x = volatileVar;
cli();
sei();
 

  cli();
sei();
x = volatileVar;
 

(為了完整起見,我必須說,像sei / cli宏所隱含的那樣,內存障礙實際上可以消除對 volatile 的使用,如果 all 這些障礙被括在括號中。)

很好地討論了揮發的性能:)
我總是喜歡在[ISO / IEC 9899:1999](http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1124.pdf)中提及volatile的定義6.7.3(6):`具有volatile限定類型的對象可能以實現方式未知的方式修改或具有其他未知的副作用。`更多的人應該閱讀它。
值得一提的是,如果您的唯一目標是實現內存屏障而不是防止中斷,那麼cli / sei的解決方案就太繁瑣了。這些宏會生成實際的“ cli” /“ sei”指令,以及額外的緩衝存儲器,正是這種緩衝導致了障礙。要僅具有一個內存屏障而不禁用中斷,您可以使用主體來定義自己的宏,例如`__asm__ __volatile __(“” :::“ memory”)`(即具有內存破壞符的空彙編代碼)。
允許編譯器對“ volatile”讀寫進行重新排序嗎?是否必須告訴CPU不要重新排序?
重新排序嵌入式彙編程序的所有編譯器都損壞。這些宏應直接擴展到彙編器。同樣值得注意的是,SEI + CLI是“向後的Atmel風格”。摩托羅拉彙編器從Atmel那裡竊取了語法並將其倒退,它使用SEI阻止全局中斷,並使用CLI允許它們。
@NicHartley No. C17 5.1.2.3§6定義了_observable behavior_:“嚴格按照摘要規則評估對易失對象的訪問 C標準並不能真正確定總體上需要在哪些內存屏障。使用volatile的表達式的末尾有一個序列點,並且其後的所有內容都必須“在……之後進行排序”。各種各樣的內存障礙編譯器供應商選擇散佈各種神話,以將內存障礙的責任歸咎於程序員,但這違反了“抽像機”的規則。
@Lundin糟糕,當我嘗試找到它時,我跳過了它。謝謝!
@Ruslan您是正確的。實際上,可以添加一個完整的內存屏障本身可能是一個嚴重的瓶頸,而可以通過(僅)聲明那些需要可變的變量來緩解。
“ _每次在C代碼中出現結果,都將要求編譯器執行對RAM_的訪問。”這是一種可能的解釋。我在標準中看不到有任何影響。該變量是本地變量,其地址從不使用,它可以放入寄存器中。
@Lundin“ _但是違反了”抽像機“ _”的規則,哪個規則?
@Lundin我認為5.1.2.4§1清楚地表明5.1.2.3與單線程環境有關。有關其他示例,請參見§14,§16等(我使用C11複製,但是如果更改了它們,我會感到驚訝)。之前順序排序可能是誤稱與C89 / 90兼容的,而C89 / 90並未涉及多線程環境。通常,“ volatile”用於MMIO,而不用於線程內同步AFAIK。
@curiousguy是的,我知道:)為簡單起見,我在這裡採取了一條捷徑。從語義上講,本地揮發物可能沒有任何意義。僅用於調試。我想這就是為什麼它們不在標準中。因此,我不得不說“如果結果是* global * volatile而不是...”,但這會使示例更加混亂,因為它隨後會將* local *(非易失性)變量與*全局*(易失)。關於如何改進示例或說明的任何建議?也許只是使`result`成為全球性的?
volatile的第三個用例是在調試期間:您設置了一個斷點,並想知道監視中變量的值。如果它不是易失性的,它可能會被優化,並且您會在調試器中看到錯誤的值。
@JimmyB本地volatile可能對諸如volatile data_t data = {0}之類的代碼很有用;set_mmio(&data);while(!data.ready);`
@MaciejPiechotka感謝您的提示!我從不需要它,但這是本地揮發物的一個完全有效的用例。
相關內容:[MCU編程-C ++ O2優化在循環時中斷](https://electronics.stackexchange.com/q/387181)顯示瞭如何使用“ volatile sig_atomic_t”與信號處理程序進行可移植交互,或將C ++ 11與std :: memory_order_relaxed作為volatile的替代方案,用於無鎖的目標。(如果不是非鎖定的,則不能在主線程和中斷處理程序之間使用它:這可能會導致死鎖。)
@Ruslan:可以使用C11`atomic_signal_fence(memory_order_release)`獲取僅編譯器的內存屏障,足以訂購wrt。同一CPU上的其他線程或中斷。或者,對於一個asm指令來說,加載或存儲的原子性太大,您可以通過執行一次額外的寫入/一次額外的讀取以及重試循環(即一個seqlock)來解決只寫/只讀通信問題。https://zh.wikipedia.org/wiki/Seqlock,或者對於您知道正在增加的時間戳,只需在讀取高半部分後重新檢查低半部分以查看其是否包裹(或類似的東西)
Goswin von Brederlow
2018-11-29 19:14:06 UTC
view on stackexchange narkive permalink

volatile關鍵字告訴編譯器對變量的訪問具有明顯的效果。這意味著每次您的源代碼使用變量時,編譯器必須創建對變量的訪問。是讀或寫訪問權限。

這樣做的效果是,代碼也會觀察到正常代碼流之外的變量的任何更改。例如。如果中斷處理程序更改了該值。或者,如果變量實際上是一些硬件寄存器,它會自行更改。

這個巨大的好處也是它的缺點。每次對變量的訪問都會遍歷該變量,並且該值永遠不會保存在寄存器中,以便在任何時間段內都能更快地進行訪問。這意味著一個可變變量將很慢。幅度較慢。因此,僅在實際需要的地方使用volatile。

就您而言,就您顯示的代碼而言,僅當您通過 adcValue = readADC(); 自己更新全局變量時,才會更改全局變量。編譯器知道何時會發生這種情況,並且永遠不會在可能調用 readFromADC()函數的東西上將adcValue的值保存在寄存器中。或任何它不知道的功能。或會操縱可能指向 adcValue 等的指針的任何內容。確實不需要volatile,因為變量永遠不會以不可預測的方式改變。

我同意這個答案,但“幅度較慢”聽起來太可怕了。
在現代超標量CPU上,可以在不到cpu的周期內訪問CPU寄存器。另一方面,對實際未緩存內存的訪問(請記住某些外部硬件會對此進行更改,因此不允許CPU緩存)可以在100-300個CPU週期內。所以,是的,數量級。在AVR或類似的微控制器上不會很糟糕,但是問題並沒有說明硬件。
在嵌入式(微控制器)系統中,RAM訪問的代價通常要少得多。例如,AVR僅需兩個CPU週期即可讀取或寫入RAM(一次寄存器到寄存器的移動需要一個週期),因此將內容保存在寄存器中所節省的成本接近(但從未達到)最大值。每次訪問2個時鐘週期。-當然,相對而言,將值從寄存器X保存到RAM,然後立即將該值重新加載到寄存器X中以進行進一步計算將花費2x2 = 4而不是0個週期(當僅將值保留在X中時),因此是無限的慢點 :)
僅當從未從中斷處理程序中調用`readADC`時才如此。
在“寫入或讀取特定變量”的上下文中,“幅度變慢”,是的。但是,在一個完整程序的上下文中,它可能比一遍又一遍地對一個變量進行讀/寫操作要重要得多,不,不是。在那種情況下,整體差異可能“很小到可以忽略”。在對性能進行斷言時,應注意弄清楚斷言是與某個特定操作還是整個程序有關。將不常用的操作速度降低300倍幾乎不是什麼大問題。
@aroth您的陳述可以理解為“使所有事物變得不穩定,除非對性能至關重要”,並且錯過了使事物變得不穩定的原因。
你的意思是,最後一句話?從“過早的優化是萬惡之源”的意義上講,這意味著更多。顯然,您不應該僅僅因為_而在所有對像上都使用volatile,但是在您由於先發性能擔憂而認為合法調用它的情況下,也不應迴避它。
@aroth感謝您闡明這一點:)我確實按照您的意圖閱讀了您的評論,但是擔心經驗不足的用戶會誤解它。我可能只是偏執狂;-)
Lundin
2018-11-29 21:12:06 UTC
view on stackexchange narkive permalink

在兩種情況下,必須在嵌入式系統中使用 volatile

  • 從硬件寄存器讀取時。

    這意味著內存映射寄存器本身是MCU內部硬件外圍設備的一部分。它可能會具有一些神秘的名稱,例如“ ADC0DR”。必須使用工具供應商或您自己提供的某些寄存器映射,使用C代碼定義此寄存器。要自己做,您可以做(假設16位寄存器):

      #define ADC0DR(*(易失性uint16_t *)0x1234)
     

    其中0x1234是MCU映射寄存器的地址。由於 volatile 已經是上述宏的一部分,因此對其的任何訪問都將是volatile限定的。所以這段代碼很好:

      uint16_t adc_data;
    adc_data = ADC0DR;
     
  • 使用ISR的結果在ISR和相關代碼之間共享變量時。

    如果您有這樣的事情:

      uint16_t adc_data = 0;
    
    無效的adc_stuff(無效)
    {
      if(adc_data > 0)
      {
        do_stuff(adc_data);
      }
    }
    
    中斷void ADC0_interrupt(void)
    {
      adc_data = ADC0DR;
    }
     

    然後,編譯器可能會認為:“ adc_data始終為0,因為它不會在任何地方更新。並且永遠不會調用ADC0_interrupt()函數,因此無法更改該變量”。編譯器通常不會意識到中斷是由硬件而不是軟件調用的。因此,編譯器將刪除代碼 if(adc_data > 0){do_stuff(adc_data); } ,因為它認為它永遠不可能是真實的,從而導致了一個非常奇怪且難以調試的錯誤。

    通過聲明 adc_data volatile ,不允許編譯器做出任何此類假設,也不允許優化對變量的訪問。


重要說明:

  • ISR必須始終在硬件驅動程序中聲明。在這種情況下,ADC ISR應該位於ADC驅動器內部。除了驅動程序外,其他任何操作都不應與ISR進行通信-其他所有操作都是意大利麵條式編程。

  • 編寫C時,必須保護ISR與後台程序 之間的所有通信免受競爭條件的影響。 總是,每次都沒有例外。 MCU數據總線的大小無關緊要,因為即使您用C語言執行一個8位拷貝,該語言也不能保證操作的原子性。除非您使用C11功能 _Atomic 。如果此功能不可用,則必須使用某種信號量或在讀取等過程中禁用中斷。內聯彙編程序是另一種選擇。 volatile 不保證原子性。

    這會發生什麼:
    -將值從堆棧加載到寄存器中
    -發生中斷
    -使用寄存器中的值

    然後,“使用值”部分本身是否為單條指令也沒關係。可悲的是,所有嵌入式系統程序員中有很大一部分人對此一無所知,這可能使其成為有史以來最常見的嵌入式系統錯誤。總是斷斷續續,難以挑釁,很難找到。


正確編寫ADC驅動程序的示例如下所示(假設C11 _Atomic 不可用):

adc.h

  // adc.h
#ifndef ADC_H
#定義ADC_H

/ *此處的其他初始化例程* /

uint16_t adc_get_val(void);

#萬一
 

adc.c

  // adc.c
#include“ adc.h”

#定義ADC0DR(*(易失性uint16_t *)0x1234)

靜態揮發性布爾信號量= false;
靜態易失性uint16_t adc_val = 0;

uint16_t adc_get_val(無效)
{
  uint16_t結果;
  信號量= true;
    結果= adc_val;
  信號量= false;
  返回結果;
}

中斷void ADC0_interrupt(void)
{
  if(!信號量)
  {
    adc_val = ADC0DR;
  }
}
 
  • 此代碼假定中斷本身無法被中斷。在這樣的系統上,一個簡單的布爾值可以充當信號量,並且不必是原子的,因為如果在設置布爾值之前發生中斷,則不會造成任何危害。上述簡化方法的缺點是,在發生競爭情況時,它將使用先前的值代替ADC讀取。也可以避免這種情況,但是隨後代碼變得更加複雜。

  • 此處 volatile 可防止優化錯誤。它與硬件寄存器中的數據無關,只是與ISR共享數據。

  • static 通過將變量設置為驅動程序本地變量來防止意大利麵條編程和名稱空間污染。(這在單核,單線程應用程序中很好,但在多線程應用程序中則沒有。)

很難調試是相對的,如果刪除了代碼,您會注意到您有價值的代碼已經消失了-這是一個非常大膽的聲明,說明存在問題。但我同意,效果可能非常奇怪且難以調試。
@Arsenal如果您有一個不錯的調試器,可以用C內聯彙編程序,並且至少了解一點asm,那麼可以輕鬆發現。但是對於較大的複雜代碼,機器生成的大量asm並非難事。或者,如果您不認識asm。或者,如果您的調試器是廢話並且不顯示asm(cougheclipsecough)。
那時使用Lauterbach調試器可能會使我有點寵壞。如果您嘗試在經過優化的代碼中設置一個斷點,則會將其設置為其他地方,並且您知道那裡發生了什麼。
@Arsenal是的,在Lauterbach中可以得到的混合C / asm絕不是標準。如果有的話,大多數調試器會在單獨的窗口中顯示asm。
信號量絕對應該是易失的!實際上,這是最基本的用例,它需要“ volatile”:從一個執行上下文向另一個執行上下文發出信號。-在您的示例中,編譯器可能只是省略了“ semaphore = true;”,因為它“看到”在其值被`semaphore = false;`覆蓋之前從未讀取過。
我不想在這裡引起一個很長的註釋線程,但是如果保證在指令集/硬件本質上原子地移動數據寬度的情況下確保不訪問中間值狀態,那麼實際上不需要信號量。我不了解任何現代架構,例如,讀取或寫入字節(uint8_t)會導致看到除先前值或新值(介於兩者之間的值)之外的任何內容。
我試圖解釋@vicatcu。您的系統是否可以原子讀取uint8_t無關緊要(所有主流CPU都可以讀取)。因為C指令'uint8_t a = b;`_不能保證_轉換為一條指令。相反,它很可能轉換為(偽代碼彙編器)“ LOAD reg1,b”,“ WRITE SP,reg1”或類似的東西。而且,如果這2條指令之間發生中斷,則數據丟失,無論您的CPU是否可以在一條指令中處理x位數據。
* ISR必須始終在硬件驅動程序內聲明。*對於許多MCU架構(尤其是沒有抽象層的AVR或PIC),該語句甚至都沒有意義。
@Lundin可以肯定,但是如果序列被中斷,您將不會獲得“隨機”或“部分更改”的值,您將在中斷觸發之前或中斷觸發之後獲取該值。鑑於中斷無論如何都是異步的,所以這不是不正確或有害的行為。
lundin添加到@vicatcu:如果特定硬件上的數據類型是原子的(如uint8_t將是原子的),則信號量將保持不變。IRQ可能發生在您釋放信號量之後,返回之前,並且您獲得了過時的價值。與沒有信號燈的情況相同。但是,有了信號量,您現在可以在中斷處理程序中釋放值,以防保持信號量。repeadatley讀取ADC的程序可能永遠不會獲得任何更新的值。如果您的IRQ處理程序編寫的數據不適合原子類型,則必須使用額外的代碼。但是您不使用信號量。
@vicatcu即使是原子的,也不會部分更改。但是隨機,哦,是的。我曾經遇到的一個真正的硬錯誤的軼事:我有一個有點像PWM的代碼。它寫入端口的某些引腳以生成位流。很少會偶爾發出2位長序列的垃圾序列,而不是1位長。處理位衝擊的ISR與調用方完全沒有共享任何變量。經過長時間的痛苦調試之後,我最終找到原因是狀態LED偶爾由主程序寫入,該LED與位敲打位於同一端口。->
發生的結果是,主程序在設置LED之前先堆疊端口寄存器,然後ISR擊中並拔出其引腳,然後PC返回到主程序,用先前的舊值覆蓋端口寄存器,從而導致該位流以保留其先前的值一個週期。我為端口訪問引入了一個信號量,該錯誤消失了。我們根本不能低估這些錯誤的嚴重性和危險性。
*編譯器可能會認為:“ adc_data`始終為0,因為它不會在任何地方更新。並且永遠不會調用ADC0_interrupt()函數,因此無法更改變量*-我認為這可能是唯一的方法如果發生以下情況,則可能是:1)將adc_data設置為“靜態”,以及2)將ADC0_interrupt設置為靜態(這對於IRQ例程而言可能是不可能的。在任何其他組合中,編譯器都無法假定“ adc_data”或“ ADC0_interrupt”`在其他地方沒有用作`extern`。
@Groo中斷最好應為“靜態”,這可能會或可能不會。它取決於中斷向量表是基於函數指針還是原始整數地址。在後者的情況下,ISR可能是“靜態的”。如果是前者,則需要通過給ISR外部鏈接,將ISR暴露給包含向量表的文件。這兩種不同的情況都是常用的。此外,在給定非標準中斷關鍵字的情況下,編譯器可能會採取任何措施或採取任何措施。無論如何,這是一個經常編寫的錯誤,儘管在較早的編譯器中更是如此。
vicatcu
2018-11-29 19:01:46 UTC
view on stackexchange narkive permalink

在嵌入式C應用程序中volatile關鍵字的主要用途是標記在中斷處理程序中寫入的全局變量。在這種情況下,它當然不是可選的。

沒有它,編譯器將無法證明在初始化之後就已經寫入了該值,因為它無法證明曾經調用過中斷處理程序。因此,它認為可以優化不存在的變量。

當然還有其他實際用途,但是恕我直言,這是最常見的。
如果僅在ISR中讀取該值(並從main()中更改),則可能還必須使用volatile以保證對多字節變量進行ATOMIC訪問。
@Rev1.0不,揮發性*不能*保證芳香性。這種擔憂必須單獨解決。
沒有從硬件讀取的信息,也沒有發布代碼中的任何中斷。您假設問題中沒有的東西。不能真正以當前形式回答它。
@ChrisStratton: Ups是的,您是正確的。我本人一直都在使用AVR-GCC標準庫中的ATOMIC_BLOCK,它實際上可用於禁用指定代碼塊的中斷以提供原子性。
“不標記在中斷處理程序中寫入的全局變量”。是為了標記一個變量;全局或其他;它可能會被編譯器無法理解的某些內容更改。不需要中斷。它可以是共享內存,也可以是有人將探針插入內存中(最近40多年來不建議使用此方法)
我的意思是,這幾天通常與中斷處理程序代碼有關,並且我同意在中斷上下文之外(例如main),在讀取或寫入可在中斷上下文中讀取或寫入的多字節值時應禁用中斷。在avr-gcc中使用ATOMIC_BLOCK是表達意圖的一種干淨方法。
請提供編譯器證明,永遠不會寫入非易失性全局變量。(你不能。)
kkrambo
2018-11-29 19:21:35 UTC
view on stackexchange narkive permalink

在問題中提出的代碼段中,尚無使用volatile的理由。 adcValue 的值來自ADC無關緊要。並且 adcValue 處於全局狀態應該使您懷疑 adcValue 是否應該具有可變性,但這不是其本身的原因。

具有全局性是一個線索,因為它打開了從多個程序上下文訪問 adcValue 的可能性。程序上下文包括中斷處理程序和RTOS任務。如果全局變量被一個上下文更改,則其他程序上下文無法假定它們知道先前訪問的值。每個上下文每次使用變量值時都必須重新讀取該變量值,因為該值可能已在其他程序上下文中更改。程序上下文不知道何時發生中斷或任務切換,因此它必須假定由於可能的上下文切換,多個上下文使用的任何全局變量可能在變量的任何訪問之間改變。這就是volatile聲明的用途。它告訴編譯器該變量可以在您的上下文之外更改,因此每次訪問都讀取該變量,並且不要假設您已經知道該值。

如果該變量被內存映射到硬件地址,則硬件所做的更改實際上是程序上下文之外的另一個上下文。因此,內存映射也是一個線索。例如,如果您的 readADC()函數訪問一個內存映射的值以獲取ADC值,則該內存映射的變量可能應該是易失的。

所以回到您的問題,如果您的代碼還有更多內容,並且 adcValue 被在不同上下文中運行的其他代碼訪問,那麼, adcValue 應該易揮發。

Damien
2018-11-29 18:37:13 UTC
view on stackexchange narkive permalink

volatile 參數的行為在很大程度上取決於您的代碼,編譯器和完成的優化。

我個人使用 volatile 有兩種用例:

  • 如果我想用調試器查看一個變量,但是編譯器已經對其進行了優化(意味著它已經刪除了它,因為它發現沒有必要使用該變量),添加了 volatile 將強制編譯器保留它,因此可以在調試時看到。

  • 如果變量可能在“代碼外”更改,通常是在您有一些硬件訪問它,或者將變量直接映射到地址時。

在嵌入式系統中,有時編譯器中也會出現許多錯誤,無法進行優化,有時候 volatile 可以解決問題。

鑑於您的變量是全局聲明的,因此只要在代碼上使用該變量(至少已讀寫),就可能不會對其進行優化。

示例:

  void test()
{
    整數a = 1;
    printf(“%i”,a);
}
 

在這種情況下,變量可能會被優化為printf(“%i”,1);

  void test()
{
    volatile int a = 1;
    printf(“%i”,a);
}
 

不會被優化

另一個:

  void delay1Ms()
{
    無符號整數i;
    對於(i = 0; i<10; i ++)
    {
        delay10us(10);
    }
}
 

在這種情況下,編譯器可能會優化(如果您針對速度進行了優化),從而丟棄了變量

  void delay1Ms()
{
       delay10us(10);
       delay10us(10);
       delay10us(10);
       delay10us(10);
       delay10us(10);
       delay10us(10);
       delay10us(10);
       delay10us(10);
       delay10us(10);
       delay10us(10);
}
 

對於您的用例,“它可能取決於”其餘代碼,在其他地方使用 adcValue 的方式以及您使用的編譯器版本/優化設置。

有時候,沒有優化但無法優化的代碼會很煩人。

  uint16_t adcValue;
void readFromADC(void)
{
  adcValue = readADC();
  printf(“%i”,adcValue);
}
 

這可能已優化為printf(“%i”,readADC());

  uint16_t adcValue;
void readFromADC(void)
{
  adcValue = readADC();
  printf(“%i”,adcValue);
  callAnotherFunction(adcValue);
}
 

-

  uint16_t adcValue;
void readFromADC(void)
{
  adcValue = readADC();
  printf(“%i”,adcValue);
}

無效anotherFunction()
{
   //用adcValue做某事
}
 

這些可能不會進行優化,但是您永遠不會知道“編譯器有多好”,並且可能會隨編譯器參數而變化。通常,具有良好優化的編譯器均已獲得許可。

不知道第一點。謝謝。但是__(意味著它已刪除它,因為它發現沒有必要使用此變量)是什麼意思__?
例如a = 1;b = a;和c = b;編譯器可能會等一下,a和b沒用,我們直接將1放到c中即可。當然,您不會在代碼中這樣做,但是編譯器比發現這些代碼要好,而且如果您嘗試立即編寫優化的代碼,則將難以理解。
使用正確的編譯器的正確代碼不會在打開優化的情況下中斷。編譯器的正確性有點問題,但是至少對於IAR,我還沒有遇到過優化導致代碼中斷的情況。
不應該同意,但是使用Microchip的XC編譯器時,我遇到了優化破壞代碼的問題。@Arsenal
在很多情況下,優化也會破壞代碼,這是當您也涉足[UB](https://en.wikipedia.org/wiki/Undefined_behavior)領域時。
是的,volatile的副作用是它可以幫助調試。但這不是使用volatile的充分理由。如果您的目標是輕鬆調試,則可能應該關閉優化。這個答案甚至沒有提到中斷。
在調試參數中添加“ volatile”會強制編譯器將變量存儲在RAM中,並在將值分配給變量後立即更新該RAM。在大多數情況下,編譯器不會“刪除”變量,因為我們通常不會寫沒有效果的賦值,但是編譯器可能會決定將變量保留在某些CPU寄存器中,並且可能以後再也不會將該寄存器的值寫入RAM。調試器通常無法找到保存該變量的CPU寄存器,因此無法顯示其值。
@kkrambo仍然在代碼中發生在中斷中,編譯器應該知道如何處理中斷。我從來沒有遇到過由於中斷而不得不使用volatile關鍵字的情況。
@Damien這是一個非凡的聲明,沒有任何標准文檔的支持。我希望它不會再咬你了...
-1
-1
使變量為“ volatile”僅可確保如果您在執行該變量讀取的指令上設置了斷點,則可以在調試器中將變量更改為任意值V,並且程序的行為就像寫V一樣發生,您可以在對該變量的寫操作上設置一個斷點。這並不意味著您可以在這些中斷期間觀察到處於明確定義狀態的非易失性對象,因為可以優化對這些其他對象的更改,並且可以在易失性操作周圍移動代碼。
@pipe假定信號或中斷處理程序中的代碼不會中斷正在運行的函數,但是無論何時您調用外部(單獨編譯)的函數,都必須假定可以調用該代碼。
“ _這可能已針對printf(”%i“,readADC()); _”進行了優化?
@pipe的代碼破解是freertos之一,不是我的。中斷不執行代碼,它只是將執行集跳轉到另一個地址,並且在某些uC中可以保存上下文,但是中斷處理程序中的代碼是編譯器範圍內代碼的一部分。
Rev1.0
2018-11-29 18:59:47 UTC
view on stackexchange narkive permalink

“直接從硬件更改的全局變量”

僅僅因為該值來自某個硬件ADC寄存器,並不意味著它被硬件“直接”更改。

在您的示例中,您僅調用readADC(),它將返回一些ADC寄存器值。對於編譯器而言,這很好,因為知道此時已為adcValue分配了新值。

如果您使用ADC中斷例程來分配新值(在準備好新ADC值時調用),則情況會有所不同。在這種情況下,編譯器不會知道何時調用相應的ISR,可能會決定不會以這種方式訪問adcValue。這是易揮發的地方。

由於您的代碼從不“調用” ISR函數,因此編譯器會看到該變量僅在沒有人調用的函數中更新。因此,編譯器對其進行了優化。
這取決於其餘的代碼,如果沒有在任何地方讀取adcValue(例如僅通過調試器讀取),或者在一個位置僅讀取一次,則編譯器可能會對其進行優化。
@Damien:它總是“依賴”的,但是我的目的是解決實際的問題“在這種情況下,我應該使用關鍵字volatile嗎?”越短越好。
user
2018-12-03 17:02:30 UTC
view on stackexchange narkive permalink

很多技術說明,但我想專注於實際應用。

volatile 關鍵字強制編譯器每次使用時從內存中讀取或寫入變量的值。通常,編譯器會嘗試優化而不進行不必要的讀取和寫入,例如通過將該值保存在CPU寄存器中,而不是每次都訪問內存。

這在嵌入式代碼中有兩個主要用途。首先,它用於硬件寄存器。硬件寄存器可以更改,例如ADC結果寄存器可由ADC外設寫入。硬件寄存器在訪問時也可以執行操作。一個常見的例子是UART的數據寄存器,該寄存器通常在讀取時清除中斷標誌。

編譯器通常會假設值永遠不會改變,因此無需重複訪問,通常會嘗試優化寄存器的重複讀寫,但是 volatile 關鍵字將強制執行每次執行一次讀取操作。

第二種常見用法是用於中斷代碼和非中斷代碼所使用的變量。不會直接調用中斷,因此編譯器無法確定何時執行,因此假定中斷內的任何訪問都不會發生。由於 volatile 關鍵字會強制編譯器每次訪問變量,因此將刪除此假設。

請務必注意, volatile 關鍵字不能完全解決這些問題,因此必須小心避免。例如,在8位系統上,一個16位變量需要兩次內存訪問才能進行讀取或寫入,因此,即使編譯器被迫進行這些訪問,它們也依序發生,並且硬件有可能對第一次訪問或兩者之間會發生中斷。

supercat
2018-11-30 23:07:45 UTC
view on stackexchange narkive permalink

在沒有 volatile 限定符的情況下,在代碼的某些部分中,對象的值可能存儲在多個位置。例如,考慮以下內容:

  int foo;
int someArray [64];
無效測試(無效)
{
  我
  foo = 0;
  對於(i = 0; i<64; i ++)
    如果(someArray [i] > 0)
      foo ++;
}
 

在C語言的早期,編譯器會處理該語​​句

  foo ++;
 

通過步驟:

 將foo加載到寄存器中
遞增該寄存器
將寄存器存儲回foo
 

但是,更複雜的編譯器將認識到,如果“ foo”的值在循環期間保存在寄存器中,則只需要在循環之前加載一次,然後再存儲一次即可。但是,在循環期間,這意味著“ foo”的值將保存在兩個位置-全局存儲內和寄存器內。如果編譯器可以看到循環中可能會訪問“ foo”的所有方式,那麼這將不是問題,但是如果以某種編譯器不知道的機制訪問“ foo”的值,則可能會造成麻煩。例如中斷處理程序。

該標準的作者可能有可能添加一個新的限定詞,該限定詞將明確邀請編譯器進行此類優化,並說老式的語義將在不存在該語義的情況下適用,但在某些情況下,這些優化是有用的代碼大大超過了有問題的代碼,因此標准允許編譯器假定在沒有證據表明這樣的優化是安全的情況下,它們是安全的。 volatile 關鍵字的目的是提供此類證據。

在某些情況下,某些編譯器作者和程序員之間會發生一些爭論:

  unsigned short volatile * volatile output_ptr;
unsigned volatile output_count;

無效interrupt_handler(無效)
{
  如果(output_count)
  {
*(((unsigned short *)0xC0001230)= * output_ptr;//硬件I / O寄存器
    *(((無符號短*)0xC0001234)= 1;//硬件I / O寄存器
    *(((無符號短*)0xC0001234)= 0;//硬件I / O寄存器
    output_ptr ++;
    output_count--;
  }
}

void output_data_via_interrupt(unsigned short * dat,unsigned count)
{
  output_ptr = dat;
  output_count =計數;
  while(輸出計數)
     ;//等待中斷以輸出數據
}

無符號的短output_buffer [10];

無效測試(無效)
{
  output_buffer [0] = 0x1234;
  output_data_via_interrupt(output_buffer,1);
  output_buffer [0] = 0x2345;
  output_buffer [1] = 0x6789;
  output_data_via_interrupt(output_buffer,2);
}
 

從歷史上看,大多數編譯器要么允許編寫 volatile 存儲位置可能觸發任意副作用,而且避免在此類存儲中將任何值緩存在寄存器中,否則它們將避免這種可能性從對調用不合格的“內聯”函數的寄存器中的值進行緩存,從而將0x1234寫入 output_buffer [0] ,設置內容以輸出數據,等待數據完成,然後將0x2345寫入 output_buffer [0] ,然後從那裡繼續。該標準不需要 require 實現,以將將 output_buffer 的地址存儲到 volatile 限定的指針中的行為視為可能發生錯誤的標誌。但這是通過編譯器無法理解的方式實現的,因為作者認為編譯器是針對各種平台和目的的編譯器的作者,他們會認識到這樣做的目的是在那些平台上滿足這些目的,而無需告知。因此,某些“靈巧”的編譯器(例如gcc和clang)將假定即使將 output_buffer 的地址寫入兩個存儲在 output_buffer [0] code之間的volatile限定的指針中>,這沒有理由假設任何東西都可能關心那個對象當時持有的價值。

此外,雖然直接從整數強制轉換的指針很少用於除編譯器不太可能理解的方式之外的其他用途,但標準再次不需要編譯器將此類訪問視為易失性。因此,像gcc和clang之類的“聰明”編譯器可能會省略對 *((unsigned short *)0xC0001234)的首次寫入,因為此類編譯器的維護者寧願聲明忽略了限定條件的代碼諸如 volatile 之類的東西被“破壞”,而不是意識到這種代碼很有用。許多供應商提供的頭文件都省略了 volatile 限定詞,並且與供應商提供的頭文件兼容的編譯器比沒有供應商的頭文件有用。



該問答將自動從英語翻譯而來。原始內容可在stackexchange上找到,我們感謝它分發的cc by-sa 4.0許可。
Loading...