組み込みC開発でvolatileを使用する


44

volatileキーワードを使用してコンパイラが判断できない方法で変化する可能性のあるオブジェクトに最適化を適用しないようにするためのキーワードの使用に関するいくつかの記事とStack Exchangeの回答を読んでいます。

ADCから読み取り(変数を呼び出しましょうadcValue)、この変数をグローバルとして宣言volatileしている場合、この場合はキーワードを使用する必要がありますか?

  1. volatileキーワードを使用せずに

    // Includes
    #include "adcDriver.h"
    
    // Global variables
    uint16_t adcValue;
    
    // Some code
    void readFromADC(void)
    {
       adcValue = readADC();
    }
    
  2. volatileキーワードを使用する

    // Includes
    #include "adcDriver.h"
    
    // Global variables
    volatile uint16_t adcValue;
    
    // Some code
    void readFromADC(void)
    {
       adcValue = readADC();
    }
    

私の場合(ハードウェアから直接変更されるグローバル変数)のベストプラクティスによると、使用することvolatileは必須ですが、デバッグ時に両方のアプローチに違いは見られないため、この質問をしています。


1
多くのデバッグ環境(確かにgcc)は最適化を適用しません。プロダクションビルドは通常(選択に応じて)なります。これは、ビルド間の「興味深い」違いにつながる可能性があります。リンカの出力マップを見ると参考になります。
ピータースミス

22
「私の場合(ハードウェアから直接変更されるグローバル変数)」-グローバル変数はハードウェアでなく、コンパイラが認識しているCコードによってのみ変更れます。- ADCは、それの結果を提供するハードウェアレジスタは、しかし、しなければならないコンパイラはその値が変更されたときにあれば/知ることができないので、揮発性(それはADCのハードウェアが変換を完了したときに/場合は変更されます。)
JimmyB

2
両方のバージョンで生成されたアセンブラを比較しましたか?それはあなたがボンネットの下に何が起こっているかを示す必要があります
Mawg

3
@stark:BIOS?マイクロコントローラーで?キャッシュルールとメモリマップの設計の一貫性により、メモリマップドI / Oスペースはキャッシュ不可能です(アーキテクチャにそもそもデータキャッシュがある場合でも保証されません)。しかし、volatileはメモリコントローラキャッシュとは関係ありません。
ベンフォークト

1
@Davislor言語標準では、これ以上何も言う必要はありません。volatileオブジェクトへの読み取りは実際のロードを実行し(コンパイラーが最近実行し、通常は値が何であるかを知っている場合でも)、そのようなオブジェクトへの書き込みは実際のストアを実行します(同じ値がオブジェクトから読み取られた場合でも) )。そのためif(x==1) x=1;、書き込みでは、非揮発性に対して最適化され、揮発性xがある場合xは最適化できません。OTOH外部デバイスにアクセスするために特別な指示が必要な場合、それらを追加するのはあなた次第です(たとえば、メモリ範囲をライトスルーする必要がある場合)。
curiousguy

回答:


87

の定義 volatile

volatileコンパイラに通知せずに変数の値が変更される可能性があることをコンパイラに伝えます。したがって、Cプログラムが値を変更していないように見えるため、コンパイラは値が変更されなかったと想定することはできません。

一方、それは変数の値がコンパイラーが知らない他の場所で必要とされる(読み取る)ことを意味します。したがって、変数へのすべての割り当てが実際に書き込み操作として実行されることを確認する必要があります。

ユースケース

volatile 必要なとき

  • ハードウェアレジスタ(またはメモリマップドI / O)を変数として表す-レジスタが読み取られない場合でも、コンパイラは「愚かなプログラマ。自分の変数に値を保存しようと考えて、書き込み操作をスキップしないでください」私たちが書き込みを省略しても気づかないでしょう。」逆に、プログラムが変数に値を書き込まない場合でも、その値はハードウェアによって変更される可能性があります。
  • 実行コンテキスト間で変数を共有する(ISR /メインプログラムなど)(@kkramoの回答を参照)

の影響 volatile

変数が宣言されるとvolatile、コンパイラーは、プログラムコード内の変数へのすべての割り当てが実際の書き込み操作に反映され、プログラムコード内のすべての読み取りが(マップされた)メモリから値を読み取ることを確認する必要があります。

不揮発性変数の場合、コンパイラは変数の値が変化するかどうか/いつ変化するかを知っていると想定し、さまざまな方法でコードを最適化できます。

1つは、CPUレジスタの値を保持することにより、コンパイラーがメモリへの読み取り/書き込みの回数を減らすことができることです。

例:

void uint8_t compute(uint8_t input) {
  uint8_t result = input + 2;
  result = result * 2;
  if ( result > 100 ) {
    result -= 100;
  }
  return result;
}

ここで、コンパイラはおそらくresult変数にRAMを割り当てず、CPUレジスタ以外の中間値を保存しません。

result揮発性の場合result、Cコード内で出現するたびに、コンパイラはRAM(またはI / Oポート)へのアクセスを実行する必要があり、パフォーマンスが低下します。

第二に、コンパイラーは、パフォーマンスおよび/またはコードサイズのために不揮発性変数の操作を並べ替えることができます。簡単な例:

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

再注文可能

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

99を2回ロードする必要がないため、アセンブラー命令を保存できます。

abおよびcが揮発性の場合、コンパイラーは、プログラムで指定されたとおりに正確な順序で値を割り当てる命令を発行する必要があります。

他の典型的な例は次のとおりです。

volatile uint8_t signal;

void waitForSignal() {
  while ( signal == 0 ) {
    // Do nothing.
  }
}

この場合、そうでsignalはない場合、volatileコンパイラはwhile( signal == 0 )無限ループであると「考える」ことになり(ループ内のsignalコードによって変更されることはないため)、同等のものを生成する可能性があります

void waitForSignal() {
  if ( signal != 0 ) {
    return; 
  } else {
    while(true) { // <-- Endless loop!
      // do nothing.
    }
  }
}

volatile値の思いやりのある取り扱い

前述のように、volatile変数は実際に必要とされるよりも頻繁にアクセスされるとパフォーマンスが低下する可能性があります。この問題を軽減するには、次のように不揮発性変数に割り当てて値を「不揮発性」にすることができます。

volatile uint32_t sysTickCount;

void doSysTick() {
  uint32_t ticks = sysTickCount; // A single read access to sysTickCount

  ticks = ticks + 1; 

  setLEDState( ticks < 500000L );

  if ( ticks >= 1000000L ) {
    ticks = 0;
  }
  sysTickCount = ticks; // A single write access to volatile sysTickCount
}

これは、あなたがたときに、可能な限り迅速には、同じハードウェアやメモリを複数回アクセスしていないようにしたいISRの中に特に有益であるかもしれないあなたは、あなたのISRの実行中に値が変更されませんので、それが必要とされていません知っています。これはsysTickCount、上記の例のように、ISRが変数の値の「プロデューサー」である場合に一般的です。AVRでは、関数doSysTick()がメモリ内の同じ4バイトにアクセスすることは特に苦痛です(4命令=アクセスあたり8 CPUサイクルsysTickCount)2回ではなく5回または6回、プログラマは値がそうでないことを知っているので実行中に他のコードから変更されdoSysTick()ます。

このトリックでは、本質的にコンパイラが不揮発性変数に対して行うのとまったく同じことを行います。つまり、必要な場合にのみメモリから読み取り、しばらくの間レジスタに値を保持し、必要な場合にのみメモリに書き戻します; 今回は、あなたは /書き込みが読み取ったときに/場合は、より良いコンパイラよりも知っている必要がありますが、この最適化タスクからコンパイラを和らげるので、起こるとそれを自分で行います。

の制限 volatile

非原子アクセス

volatileマルチワード変数へのアトミックアクセスを提供しませ。そのような場合、を使用することに加えて、他の手段で相互排除を提供する必要がありますvolatile。AVRでは、ATOMIC_BLOCKfrom コール<util/atomic.h>または単純なcli(); ... sei();コールを使用できます。それぞれのマクロもメモリバリアとして機能します。これは、アクセスの順序に関して重要です。

実行順序

volatile他の揮発性変数に関してのみ厳密な実行順序を課します。これは、たとえば

volatile int i;
volatile int j;
int a;

...

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;

(完全を期すためにvolatileすべてのアクセスがこれらのバリアで囲まれている場合、sei / cliマクロによって暗示されるようなメモリバリアは、実際にはの使用を不要にする可能性があると言わなければなりません。)


7
パフォーマンスの非揮発性についての良い議論:)
awjlogan

3
ISO / IEC 9899:1999 6.7.3(6)のvolatileの定義について常に言及したいと思います: An object that has volatile-qualified type may be modified in ways unknown to the implementation or have other unknown side effects. より多くの人々がそれを読むべきです。
Jeroen3

3
割り込みを防ぐのではなく、メモリバリアを達成することが唯一の目標である場合、cli/ seiは重すぎる解決策であることに言及する価値があります。これらのマクロは、実際のcli/ sei命令を生成し、さらにメモリを破壊します。これが障壁となります。割り込みを無効にせずにメモリバリアのみを使用するには、次のようなボディを使用して独自のマクロを定義できます__asm__ __volatile__("":::"memory")(つまり、メモリクローバーを使用した空のアセンブリコード)。
ルスラン

3
@NicHartley No. C17 5.1.2.3§6は、観察可能な動作を定義しています。「揮発性オブジェクトへのアクセスは、抽象マシンの規則に従って厳密に評価されます。」C規格では、メモリバリアが全体的に必要な場所が明確ではありません。使用する式の最後にvolatileシーケンスポイントがあり、それ以降はすべて「後にシーケンス」する必要があります。その表現、ある種の記憶の障壁です。コンパイラベンダーは、あらゆる種類の神話を広めて、プログラマにメモリバリアの責任を負わせることを選択しましたが、それは「抽象マシン」のルールに違反しています。
ランディン

2
@JimmyBローカルvolatileは、などのコードに役立つ場合がありますvolatile data_t data = {0}; set_mmio(&data); while (!data.ready);
マチェイピエチョトカ

13

volatileキーワードは、変数へのアクセスが目に見える効果があることをコンパイラーに伝えます。つまり、ソースコードが変数を使用するたびに、コンパイラは変数へのアクセスを作成する必要があります。読み取りまたは書き込みアクセスであること。

この効果は、通常のコードフロー以外の変数への変更もコードによって監視されることです。たとえば、割り込みハンドラが値を変更した場合。または、変数が実際に変更されるハードウェアレジスタである場合。

この大きな利点はその欠点でもあります。変数へのすべてのアクセスが変数を通過し、値がレジスターに保持されることはありません。これは、揮発性変数が遅いことを意味します。大きさが遅くなります。そのため、実際に必要な場合にのみvolatileを使用してください。

あなたの場合、コードを示した限り、グローバル変数は、自分で更新したときにのみ変更されadcValue = readADC();ます。コンパイラは、これがいつ発生するかを知っており、readFromADC()関数を呼び出す可能性のある何かにわたってadcValueの値をレジスタに保持することはありません。または、知らない関数。または、指すポインタadcValueなどを操作するもの。変数は予測不可能な方法で変化しないため、実際にはvolatileの必要はありません。


6
私はこの答えに同意しますが、「マグニチュードが遅い」というのは悲惨すぎます。
kkrambo

6
最新のスーパースカラーCPUでは、CPUレジスタはCPUサイクル未満でアクセスできます。一方、実際のキャッシュされていないメモリへのアクセス(外部ハードウェアによっては変更されるため、CPUキャッシュは許可されません)は、100〜300 CPUサイクルの範囲になります。だから、はい、大きさ。AVRまたは同様のマイクロコントローラーではそれほど悪くありませんが、質問ではハードウェアを指定していません。
ゴスウィンフォンブレダロー

7
組み込み(マイクロコントローラー)システムでは、RAMアクセスのペナルティははるかに少ないことがよくあります。たとえば、AVRはRAMの読み取りまたは書き込みに2 CPUサイクルしかかかりません(レジスタ間の移動には1サイクルかかります)。アクセスごとに2クロックサイクル。-もちろん、相対的に言えば、レジ​​スタXからRAMに値を保存し、さらに計算するためにその値をすぐにレジスタXにリロードすると、0サイクルではなく2x2 = 4がかかります(Xに値を保持する場合)。したがって、無限になります。遅い:)
JimmyB

1
「特定の変数への書き込みまたは特定の変数からの読み取り」のコンテキストでは、「大きさが遅くなります」。ただし、1つの変数の読み取り/書き込みを何度も何度も繰り返す可能性が高い完全なプログラムのコンテキストでは、実際にはそうではありません。その場合、全体的な違いは「無視できるほど小さい」可能性があります。パフォーマンスに関するアサーションを作成するときは、アサーションが特定の1つの操作またはプログラム全体に関連するかどうかを明確にするように注意する必要があります。使用頻度の低いopを約300倍に減速することは、たいしたことではありません。
アロス

1
つまり、最後の文ですか?これは、「時期尚早な最適化がすべての悪の根源」という意味ではるかに意味があります。もちろんvolatile、すべてのことで使用するべきではありませんが、先制的なパフォーマンスの心配のために合法的に要求されていると思われる場合でも、それを避けてはいけません。
アロス

9

組み込みCアプリケーションでのvolatileキーワードの主な用途は、割り込みハンドラーに書き込まれるグローバル変数をマークすることです。この場合、確かにオプションではありません。

これがないと、コンパイラーは、割り込みハンドラーが呼び出されたことを証明できないため、初期化後に値が書き込まれたことを証明できません。したがって、存在しない変数を最適化できると考えています。


2
確かに他の実用的な用途が存在しますが、これが最も一般的です。
vicatcu

1
値がISRでのみ読み取られる(およびmain()から変更される)場合、潜在的に同様にvolatileを使用してマルチバイト変数のATOMICアクセスを保証する必要があります。
Rev1.0

15
@ Rev1.0いいえ、volatile アロミシティを保証しません。その懸念は個別に対処する必要があります。
クリスストラットン

1
投稿されたコードには、ハードウェアからの読み取りも割り込みもありません。あなたはそこにない質問から物事を仮定しています。現在の形式では実際に回答できません。
ランディン

3
「割り込みハンドラーで書き込まれるグローバル変数をマークする」いいえ。変数をマークすることです。グローバルまたはその他。コンパイラの理解外の何かによって変更される可能性があること。割り込みは不要です。共有メモリか、メモリにプローブを突き刺す人(後者は40年以上前のものにはお勧めできません)
UKMonkey

9

volatile組み込みシステムで使用する必要がある場合が2つあります。

  • ハードウェアレジスタから読み取るとき。

    つまり、MCU内のハードウェア周辺機器の一部であるメモリマップドレジスタ自体です。「ADC0DR」のような不可解な名前を持っている可能性があります。このレジスタは、ツールベンダーが提供するレジスタマップを介して、または自分でCコードで定義する必要があります。自分で行うには、次のようにします(16ビットのレジスタを想定):

    #define ADC0DR (*(volatile uint16_t*)0x1234)

    0x1234は、MCUがレジスタをマップしたアドレスです。以来、volatile既に上記マクロの一部であり、それへのアクセスは、揮発性の修飾であろう。したがって、このコードは問題ありません。

    uint16_t adc_data;
    adc_data = ADC0DR;
  • ISRの結果を使用してISRと関連コード間で変数を共有する場合。

    このようなものがある場合:

    uint16_t adc_data = 0;
    
    void adc_stuff (void)
    {
      if(adc_data > 0)
      {
        do_stuff(adc_data);
      } 
    }
    
    interrupt 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で1つの8ビットコピーを実行したとしても、言語は操作の原子性を保証できないためです。C11機能を使用しない限り_Atomic。この機能を使用できない場合は、何らかの方法でセマフォを使用するか、読み取り中などの割り込みを無効にする必要があります。インラインアセンブラは別のオプションです。volatile原子性を保証しません。

    発生する可能性があるのは次のとおりです
    。-スタック からレジスタに値を
    ロードする-割り込みが発生する-レジスタから値を使用する

    そして、「値を使用する」部分自体が単一の命令であるかどうかは関係ありません。残念なことに、すべての組み込みシステムプログラマーの大部分はこれを忘れており、おそらく最も一般的な組み込みシステムのバグになっています。常に断続的で、刺激しにくく、見つけにくい。


正しく記述されたADCドライバーの例は次のようになります(C11 _Atomicが利用できないと仮定):

adc.h

// adc.h
#ifndef ADC_H
#define ADC_H

/* misc init routines here */

uint16_t adc_get_val (void);

#endif

adc.c

// adc.c
#include "adc.h"

#define ADC0DR (*(volatile uint16_t*)0x1234)

static volatile bool semaphore = false;
static volatile uint16_t adc_val = 0;

uint16_t adc_get_val (void)
{
  uint16_t result;
  semaphore = true;
    result = adc_val;
  semaphore = false;
  return result;
}

interrupt void ADC0_interrupt (void)
{
  if(!semaphore)
  {
    adc_val = ADC0DR;
  }
}
  • このコードは、割り込み自体を中断できないと想定しています。そのようなシステムでは、単純なブール値はセマフォとして機能できます。ブール値が設定される前に割り込みが発生しても害がないため、アトミックである必要はありません。上記の簡略化された方法の欠点は、代わりに以前の値を使用して、競合状態が発生するとADC読み取りを破棄することです。これも回避できますが、コードはより複雑になります。

  • ここでvolatileは、最適化のバグから保護します。ハードウェアレジスタからのデータとは関係なく、データがISRと共有されるだけです。

  • static変数をドライバーに対してローカルにすることにより、スパゲッティプログラミングと名前空間の汚染から保護します。(これは、シングルコア、シングルスレッドのアプリケーションでは問題ありませんが、マルチスレッドのアプリケーションでは問題ありません。)


デバッグが難しいのは相対的です。コードを削除すると、価値のあるコードがなくなったことに気付くでしょう。これは、何かが間違っているというかなり大胆なステートメントです。しかし、私は同意します。非常に奇妙でデバッグしにくいエフェクトが存在する可能性があります。
アーセナル

@Arsenal Cでアセンブラーをインライン化する優れたデバッガーがあり、少なくとも少しのasmを知っている場合、はい、簡単に見つけることができます。しかし、より大規模で複雑なコードの場合、マシンで生成されたasmの大部分は簡単ではありません。または、あなたがASMを知らない場合。または、デバッガががらくたでasm(cougheclipsecough)を表示しない場合。
ランディン

ラウターバッハデバッガーを使用すると、少し甘やかされるかもしれません。最適化されたコードにブレークポイントを設定しようとすると、どこか別の場所に設定され、そこで何かが起こっていることがわかります。
アーセナル

@Arsenal Yep、Lauterbachで入手できる混合C / asmの種類は決して標準ではありません。ほとんどのデバッガーは、別のウィンドウにasmを表示します(ある場合)。
ランディン

semaphore間違いなくあるはずvolatileです!実際、それ最も基本的なユースケースですvolatile:ある実行コンテキストから別の実行コンテキストに何かを通知します。-あなたの例では、コンパイラは、semaphore = true;によって上書きされる前に値が読み取られないことを「見る」ため、単に省略できますsemaphore = false;
JimmyB

5

質問で提示されたコードスニペットでは、volatileを使用する理由はまだありません。の値がadcValueADC に由来することは無関係です。そしてadcValue、グローバルであることは、あなたadcValueが不安定であるべきかどうか疑わしくなるでしょうが、それ自体は理由ではありません。

グローバルであることは、adcValue複数のプログラムコンテキストからアクセスできる可能性を開くための手がかりです。。プログラムコンテキストには、割り込みハンドラとRTOSタスクが含まれます。グローバル変数が1つのコンテキストによって変更された場合、他のプログラムコンテキストは、以前のアクセスからの値を知っていると想定できません。値は異なるプログラムコンテキストで変更されている可能性があるため、各コンテキストは、使用するたびに変数値を再読み取りする必要があります。プログラムコンテキストは、割り込みまたはタスクの切り替えがいつ発生するかを認識していないため、複数のコンテキストで使用されるグローバル変数は、コンテキスト切り替えの可能性があるため、変数のアクセス間で変化する可能性があると想定する必要があります。これがvolatile宣言の目的です。この変数はコンテキストの外部で変更できることをコンパイラに伝えるため、アクセスするたびに読み取って、すでに値を知っていると仮定しないでください。

変数がハードウェアアドレスにメモリマッピングされる場合、ハードウェアによって行われた変更は、プログラムのコンテキスト外の別のコンテキストになります。そのため、メモリマップも手がかりになります。たとえば、readADC()関数がメモリマップされた値にアクセスしてADC値を取得する場合、そのメモリマップされた変数はおそらく揮発性であるはずです。

あなたの質問に戻って、あなたのコードにもっとありadcValue、別のコンテキストで実行される他のコードからアクセスされる場合、はい、adcValue揮発性である必要があります。


4

「ハードウェアから直接変更されるグローバル変数」

値が一部のハードウェアADCレジスタから取得されるからといって、ハードウェアによって「直接」変更されることを意味するわけではありません。

この例では、readADC()を呼び出すだけで、ADCレジスタ値が返されます。これはコンパイラに関しては問題ありません。adcValueにはその時点で新しい値が割り当てられることを知っています。

ADC割り込みルーチンを使用して新しい値を割り当てる場合は、これが異なります。これは、新しいADC値の準備ができたときに呼び出されます。その場合、コンパイラーは対応するISRがいつ呼び出されるかについての手掛かりがなく、adcValueがこの方法でアクセスされないことを決定する場合があります。これはvolatileが役立つ場所です。


1
コードがISR関数を「呼び出す」ことはないため、コンパイラは、誰も呼び出さない関数でのみ変数が更新されることを認識します。そのため、コンパイラはそれを最適化します。
スワナンド

1
コードの残りの部分に依存します。adcValueがどこにも読み込まれていない場合(デバッガーを介してのみ読み込まれる場合など)、または1か所で一度だけ読み込まれる場合、コンパイラはおそらく最適化します。
ダミアン

2
@Damien:常に「依存」しますが、実際の質問「この場合はvolatileキーワードを使用すべきですか?」に対処することを目指していました。できるだけ短くします。
-Rev1.0

4

volatile引数の動作は、コード、コンパイラ、および実行された最適化に大きく依存します。

私が個人的に使用するユースケースは2つありますvolatile

  • デバッガーで見たい変数があるが、コンパイラーがそれを最適化した場合(この変数を持つ必要がないことがわかったため削除したことを意味します)、追加volatileするとコンパイラーはそれを保持するように強制されますデバッグで確認できます。

  • 変数が「コード外」に変更される可能性がある場合、通常、アクセスするハードウェアがある場合、または変数をアドレスに直接マップする場合。

組み込みでもコンパイラーにかなりのバグが時々あり、実際には機能しない最適化を行い、時にはvolatile問題を解決できます。

変数がグローバルに宣言されている場合、変数がコードで使用されている限り、少なくとも書き込まれ、読み取られている限り、おそらく最適化されません。

例:

void test()
{
    int a = 1;
    printf("%i", a);
}

この場合、変数はおそらくprintf( "%i"、1)に最適化されます。

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

最適化されません

もう一つ:

void delay1Ms()
{
    unsigned int i;
    for (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);
}

void anotherFunction()
{
   // Do something with adcValue
}

これらはおそらく最適化されませんが、「コンパイラーがどれほど優れているか」は決して知らず、コンパイラーのパラメーターによって変わる可能性があります。通常、最適化が良好なコンパイラはライセンスされています。


1
たとえば、a = 1; b = a; およびc = b; コンパイラは1分待つと考えるかもしれません。aとbは役に立たないので、cに1を直接入れてみましょう。もちろん、コードでそれを行うことはありませんが、コンパイラはこれらを見つけることよりも優れています。また、最適化されたコードをすぐに書き込もうとすると読みにくくなります。
ダミアン

2
最適化がオンになっていると、正しいコンパイラーで正しいコードが壊れることはありません。コンパイラの正確さは少し問題ですが、少なくともIARでは、最適化によって本来あるべきではないコードが壊れるという状況に遭遇していません。
アーセナル

5
最適化がコードを壊すケースの多くは、UBの領域にも進出しているときです。–
パイプ

2
はい、volatileの副作用は、デバッグを支援できることです。しかし、それはvolatileを使用する正当な理由ではありません。簡単なデバッグが目的の場合は、おそらく最適化をオフにする必要があります。この答えは割り込みについても言及していません。
kkrambo

2
デバッグ引数に追加すると、volatileコンパイラーは変数をRAMに保存し、変数に値が割り当てられるとすぐにそのRAMを更新します。ほとんどの場合、コンパイラは変数を「削除」しません。なぜなら、通常は効果なしで割り当てを書き込むことはありませんが、変数をいくつかのCPUレジスタに保持することを決定し、後でそのレジスタの値をRAMに書き込まないか、まったく書き込まない可能性があるためです。デバッガーは、変数が保持されているCPUレジスターの特定に失敗することが多く、そのためその値を表示できません。
JimmyB

1

技術的な説明はたくさんありますが、実際のアプリケーションに集中したいと思います。

このvolatileキーワードは、コンパイラーが使用されるたびにメモリーから変数の値を読み書きすることを強制します。通常、コンパイラは最適化を試みますが、毎回メモリにアクセスするのではなくCPUレジスタに値を保持するなどして、不要な読み取りと書き込みを行いません。

これには、埋め込みコードで主に2つの用途があります。まず、ハードウェアレジスタに使用されます。ハードウェアレジスタは変更できます。たとえば、ADC結果レジスタはADC周辺機器によって書き込むことができます。ハードウェアレジスタは、アクセス時にアクションを実行することもできます。一般的な例は、UARTのデータレジスタです。これは、読み取り時に割り込みフラグをクリアすることがよくあります。

コンパイラーは通常、値が決して変わらないという前提でレジスターの読み取りと書き込みの繰り返しを最適化しようとするため、アクセスし続ける必要はありませんが、 volatileキーワードは毎回強制的に読み取り操作を実行させます。

2番目の一般的な使用法は、割り込みコードと非割り込みコードの両方で使用される変数です。割り込みは直接呼び出されないため、コンパイラはいつ実行するかを判断できないため、割り込み内のアクセスは発生しないと想定しています。このvolatileキーワードにより、コンパイラは毎回変数にアクセスするように強制されるため、この仮定は削除されます。

volatileキーワードはこれらの問題に対する完全な解決策ではないことに注意することが重要です。これらの問題を回避するには注意が必要です。たとえば、8ビットシステムでは、16ビット変数は読み取りまたは書き込みのために2つのメモリアクセスを必要とするため、コンパイラがそれらのアクセスを順番に行わなければならない場合でも、ハードウェアが最初のアクセスまたは2つの間に発生する割り込み。


0

volatile修飾子がない場合、コードの特定の部分でオブジェクトの値が複数の場所に格納される場合があります。たとえば、次のようなものがあるとします:

int foo;
int someArray[64];
void test(void)
{
  int i;
  foo = 0;
  for (i=0; i<64; i++)
    if (someArray[i] > 0)
      foo++;
}

Cの初期には、コンパイラーはステートメントを処理していました。

foo++;

手順を介して:

load foo into a register
increment that register
store that register back to foo

ただし、より洗練されたコンパイラは、ループ中に「foo」の値がレジスタに保持されている場合、ループの前に1回だけロードし、その後に1回だけ保存する必要があることを認識します。ただし、ループ中は、「foo」の値が2つの場所(グローバルストレージ内とレジスター内)に保持されることを意味します。コンパイラーがループ内で「foo」にアクセスする可能性があるすべての方法を見ることができる場合、これは問題になりませんが、コンパイラーが知らない何らかのメカニズムで「foo」の値にアクセスすると、割り込みハンドラなど)。

規格の作成者は、コンパイラにそのような最適化を明示的に勧める新しい修飾子を追加し、旧式のセマンティクスが存在しない場合に適用されると考えていたかもしれませんが、最適化が非常に役立つ場合問題がある場合は、標準では代わりに、コンパイラは、そうではないという証拠がなくても、そのような最適化は安全であると仮定することができます。volatileキーワードの目的は、そのような証拠を提供することです。

一部のコンパイラライターとプログラマの間のいくつかの競合は、次のような状況で発生します。

unsigned short volatile *volatile output_ptr;
unsigned volatile output_count;

void interrupt_handler(void)
{
  if (output_count)
  {
    *((unsigned short*)0xC0001230) = *output_ptr; // Hardware I/O register
    *((unsigned short*)0xC0001234) = 1; // Hardware I/O register
    *((unsigned short*)0xC0001234) = 0; // Hardware I/O register
    output_ptr++;
    output_count--;
  }
}

void output_data_via_interrupt(unsigned short *dat, unsigned count)
{
  output_ptr = dat;
  output_count = count;
  while(output_count)
     ; // Wait for interrupt to output the data
}

unsigned short output_buffer[10];

void test(void)
{
  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保存場所の書き込みが任意の副作用を引き起こす可能性を考慮し、そのようなストア全体のレジスタに値をキャッシュすることを回避するか、そうでない場合は、修飾されていない「インライン」であるためoutput_buffer[0]、0x1234に書き込み、データを出力するように設定し、完了するのを待ってから0x2345を書き込みoutput_buffer[0]、そこから続行します。標準は必要としませんのアドレスを格納する行為治療するための実装をoutput_bufferAにしますvolatile-修飾されたポインタは、何かが起こる可能性があることを示す記号として、コンパイラが理解できないことを意味します。言われることなく。その結果、gccやclangなどの「賢い」コンパイラーoutput_bufferは、アドレスが2つのストア間のvolatile修飾ポインターに書き込まれている場合でも、output_buffer[0]と仮定します。その時。

さらに、整数から直接キャストされるポインターは、コンパイラーが理解しそうにない方法で物事を操作すること以外の目的で使用されることはほとんどありませんが、標準では、コンパイラーがそのようなアクセスをとして処理する必要はありませんvolatile。そのため、*((unsigned short*)0xC0001234)gccやclangのような「賢い」コンパイラーは最初の書き込みを省略できます。そのようなコンパイラーのメンテナーは、そのvolatileようなコードとの互換性が有用であると認識するよりも、「壊れた」などの修飾を怠るコードを要求するためです。ベンダーが提供するヘッダーファイルの多くはvolatile修飾子を省略しており、ベンダーが提供するヘッダーファイルと互換性のあるコンパイラーは、そうでないものよりも便利です。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.