電気系の設計者は今日も休みのため、自分も今日も一日中リファクタリング。
今日はByte単位で文字列を切った貼ったしている箇所を、Cのライブラリ関数などを使ってシンプルにしていった。1 byteごとに文字を代入している20行くらいのコードがsprintf( )で1行にしていくのはなかなか快感だったが、調子に乗って書き換えていたら、あるときボタン操作でプログラムが固まるようになっていることに気がついた。
もしかしてこのマイコンの開発環境のsprintf()にはバグがあるのか?と思って調べてみても、他の箇所で使っているsprintf()は正常に動作している。さんざんテストコードを走らせてみたが、やはり特定の関数の中でだけ挙動がおかしい。ならばと思って、その関数をとにかくシンプルにして試してみたが、それでも解決しない。
ということは、問題の原因はその関数の他にある可能性が高い。
そのような影響が遠くに及ぶバグと言えば、C言語の場合に一番疑わしいのはメモリ破壊だが、もともとこのソースでは、文字列操作やポインタなどメモリ破壊の起こりやすいコードは多くない。今回の場合は、次に疑わしいのはタイマ割り込みの使いかたのミスだが、結果的にはこれが当たりだったようだ。
実は前から気になっていたことなのだが、このプログラムは装置のボタンをタイマ割り込みで定期的に監視して、ボタンの押下状態が変化したらそれに応じた処理を実行する設計になっている。この設計には気がかりというか危なっかしい面がある。もしなんらかの理由で、ボタン操作に応答する関数の実行時間が長くなって、タイマのインターバル中に関数が終わらないようになると、次のタイマ割り込みによってこの関数への再入が起こる可能性がある。関数が再入可能になっていれば問題は起こりにくいが、再入可能でない場合にはデータが破壊されるなどの問題が起こる。
こちょこちょ調べたところ、今回はsprintf( )への再入が起こったために処理が固まってしまったようだ。これを回避するには、タイマ割り込み中で時間のかかる処理をしないように修正すれば良い。つまり、タイマ割り込みではボタンの変化の検出だけを行い、実際の処理は割り込みの外で行うようにする。タイマ割り込みでは、ボタンの変化に関する情報を変数に書き込み、すぐに終了する。そこで変数に記録した情報は、メインの無限ループ中で監視し、処理が必要であればメインループ中で行わせる。
こんな感じ。
int key_change = NO_KEY_CHANGE; int busy_flag = NOT_BUSY; //タイマ割り込みハンドラ _irq void Int_Handler(void) { if (busy_flag == NOT_BUSY) { key_change = DetectKeyChange(); //検出はタイマ間隔以内に終える } } void main(void) { .... //無限メインループ for (;;) { if (key_change != NO_KEY_CHANGE) { busy_flag = BUSY; //キーの検出を止める switch (key_change) { case A_BUTTON_DOWN: Deal_A_ButtonDown(); break; case B_BUTTON_DOWN: Deal_B_ButtonDown(); break; .... } busy_flag = NOT_BUSY; //キーの検出を再開する } } }
メインループでの処理の間、タイマを止めるという手もあるが、今回は他の用途でもタイマ割り込みを使っていて、止めるわけにいかなかったので、busyフラグで排他処理をした。