PR

【組み込みC++】割り込みハンドラ実装でやりがちなNG 5選とその対策を紹介

組み込み開発

割り込み処理って、組み込み開発では避けて通れない領域ですよね。でも、「なぜか動かない」「たまにフリーズする」「原因がわからない」といったトラブル、経験ありませんか?

実は、割り込みハンドラの中に やってはいけない処理 が潜んでいることが多いんです。

この記事では、現場でよく見かける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点です。

  • printfは時間がかかる処理(内部でロックやフォーマット処理あり)
  • UARTやDMAと競合してバグを誘発
  • タイミングに依存するバグは再現が難しく、デバッグ地獄

特に3つ目は要注意。タイミング依存の不具合は本当に厄介で、ログ表示してる間は動くのに、ログを無効化すると動かなくなる。。。なんてことがざらにあります。

対策は1つ目と同様、データ格納だけしておいて、メイン処理側でログ出力させる方法があります。

  • 割り込み内ではログ出力を避ける
  • ログが必要なら、リングバッファにpushして後処理
#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です。

  • 割り込み中にメインループでも同じ変数にアクセスしていると、競合が発生
  • 特に、読み→加算→書き込みの3ステップはアトミックではない
  • 結果、加算しているのに値が増えないという不可解な現象が起きる

対策

  • アトミック操作を使う(可能なら)
  • もしくは、クリティカルセクションで排他制御する

アトミック操作とは「割り込みされずに1回で安全に完了する処理」のことです。
一部のCPUでは ++ はアトミックではないため、読み→加算→書き込みが途中で割り込まれると、意図しない値になります。

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パターンを再掲します:

  • 割り込み内で長時間処理をしない
  • printfやログ出力は避ける
  • 共有変数は保護して扱う
  • フラグは立てっぱなしにしない
  • 割り込みのネストには注意

割り込みは組み込み開発におけるリアルタイム性を支える重要な要素です。
しかしその一方で、実装を誤ると「再現しにくいバグ」「原因不明のフリーズ」など、デバッグが極めて困難なトラブルを引き起こします。

割り込みの基本は「最速で抜ける」「外で処理する」「共有データは慎重に扱う」の3点に尽きます。

何気ない実装ミスが、後々の不具合や信頼性低下に直結するのが割り込みの怖いところです。
だからこそ、正しい設計パターンを身につけることが品質向上の第一歩になります。

コメント

タイトルとURLをコピーしました