概要
割り込み(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の設計原則
-
短く・速く: ISRはできる限り短い処理のみ行い、重い処理はタスクに委譲します。目安はマイクロ秒〜数十マイクロ秒以内。
-
ブロッキング禁止: ISR内でFreeRTOSの
xQueueReceive()(待機ブロッキングあり)などを呼んではいけません。ISR専用API(...FromISR())を使います。 -
割り込みフラグのクリア: ISRの最初または最後に割り込み要因フラグを必ずクリアします。クリアを忘れると無限に割り込みが発生します。
-
共有データ保護: 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)での定番実装です。