C言語に限らず,コンパイル方式のプログラミング言語はコンパイルをして機械語に変換してから実行する.
このとき,大抵のコンパイラは最適化を行う.ようはプログラムの無駄を省き,コードサイズや実行速度を改善しましょうというものだ.
例として,
int a; a = 100;if(a == 100){ a++;}else{ a--;}
という処理があったとする.
このとき,変数aに100を代入してからif文を評価する間にはaに関する処理がないため,aの値は変わらないことがわかる.
そのため,a == 100 は常に真となるため,最適化によって次のようなプログラムになるだろう.
int a; a = 100; a++;
(もしかしたら int a = 101; まで最適化されるかもしれない.)
if文がなくなることで実行スピードが向上し,コードサイズも減少する.
最適化はコンパイラや最適化の設定によっても変わるため,すべてのコンパイラで上記のような結果になるとは限らない.
最適化によるバグがよく起こる処理として,直進走行,スラローム,超信地旋回などの走行処理が挙げられる.
症状としては指定した距離,角度になってもモータが止まらない,もしくは動かないというものである.
例として,NBMの直進関数の一部を挙げる.
void straight(short length, short top_speed, short end_speed, short acc, char wall_control){ motor_step_sum = 0; //モータ動作開始 target_speed = top_speed; acceleration = acc; run_mode = STRAIGHT; long brake_step = target_step - len_to_step(brake_length); //減速開始まで待つ while(motor_step_sum < brake_step); target_speed = end_speed; //目標距離に到達するまで待つ while(motor_step_sum < target_step); target_speed = 0;}
motor_step_sumは現在の左右の累計ステップ数(=走行距離)が入る変数で,割り込み処理内で勝手に更新される.
taeget_speedに速度を代入すると割り込み処理でモータが動き始める.
処理の流れとしては,
motor_step_sumを0にリセット
↓
モータ動作開始
↓
減速開始ステップ数,目標ステップ数を計算
↓
motor_step_sumが減速開始ステップ数に達するまで待つ
↓
減速開始
↓
motor_step_sumが目標ステップ数に達するまで待つ
↓
モータ停止
となる.
問題になるのはwhileを使った指定距離進むまで待つ処理である.
motor_step_sumは割り込み処理で値が更新される変数であるが,関数の頭でmotor_step_sumを0にリセットしているため,straight関数の中では0のまま値が変わらない変数とコンパイラに認識されてしまう.
このため,whileの継続条件がwhile(0 < 0以上の値)と認識され,常に真という扱いになってしまい,最適化によって無限ループに変換されてしまう.
条件によっては常に偽と判断され,whileそのものが削除されてしまうこともある.
こういった最適化により,無限ループに変換されてしまった場合は指定した距離,角度になってもモータが止まらず,ループが削除された場合は即座に target_speed = 0;が呼ばれてしまい,動かなくなってしまう.
void straight(short length, short top_speed, short end_speed, short acc, char wall_control){ motor_step_sum = 0; //モータ動作開始 target_speed = top_speed; acceleration = acc; run_mode = STRAIGHT; long brake_step = target_step - len_to_step(brake_length); LED_on(1); //減速開始まで待つ while(motor_step_sum < brake_step); LED_on(2); target_speed = end_speed; //目標距離に到達するまで待つ while(motor_step_sum < target_step); LED_on(3); target_speed = 0; LED_on(4);}
処理の間にLEDの点灯や文字の出力を行ってどこまで処理が実行できるかを確認する.
今回はwhileループが無限ループになってしまう現象のため,LED1までしか点灯しない.
そのため,LED1とLED2の点灯処理の間のwhileループで問題が起こっている事がわかる.
//減速開始まで待つ while(motor_step_sum < brake_step){ xprintf(%d %d\n, motor_step_sum, brake_step); }
無限ループになってしまうwhile内で問題の変数の値を出力してみる.
おそらく,無限ループは発生せず,想定通りの動きをするようになるだろう.
無限ループになってしまうループ内でなにかしらの処理を行うようにすると問題が起こらなくなるというのはよくあることで,最適化の対象でなくなるためと思われる.
こうなると「原因は最適化だな」と想像できる.
e2studioのプロジェクト→右クリック→プロパティから,最適化レベルを0にしてコンパイルしなおして実行してみる.
今回の例では最適化によるバグのため,最適化を切れば問題は起こらなくなる.
ここまでデバッグを進めると,motor_step_sum変数が割り込み処理で更新されるはずなのに最適化によってwhileが無限ループになってしまっているということがわかった.
そこで,motor_step_sum変数にvolatile修飾子をつける.
volatile long motor_step_sum;
変数を宣言するときにvolatileをつけるだけである.
これはコンパイラに対してこの変数に対しては最適化を行わないように指定する修飾子である.
これは特に割り込み処理とメイン処理の両方で使用する変数で,なおかつメイン処理でなにかを待つ処理(=ポーリング)で使用している変数につけるべきである.
最近のマイコンは動作周波数も上がり,ROM容量も増えたため,最適化によるバグが起こるくらいなら最初から最適化を切ってしまうというのも一つの手である.
...