概要
ISR(Interrupt Service Routine、割り込みサービスルーチン)は、割り込み(インタラプト)が発生したときにCPUが自動的に実行するハンドラ関数です。「割り込みハンドラ」「インタラプトハンドラ」とも呼ばれます。
ISRはCPUがベクタテーブルから該当する関数アドレスを読み取り、自動的に呼び出します。ISRの実行中は通常タスクが中断されているため、ISRは「できる限り短時間で完了する」ことが設計上の大原則です。処理時間が長いISRは他の割り込みやタスクの応答性を悪化させ、リアルタイム性を損ないます。
ISRの主な役割は以下の3つです。
- 割り込み要因(フラグ)のクリア
- データのコピーまたはフラグのセット
- 上位タスクへの通知(RTOS使用時)
重い処理はISRで行わず、高優先度タスクに委譲する「Deferred Interrupt Processing」パターンが推奨されます。
歴史・背景
ISRという概念はコンピュータの割り込み機構の誕生とともに存在します。1950年代後半のメインフレームコンピュータでは、I/O完了を通知するためのサービスルーチンが最初期のISRにあたります。
8ビットマイコン時代(1970〜1980年代)では、ISRはアセンブリ言語で記述されることが多く、レジスタの退避・復元を手動で行う必要がありました。C言語コンパイラが割り込みハンドラをサポートするようになったのは1980年代中盤以降で、interruptや__interruptなどのキーワードが使われました。
ARM Cortex-Mアーキテクチャ(2004年〜)はハードウェアによるレジスタ自動退避(R0-R3, R12, LR, PC, xPSR)を導入し、ISRへの切り替えコストを最小化。「Tail-chaining」(ISR終了後、次の待機ISRを呼び出す際のオーバーヘッドを最小化)など、リアルタイム性向上のための最適化もハードウェアに組み込まれています。
技術仕様
ISRのC言語での定義
マイコン・ツールチェーンごとにISRの定義方法が異なります。
ARM Cortex-M(GCC + CMSIS)
/* ベクタテーブルに関数名を登録するだけでISRとして機能 */
/* 関数名はベクタテーブル定義(startup.s)と一致させる必要がある */
void TIM2_IRQHandler(void) {
/* タイマ割り込みハンドラ */
if (__HAL_TIM_GET_FLAG(&htim2, TIM_FLAG_UPDATE)) {
__HAL_TIM_CLEAR_FLAG(&htim2, TIM_FLAG_UPDATE);
/* 処理 */
}
}
AVR(AVR-GCC)
#include <avr/interrupt.h>
ISR(TIMER1_COMPA_vect) {
/* タイマ1比較A割り込み */
PORTB ^= (1 << PB0); /* LEDトグル */
}
ESP32(ESP-IDF)
/* IRAM_ATTR: ISRをRAMに配置してフラッシュキャッシュミスを防ぐ */
void IRAM_ATTR gpio_isr_handler(void *arg) {
uint32_t gpio_num = (uint32_t)arg;
xQueueSendFromISR(gpio_evt_queue, &gpio_num, NULL);
}
ISRの実行時間ガイドライン
| システム種別 | ISR最大実行時間目安 |
|---|---|
| ハードリアルタイム制御(<1ms周期) | 1〜10μs |
| 通常のRTOSアプリ | 10〜100μs |
| ソフトリアルタイム(音声・通信) | 100μs〜1ms |
| 汎用組み込みLinux | 1ms程度まで許容(用途依存) |
ARM Cortex-MのISR自動退避レジスタ
割り込み発生時にハードウェアが自動的にスタックへ保存するレジスタ:
自動保存(ハードウェア): R0, R1, R2, R3, R12, LR, PC, xPSR
手動保存が必要な場合 : R4〜R11(ISR内で使用する場合はコンパイラが自動挿入)
FPU使用時はS0〜S15、FPSCR も自動保存の対象になります(FPCCR.ASPEN有効時)。
動作原理
ISR実行フローの詳細
[ペリフェラルが割り込み要求(IRQ)を送出]
↓
[NVICが割り込みをペンディング状態にする]
↓
[現在の割り込みマスクと優先度を確認]
→ 受け付け可能:スタックへ8レジスタを自動退避(約12クロック)
→ 不可能:ペンディングのまま待機
↓
[ベクタテーブルから ISR アドレスを読み取る]
↓
[ISR を実行]
1. 割り込みフラグをクリア(必須)
2. データ処理(最小限)
3. 必要に応じてRTOSへ通知(FromISR API)
↓
[ISR から返る(BX LR)]
↓
[ハードウェアがスタックから8レジスタを自動復元]
↓
[RTOSによりコンテキストスイッチが発生する場合あり]
↓
[元のタスクまたは高優先度タスクへ復帰]
Deferred Interrupt Processing(遅延割り込み処理)
ISRで重い処理を行わず、高優先度タスクへ処理を委譲するパターンです。RTOSを使った実装例:
/* ISR:最小限の処理のみ */
void USART1_IRQHandler(void) {
uint8_t byte = USART1->DR; /* データ読み取り(フラグ自動クリア) */
/* キューへ送信(ブロッキングなし) */
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xQueueSendFromISR(uart_rx_queue, &byte, &xHigherPriorityTaskWoken);
/* 高優先度タスクが起床した場合、ISR終了後にコンテキストスイッチ */
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
/* 高優先度タスク:実際の処理 */
void uartProcessTask(void *pvParameters) {
uint8_t byte;
for (;;) {
/* キューからデータを受け取るまでブロック */
if (xQueueReceive(uart_rx_queue, &byte, portMAX_DELAY) == pdTRUE) {
process_received_byte(byte); /* 重い処理をここで実行 */
}
}
}
ISR内で禁止される操作
RTOSを使う場合、ISR内でタスクがブロックしうるAPIを呼び出すと、カーネルの状態が破壊されます。
| 禁止(タスクコンテキスト用) | 代替(ISRコンテキスト用) |
|---|---|
xQueueSend() | xQueueSendFromISR() |
xSemaphoreGive() | xSemaphoreGiveFromISR() |
vTaskDelay() | 使用不可(スリープ禁止) |
vTaskNotifyGive() | vTaskNotifyGiveFromISR() |
xTaskCreate() | 使用不可 |
printf() | 使用不可(内部でmutex使用) |
用途・ユースケース
タイマ割り込みによる制御ループ
産業機器のモーター制御では、タイマ割り込みで正確な周期を確保し、ISR内またはISR起床タスクでPID演算を行います。
/* 1kHz制御ループ用タイマISR(STM32 HAL) */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
if (htim->Instance == TIM6) {
/* ADC値を取得してキューへ(合計10μs以内) */
uint16_t adc_val = HAL_ADC_GetValue(&hadc1);
BaseType_t xHPTW = pdFALSE;
xQueueOverwriteFromISR(adc_queue, &adc_val, &xHPTW);
portYIELD_FROM_ISR(xHPTW);
}
}
DMA転送完了割り込み
DMA転送完了をISRで検知し、次の処理を開始します。
/* SPI DMA受信完了割り込み */
void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) {
if (hspi->Instance == SPI1) {
/* 受信完了フラグをセット(タスクがポーリングまたはセマフォ待ち) */
BaseType_t xHPTW = pdFALSE;
xSemaphoreGiveFromISR(spi_rx_semaphore, &xHPTW);
portYIELD_FROM_ISR(xHPTW);
}
}
外部イベント検出
センサのデータ準備完了(DRDY)ピンや、ボタンのエッジ検出に使用します。
/* IMUのDRDYピン(PA0)でエッジ割り込み) */
void EXTI0_IRQHandler(void) {
__HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0); /* フラグクリアを最初に */
BaseType_t xHPTW = pdFALSE;
xTaskNotifyFromISR(imuTaskHandle, 0x01, eSetBits, &xHPTW);
portYIELD_FROM_ISR(xHPTW);
}
実装・開発のポイント
ISR設計のベストプラクティス
-
割り込みフラグの即時クリア ISRの先頭で割り込みフラグをクリアします。クリアを忘れると無限割り込みが発生します。ペリフェラルによっては読み取りで自動クリアされるものもあります。
-
volatile修飾子の適切な使用 ISRとタスク間で共有するグローバル変数には必ずvolatileを付けます。コンパイラがレジスタキャッシュを誤用することを防ぎます。 -
再入可能性(Reentrant)の確保 同じISRが多重割り込みで再入される可能性がある場合、スタティック変数の使用を避けます。
-
ESP32での
IRAM_ATTR指定 ESP32はコードをFlashから実行します。ISRにキャッシュミスが発生すると数百μsのレイテンシが生じるため、ISR関数にはIRAM_ATTRを付けてRAM実行させます。 -
ネスト割り込みの設計 高優先度の割り込みが低優先度のISR実行中に発生することを考慮し、ISR内でのデータ破壊が起きないよう設計します。
デバッグ手法
- ロジックアナライザ: ISR内でGPIOをトグルし、ロジックアナライザで実行時間を実測
- ITM(Instrumentation Trace Macrocell): SWO端子経由でISR開始・終了のタイムスタンプをリアルタイム取得
- DWT CYCCNTカウンタ: ISR内でサイクルカウンタを記録して実行時間を計測
/* ISR実行時間計測例 */
void TIM2_IRQHandler(void) {
uint32_t start = DWT->CYCCNT;
/* ... ISR処理 ... */
uint32_t elapsed = DWT->CYCCNT - start;
if (elapsed > isr_wcet) isr_wcet = elapsed; /* 最大値を記録 */
}
他技術との比較
| 観点 | ISR(割り込みハンドラ) | RTOSタスク | ポーリングループ |
|---|---|---|---|
| 応答レイテンシ | 最低(数μs) | 低〜中(スケジューラ依存) | ポーリング周期に依存 |
| 処理時間 | 極短(μs〜数十μs) | 自由(ms単位も可) | 自由 |
| コンテキスト | 特殊(スタック自動退避) | 通常タスクと同様 | 通常実行と同様 |
| ブロッキング | 禁止 | 可能 | 可能 |
| 実装複雑さ | 中(制約あり) | 低(通常Cコード) | 低 |
| CPU効率 | 高(イベント時のみ) | 高(ブロッキング中はスリープ) | 低(常時実行) |
ISRはリアルタイム組み込みシステムの基本構成要素であり、その設計品質が全体のリアルタイム性と安定性を左右します。「ISRを短く保ち、重い処理はタスクへ」という原則を守ることで、保守性の高いシステムを実現できます。