ろぐれこーど

限界組み込みエンジニアの学習記録とちょっぴりポエム

volatile修飾子の使いどころ (組み込み, C)

C言語におけるvolatile修飾子をどういったときに使う必要があるかを整理したかったので調べました。

結論

以下パターンに当てはまるとき、変数にvolatile修飾子をつけます。

基本的にはレジスタ操作・参照を伴う場合につけとけばいいと思います。

ここではメモリマップドI/Oを前提とします。メモリマップドI/Oについては以下の記事が詳しそうです。コスト面で優れていて、昨今のマイコンはほぼメモリマップドI/Oらしいのでそこは意識しなくても良いかもです。

www.kumikomi.net

説明

volatileコンパイラ最適化を抑制するために使われます。(という説明が厳密に正しいかは不明ですが、殆どの場合においてこれで説明されても問題ないと思っています…)

結論で挙げたパターンに合致する具体例を挙げます。ただし、最適化はあくまでコンパイラ依存であるため、ここで列挙するパターンが必ずしも最適化の対象となるとは限りません。

レジスタ操作

他で参照しないレジスタへの書き込み

特定レジスタへの書き込みをするときは以下のようなコードになります。

uint8_t *pi = (uint8_t*)0xFFFF0000;

*pi = 0xFF;
// piはここ以外で使用しない

コード上でpiが他で参照されない場合、「使用されない変数」として最適化の対象となり、アセンブリ上は書き込み命令が削除される恐れがあります。これを回避するために、以下のようにしてvolatile宣言をします。

volatile uint8_t *pi = (uint8_t*)0xFFFF0000;

注意点として、volatileの位置を変えると最適化抑制の対象が変わるそうです。この辺はconstと同じですね。

// piが指す値を最適化抑制
volatile uint8_t *pi = (uint8_t*)0xFFFF0000;
// piのアドレス自体を最適化抑制
uint8_t * volatile pi = (uint8_t*)0xFFFF0000;

後者で使うことはまずないと思われるので、volatileは先頭につけておけば問題ないでしょう。

特定の手順での書き込み

マイコンや周辺機器のレジスタ操作は、メーカーによってその操作手順を指定されている場合があります。「〇〇機能を使うには、××レジスタを特定の手順で書き込んでください」といった感じです。この手順をシンプルに書くと以下のようになります。

uint8_t *pi = (uint8_t*)0xFFFF0000;

*pi = 0x00;
*pi = 0x01;
*pi = 0x02;

この場合、見かけ上は最後の命令のみ意味があるように見えるので、コンパイラは前2つの書き込み命令を消してしまいます。これは意図した動きではないため、pivolatileつきで宣言する必要があります。

レジスタ参照

他で書き込みしないレジスタへの参照

外部要因によって書き換えられるレジスタを参照した判定を考えます。

uint8_t *pi = (uint8_t*)0xFFFF0000;

if (*pi == 0x00){
    // 処理
}
// 他でpiへの書き込みはない

piの指す値が0で初期化されていた場合、コンパイラはこの判定を無駄であると判断し、判定を削除して毎回if文内の処理を実行する可能性があります。その時点でのハードウェアの状態を参照して処理をしたい場面では注意が必要です。

速度重視の最適化が起こる場合

外部信号の受信完了を通知するレジスタのビットがあるとします。このとき、受信待ち→受信完了検知後に何らかの処理をしたい場合を想定すると、例えば以下のような処理になります。

uint8_t *pi = (uint8_t*)0xFFFF0000;
while(1){                        // 受信待ち
    if ((*pi & 0x01) != 0){     // 受信完了?
        // 受信後処理
    }
    // piはwhile文中では更新されない
}

piがwhileの中で更新されないと、コンパイラは「無限ループ中で毎度判定をするのは無駄」と判断し、以下のコードに対応するアセンブリを生成する可能性があります。

uint8_t *pi = (uint8_t*)0xFFFF0000;
if ((*pi & 0x01) != 0){
    while(1){
        //受信後処理
    }
}

これでは受信待ち処理のために組み込んだwhileが意図通りに動きません。この場合でもpivolatileとすると、こういった最適化の対象から外れます。

wait処理

レジスタ操作では「次の命令は○us後にして下さい」といった指定がされる場合もあります。そのための最も単純な方法として、カウンタを使用したループが挙げられます。

unsigned int i;
unsigned int cnt;

for(i=0;i<1000;i++){     // 指定の時間waitする
    cnt++;
}
// cntはここ以外で使用しない

やはりコンパイラはコードから意図を組んでくれませんので、volatileによって最適化を抑制しなければなりません。この場合はcntが対象となります。

まとめ

色々パターンを出してみたはいいものの、やっぱり「とりあえずつけとけ」に落ち着きそうです。

ただし、ここでは列挙できていないパターンも存在します。

docs.oracle.com

setjmplongjmpを使用する場合にあてはまるそうです。正直setjmpとか初めて知りました。まだまだ勉強不足です。

また、色々調べているうちに誤った使用法が挙げられていました。

  • マルチスレッドの同期にvolatile変数を使用
  • 複数スレッドでアクセスする変数をvolatileにする

つけても直接的な影響が出ないこともあるため、問題視されていないのかもしれません。詳細は以下で議論されています。

yohhoy.hatenadiary.jp

ja.stackoverflow.com

i-saint.hatenablog.com

参考

sunafukin2go.hatenablog.com

www.kumikomi.net