STM32における外部割込(EXTI)とNVIC制御の実装手法

システム割込みの基礎概念

プロセッサの動作において、割込みは主ループの実行を一時停止し、緊急度に応じたサブルーチンへ処理を委譲する機構です。特定の物理信号や内部タイマの経過、通信バッファの到達など、所定のイベントが発生した瞬間にCPUの制御権が切り替わり、処理完了後に元の文脈に復帰します。この機構により、ポーリング方式に依存しない非同期処理が実現され、システムのリアルタイム性とCPU使用効率の向上が期待できます。

複数の割込み源が同時に要求を出した際は、優先度に基づいて仲裁が行われます。開発者はアプリケーションの要件に応じて優先度を設定し、時間的制約の厳しいイベントを先に処理させることができます。さらに、実行中の割込み処理中により高い優先度の要求が発生した場合、現在の処理を一時保存し新しい割込みを処理するネスト動作が許可されます。ネストの発生可否や順序は、事前に定義された優先度階層によって厳密に制御されます。

STM32の割込み管理アーキテクチャ

STM32シリーズはCortex-Mコアを搭載しており、割込み制御はNVIC(Nested Vectored Interrupt Controller)によって一元管理されています。利用可能なマスク可能な割込みラインは68本を数え、タイマ、ADC、UART、SPI、I2C、RTCなど多様な周辺機能が接続可能です。各チャンネルは4ビットの優先度レジスタを持ち、16段階の階層を組むことができます。この4ビットは上位ビット(プリエンプション優先度)と下位ビット(サブ優先度)に分割可能で、分割比率はソフトウェアから動的に変更できます。

プリエンプション優先度が高い割込みは、低い優先度の処理を強制的に中断(ネスト)できます。サブ優先度は同等のプリエンプションを持つ場合に、どの割込みを先にキューイングするかを決定します。両方が一致する場合は、ハードウェアが割り当てたベクタ番号の小さい方が優先されます。

割込み発生時、ハードウェアは固定アドレスのジャンプテーブルである割込みベクタテーブルを検索します。コンパイラが動的に配置する実際の処理関数のアドレスとハードウェアの固定ジャンプ先を橋渡しするため、テーブル内には対応する関数のポインタが登録されています。これにより、柔軟な関数配置と高速なハードウェア応答性を両立させています。

EXTIモジュールの動作原理

EXTI(External Interrupt)は、GPIOピンの信号変化を監視し、所定のトリガ条件が満たされた瞬間にNVICへリクエストを送出する周辺モジュールです。トリガ形態として立ち上がりエッジ、立ち下がりエッジ、両エッジ、ソフトウェア起動が選択可能です。GPIOポートの物理的なピンの数に関わらず、EXTIラインは0から15まで16本(および4本の専用ライン)で構成されており、同じ番号のピン(例:PA0とPB0)は同じEXTIラインに共有接続されています。同時使用は出来ませんが、AFIO(Alternate Function I/O)レジスタを介してどのポートのピンをEXTIラインに接続するかを柔軟に切り替えられます。

EXTIの出力は2種類あります。1つはNVICを介してCPUのプログラムカウンタを強制的に切り替える「割込みモード」であり、もう1つは周辺機能への信号伝達に特化した「イベントモード」です。信号の多重化にはAFIO内のデータセレクタが使用され、ORゲート構造によって複数のピン入力が1つのEXTIラインにまとめられます。

実装事例:外部信号の検出と処理

赤外線遮断センサのカウント制御

対向型赤外線センサの遮断を検知し、カウント値を蓄積する実装例です。PB14を下降エッジトリガとして設定し、ノイズ対策として割込み内で実際のピン電位を再度検証するデバウンス処理を組み込んでいます。

/* main.c */
#include "stm32f10x.h"
#include "OLED.h"
#include "IRCounter.h"

int main(void)
{
    OLED_Init();
    IRCounter_Config();
    
    OLED_ShowString(1, 1, "Count:");
    
    while (1)
    {
        OLED_ShowNum(1, 7, IRCounter_Read(), 5);
    }
}

/* IRCounter.c */
#include "stm32f10x.h"

static volatile uint32_t sensor_tick_accum = 0;

void IRCounter_Config(void)
{
    /* クロック有効化 */
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE);
    
    /* GPIO入力設定(内部プルアップ) */
    GPIO_InitTypeDef gpio_cfg;
    gpio_cfg.GPIO_Pin = GPIO_Pin_14;
    gpio_cfg.GPIO_Mode = GPIO_Mode_IPU;
    gpio_cfg.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB, &gpio_cfg);
    
    /* AFIO: PB14をEXTI14ラインに接続 */
    GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource14);
    
    /* EXTI設定 */
    EXTI_InitTypeDef exti_cfg;
    exti_cfg.EXTI_Line = EXTI_Line14;
    exti_cfg.EXTI_Mode = EXTI_Mode_Interrupt;
    exti_cfg.EXTI_Trigger = EXTI_Trigger_Falling;
    exti_cfg.EXTI_LineCmd = ENABLE;
    EXTI_Init(&exti_cfg);
    
    /* NVIC優先度グループ設定 */
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    
    /* 割込みチャンネル有効化 */
    NVIC_InitTypeDef nvic_cfg;
    nvic_cfg.NVIC_IRQChannel = EXTI15_10_IRQn;
    nvic_cfg.NVIC_IRQChannelPreemptionPriority = 1;
    nvic_cfg.NVIC_IRQChannelSubPriority = 1;
    nvic_cfg.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&nvic_cfg);
}

uint32_t IRCounter_Read(void)
{
    return sensor_tick_accum;
}

void EXTI15_10_IRQHandler(void)
{
    if (EXTI_GetITStatus(EXTI_Line14))
    {
        /* ピン電位再確認によるチャタリング除去 */
        if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_14) == Bit_RESET)
        {
            sensor_tick_accum++;
        }
        EXTI_ClearITPendingBit(EXTI_Line14);
    }
}

/* IRCounter.h */
#ifndef __IRCOUNTER_H
#define __IRCOUNTER_H
void IRCounter_Config(void);
uint32_t IRCounter_Read(void);
#endif

システム起動時にOLEDに初期文字列が表示され、光軸が遮断されるたびにカウント値が1増分されます。トリガ設定を変更すれば、遮断時、復帰時、または両方の遷移でカウントを記録できます。

ロータリーエンコーダの回転追跡

ロータリーエンコーダは回転量や方向をデジタル信号として出力するセンサです。A相とB相の信号間に90度の位相差が存在するため、片方のエッジを検知した時点で他方の信号レベルを観察すれば、回転方向を判別できます。ここではPB0とPB1をそれぞれEXTI0とEXTI1に接続し、下降エッジで同期して方向判定を行っています。

/* main.c */
#include "stm32f10x.h"
#include "OLED.h"
#include "RotaryEncoder.h"

static int32_t display_value = 0;

int main(void)
{
    OLED_Init();
    RotaryEncoder_Config();
    
    OLED_ShowString(1, 1, "Num:");
    
    while (1)
    {
        display_value += RotaryEncoder_Update();
        OLED_ShowNum(1, 5, display_value, 5);
    }
}

/* RotaryEncoder.c */
#include "stm32f10x.h"

static volatile int32_t rot_position_acc = 0;

void RotaryEncoder_Config(void)
{
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE);
    
    GPIO_InitTypeDef gpio_cfg;
    gpio_cfg.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
    gpio_cfg.GPIO_Mode = GPIO_Mode_IPU;
    gpio_cfg.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB, &gpio_cfg);
    
    GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource0);
    GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource1);
    
    EXTI_InitTypeDef exti_cfg;
    exti_cfg.EXTI_Line = EXTI_Line0 | EXTI_Line1;
    exti_cfg.EXTI_Mode = EXTI_Mode_Interrupt;
    exti_cfg.EXTI_Trigger = EXTI_Trigger_Falling;
    exti_cfg.EXTI_LineCmd = ENABLE;
    EXTI_Init(&exti_cfg);
    
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    
    NVIC_InitTypeDef nvic_cfg;
    nvic_cfg.NVIC_IRQChannelPreemptionPriority = 1;
    nvic_cfg.NVIC_IRQChannelCmd = ENABLE;
    
    nvic_cfg.NVIC_IRQChannel = EXTI0_IRQn;
    nvic_cfg.NVIC_IRQChannelSubPriority = 1;
    NVIC_Init(&nvic_cfg);
    
    nvic_cfg.NVIC_IRQChannel = EXTI1_IRQn;
    nvic_cfg.NVIC_IRQChannelSubPriority = 2;
    NVIC_Init(&nvic_cfg);
}

int32_t RotaryEncoder_Update(void)
{
    int32_t snapshot = rot_position_acc;
    rot_position_acc = 0;
    return snapshot;
}

void EXTI0_IRQHandler(void)
{
    if (EXTI_GetITStatus(EXTI_Line0))
    {
        if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == Bit_RESET)
        {
            if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == Bit_RESET)
            {
                rot_position_acc--;
            }
        }
        EXTI_ClearITPendingBit(EXTI_Line0);
    }
}

void EXTI1_IRQHandler(void)
{
    if (EXTI_GetITStatus(EXTI_Line1))
    {
        if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == Bit_RESET)
        {
            if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == Bit_RESET)
            {
                rot_position_acc--;
            }
        }
        EXTI_ClearITPendingBit(EXTI_Line1);
    }
}

/* RotaryEncoder.h */
#ifndef __ROTARYENCODER_H
#define __ROTARYENCODER_H
void RotaryEncoder_Config(void);
int32_t RotaryEncoder_Update(void);
#endif

OLEDには現在値がリアルタイムで反映されます。時計回りに回転させると値が増加し、反時計回りに回転させると値が減少します。両方のチャンネルで同様の位相判定ロジックを適用することで、片方のエッジを逃しても誤動作しない冗長性が確保されています。

タグ: STM32 EXTI NVIC 組み込みプログラミング C言語

5月15日 03:35 投稿