【C言語|組み込み】volatileとは?付けないと動かない例をArduino(Wokwi)で再現してみた

C言語のvolatile がよく分からない…。誰か詳しく教えて…。

C言語の入門書ではあまり詳しく説明されないのに、組み込みの現場では必須となるキーワードがあります。それがvolatileです。

「変数に付けるおまじない」くらいの説明で済まされることも多いのですが、実はvolatileを付け忘れるとプログラムが正しく動かなくなることがあります。

この記事では、volatileとは何かを整理した上で、「volatileを付けないと本当に動かなくなるのか?」を、ブラウザ上のArduinoシミュレーター(Wokwi)で実際に再現してみます。

この記事で分かること
  • volatileの意味と、なぜ組み込みで必須なのか
  • volatileを付け忘れるとプログラムがどう壊れるか(実演)
  • どんな変数にvolatileを付けるべきか
目次

先に結論

  • volatileは「この変数は、コンパイラの知らないところで値が変わる可能性があるので、最適化しないでそのまま毎回読み書きしてください」とコンパイラに伝えるキーワード
  • 割り込み処理の中で変更される変数にvolatileを付け忘れると、メインループがその変化に気づけず、プログラムが止まったように見えることがある
  • 実際にWokwi上で、volatile無しだと動かず、volatileを付けると動くことを確認した

volatile無しの場合:ボタンを押してもLED点灯しない

volatile有りの場合:ボタンを押すとLED点灯する

volatileとは

コンパイラは、プログラムを速く小さくするために「最適化」を行います

たとえば、以下のようなループがあったとします。

int flag = 0;

while (flag == 0) {    /* flagが1になるのを待つ */}

コンパイラはコード全体を見て、

このループの中ではflagを誰も変更していない。だったら毎回メモリから読み直す必要はない

と判断し、最初に1回だけ読んだ値(今回だと0)を使い回すようにコードを変換してしまうことがあります。

普通のプログラムならこの最適化で問題ありません。しかし、コンパイラの見えないところで値が変わる変数が相手だと、この最適化が裏目に出ます。

volatileを付けると、コンパイラは最適化を諦めて、その変数を毎回律儀にメモリから読み直すようになります。

volatile int flag = 0;  /* 毎回メモリから読み直してくれる */

なぜ組み込みで必須なのか

「コンパイラの見えないところで値が変わる変数」は、組み込みの世界には日常的に存在します。代表例は次の2つです。

  • 割り込み処理の中で変更される変数:メインループの外側で、ボタン押下やタイマーをきっかけに突然実行される処理(割り込み)がフラグを書き換える
  • ハードウェアのレジスタ:センサーや通信ポートの状態を表すメモリ領域は、プログラムが何もしなくてもハードウェア側が勝手に値を変える

どちらも「コードの流れだけを見ているコンパイラ」からは変更が見えないため、volatileで最適化を止めてあげる必要があります。

実演:volatile無しだと本当に動かないのか

ここからは、Wokwi(ブラウザ上のArduinoシミュレーター)で実際に確認してみます。Wokwiの基本的な使い方は以下の記事で紹介しています。

用意したプログラム

「ボタンを押したら割り込みでフラグを立て、メインループがフラグに気づいたらLEDを点灯する」だけのシンプルなプログラムです。

const int LED_PIN = 8;
const int BUTTON_PIN = 2;

int pressed = 0;   /* まずはvolatile無しで試す */

void onButtonPress() {
  pressed = 1;   /* 割り込み処理:ボタンが押されたらフラグを立てる */
}

void setup() {
  pinMode(LED_PIN, OUTPUT);
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), onButtonPress, FALLING);
}

void loop() {
  while (pressed == 0) {
    /* フラグが立つのを待つ */
  }
  digitalWrite(LED_PIN, HIGH);   /* ボタンに気づいたらLED点灯 */
}

volatile無しの実行結果

ボタンを何度押しても、LEDは点灯しませんでした。

割り込み処理(onButtonPress)自体は動いてpressedを1にしているのですが、メインループ側はコンパイラの最適化によって「最初に読んだpressed = 0」を使い回しており、変化に気づけていません。

volatileを付けた実行結果

変数の宣言を1行変えるだけです。

volatile int pressed = 0;   /* volatile有りとする */

今度はボタンを押した瞬間にLEDが点灯しました。変更点はvolatileを付けたことだけです。

注意:「たまたま動いてしまう」こともある

この再現は、コンパイラの最適化の挙動に依存します。最適化のレベルや書き方によっては、volatileを付け忘れていても「たまたま動いてしまう」ことがあります。

これがvolatileの怖いところで、開発中は動いていたのに、コンパイラや最適化設定が変わった途端に動かなくなる、という発見しにくい不具合の原因になります。

「たまたま動く」に頼らず、該当する変数には最初からvolatileを付けておくのが安全です。

どんな変数にvolatileを付けるべきか

代表例として、以下のような変数にはvolatileが必要なケースが多いです。

ただし、本当に必要かどうかはその都度確認が必要です。

  • 割り込み処理(ISR)の中で読み書きされ、メインループでも使われる変数
  • ハードウェアのレジスタをポインタ経由で読み書きする場合

なお、マルチスレッド環境の共有変数は、volatileではなく排他制御の仕組みを使うのが正解です。volatileは万能ではありません。

まとめ

  • volatileは「コンパイラの見えないところで値が変わる変数」への最適化を止めるキーワード
  • 割り込みで変更されるフラグにvolatileが無いと、メインループが変化に気づけず止まったように見える
  • Wokwi上で、volatile無し→動かない、volatileあり→動く、を実際に確認できました
  • 「たまたま動く」こともあるのが怖いところ。該当する変数には最初から付けておくのが安全

最後まで読んでいただき、ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

目次