前回の記事では、int、uint8_t、uint32_tなどのデータ型とデータサイズについて解説しました。
今回は、データ型と一緒に頻繁に登場するconstとvolatileについて解説します。
組み込み開発のコードを読んでいると、次のような記述を見かけることがあります。
const uint8_t ledPin = 5;
volatile bool interruptFlag = false;
volatile uint32_t *reg;
constとvolatileはどちらも、型に対して追加の性質を指定する型修飾子です。
特にvolatileは、GPIOなどのハードウェアレジスタや割り込み処理を扱う組み込み開発では非常に重要です。
この記事では、constとvolatileの基本から、コンパイラの最適化、割り込み、メモリマップドI/O、ポインタとの組み合わせまで順番に解説します。
constとvolatileとは?
まず、大まかな違いを確認しましょう。
| 修飾子 | 主な意味 |
|---|---|
const | その名前を通して値を変更しない |
volatile | 値がプログラムから見えない要因で変化する可能性があることをコンパイラへ伝える |
非常に簡単に表現すると、
const ↓ この値はここから変更しない volatile ↓ この値はいつ変わるか分からないので 必要なアクセスを勝手に省略しないで
というイメージです。
constとは?
constは、そのオブジェクトを変更不可として扱うために使用する型修飾子です。
たとえば、LEDを接続しているGPIO番号を次のように定義したとします。
const int ledPin = 5;
ledPinはプログラムの途中で変更する必要がない値です。
そのため、後から次のように変更しようとするとコンパイルエラーになります。
ledPin = 10; // エラー
誤って変更してしまうことをコンパイラが検出できるため、プログラムの安全性や可読性を高められます。
constはどんな値に使う?
プログラムの実行中に変更する必要がない値には、constを付けることを検討できます。
- GPIO番号
- I2Cアドレス
- 設定値
- しきい値
- 固定テーブル
- 関数から変更させたくないデータ
たとえばI2Cセンサーのアドレスなら、次のように書けます。
const uint8_t SENSOR_ADDRESS = 0x44;
コードを読む側にも「この値は変更するものではない」と伝わります。
constは「定数」と完全に同じ意味?
constは初心者向けには「定数」と説明されることもありますが、より正確にはその名前を通した変更を禁止するための型修飾と考えると理解しやすくなります。
特にポインタと組み合わせた場合、この違いが重要になります。
単純な変数では、まず「constを付けるとプログラムから書き換えられない」と覚えておけば十分です。
constを付けると必ずFlashに入る?
constについて、「constを付けるとRAMではなくFlashに保存される」と説明されることがあります。
しかし、constを付けたデータが必ずFlashへ配置されるとは限りません。
実際の配置は、変数の種類、記憶域期間、リンカスクリプト、コンパイラやSDKなどによって決まります。
constの本来の役割は、保存先を指定することではなく、そのオブジェクトを変更不可として扱うことです。
volatileとは?
次に、組み込み開発で特に重要なvolatileを見てみましょう。
volatileは、そのオブジェクトへのアクセスが通常のプログラムの流れだけでは予測できない意味を持つことをコンパイラへ伝えるために使用します。
代表的なのが次のようなケースです。
- 割り込み処理から変更される変数
- ハードウェアレジスタ
- DMAなどCPU以外から更新される領域
こうした値は、通常のコードだけを見ていると変化していないように見えても、実際には外部要因によって変化する可能性があります。
なぜvolatileが必要なの?
理由を理解するには、コンパイラの最適化を知る必要があります。
コンパイラはプログラムを高速・効率的に実行できるように、意味が変わらない範囲でコードを最適化します。
たとえば概念的に、次のようなコードがあるとします。
while (flag == 0) {
}
通常の変数としてflagを扱っている場合、コンパイラから見ると「このループの中ではflagを書き換えていない」と判断できる場合があります。
その結果、最適化によって毎回メモリを読み直す必要がないと判断される可能性があります。
ところが、実際には割り込み処理がflagを書き換える場合があります。
割り込みでvolatileを使う例
たとえば、割り込みが発生したことをメイン処理へ知らせるフラグを考えてみます。
volatile bool interruptFlag = false;
void IRAM_ATTR onInterrupt()
{
interruptFlag = true;
}
void loop()
{
if (interruptFlag) {
interruptFlag = false;
// 割り込み後の処理
}
}
interruptFlagは、通常のloop()だけでなく割り込みハンドラからも変更されます。
そこでvolatileを付けて、通常の処理の外側から値が変化する可能性があることをコンパイラへ伝えます。
割り込みについては「割り込みとは?マイコンがイベントをすぐ処理できる仕組みを初心者向けに解説」もあわせて確認してください。
volatileを付けると何が変わる?
volatileで修飾されたオブジェクトへのアクセスは、コンパイラにとって通常の変数アクセスとは異なる意味を持ちます。
簡単に言えば、ソースコード上で必要なvolatileアクセスを「値は変わっていないはずだから」と勝手に省略したり、通常の変数と同じように扱ったりしないようにします。
volatileなし
メモリから読み出す
↓
CPU内部で値を利用
↓
「変わらない」と判断できれば
再読み込みを省略できる場合がある
volatileあり
必要なvolatileアクセス
↓
その都度、意味のあるアクセスとして扱う
ハードウェアレジスタとvolatile
volatileが非常によく使われるもう1つの場所が、ハードウェアレジスタです。
以前の記事で解説したメモリマップドI/Oでは、特定のメモリアドレスへアクセスすることでGPIOやUARTなどの周辺回路を制御します。
概念的には、次のようなコードです。
volatile uint32_t *reg =
(volatile uint32_t *)0x40000000;
このアドレスがハードウェアレジスタに対応している場合、値はCPUが通常のプログラムとして書き換えなくても、周辺回路の状態によって変化する可能性があります。
また、レジスタへの書き込み自体がハードウェアに対する命令になることもあります。
そのため、コンパイラに不要な読み書きだと判断されて消されては困ります。
そこでハードウェアレジスタを表す定義にはvolatileがよく使われます。
詳しくは「メモリマップドI/O(MMIO)とは?アドレスでマイコンの周辺回路を制御する仕組みを解説」も参照してください。
レジスタが読み取り専用ならconst volatile
constとvolatileは同時に指定することもできます。
const volatile uint32_t *statusReg;
これは、ポインタの指す値について、
const→ このポインタを通して変更しないvolatile→ 外部要因で値が変化する可能性がある
という2つの性質を指定しています。
たとえばハードウェアが更新する読み取り専用ステータスレジスタを表現するときに、この考え方が使われます。
volatileは「割り込み用キーワード」ではない
volatileは割り込みの説明でよく登場するため、「割り込みで使う変数に付けるキーワード」と覚えてしまうことがあります。
しかし本質はそれだけではありません。
プログラムから見える通常の処理だけでは予測できないアクセスや変化があることを、コンパイラへ伝えるための型修飾子です。
- 割り込み
- MMIOレジスタ
- DMA
- 一部の低レベルな共有状態
などが代表例です。
volatileだけで排他制御はできない
ここは非常に重要です。
volatileを付けても、複数の処理から同じ変数へ安全にアクセスできるようになるわけではありません。
たとえば次の処理を考えます。
volatile uint32_t count = 0;
count++;
count++は概念的には、
値を読み出す ↓ 1を加える ↓ 値を書き戻す
という複数の処理になる可能性があります。
その途中で割り込みや別タスクが同じデータを変更すると、期待した結果にならない可能性があります。
volatileは原子性(atomicity)や排他制御を保証するものではありません。
RTOSのタスク間共有にもvolatileだけでは不十分
RTOSで複数のタスクが同じデータへアクセスする場合にも注意が必要です。
volatileを付けただけでは、タスク間の同期や競合を解決できません。
用途に応じて、
- ミューテックス
- セマフォ
- キュー
- クリティカルセクション
- アトミック操作
などの仕組みを使用します。
このあたりは今後のRTOSの記事で詳しく扱います。
constとポインタ
constとポインタを組み合わせると、少し複雑に見える記述が登場します。
const uint32_t *p;
これは「pを通して指しているuint32_tの値を変更しない」という意味です。
uint32_t value = 100;
const uint32_t *p = &value;
// *p = 200; // エラー
value = 200; // value自体は変更可能
ここで重要なのは、valueそのものがconstになったわけではないことです。
ポインタそのものをconstにする
次の書き方では意味が変わります。
uint32_t * const p = &value;
この場合は、ポインタpが保持するアドレスを変更できません。
uint32_t value1 = 100;
uint32_t value2 = 200;
uint32_t * const p = &value1;
*p = 300; // OK
// p = &value2; // エラー
つまり、constの位置によって「何を変更できないのか」が変わります。
constポインタの違いを整理
| 宣言 | 意味 |
|---|---|
const uint32_t *p | pを通して指し先を書き換えない |
uint32_t * const p | p自身が指すアドレスを変更できない |
const uint32_t * const p | pを通した指し先の変更も、p自身の変更もできない |
ポインタについては「ポインタとは?アドレス・&・*の意味を初心者向けに解説」も確認してください。
volatileとポインタ
ハードウェアレジスタを扱うコードでは、volatileとポインタの組み合わせが頻繁に登場します。
volatile uint32_t *reg;
これは「regが指しているuint32_tのオブジェクトがvolatile」という意味です。
つまり、そのアドレスの値はハードウェアなどによって変化する可能性があるため、アクセスを通常の変数と同じように最適化してはいけないことを表します。
const volatileという組み合わせ
最初は矛盾しているように見えるのが、
const volatile uint32_t status;
という組み合わせです。
「変更できないのに、値が変わるとはどういうこと?」と思うかもしれません。
ここでは、
const ↓ このプログラム上のアクセスからは変更しない volatile ↓ しかし外部要因によって値が変わる可能性がある
という意味になります。
ハードウェアが更新する読み取り専用レジスタをイメージすると理解しやすいでしょう。
KUMITATE-C3で考えてみよう
KUMITATE-C3でArduinoプログラムを書く場合にも、constとvolatileを使う場面があります。
たとえば、LEDのGPIO番号は実行中に変更しないのでconstにできます。
const int LED_PIN = 0;
一方、GPIO割り込みで更新するフラグはvolatileとして宣言できます。
const int SW_PIN = 21;
volatile bool switchPressed = false;
void IRAM_ATTR onSwitch()
{
switchPressed = true;
}
void setup()
{
pinMode(LED_PIN, OUTPUT);
pinMode(SW_PIN, INPUT_PULLUP);
attachInterrupt(
digitalPinToInterrupt(SW_PIN),
onSwitch,
FALLING
);
}
void loop()
{
if (switchPressed) {
switchPressed = false;
digitalWrite(LED_PIN, !digitalRead(LED_PIN));
}
}
この例では、
LED_PIN→ 実行中に変更しないのでconstSW_PIN→ 実行中に変更しないのでconstswitchPressed→ 割り込みから変更されるのでvolatile
という使い分けになっています。
volatileを付ければ何でも解決するわけではない
volatileは重要ですが、「よく分からない変数にはとりあえずvolatileを付ける」という使い方は避けましょう。
volatileは、
- スレッドセーフを保証しない
- 排他制御を行わない
- 複数の処理を1回の不可分な処理にはしない
- データ競合そのものを解決しない
という点が重要です。
volatileはあくまで、コンパイラに対して、そのオブジェクトへのアクセスを通常の変数とは異なるものとして扱わせるための仕組みです。
constを使うメリット
- 誤った値の変更をコンパイル時に発見しやすい
- そのデータが変更されないことをコードから読み取れる
- 関数が引数を書き換えないことを表現できる
- APIの意図を明確にできる
- コンパイラが最適化に利用できる場合がある
特に大きなプログラムでは、「変更できるデータ」と「変更してはいけないデータ」を型として区別できることが重要になります。
volatileを使う代表的な場面
| 場面 | volatileが使われる理由 |
|---|---|
| 割り込み | 通常処理とは別のタイミングで値が変化する |
| MMIOレジスタ | ハードウェアによって値が変化したり、アクセス自体に意味がある |
| DMA | CPU以外の機能がメモリを更新する場合がある |
| 低レベルな共有状態 | 通常のコードだけでは変化を判断できない場合がある。ただし同期は別途必要 |
これまで学んだ内容とのつながり
constとvolatileを理解すると、これまで学習してきた内容がかなりつながってきます。
データ型 ↓ const / volatile ↓ ポインタ ↓ メモリアドレス ↓ メモリマップドI/O ↓ ハードウェアレジスタ
そして割り込み側では、
GPIO ↓ 割り込み発生 ↓ 割り込みハンドラ ↓ volatileなフラグを変更 ↓ メイン処理が確認
という形で、GPIOや割り込みの記事ともつながります。
よくある間違い
- constを付ければ必ずFlashに配置されると思う
- volatileを付ければ割り込みとの共有がすべて安全になると思う
- volatileを排他制御の代わりに使う
- volatileを付ければ演算がアトミックになると思う
- すべてのグローバル変数へvolatileを付ける
- constポインタと「constへのポインタ」を混同する
まとめ
constとvolatileは、変数やポインタの性質をコンパイラへ伝えるための重要な型修飾子です。
constは、そのアクセスを通した変更を禁止するために使うvolatileは、通常のプログラムだけでは予測できないアクセスや変化があることをコンパイラへ伝える- constを付けても必ずFlashへ配置されるわけではない
- volatileは割り込みフラグやハードウェアレジスタでよく使われる
- volatileは排他制御やアトミック性を保証しない
- constとvolatileを同時に指定することもできる
- constとポインタを組み合わせる場合はconstの位置によって意味が変わる
- 組み込み開発ではMMIO・割り込み・DMAなどを理解するうえでvolatileが重要になる
Arduinoの短いプログラムではconstやvolatileを深く意識しなくても動くことがあります。
しかし、割り込み、レジスタ、ドライバ、RTOSなどへ進むと、「なぜここにvolatileが付いているのか」「このconstは何を変更禁止にしているのか」を理解できることが重要になります。

