割り込み処理って、組み込み開発では避けて通れない領域ですよね。でも、「なぜか動かない」「たまにフリーズする」「原因がわからない」といったトラブル、経験ありませんか?
実は、割り込みハンドラの中に やってはいけない処理 が潜んでいることが多いんです。
この記事では、現場でよく見かけるNG実装パターンを5つ紹介し、それぞれの対策もわかりやすく解説します。
それでは、どうぞ。
割り込み内で長い処理を書いてしまう
- 「割り込みが来た=すぐ処理しなきゃ」と思って、その場で完結させたくなる
- 時間がかかるとは知らずに、普通の関数をそのまま呼んでしまう
- 非RTOS環境だと、メインループが空回りするのが不安で処理を詰め込むことも
こんな時にやりがちなミスはこちら。
void ADC_IRQHandler(void) {
...
calc_average(); // 平均値をその場で計算
send_uart(); // UART送信までやってしまう
}
このコード、以下の理由でNGです。
以下のような対策が考えられます。
対策後のコードはこんな感じ。フラグを立ててメインループで処理させることで回避できます。
RTOS環境の場合はイベントフラグを使うのがベターです。
// グローバル変数
volatile bool adc_ready = false;
void ADC_IRQHandler(void) {
adc_ready = true; // フラグだけ立てる
}
// メインループ側
int main(void) {
while (1) {
if (adc_ready) {
adc_ready = false;
int adc_value = ADC_Read();
int val = adc_value;
int avg = calc_average(val);
send_uart(avg);
}
}
}
割り込み内でprintfやログ出力を行う
- デバッグ中、「割り込みが本当に来てるか?」をprintfで確認したくなる
- シリアル出力が可視化の手段になっており、つい割り込み内にも多用してしまう
- ログレベルの切り替えなどの仕組みが整っておらず、手軽にトレースする手段として使ってしまう
こんな時にやりがちなミスがこちら。
void UART_IRQHandler(void) {
...
printf("UART interrupt received\n");
}
NGの理由は以下の3点です。
特に3つ目は要注意。タイミング依存の不具合は本当に厄介で、ログ表示してる間は動くのに、ログを無効化すると動かなくなる。。。なんてことがざらにあります。
対策は1つ目と同様、データ格納だけしておいて、メイン処理側でログ出力させる方法があります。
#define LOG_BUF_SIZE 128
volatile char log_buf[LOG_BUF_SIZE];
volatile int log_index = 0;
void UART_IRQHandler(void) {
if (log_index < LOG_BUF_SIZE) {
log_buf[log_index++] = 'U'; // 簡易的な記録
}
}
// メインループ側
int main(void) {
while (1) {
if (log_index > 0) {
__disable_irq();
int len = log_index;
log_index = 0;
__enable_irq();
for (int i = 0; i < len; ++i) {
putchar(log_buf[i]);
}
}
}
}
共有変数を保護せずに書き換える
続いては、
volatile
を使えばOKだと誤解されがち- 単純な変数なら問題ないと思い、保護の必要性を見落とす
- 複雑なロジックでないため、つい軽視してしまう
こんな時によくあるのは、以下のようなNG例。
volatile int counter = 0;
void TIMER_IRQHandler(void) {
counter++; // これ、実は危ない
}
以下のような理由でNGです。
対策
volatile int counter = 0;
void TIMER_IRQHandler(void) {
counter++; // NG:mainが同時に読む可能性あり
}
// メインループ側
int main(void) {
while (1) {
int snapshot;
__disable_irq();
snapshot = counter;
__enable_irq();
printf("Count: %d\n", snapshot);
delay_ms(100);
}
}
または、stdatomic.h
(C11)を使ったり、RTOSのAPIで排他制御も有効です。
フラグを立てっぱなしにする
- 割り込み側でフラグを立てるのは正しいが、処理側でのフラグクリアを忘れがち
- 処理ループが「一度しか通らない前提」になっており、再度割り込みが来る前に必ず処理が終わると過信
- 複数イベントが絡むと、フラグの管理が複雑になりがち
こちらは割り込み側の処理ではなく、main側の処理がNGな例です。
volatile bool flag = false;
void GPIO_IRQHandler(void) {
flag = true;
}
// メインループ側
int main(void) {
while (1) {
if (flag) {
・・・ // フラグクリアしていないため、無限にこの処理が実行される
}
}
}
こんな現象が起きる可能性があります。
対策
volatile bool flag = false;
void GPIO_IRQHandler(void) {
flag = true; // 割り込みでイベント通知
}
// メインループ側
int main(void) {
while (1) {
if (flag) {
flag = false; // フラグを必ずクリア
handle_gpio_event();
}
}
}
割り込みをネストさせて暴走する
- 「一部の割り込みだけ許可したい」という意図で
__enable_irq()
を使ってしまう - 他の割り込みが遅延するのを嫌って、強引にすべて許可してしまう
- ネストによるスタック消費や優先度設計の副作用を理解しきれていない
こんな時にやってしまうパターンはこちら。
void DMA_IRQHandler(void) {
__enable_irq(); // 割り込み中にさらに割り込み許可
...
}
なぜNGか?
対策
例:NVICでDMAは低優先、緊急イベントは高優先で設定
void DMA_IRQHandler(void) {
// __enable_irq() は使用しない
dma_status = DMA_ReadStatus();
dma_flag = true;
}
// メインループ側
int main(void) {
while (1) {
if (dma_flag) {
dma_flag = false;
handle_dma(dma_status);
}
}
}
まとめ|割り込みハンドラ実装で気をつけるべきこと
この記事で紹介したNGパターンを再掲します:
割り込みは組み込み開発におけるリアルタイム性を支える重要な要素です。
しかしその一方で、実装を誤ると「再現しにくいバグ」「原因不明のフリーズ」など、デバッグが極めて困難なトラブルを引き起こします。
割り込みの基本は「最速で抜ける」「外で処理する」「共有データは慎重に扱う」の3点に尽きます。
何気ない実装ミスが、後々の不具合や信頼性低下に直結するのが割り込みの怖いところです。
だからこそ、正しい設計パターンを身につけることが品質向上の第一歩になります。
コメント