この記事の要約
「volatile」修飾子を使え。
環境
- MacBook Pro 2021 M1 Pro
- PlatformIO 6.1.3
- Arduino Pro(ATmega328p 3.3V)
やりたかったこと
7セグメントLEDに0と3の数字を交互に表示。
注意
7セグメントLEDはパラレルで送る仕様(1ピン1ピンに7セグのそれぞれのセグが紐付けられている)となっているが、シリアル通信でも問題はない。(ArduinoだったらShiftOut関数という便利なものもある。)
また、今後自分が使いやすいようにしたためダイナミック点灯を実装しての表示となる。
実際どういう動作をしたの?
3の数字しか表示されなかった。
プログラムが間違っているんだろ!と言う人もいるかもしれない。なのでまずは、コードを実際に見ていただきたい。
#include "a.h" //このヘッダーファイルにピン情報等記述
void setup(){
_init(); //a.hに定義したATMEGA328P初期化用関数
}
void loop(){
l_seg = seg_data[0]; // l_segはa.hに定義済み この内容が左側の7セグにダイナミック点灯
delay(1000); //1秒(1000ms)停止
l_seg = seg_data[3];
delay(1000);
}
#include <arduino.h>
unsigned char seg_data[] = {
0b00111111,
・
・
・
//0~9までの7セグの表示定義
};
void _init(){
DDRD = 0xFF; //ポートDに対して全て出力
//割り込み処理をするための設定
TCCR1A = 0;
TCCR1B = 0x0B;
OCR1A = 124;
TIMSK1 = 0x02;
}
static unsigned char l_seg = 0;
static unsigned char r_seg = 0;
//割り込み処理によるダイナミック点灯関数
ISR(TIMER1_COMPA_vect){
static char count = 0x00;
count = (count + 1) & 0x0F;
if(count < 8){
digitalWrite(L7,HIGH);
digitalWrite(R7,LOW);
PORTD = l_seg; //PORTDが表示用レジスタ 左側表示
}
else {
digitalWrite(L7,LOW);
digitalWrite(R7,HIGH);
PORTD = r_seg; //右側表示
}
}
プログラムを要約すると、時間割り込みをしてダイナミック点灯をちらつきなく実現しているということだ。とりあえず、「l_seg」変数に値を代入すると左側の7セグに表示、「r_seg」変数に値を代入すると右側の7セグに表示されるということを理解していただきたい。
それを踏まえた上で、Arduinoのメイン関数と言える「loop」を見てほしい。
void loop(){
l_seg = seg_data[0]; // l_segはa.hに定義済み この内容が左側の7セグにダイナミック点灯
delay(1000); //1秒(1000ms)停止
l_seg = seg_data[3];
delay(1000);
}
間違いなく、
7セグ左側に0の表示→1秒待つ→7セグ左側に3の表示→1秒待つ→7セグ左側に0の表示...
となるはずだ。
だが、実際の動作はこうだった。
7セグ左側に3の表示...
は??????????????????
何故こうなったか
これには私の使ったC++のコンパイラが深く関係していた。そもそもコンパイラとは、このように人間がわかり易い言葉(C系統は分かりづらいが)で書かれたコードを2進数である機械語に翻訳するものだ。青狸のほんやくコンニャクだと思ってもらえれば良い。
ところで、このコンパイラくんはプログラムを自動的に最適化してくれる機能がある。例えば、
int num;
void main()
{
num = 0;
num = 3;
num = 13;
printf("%d", num);
}
この場合表示される数字はなんだろうか。
13
こうなる。前にどのような値が代入されていようが、最終的な値は13である。当たり前だ。
ではこれを最適化してみよう。この、「num」に代入されている値は最終的に13となる。では、その前の変数への代入は意味がないと判断できるだろう。
int num;
void main()
{
num = 13;
printf("%d", num);
}
実際の動作は上のと変わらないはずである。むしろ、2行分なくなったお陰でこのプログラムが実行される時間が微々たるものではあるが減っている。通常コンパイラにはこのような機能がデフォルトで付いているはずだ。
ではここで、先程のArduinoの「loop」関数を見てみよう。
void loop(){
l_seg = seg_data[0];
delay(1000);
l_seg = seg_data[3];
delay(1000);
}
実はloop関数とはいえ、中身はただの一直線のコードである。ただ、この関数が繰り返し無限に何回も実行されるだけだからだ。
さあ、先程と同じようにコンパイラの気持ちになって最適化してみよう。
最終的に「l_seg」に代入される値は「seg_data[3]」となる。なので、その前の変数の代入を削除してしまおう。
void loop(){
delay(1000);
l_seg = seg_data[3];
delay(1000);
}
なんと!!! 最初に「l_seg」に代入される0の7セグのデータが消えてしまったではないか!!!
つまり、これが最初の3しか表示されない原因だったみたいだ。
この最適化の正体は「冗長なコード」を消しているというものになる。つまり、C++のコンパイラはこれを「冗長なコード」と判断して、処理を最適化していた。
こうしてあげよう
先程の「a.h」に「volatile」修飾子をつけてあげよう。
~中略~
static volatile unsigned char l_seg = 0;
static volatile unsigned char r_seg = 0;
volatileって?
Oracle社のドキュメントに書かれていたものを引用させて頂く。
volatile は文字どおりの解釈を意味する
これまでの例ではすべて const を使用してきましたが、これは const が概念的に簡単であるためです。 しかし、volatile はどのような意味でしょうか。volatile という言葉は「揮発性の」、つまりすぐに変わってしまうという意味を持ちます。そのためコンパイラでは、コード生成時にこのようなオブジェクトにアクセスするためのショートカットは行われません。ANSI/ISO C では、オブジェクトを volatile 修飾型として宣言するかどうかはプログラマの責任であると規定しています。
引用元 https://docs.oracle.com/cd/E19205-01/820-1209/bjakl/index.html
よく分からん!
自分なりの解釈として人に説明するならばこう言う。
「最適化するな!!!」
厳密に言うと間違いなく違う表現にはなるが、これが一番簡単だろう。
結論
タイトルの結論としては、コンパイラの最適化には注意しようということになる。ではそのコンパイラに対して、最適化をするなと指定するには「volatile」修飾子を使おう。
参考文献
- http://www.musashinodenpa.com/arduino/ref/index.php?f=0&pos=1780
- http://www.kumikomi.net/archives/2003/05/10kumi.php より 「volatileを指定したくなるとき」
- https://dlrecord.hatenablog.com/entry/2020/09/12/141331
上記の記事、サイトを参考にしました。本当にありがとうございました。