ソースコードを見ていると、volatile修飾子をを使用している箇所を見かけます。
C言語の教本にも載っていないし、調べてもなかなか自分が欲しい情報がまとめて出てこないことが多かったので、備忘録としてまとめました。
volatileについて
volatileを使うタイミングは、「コンパイラの最適化防止のため」のようです。
最適化とは、プログラムに変換するときに、書いたソースコードの無駄な処理を削除してくれる処理です。
最適化によって、プログラムのサイズを小さくできるので、速度向上に繋がるみたいです。また、最適化にもレベル設定があるので、どこまでコンパイラが最適化するかは、そのレベル設定次第です。
では、最適化防止に使用するタイミングは、いつなのでしょうか?
ぼくの経験と色々調べた結果、以下の2つが該当するのかなと思います。
- 消したくない変数を残すため
- レジスタの操作のため
これらについて、まとめていきます。
消したくない変数を残すため
とりあえず以下のソースコードを見て下さい。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
#include<stdio.h> /*---- グローバル変数 ----*/ int gloval_param; /*-----------------------------*/ /*--------- main関数 ---------*/ /*-----------------------------*/ int main(void){ int aaa = 10; /* グローバル変数にaaaの値を保存 */ gloval_param = aaa; /* 保存後aaaの値をインクリメント */ aaa++; /* aaaの値を表示 */ printf("aaa...%d",aaa); return 0; } |
見てほしいのが、グローバル変数”gloval_param”です。
13行目に変数aaaの値を保存していますが、その後gloval_paramを使用している(見ている)箇所がありません。
無駄な処理に見えますが、例えばデバッグで変数aaaがインクリメントされる前の値を見るためであれば、上記処理は必要な処理になります。
このソースコードを解析する時、コンパイラは、「gloval_paramに値を入れてるけど、別に他で使っていないのであれば、別に消しても変わらないよね」と判断します。
そしてどうなるかと言うと、13行目の「gloval_param = aaa;」処理自体を削除します。
そうなると、グローバル変数gloval_paramを使用している箇所も無くなるので、変数の定義自体も不要になり、ソースコードから削除されます。
つまり、最適化後のソースコードはこんな感じ。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#include<stdio.h> /*-----------------------------*/ /*--------- main関数 ---------*/ /*-----------------------------*/ int main(void){ int aaa = 10; /* 保存後aaaの値をインクリメント */ aaa++; /* aaaの値を表示 */ printf("aaa...%d",aaa); return 0; } |
グローバル変数gloval_paramが無くてもプログラムに影響が無いので、ソースコードから消えました。
デバッグで変数aaaがインクリメントされる前の値を見ようとしていたのに、最適化で消えてしまうので、これではデバッグが上手く出来ませんね。
このような現象を避けるためには、volatile修飾子を付けて定義をすれば、最適化されずに処理が残ります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
#include<stdio.h> /*---- グローバル変数 ----*/ volatile int gloval_param;/ /*-----------------------------*/ /*--------- main関数 ---------*/ /*-----------------------------*/ int main(void){ int aaa = 10; /* グローバル変数にaaaの値を保存 */ gloval_param = aaa; /* 保存後aaaの値をインクリメント */ aaa++; /* aaaの値を表示 */ printf("aaa...%d",aaa); return 0; } |
レジスタ操作のため
そもそもレジスタ操作について、ぼくがあまり分かっていなかったので、まずそちらをまとめていきます。
レジスタ操作ってどうやるの?
組み込みのマイコンの仕様を見ると、「○○の機能を使うには、XXXレジスタを△△の値で書き込むこと」みたいな感じで書かれていることがあります。
レジスタに値を設定する方法は、以下のイメージです。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#include<stdio.h> /*---- レジスタ ----*/ #define REG_SAMPLE (*(volatile unsigned char *)0xFFCC) /*-----------------------------*/ /*--------- main関数 ---------*/ /*-----------------------------*/ int main(void){ /* REG_SAMPLEというレジスタ(0xFFCC番地)に"0x10"という値を書き込む */ REG_SAMPLE = 0x10; return 0; } |
現場でも上記のようにレジスタを設定している処理があるのですが、「REG_SAMPLE = 0x10;」という処理のようなコードを実際に見たとき、定数値に定数値を入れてるように見えて、全然意味が分かりませんでした。
しかし、これは少しずつ紐解いていけば分かってきます。
1 2 |
/*---- レジスタ ----*/ #define REG_SAMPLE (*(volatile unsigned char *)0xFFCC) |
まず、ここの定義ですよね。
「(unsigned char *)0xFFCC」とは何かですが、「(unsigned char *)」と書くことで、バイト領域へのポインタという意味になります。なので、これで「0xFFCC番地へのバイト領域」と、示すことが出来ます。
ポインタにアクセスするには、「参照演算子”*”」を使えば良かったですね。
なので、0xFFCC番地へアクセスする方法は、「*(unsigned char *)0xFFCC」と書くことが出来ます。
これでvolatile修飾子以外は、同じようになりました。volatileについては、後でまとめるとします。
つまり、define定義しているREG_SAMPLEは、0xFFCC番地へアクセスすることが出来る定義です。こういった定義を「ポート定義」と呼ぶみたいです。
1 2 |
/* REG_SAMPLEというレジスタ(0xFFCC番地)に"0x10"という値を書き込む */ REG_SAMPLE = 0x10; |
さて、ここの処理についてまとめていきます。
ここまでの説明で、define定義の定数値に定数値(0x10)を入れているわけではないことが分かると思います。
これはコメントの通り、0xFFCC番地に0x10という値を書き込んでいるんですね。
これがレジスタ操作であり、この”0x10″を書き込んだことによって、マイコン仕様によって特定の動作をします。
ポート定義のvolatile修飾子について
1 2 |
/*---- レジスタ ----*/ #define REG_SAMPLE (*(volatile unsigned char *)0xFFCC) |
ポート定義にはvolatile修飾子が付いていますが、これはなぜでしょうか?
たとえば、以下のコードを見て下さい。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#include<stdio.h> /*---- レジスタ ----*/ #define REG_SAMPLE (*(unsigned char *)0xFFCC) /*-----------------------------*/ /*--------- main関数 ---------*/ /*-----------------------------*/ int main(void){ /* REG_SAMPLEというレジスタ(0xFFCC番地)に値を書き込む */ REG_SAMPLE = 0x01; /* 特定の動作Aが実行される */ REG_SAMPLE = 0x02; /* 特定の動作Bが実行される */ REG_SAMPLE = 0x04; /* 特定の動作Cが実行される */ return 0; } |
REG_SAMPLEのポート定義から、volatile修飾子を外し、レジスタ操作処理を変えました。
“0x01″,”0x02”,”0x04″という値を書き込んだ場合、マイコンがそれぞれ異なった動作をすると仮定します。
本当は特定の動作A,B,Cを実行したいので、3つの値を設定する処理は必要な動作になります。
しかしコンパイラからすると、「REG_SAMPLEに色々書き込んでいるけど、最終的に”0x04″を代入するんだから、それだけで良いよね」的な判断をする場合があります。
なので、「REG_SAMPLE = 0x04;」の処理だけが残り、前2つの値を設定している処理は削除されてしまいますので、特定の動作AとBは実行されなくなります。
こういった意図しない動作をさせないために、volatile修飾子を付けておく必要があるというみたいです。
まとめ
volatileは最適化防止のためなので、デバッグソフトを作るときやレジスタ操作を自分でコードを組むときぐらいしか使わないのかなと思いました。
ただし知っておくべき知識なので、覚えておきます。