リアルタイム制御

割り込み(インタラプト)

外部イベントで処理を中断し対応する仕組み。

概要

割り込み(Interrupt、インタラプト)は、CPUが現在実行中のプログラムを一時中断し、特定のイベントに対応する処理(ISR: 割り込みハンドラ)へ制御を移す仕組みです。処理が終わると元のプログラムへ復帰します。

ポーリング(状態を繰り返し確認する方式)と対比される概念であり、割り込みを使うことでCPUは通常処理を続けながら、必要なタイミングでのみイベントに応答できます。これにより「即時性」と「CPU効率」を両立できます。

組み込みシステムでは、センサの値変化(GPIOの立ち上がりエッジ)、通信データの受信完了(UART RX)、タイマの満了、ADC変換完了など、あらゆる外部イベントの通知に割り込みが使われます。

歴史・背景

割り込みの概念は1954年にUNIVACシステムが「Input/Output割り込み」を実装したのが初期の例とされています。1959年にはIBM 709がハードウェア割り込みを本格採用し、CPUとI/Oの並行動作を実現しました。

初期のマイコンでは割り込みは単純な「外部INT端子への信号入力」のみでしたが、1980年代以降はベクタ割り込みコントローラ(VIC: Vectored Interrupt Controller)が普及し、複数割り込み源ごとに異なるISRへ自動的にジャンプする仕組みが確立されました。

ARMアーキテクチャではNVIC(Nested Vectored Interrupt Controller)が標準化され、ARM Cortex-Mシリーズに搭載。最大240個の割り込み源、256レベルの優先度、ネスト(多重割り込み)をハードウェアで管理できます。NVICはCortex-Mの重要な特徴の一つで、割り込みレイテンシの短縮(Tail-chaining、Late-arriving等の最適化)を実現しています。

技術仕様

割り込みの種類

種別説明
ハードウェア割り込み外部ペリフェラルからの信号GPIO、UART RX、タイマ
ソフトウェア割り込みソフトウェアが発行(SVC命令等)RTOSのシステムコール
例外(Exception)CPU内部の異常検知ゼロ除算、不正メモリアクセス
NMIマスク不可割り込み電源断検知、ウォッチドッグ

ARM Cortex-Mの割り込み仕様

ARM Cortex-Mシリーズ(STM32等で採用)のNVIC仕様:

項目仕様
最大割り込み数240本(外部割り込み)
優先度レベル最大256段階(実装依存、通常4〜16段)
割り込みネストハードウェアで自動管理
割り込みレイテンシ12クロックサイクル(Tail-chaining時は6クロック)
レジスタ自動退避R0-R3, R12, LR, PC, xPSRを自動スタック保存

割り込みレイテンシの構成要素

割り込みレイテンシ = 割り込み検知遅延
                   + CPUパイプラインフラッシュ
                   + レジスタ自動退避時間
                   + ISRへの分岐時間

Cortex-M4(168MHz)での実測例:

  • 最短割り込みレイテンシ: 約200ns(12クロック)
  • 割り込み禁止区間後の最悪レイテンシ: 数μs〜数十μs(コードに依存)

割り込み優先度の設定

Cortex-MのNVICでは優先度ビット数がマイコンにより異なります。STM32F4では4ビット(16段階)が一般的です。RTOSを使う場合、RTOSが使用する優先度レベルとアプリケーション割り込みの優先度を適切に分離する必要があります。

FreeRTOSではconfigLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITYより低い優先度の割り込みのみFreeRTOS APIを呼び出せます。

動作原理

割り込み発生から復帰までの流れ

[通常タスク実行中]

[ペリフェラルが割り込み要求(IRQ)をNVICへ送信]

[NVICが優先度を確認]
  → 現在の優先度より高い場合:割り込み受け付け
  → 低い場合:ペンディング(保留)

[CPUがR0-R3, R12, LR, PC, xPSRを自動スタックへ保存](約12クロック)

[ベクタテーブルからISRアドレスを取得]

[ISR(割り込みハンドラ)実行]
  → 割り込み要因フラグをクリア
  → 最小限の処理(データコピー、フラグセット等)
  → RTOSへ通知(キュー送信等)

[スタックからレジスタを自動復元]

[元のタスクへ復帰(またはRTOSによるコンテキストスイッチ)]

ベクタテーブル

割り込みごとのISRアドレスを格納する配列です。ARM Cortex-Mではメモリ先頭(通常0x08000000付近)に配置されます。

/* STM32 ベクタテーブルの例(startup_stm32f4xx.s / startup.c より) */
/* 割り込みハンドラのアドレスを配列で定義 */
typedef void (*IRQHandler_t)(void);

__attribute__((section(".isr_vector")))
const IRQHandler_t vector_table[] = {
    (IRQHandler_t)&_estack,     /* MSP初期値 */
    Reset_Handler,              /* リセット */
    NMI_Handler,                /* NMI */
    HardFault_Handler,          /* ハードフォルト */
    /* ... */
    TIM1_UP_IRQHandler,         /* TIM1更新割り込み */
    USART1_IRQHandler,          /* USART1割り込み */
    /* ... */
};

割り込み禁止とクリティカルセクション

割り込みが許可されている最中に共有データへアクセスすると、ISRによるデータ破壊(競合)が発生します。これを防ぐために一時的に割り込みを禁止します。

/* Cortex-M での割り込み禁止・許可 */
__disable_irq();    /* PRIMASK=1, 全割り込み禁止(NMI除く) */
/* ... 共有データへのアクセス ... */
__enable_irq();     /* PRIMASK=0, 割り込み許可 */

/* FreeRTOSのクリティカルセクション(タスクコンテキスト) */
taskENTER_CRITICAL();
/* ... 共有データへのアクセス ... */
taskEXIT_CRITICAL();

/* FreeRTOSのクリティカルセクション(ISRコンテキスト) */
UBaseType_t savedMask = taskENTER_CRITICAL_FROM_ISR();
/* ... 共有データへのアクセス ... */
taskEXIT_CRITICAL_FROM_ISR(savedMask);

用途・ユースケース

ペリフェラルデータ受信

UARTでシリアルデータを受信する場合、受信バイトごとにRX割り込みが発生します。ISR内でリングバッファへデータをコピーし、メインタスクがバッファを読む非同期処理が一般的です。

/* UART受信割り込みハンドラ(STM32 HAL) */
void USART1_IRQHandler(void) {
    HAL_UART_IRQHandler(&huart1);
}

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    if (huart->Instance == USART1) {
        /* リングバッファへコピー(ISRは短く) */
        ring_buffer_push(&uart_rx_buf, rx_byte);
        HAL_UART_Receive_IT(&huart1, &rx_byte, 1); /* 次の受信準備 */
    }
}

タイマ割り込みによる周期処理

制御ループ(PID制御等)はタイマ割り込みで一定周期ごとに実行されます。

/* 1ms周期タイマ割り込み(STM32 TIM2) */
void TIM2_IRQHandler(void) {
    HAL_TIM_IRQHandler(&htim2);
}

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
    if (htim->Instance == TIM2) {
        /* 制御フラグセット → メインループで処理 */
        control_flag = 1;
        /* またはRTOSタスクへ通知 */
        BaseType_t xHigherPriorityTaskWoken = pdFALSE;
        vTaskNotifyGiveFromISR(controlTaskHandle, &xHigherPriorityTaskWoken);
        portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
    }
}

GPIOエッジ検出

ボタン押下、ロータリーエンコーダ、外部センサのパルス検出などにエッジ割り込みを使用します。

/* GPIO外部割り込み(STM32 EXTI) */
void EXTI0_IRQHandler(void) {
    HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);
}

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
    if (GPIO_Pin == GPIO_PIN_0) {
        button_pressed = 1; /* デバウンス処理はメインループで */
    }
}

実装・開発のポイント

ISRの設計原則

  1. 短く・速く: ISRはできる限り短い処理のみ行い、重い処理はタスクに委譲します。目安はマイクロ秒〜数十マイクロ秒以内。

  2. ブロッキング禁止: ISR内でFreeRTOSのxQueueReceive()(待機ブロッキングあり)などを呼んではいけません。ISR専用API(...FromISR())を使います。

  3. 割り込みフラグのクリア: ISRの最初または最後に割り込み要因フラグを必ずクリアします。クリアを忘れると無限に割り込みが発生します。

  4. 共有データ保護: ISRとメインコード間で共有する変数はvolatile修飾子を付け、コンパイラの最適化でキャッシュされないようにします。

/* 共有変数にはvolatileを付ける */
volatile uint8_t data_ready = 0;
volatile uint16_t adc_value = 0;

void ADC_IRQHandler(void) {
    adc_value = ADC1->DR;     /* レジスタ読み取り(割り込みフラグも自動クリア) */
    data_ready = 1;
}

優先度の設計

割り込み優先度は、応答時間の要件に応じて設計します。最も時間制約が厳しい処理(モーター制御タイマ等)に最高優先度を与え、それ以外は段階的に下げます。

RTOSを使う場合、FreeRTOSのconfigMAX_SYSCALL_INTERRUPT_PRIORITY以下の優先度(数値は大きい方が低優先度)にはFreeRTOS APIが使えないため注意が必要です。

他技術との比較

観点割り込みポーリングDMA
CPU効率高(イベント時のみ起動)低(常時確認)最高(CPUを介さない)
応答レイテンシ低(数μs)ポーリング周期に依存非常に低(DMA完了割り込みのみ)
実装複雑さ中(ISR設計が必要)低(ループのみ)高(DMAチャネル設定が複雑)
向いている用途非同期イベント・低頻度高速・予測可能なイベント大量データ転送(UART/SPI/I2S)
デバッグのしやすさ難(タイミング依存)易(逐次実行)難(ハードウェア依存)

DMAは大量データ転送でCPUを解放し、DMA完了時に割り込みで通知するパターンが最も効率的です。割り込みとDMAの組み合わせは、高速シリアル通信(SPI/I2S/UART)での定番実装です。

関連用語

参考リンク