volatileということばを聞いたことがありますか? また,volatileの使いかたを知っている人はいますか? volatileは,C言語の解説書ではあまり詳しく説明されていませんが,組み込みソフトウェアでは必須です.
volatileは型修飾子の一つです.型修飾子は型の持つ性質を表すものです.volatileの仲間にはconstがあります.constを思い浮かべれば,型修飾子がどのようなものかを想像できると思います.さて,volatileの説明をKernighan&Ritchieの本『プログラミング言語 C』から引用すると,
「volatileの目的は,黙っていると処理系で行われる最適化を抑止することにある.例えば,メモリ・マップ方式の入出力をもつマシンでは,ステータス・レジスタに対するポインタは,ポインタによる見かけ上,冗長な参照をコンパイラが除去するのを防ぐのに,volatileへのポインタと宣言することが可能である.」
と書かれています.わかりますか? この説明を理解するためには二つのポイントがあります.一つは「
黙っていると処理系で行われる最適化」,もう一つは「
見かけ上,冗長な」です.これらを説明しながらvolatileを解説します.
●Cコンパイラの最適化を抑止しなければならない理由
まずは,「黙っていると処理系で行われる最適化」について説明します.これはCコンパイラの最適化を指します.Cコンパイラの最適化は,何に焦点を当てるかによっていろいろな最適化があり,そのための手法もいろいろあります.ここでは解説に必要な点しか触れませんが,Cコンパイラの最適化の詳細は今後のしごとにまちがいなく役立つので,ぜひ機会を見て理解しておいてください.
さて,次のプログラムを見てください.
int data;
while (1) {
if ((data&4) != 0) {
処理1; /* dataに対する代入はないものとする */
}
}
このプログラムは,dataのある1ビットの値が1であれば(「ビットが立っていれば」と言う)処理1を実行しろという内容です(プログラム内の&は,ビットごとのANDをとる演算子である).このプログラムのwhile文中ではdataに対する代入がないので,dataの値は変わりません.そのため,while文のループで毎回dataのある1ビットが立っているかどうかを試すのは効率が悪いということがわかります.ですから,ちゃんとしたコンパイラならば,上のプログラムを次のように解釈して,コードを生成します.
int data;
if ((data&4) != 0) {
while (1) {
処理1;
}
}
こうすることにより,処理速度が速くなります.これがCコンパイラが行う速度重視の最適化の一例です.
さて次に,「見かけ上,冗長な」を説明します.例えば,シリアル・インターフェースを使ったデータの受信を考えてみます.メモリ・マップ方式のシリアル・ポートを持つマイコン注4-1の場合,受信状態を表すレジスタ(ステータス・レジスタと呼ばれる)を読み出せば,そのシリアル・インターフェース経由でデータを受け取ったかどうかがわかります注4-2.では,そのレジスタを読み出すにはどうすればよいのでしょうか?
メモリ・マップ方式の場合は,一つのレジスタに対してあるアドレスが割り付けられます.つまり,割り当てられたアドレスの内容を読み出せばよいわけです.受信状態を表すレジスタのアドレスを0x00efd00c番地,レジスタの長さを4バイトと仮定すると,受信状態を表すレジスタの読み出しは,
*(unsigned long int *)(0x00efd00c)
となります.レジスタのアドレスが0x00efd00c,レジスタ長が4バイトなので,0x00efd00cを(unsigned long int *)でポインタ型にキャストして読み出すわけです(前項の「3.array[−1]はなぜ動くのか」を参照).さらに,受信が完了したかどうかはレジスタの特定のビットが立っているかどうかによって判定すると仮定すれば,その判定とそれに続く受信処理は,例えば次のように書けます.
if (((*(unsigned long int *) (0x00efd00c))&4) != 0) {/* 受信完了か */
受信処理;
}
しかし,いつ受信が完了するかわからないので,while(1)でループを回して(ポーリング方式注4-3)レジスタをチェックしに行くことにします.これでプログラミングは完了です.
while (1) {
if ((*(unsigned long int *) (0x00efd00c))&4) != 0) {/* 受信完了か */
受信処理;
}
}
何か気づきませんか? このプログラムは先ほど説明した最適化のパターンにはまってしまいます.つまり,Cコンパイラが,
if ((*(unsigned long int *) (0x00efd00c))&4) != 0) {/* 受信完了か */
while (1) {
受信処理;
}
}
と最適化してしまうかもしれません.これでは,レジスタを読みに行く最初の1回で受信完了になっていないと,永遠にデータを受信できないかもしれません.
先の最適化のところで書いたように,受信処理で0x00efd 00c番地への代入がない限り,while文のループで毎回参照するのは効率が悪いのです.つまり「見かけ上,冗長」なのです.しかしこの例では,シリアル・インターフェースのデータ受信が完了した時点で,上のプログラムとは関係のないマイコンのハードウェアがレジスタの値を書き換えてしまうので,毎回参照することに意味があるのです.
このギャップを解消するのがvolatile宣言です.下のようにvolatileを付けることで,Cコンパイラによる最適化は行われず,意図しているように動くことになります.
while (1) {
if ((*(volatile unsigned long int *)(0x00efd00c))&4) != 0) { /* 受信完了か */
受信処理;
}
}
言い換えると,volatileを付けたデータの値が要求されると,前に同じデータの値が要求されていたとしてもその値を使わずに,その時点のデータの値を必ず読み取るようなコードが生成されます.「
直前の同じデータの値を使わずに,その時点のデータの値を読み取る」というのは,「
データに書き込まれた値は更新ですぐに消えてしまうので,必要となった時点のデータの値を読み取る」ことだと理解できます.「値はすぐに消えてしまう」ところから,揮発性=volatileというネーミングになったのでしょう.
この例と同様のこと(知らない間に勝手にデータの内容が書き変わるということ)は,割り込み処理やマルチタスクを用いた場合にも発生します.組み込みソフトウェアの場合には,つねに考慮しておくべき状況と言えます.
[Quote]:
http://www.kumikomi.net/article/explanation/2003/10kumi/13.html
テーマ : プログラミング - ジャンル : コンピュータ