STM32割り込み設定の完全ガイド:Keil uVision5環境での実践的実装
嵌入式システム開発において、割り込みシステムは必須の知識です。Keil uVision5を使用してSTM32(ここでは一般的なF103シリーズを例)の外部割り込みを正しく設定する方法について、この記事では詳しく解説します。多くの開発者が直面する典型的な問題を避け、効率的な割り込み処理を実現するための具体的な手順を紹介します。
割り込みの必要性:なぜポーリングではなく割り込みを使うのか?
割り込みの基本概念を理解することが重要です。ボタン状態を継続的にチェックするポーリング方式の欠点は、CPUの大部分のリソースを消費してしまうことです。複数のタスク(シリアル通信、タイマー処理、センサーデータ取得など)を同時に処理する必要がある場合、ポーリング方式ではシステム全体のパフォーマンスが著しく低下します。
割り込みはイベント駆動型の処理方式であり、特定のイベント(ボタン押下など)が発生したときのみCPUが割り込み処理を実行します。これにより、CPUのリソースを効率的に活用でき、システム全体の応答性と効率が大幅に向上します。
STM32割り込みシステムの主要コンポーネント
STM32の割り込みシステムを理解するには、3つの主要コンポーネントの関係を把握する必要があります:
- EXTI(External Interrupt Controller):GPIOピンの状態変化を監視する役割を担います
- NVIC(Nested Vectored Interrupt Controller):Cortex-Mコア内蔵の割り込みコントローラーで、割り込みの優先順位を管理します
- 割り込みベクタテーブル:割り込み発生時に実行される関数のアドレスを格納するテーブル
これらのコンポーネントは連携して動作し、外部イベントから割り込み処理関数までのシグナルフローを形成します。
NVICの優先順位管理
NVICの特徴的なのは、プリエンプティブ優先度と**サブ優先度(レスポンス優先度)**の2段階の優先度システムです。これは病院の救急搬送システムに似ています:
- 高いプリエンプティブ優先度を持つ割り込みは、現在実行中の低い優先度の割り込みを中断できます
- 同じプリエンプティブ優先度の場合は、サブ優先度によって処理順序が決まります
- サブ優先度は処理順序に影響しますが、プリエンプションはできません
ARM Cortex-M3/M4では4ビットの優先度が使用でき、SCB->AIRCRレジスタを設定することで優先度グループを変更できます。一般的に使用される設定は以下の通りです:
| グループ | プリエンプティビット数 | サブ優先度ビット数 | 特徴 |
|---|---|---|---|
| Group 0 | 0 | 4 | すべての割り込みがネスト不可 |
| Group 1 | 1 | 3 | 最大2レベルのプリエンプション |
| Group 2 | 2 | 2 | 推奨設定!最大4レベルのプリエンプション |
| Group 3 | 3 | 1 | 8レベルのプリエンプション |
| Group 4 | 4 | 0 | 完全にプリエンプション制御 |
Keilプロジェクトでは通常、以下のように設定します:
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
✅ 初心者にはGroup 2の使用を推奨します。柔軟性と複雑さのバランスが取れています。
EXTIのピンマッピング原理
多くの開発者がこの部分でつまずきます。STM32では、PA0、PB0、PC0などがすべてEXTI0に接続可能ですが、同時に有効にできるのは1つだけです。この選択はSYSCFG(System Configuration)コンポーネントのSYSCFG_EXTICR1レジスタによって制御されます。
例えば、PA0をEXTI0にマッピングするには、以下のように設定する必要があります:
SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOA, EXTI_PinSource0);
⚠️ この関数はSYSCFGのクロックに依存するため、クロックを有効にしないとマッピングが失敗します:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE); // この行は必須です!
割り込みサービス関数の命名規則
startup_stm32f103xb.sファイルを開くと、以下のような定義が見つかります:
DCD EXTI0_IRQHandler ; EXTI Line0
DCD EXTI1_IRQHandler ; EXTI Line1
これは、EXTI0割り込みを処理するためのC関数名はEXTI0_IRQHandlerである必要があり、1文字でも違うと認識されないことを意味します。また、この関数はstm32f1xx_it.cファイル内に配置する必要があります(Keilがデフォルトで生成します)。そうしないと、リンカーがシンボルを見つけられず、HardFault例外が発生する可能性があります。
実践例:PA0ボタンによる割り込みでLEDを点灯させる
完整な設定手順を以下に示します。
ステップ1:プロジェクトの作成と必要ファイルの追加
- Keil uVision5を開き、新しいプロジェクトを作成(チップタイプ:STM32F103C8T6)
- Keilが自動で対応するスタートアップファイル
startup_stm32f103xb.sをロード - 以下のソースファイルを追加:
system_stm32f1xx.cstm32f1xx_it.c- 外設ライブラリまたはHALライブラリ関連のヘッダファイル(この例では標準ライブラリを使用)
💡 ヒント:すべての割り込みサービス関数を
stm32f1xx_it.cで集中管理すると、保守が容易になります。
ステップ2:EXTI初期化関数の作成
void Button_Interrupt_Init(void) {
GPIO_InitTypeDef gpio_init;
EXTI_InitTypeDef exti_init;
NVIC_InitTypeDef nvic_init;
// 1. クロックの有効化
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// 2. PA0を入力、プルアップとして設定
gpio_init.GPIO_Pin = GPIO_Pin_0;
gpio_init.GPIO_Mode = GPIO_Mode_IN;
gpio_init.GPIO_PuPd = GPIO_PuPd_UP;
GPIO_Init(GPIOA, &gpio_init);
// 3. PA0をEXTI0にマッピング
SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOA, EXTI_PinSource0);
// 4. EXTI0の設定:立ち下がりエッジでトリガー
exti_init.EXTI_Line = EXTI_Line0;
exti_init.EXTI_Mode = EXTI_Mode_Interrupt;
exti_init.EXTI_Trigger = EXTI_Trigger_Falling;
exti_init.EXTI_LineCmd = ENABLE;
EXTI_Init(&exti_init);
// 5. NVICの設定:割り込みチャネルの有効化と優先度設定
nvic_init.NVIC_IRQChannel = EXTI0_IRQn;
nvic_init.NVIC_IRQChannelPreemptionPriority = 1;
nvic_init.NVIC_IRQChannelSubPriority = 1;
nvic_init.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&nvic_init);
}
📌 重要なポイント:
- ピンマッピングを行う前にSYSCFGクロックを有効にする必要があります
EXTI_Trigger_Fallingを使用すると、ボタンが解放されたときに割り込みが発生します(通常状態がハイ、押下でローになるため)- NVIC優先度を設定する前に
NVIC_PriorityGroupConfigを呼び出す必要があります
ステップ3:割り込みサービス関数(ISR)の実装
stm32f1xx_it.cに以下を追加します:
extern void LED_Switch(void); // LEDを制御する関数(仮定)
void EXTI0_IRQHandler(void) {
if (EXTI_GetITStatus(EXTI_Line0) != RESET) {
Delay_ms(10); // シンプルなチタリング対策
if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) == RESET) { // ボタンが押下されたことを再確認
LED_Switch();
}
EXTI_ClearITPendingBit(EXTI_Line0); // ⚠️ 割り込みフラグをクリアする必要があります!
}
}
🔥 重要な注意点:
EXTI_GetITStatus()を使用して、本当にEXTI0がトリガーされたかを確認します(習慣として)EXTI_ClearITPendingBit()は必ず呼び出してください!これを怠ると割り込みフラグがクリアされず、次回関数から退出するとすぐに再び割り込みが発生し、「割り込みストーム」を引き起こします- 遅延はソフトウェアによるチタリング対策ですが、工業用途ではハードウェアRCフィルタを併用することを推奨します
よくある問題のトラブルシューティングリスト
以下は、割り込みが機能しない主な原因とその解決策です:
| 問題現象 | 考えられる原因 | 解決策 |
|---|---|---|
| 割り込みが全く発生しない | __enable_irq()が呼び出されていない |
main()の先頭にこの行を追加 |
| 割り込みが発生しない | SYSCFGクロックが有効になっていない | RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE)を追加 |
| 割り込みが発生しない | スタートアップファイルの関数名が間違っている | EXTI0_IRQHandlerのスペルを確認 |
| 割り込みが繰り返し発生する | 割り込みフラグがクリアされていない | EXTI_ClearITPendingBit(EXTI_Line0)を追加 |
| ボタン反応が鈍い | 機械的なチタリングの影響 | 遅延を増やしたり、両エッジ検出+状態マシンに変更 |
🛠 デバッグテクニック:Keilデバッグモードでのレジスタ確認
メニューから
View → Registers Windowを開き、以下のレジスタを確認します:
EXTI_PR:ペンディングレジスタ、どのビットがセットされたか確認NVIC_ISPR:割り込みペンディングレジスタ、割り込みが正しく送信されたか確認- PRが常に1の場合、フラグがクリアされていないサインです!
安定した割り込みコードを書くためのベストプラクティス
単に動作するだけでなく、長期的な保守性とシステム安定性を考慮することが重要です。
✅ ISRでは時間のかかる処理を行わない
割り込みコンテキストではprintf、浮動小数点演算、複雑なアルゴリズムの呼び出しは避けるべきです。正しいアプローチは以下の通りです:
volatile uint8_t button_pressed_flag = 0;
void EXTI0_IRQHandler(void) {
if (EXTI_GetITStatus(EXTI_Line0)) {
button_pressed_flag = 1; // フラグのみ設定
EXTI_ClearITPendingBit(EXTI_Line0);
}
}
// メインループでの処理
while (1) {
if (button_pressed_flag) {
button_pressed_flag = 0;
handle_button_press(); // 複雑なロジックはここに
}
}
これにより、応答性を保ちつつメインプロセスへの影響を最小限に抑えられます。
✅ マクロ定義で割り込みリソースを管理する
後の変更や移植を容易にするために、マクロ定数を使用します:
#define BUTTON_PORT GPIOA
#define BUTTON_PIN GPIO_Pin_0
#define EXTI_LINE EXTI_Line0
#define BUTTON_IRQn EXTI0_IRQn
// 初期化時にマクロを使用
SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOA, EXTI_PinSource0);
exti_init.EXTI_Line = EXTI_LINE;
nvic_init.NVIC_IRQChannel = BUTTON_IRQn;
これにより、ピンを変更する際にも手動で各場所を修正する必要がなくなります。
まとめ:重要なポイント
STM32外部割り込みの動作フローを理解する上で重要なポイントを以下にまとめます:
- クロックの有効化 → GPIOとSYSCFGの両方
- GPIOの設定 → 入力モードとプルアップ/ダウン
- マッピングの設定 →
SYSCFG_EXTILineConfigでポートを選択 - EXTIの設定 → トリガ方式と割り込みの有効化
- NVICの設定 → 優先度グループ、優先度、チャネルの有効化
- ISRの実装 → 関数名の一致、フラグの確認、フラグのクリア
- 全割り込みの有効化 →
__enable_irq()
これらのステップを正しく実行すれば、割り込みは正常に動作します。
この方法を習得すれば、ボタンによるウェイクアップ、シリアル受信、タイマーオーバーフロー、DMA完了通知など、あらゆる割り込み設定に応用できます。STM32を「活性化」させる鍵は、この小さな割り込みメカニズムにあります。