volatile修飾子を正しく使う
| 設計内容分類 | 組み込みソフトウェアにおけるC言語記述 |
|---|
実現したい仕様・課題
ハードウェアのレジスタや割り込みハンドラが書き換える変数を通常の変数として扱うと、コンパイラの最適化により「値は変わらない」とみなされ、フラグ待ちのループが永久に抜けられない、レジスタの読み出しが省略される、といった最適化起因の誤動作が発生します。最適化を有効にしたまま、正しく動くコードにしたいというのが課題です。
設計のポイント
プログラムの外側(ハードウェアや割り込み)から値が変わり得る変数には、volatile修飾子を付けて宣言します。volatileは「この変数はいつ書き換わるか分からないため、最適化による読み書きの省略やキャッシュ(レジスタへの持ち込み)を行わず、参照のたびに必ずメモリへアクセスせよ」という指示です。付けるべき対象は、(1) メモリマップされたハードウェアレジスタ(ステータスレジスタ、データレジスタ等)、(2) 割り込みハンドラとメインループの間で共有するフラグ・データ、(3) 他の実行主体(RTOSの別タスクやDMA)が書き換えるメモリ、の3つが代表です。典型的な症状として、while (flag == 0); のような割り込み待ちループは、flagがvolatileでないと最適化時に無限ループへ変換されることがあります。デバッグビルド(最適化なし)では動き、リリースビルドで動かなくなる不具合の多くがこのパターンです。重要な注意点として、volatileは排他制御ではありません。読み出し・加算・書き戻しのような複合操作の途中で割り込まれる競合(アトミック性の欠如)はvolatileでは防げないため、複数バイトのデータや複合操作の共有には、割り込み禁止区間やフラグの受け渡し設計を併用します。逆に、すべての変数にvolatileを付けると最適化が全面的に阻害され性能が落ちるため、「外部から書き換わる変数だけに付ける」ことが正しい使い方です。
【コード例】
// ハードウェアレジスタ:volatile必須
#define UART_SR (*(volatile uint32_t*)0x40001000)
// 割り込みと共有するフラグ
volatile uint8_t rx_done = 0;
void uart_isr(void) { rx_done = 1; } // 割り込みハンドラ
// メインループ側:volatileがないと最適化で無限ループ化の危険
while (rx_done == 0) { /* 待ち */ }
rx_done = 0;
補足情報
設計のポイント 一覧
組み込みソフトウェアにおけるC言語記述のポイント
- リングバッファで受信データを取りこぼさない
- switch-caseによるステートマシンの実装
- 割り込みハンドラは短く書く
- constを活用する(ROM配置と書き換え防止)
- ビット演算によるレジスタ・フラグ操作
- 固定幅整数型(stdint.h)を使う
- volatile修飾子を正しく使う
- 構造体の宣言にpackedを指定する
- ソースコードのエンディアン変換
- sizeofを使用する
- 特殊文字の記述にエスケープ文字を使用する
- 変数の初期化漏れを防ぐ
- 外部バスの使用
- sprintf関数を使う
- 列挙型(enum)を使う
- 構造体を使う
- 標準関数を使う
- 条件演算子を使う
- インクリメント演算子を使う
- C言語記述の際はアプリ層とドライバ層に分ける



