STM32 #12

홍태준·2026년 2월 20일

STM32

목록 보기
12/15
post-thumbnail

Week 3 Day 2: EXTI (외부 인터럽트) 심화

학습 목표

  • EXTI(External Interrupt/Event Controller)의 내부 구조와 신호 흐름을 이해한다
  • GPIO 핀과 EXTI 라인의 매핑 관계를 파악한다
  • 엣지 트리거(Edge Trigger) 방식의 동작 원리를 이해한다
  • 소프트웨어 인터럽트 발생과 이벤트 모드의 차이를 이해한다
  • HAL 라이브러리를 통한 EXTI 설정 및 디바운싱 구현 방법을 익힌다

EXTI(External Interrupt/Event Controller)는 GPIO 핀을 비롯한 외부 신호의 엣지(Edge) 변화를 감지하여 인터럽트 또는 이벤트를 발생시키는 STM32 전용 주변장치(Peripheral)입니다. ARM Cortex-M의 NVIC가 인터럽트를 관리하는 컨트롤러라면, EXTI는 GPIO와 NVIC 사이에서 외부 신호를 감지하고 전달하는 역할을 담당합니다. STM32F4 기준으로 최대 23개의 EXTI 라인을 제공합니다.


1. EXTI 기본 구조

1.1 EXTI 라인 구성

STM32F4의 EXTI 라인 종류

EXTI 라인 0 ~ 15  : GPIO 핀 연결 (PA0~PK15)
EXTI 라인 16      : PVD (Programmable Voltage Detector) 출력
EXTI 라인 17      : RTC Alarm 이벤트
EXTI 라인 18      : USB OTG FS Wakeup
EXTI 라인 19      : Ethernet Wakeup (STM32F4xx 일부 모델)
EXTI 라인 20      : USB OTG HS Wakeup
EXTI 라인 21      : RTC Tamper / Timestamp
EXTI 라인 22      : RTC Wakeup Timer

GPIO 핀은 라인 0~15만 사용하며,
나머지는 내부 주변장치의 Wakeup/이벤트 신호에 연결됨

GPIO 핀과 EXTI 라인 매핑

EXTI 라인 번호 = GPIO 핀 번호

EXTI Line 0  → PA0, PB0, PC0, PD0, PE0, PF0, PG0, PH0, PI0
EXTI Line 1  → PA1, PB1, PC1, ...
EXTI Line 2  → PA2, PB2, PC2, ...
...
EXTI Line 15 → PA15, PB15, PC15, ...

핵심 제약:
동일한 핀 번호의 GPIO는 하나의 EXTI 라인을 공유
→ PA0과 PB0을 동시에 EXTI로 사용 불가
→ SYSCFG(System Configuration Controller)->EXTICR 레지스터로 어느 포트를 연결할지 선택

마치 트롤리 딜레마와 같이 하나의 선로에서 n개의 분기가 생길 때 SYSCFG_EXTICR 레지스터가 특정 분기를 선택하는 레버의 역할을 합니다. 당연히 기차는 하나의 선로만 따라 갈 수밖에 없게 되죠.

SYSCFG EXTICR 레지스터

SYSCFG->EXTICR[0] : EXTI 0 ~ 3 포트 선택
SYSCFG->EXTICR[1] : EXTI 4 ~ 7 포트 선택
SYSCFG->EXTICR[2] : EXTI 8 ~ 11 포트 선택
SYSCFG->EXTICR[3] : EXTI 12 ~ 15 포트 선택

각 4비트로 포트 선택:
0x0 = GPIOA
0x1 = GPIOB
0x2 = GPIOC
0x3 = GPIOD
0x4 = GPIOE
...

예시: PB3을 EXTI3으로 설정
SYSCFG->EXTICR[0] 의 [15:12] 비트 = 0x1 (GPIOB)

1.2 EXTI 내부 신호 흐름

EXTI 블록 다이어그램

GPIO 핀 입력 신호
        │
        ▼
  [엣지 감지 회로]
  ┌─────────────┐
  │ Rising Edge │ ← EXTI->RTSR (Rising Trigger Selection Register)
  │ Falling Edge│ ← EXTI->FTSR (Falling Trigger Selection Register)
  └─────────────┘
        │
        ▼
  [소프트웨어 인터럽트] ← EXTI->SWIER (Software Interrupt Event Register)
        │
        ▼
  [인터럽트 마스크] ← EXTI->IMR (Interrupt Mask Register)
        │
        ├──→ Pending Register (EXTI->PR) → NVIC → CPU (인터럽트)
        │
  [이벤트 마스크] ← EXTI->EMR (Event Mask Register)
        │
        └──→ 이벤트 라인 → Wakeup / DMA / 타이머 트리거 (이벤트)

인터럽트 모드 vs 이벤트 모드

인터럽트 모드 (Interrupt Mode):
- EXTI->IMR 마스크 활성화
- Pending 비트 설정 → NVIC 통해 CPU 인터럽트 발생
- ISR(Interrupt Service Routine) 실행
- 범용 처리에 사용

이벤트 모드 (Event Mode):
- EXTI->EMR 마스크 활성화
- CPU 인터럽트 발생 없음
- 직접 하드웨어 이벤트 라인으로 전달
  → 타이머 트리거, ADC 트리거, DMA 요청 등에 활용
- 저전력 Wakeup 신호로도 사용 (Stop/Standby 모드에서 복귀)

두 모드를 동시에 활성화하는 것도 가능

2. EXTI 레지스터 구성

2.1 주요 레지스터

EXTI 레지스터 목록

IMR  (Interrupt Mask Register)         : 인터럽트 마스크 설정
EMR  (Event Mask Register)             : 이벤트 마스크 설정
RTSR (Rising Trigger Selection Register)  : 상승 엣지 트리거 설정
FTSR (Falling Trigger Selection Register) : 하강 엣지 트리거 설정
SWIER (Software Interrupt Event Register) : 소프트웨어 인터럽트 발생
PR   (Pending Register)                : 인터럽트 Pending 상태 / 클리어

각 레지스터는 비트 단위로 EXTI 라인 번호에 대응
비트 0 → EXTI Line 0
비트 1 → EXTI Line 1
...
비트 22 → EXTI Line 22

레지스터 직접 조작 예시 (PB3, EXTI Line 3 설정)

// 1. SYSCFG 클럭 활성화
RCC->APB2ENR |= RCC_APB2ENR_SYSCFGEN;

// 2. EXTI3 소스를 GPIOB로 선택
SYSCFG->EXTICR[0] &= ~SYSCFG_EXTICR1_EXTI3;    // 비트 클리어
SYSCFG->EXTICR[0] |=  SYSCFG_EXTICR1_EXTI3_PB; // GPIOB 선택

// 3. 상승 엣지 트리거 활성화
EXTI->RTSR |= EXTI_RTSR_TR3;

// 4. 하강 엣지 트리거 활성화 (양쪽 엣지 감지 시 추가)
EXTI->FTSR |= EXTI_FTSR_TR3;

// 5. 인터럽트 마스크 언마스크 (활성화)
EXTI->IMR |= EXTI_IMR_MR3;

// 6. NVIC 설정
NVIC_SetPriority(EXTI3_IRQn, 10);
NVIC_EnableIRQ(EXTI3_IRQn);

앞서 배웠던 PWM(Pulse Width Modulation)은 출력(Output)이나 주기적인 신호 분석에 특화돼있고, EXTI는 마우스/키보드 버튼 클릭 같은 비정기적인 사건(Event) 감지에 특화돼있기 때문에 둘의 용도를 구분지어 사용해야 됩니다.

2.2 Pending Register (PR) 처리

Pending 비트 동작

인터럽트 발생 조건 충족 시:
→ EXTI->PR 해당 비트 자동 Set (1)
→ NVIC에 인터럽트 요청 전달

ISR 진입 후:
→ 반드시 Pending 비트를 클리어해야 함
→ 클리어 방법: 해당 비트에 1을 기록 (1을 써야 클리어됨, 특이한 동작)

클리어 하지 않을 경우:
→ ISR 종료 후 즉시 재진입 (인터럽트 폭주)

Pending 비트 클리어

// ISR 내부에서 Pending 비트 클리어
void EXTI3_IRQHandler(void)
{
    if (EXTI->PR & EXTI_PR_PR3)       // Pending 확인
    {
        EXTI->PR |= EXTI_PR_PR3;      // 1을 써서 클리어
        // 처리 코드
    }
}

// HAL 사용 시 자동 처리됨
void EXTI3_IRQHandler(void)
{
    HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_3);  // 내부에서 PR 클리어 후 콜백 호출
}

CPU는 한 번에 하나의 동작만 처리할 수 있기 때문에 ISR이 실행 중인 동안 다른 인터럽트가 발생했음을 잠시 기록해두는 목적으로 사용됩니다. Pending -> Active -> Inactive 순서로 진행되며 Active 상태일 때 CPU가 해당 ISR을 실행하고 Pending 비트를 지웁니다. 만약 Pending 비트를 지우지 않으면 무한루프가 발생할 수 있기 때문에 EXTI->PR |= EXTI_PR_PR0; 같은 코드로 "나 이제 이 일 다 했어"라고 표시해주는 과정이 필요합니다.


3. 트리거 설정

3.1 엣지 트리거 종류

트리거 방식 비교

Rising Edge (상승 엣지):
신호: 0 → 1 전환 시 감지
               ┌────
────────────────┘
                ↑ 인터럽트 발생

Falling Edge (하강 엣지):
신호: 1 → 0 전환 시 감지
────────────────┐
               └────
                ↑ 인터럽트 발생

Both Edge (양쪽 엣지):
0 → 1, 1 → 0 모두 감지
RTSR + FTSR 동시 설정
               ┌────┐
────────────────┘    └────
                ↑    ↑ 인터럽트 발생 (2회)

실제 사용 예시

버튼 누름 감지 (풀업 저항):
- 버튼 누름: VCC → GND → Falling Edge
- 버튼 해제: GND → VCC → Rising Edge
- 누름만 감지: Falling Edge 설정
- 누름/해제 모두 감지: Both Edge 설정

인코더 신호:
- 회전 방향 판별: 양쪽 엣지 설정 후 다른 채널과 위상 비교

UART 시작 비트:
- 1 → 0 전환 감지: Falling Edge 설정으로 Wakeup 트리거

3.2 신호 품질과 노이즈

글리치(Glitch) 문제

실제 GPIO 신호:
이상적:  ─────┐
             └──────
실제:    ─────┐┌┐┌┐
             └┘└┘└──  ← 바운싱(Bouncing) 현상

기계식 버튼의 경우 접점 바운싱으로 인해
단 1회의 버튼 조작에 수십 회의 엣지 발생 가능
→ 소프트웨어 디바운싱 또는 하드웨어 필터 필요

STM32 내장 입력 필터

EXTI 자체에는 디지털 필터 없음
GPIO 내부의 슈미트 트리거(Schmitt Trigger)로 기본 노이즈 제거

추가 필터링:
1. 하드웨어: RC 필터 (저항 + 커패시터) 외부 회로
2. 소프트웨어: 디바운싱 알고리즘 (타이머 활용)
3. CubeMX: GPIO Input에서 Pull-up/Pull-down 설정으로 기본 안정화

4. HAL 라이브러리를 통한 EXTI 설정

4.1 CubeMX 설정 절차

GPIO EXTI 설정 순서

1. Pinout & Configuration → GPIO 핀 클릭
2. Mode 선택:
   - GPIO_EXTI0 ~ GPIO_EXTI15 (핀 번호에 맞는 항목)
3. GPIO 설정:
   - GPIO mode: External Interrupt Mode with Rising/Falling/Both Edge trigger
   - GPIO Pull-up/Pull-down: 회로에 맞게 선택
4. NVIC 탭:
   - 해당 EXTI IRQ Enable 체크
   - Preemption Priority 설정
5. Code Generation → MX_GPIO_Init() 자동 생성

CubeMX 생성 코드 예시 (PC13, 하강 엣지)

// MX_GPIO_Init() 내부에 자동 생성
static void MX_GPIO_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};

    __HAL_RCC_GPIOC_CLK_ENABLE();

    GPIO_InitStruct.Pin   = GPIO_PIN_13;
    GPIO_InitStruct.Mode  = GPIO_MODE_IT_FALLING;  // 하강 엣지 인터럽트
    GPIO_InitStruct.Pull  = GPIO_NOPULL;            // 외부 풀업 사용 가정
    HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);

    HAL_NVIC_SetPriority(EXTI15_10_IRQn, 10, 0);
    HAL_NVIC_EnableIRQ(EXTI15_10_IRQn);
}

4.2 HAL EXTI 관련 함수

GPIO 초기화 시 Mode 선택

// GPIO Mode 옵션 (stm32f4xx_hal_gpio.h 정의)
GPIO_MODE_IT_RISING         // 상승 엣지 인터럽트
GPIO_MODE_IT_FALLING        // 하강 엣지 인터럽트
GPIO_MODE_IT_RISING_FALLING // 양쪽 엣지 인터럽트

GPIO_MODE_EVT_RISING         // 상승 엣지 이벤트
GPIO_MODE_EVT_FALLING        // 하강 엣지 이벤트
GPIO_MODE_EVT_RISING_FALLING // 양쪽 엣지 이벤트

EXTI 관련 HAL 함수

// ISR 내에서 호출 (Pending 클리어 + 콜백 호출)
void HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin);

// 콜백 함수 (오버라이드하여 실제 처리 구현)
// __weak 선언이므로 재정의 가능
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin);

// 소프트웨어로 EXTI 인터럽트 강제 발생
// EXTI->SWIER 레지스터 직접 조작 또는
HAL_NVIC_SetPendingIRQ(EXTI0_IRQn);  // NVIC Pending 직접 설정

stm32f4xx_it.c 구성

// EXTI Line 10~15는 하나의 IRQ 핸들러 공유
void EXTI15_10_IRQHandler(void)
{
    HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_13);  // PC13 처리
}

// EXTI Line 5~9도 공유
void EXTI9_5_IRQHandler(void)
{
    HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_5);
    HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_6);
    // 사용 중인 핀 모두 호출
}

// EXTI Line 0~4는 개별 IRQ 핸들러
void EXTI0_IRQHandler(void)
{
    HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);
}

EXTI IRQ 핸들러 공유 범위

EXTI0_IRQHandler        → EXTI Line 0 전용
EXTI1_IRQHandler        → EXTI Line 1 전용
EXTI2_IRQHandler        → EXTI Line 2 전용
EXTI3_IRQHandler        → EXTI Line 3 전용
EXTI4_IRQHandler        → EXTI Line 4 전용
EXTI9_5_IRQHandler      → EXTI Line 5, 6, 7, 8, 9 공유
EXTI15_10_IRQHandler    → EXTI Line 10, 11, 12, 13, 14, 15 공유

5. 소프트웨어 인터럽트

5.1 SWIER 레지스터

소프트웨어 인터럽트 발생 원리

EXTI->SWIER 레지스터의 특정 비트에 1을 기록하면
해당 EXTI 라인의 Pending 비트(PR)가 Set됨
→ 실제 GPIO 신호 변화 없이 인터럽트 강제 발생

Pending 비트 클리어 후 SWIER 비트도 자동으로 클리어됨

소프트웨어 인터럽트 사용 예시

// EXTI Line 0 소프트웨어 인터럽트 발생
void trigger_software_interrupt(void)
{
    EXTI->SWIER |= EXTI_SWIER_SWIER0;
    // EXTI0_IRQHandler가 즉시 호출됨
}

// 사용 사례:
// 1. 인터럽트 로직 테스트 및 검증
// 2. 다른 태스크에서 특정 ISR 강제 실행
// 3. RTOS 환경에서 태스크 간 신호 전달

5.2 이벤트 모드 활용

이벤트 모드 설정 (저전력 Wakeup)

// Stop 모드에서 GPIO 신호로 Wakeup하는 예시
GPIO_InitTypeDef GPIO_InitStruct = {0};

GPIO_InitStruct.Pin  = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_EVT_FALLING;  // 이벤트 모드 (인터럽트 아님)
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

// Wakeup 후 시스템 클럭 재설정 등 처리
// 이벤트 모드는 ISR 없이 하드웨어 직접 Wakeup
HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFE); // WFE: Wait For Event

6. 디바운싱 구현

6.1 기계식 버튼의 바운싱

바운싱 현상

버튼 누름 시 실제 신호:

이상적:  VCC ─────┐
                 └──── GND

실제:    VCC ─────┐┌┐┌┐
                 └┘└┘└── GND
                   ↑↑↑
              수 ms ~ 수십 ms 동안 반복
              EXTI 기준으로 수십 회 인터럽트 발생

6.2 타이머 기반 디바운싱

타이머 디바운싱 원리

1. EXTI 인터럽트 발생 (첫 번째 엣지)
2. 타이머 시작 (예: 20ms)
3. 타이머 만료 전 추가 엣지 발생 시 → 타이머 재시작 (무시)
4. 타이머 만료 시 → 안정된 신호로 판단 → 실제 처리 수행

구현 예시

// 전역 변수
volatile uint8_t  button_pressed  = 0;
volatile uint32_t debounce_tick   = 0;

#define DEBOUNCE_DELAY_MS  20U

// EXTI 콜백 (ISR 내에서 호출됨)
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
    if (GPIO_Pin == GPIO_PIN_13)
    {
        debounce_tick = HAL_GetTick();  // 현재 시간 기록
        button_pressed = 1;             // 처리 요청 플래그
    }
}

// 메인 루프에서 디바운싱 처리
int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();

    while (1)
    {
        if (button_pressed)
        {
            // DEBOUNCE_DELAY_MS 이상 경과했는지 확인
            if ((HAL_GetTick() - debounce_tick) >= DEBOUNCE_DELAY_MS)
            {
                button_pressed = 0;

                // 실제 GPIO 상태 재확인 (안정 상태 검증)
                if (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13) == GPIO_PIN_RESET)
                {
                    // 버튼 눌림 처리
                    HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
                }
            }
        }
    }
}

6.3 상태 머신 기반 디바운싱

보다 견고한 디바운싱 구현

typedef enum
{
    BTN_IDLE = 0,
    BTN_DEBOUNCING,
    BTN_PRESSED,
    BTN_RELEASED
} ButtonState_t;

typedef struct
{
    ButtonState_t state;
    uint32_t      timestamp;
    uint8_t       event_flag;  // 메인 루프에서 소비할 이벤트
} Button_t;

Button_t button = {BTN_IDLE, 0, 0};

#define DEBOUNCE_DELAY_MS  20U

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
    if (GPIO_Pin == GPIO_PIN_13)
    {
        if (button.state == BTN_IDLE || button.state == BTN_RELEASED)
        {
            button.state     = BTN_DEBOUNCING;
            button.timestamp = HAL_GetTick();
        }
        else if (button.state == BTN_DEBOUNCING)
        {
            // 바운싱 기간 중 재발생 → 타이머 리셋
            button.timestamp = HAL_GetTick();
        }
    }
}

void Button_Process(Button_t *btn)
{
    if (btn->state == BTN_DEBOUNCING)
    {
        if ((HAL_GetTick() - btn->timestamp) >= DEBOUNCE_DELAY_MS)
        {
            if (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13) == GPIO_PIN_RESET)
            {
                btn->state      = BTN_PRESSED;
                btn->event_flag = 1;  // 이벤트 발생 알림
            }
            else
            {
                btn->state = BTN_IDLE;  // 노이즈로 판단, 무시
            }
        }
    }
    else if (btn->state == BTN_PRESSED)
    {
        if (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13) == GPIO_PIN_SET)
        {
            btn->state = BTN_RELEASED;
        }
    }
    else if (btn->state == BTN_RELEASED)
    {
        btn->state = BTN_IDLE;
    }
}

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();

    while (1)
    {
        Button_Process(&button);

        if (button.event_flag)
        {
            button.event_flag = 0;
            HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);  // LED 토글
        }
    }
}

7. 다중 EXTI 처리

7.1 공유 IRQ 핸들러에서 다중 핀 처리

EXTI9_5, EXTI15_10 공유 핸들러

// 여러 핀이 동일 IRQ를 공유할 때
void EXTI15_10_IRQHandler(void)
{
    // HAL_GPIO_EXTI_IRQHandler 내부에서 GPIO_Pin을 확인하고
    // Pending 비트를 클리어한 뒤 콜백 호출
    HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_10);
    HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_11);
    HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_13);
}

// 콜백에서 핀 구분
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
    switch (GPIO_Pin)
    {
        case GPIO_PIN_10:
            // EXTI10 처리
            break;

        case GPIO_PIN_11:
            // EXTI11 처리
            break;

        case GPIO_PIN_13:
            // EXTI13 처리 (예: 사용자 버튼)
            break;

        default:
            break;
    }
}

직접 Pending 비트 확인 방식

void EXTI15_10_IRQHandler(void)
{
    // 어떤 라인이 발생했는지 PR 레지스터로 직접 확인
    if (EXTI->PR & EXTI_PR_PR13)
    {
        EXTI->PR |= EXTI_PR_PR13;  // 클리어
        button_flag = 1;
    }

    if (EXTI->PR & EXTI_PR_PR10)
    {
        EXTI->PR |= EXTI_PR_PR10;  // 클리어
        sensor_flag = 1;
    }
}

7.2 동일 라인 제약 조건 처리

포트 충돌 발생 예시와 해결

요구사항: PA3 (센서 입력) + PB3 (버튼) 동시 EXTI 사용
문제: 둘 다 EXTI Line 3을 공유 → 동시 사용 불가

해결 방법 1: 핀 번호 변경
PB3 대신 PB7 사용 → PA3 (EXTI3), PB7 (EXTI7) 독립 사용

해결 방법 2: 폴링 방식 혼용
우선순위가 낮은 신호는 EXTI 대신 메인 루프에서 폴링 처리

해결 방법 3: 하드웨어 논리 게이트
두 신호를 AND/OR 게이트로 결합하여 단일 핀으로 수신 (설계 단계에서 결정)

8. 실전 예제

8.1 다중 버튼 + LED 제어

시나리오: 3개 버튼, 3개 LED

// 핀 구성
// PA0: Button1 (EXTI0, Falling Edge, 풀업)
// PB4: Button2 (EXTI4, Falling Edge, 풀업)
// PC5: Button3 (EXTI9_5 공유, Falling Edge, 풀업)
// PA5, PA6, PA7: LED1, LED2, LED3 (Push-Pull 출력)

volatile uint8_t btn1_flag = 0;
volatile uint8_t btn2_flag = 0;
volatile uint8_t btn3_flag = 0;

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
    switch (GPIO_Pin)
    {
        case GPIO_PIN_0: btn1_flag = 1; break;
        case GPIO_PIN_4: btn2_flag = 1; break;
        case GPIO_PIN_5: btn3_flag = 1; break;
        default: break;
    }
}

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();

    while (1)
    {
        if (btn1_flag) { btn1_flag = 0; HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); }
        if (btn2_flag) { btn2_flag = 0; HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_6); }
        if (btn3_flag) { btn3_flag = 0; HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_7); }
    }
}

8.2 양쪽 엣지 감지로 버튼 상태 추적

누름/해제 시간 측정

volatile uint32_t press_time   = 0;
volatile uint32_t release_time = 0;
volatile uint8_t  long_press   = 0;

#define LONG_PRESS_MS  1000U

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
    if (GPIO_Pin == GPIO_PIN_13)
    {
        if (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13) == GPIO_PIN_RESET)
        {
            // 하강 엣지: 버튼 눌림
            press_time = HAL_GetTick();
        }
        else
        {
            // 상승 엣지: 버튼 해제
            release_time = HAL_GetTick();

            if ((release_time - press_time) >= LONG_PRESS_MS)
            {
                long_press = 1;  // 길게 누름
            }
        }
    }
}

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();  // GPIO_MODE_IT_RISING_FALLING으로 설정

    while (1)
    {
        if (long_press)
        {
            long_press = 0;
            // 긴 누름 처리 (예: 설정 초기화)
        }
    }
}

9. EXTI 디버깅

9.1 EXTI 상태 확인

런타임 EXTI 상태 출력

void print_exti_status(uint32_t line_mask)
{
    printf("=== EXTI Status (mask: 0x%08lX) ===\r\n", line_mask);
    printf("IMR  : 0x%08lX\r\n", EXTI->IMR  & line_mask);
    printf("EMR  : 0x%08lX\r\n", EXTI->EMR  & line_mask);
    printf("RTSR : 0x%08lX\r\n", EXTI->RTSR & line_mask);
    printf("FTSR : 0x%08lX\r\n", EXTI->FTSR & line_mask);
    printf("PR   : 0x%08lX\r\n", EXTI->PR   & line_mask);
}

// 사용 예
debug_all_exti:
    print_exti_status(0x0000FFFF);  // EXTI 0~15 확인

SYSCFG EXTICR 설정 확인

void print_syscfg_exticr(void)
{
    printf("SYSCFG EXTICR:\r\n");
    printf("EXTICR[0] (EXTI 0~3): 0x%08lX\r\n", SYSCFG->EXTICR[0]);
    printf("EXTICR[1] (EXTI 4~7): 0x%08lX\r\n", SYSCFG->EXTICR[1]);
    printf("EXTICR[2] (EXTI 8~11): 0x%08lX\r\n", SYSCFG->EXTICR[2]);
    printf("EXTICR[3] (EXTI 12~15): 0x%08lX\r\n", SYSCFG->EXTICR[3]);
}

9.2 일반적인 실수와 해결

문제 1: ISR이 한 번만 실행되고 이후 동작 안 함

// 원인: Pending 비트 미클리어
// HAL 사용 시 HAL_GPIO_EXTI_IRQHandler() 누락

// 잘못된 예시:
void EXTI0_IRQHandler(void)
{
    button_flag = 1;  // Pending 미클리어 → 다음 엣지에서 재진입 불가
}

// 올바른 예시:
void EXTI0_IRQHandler(void)
{
    HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);  // 내부에서 Pending 클리어
}

문제 2: 동일 핀 번호 다른 포트 충돌

// 원인: PA5, PB5 동시 EXTI 설정 시 마지막 설정만 유효
// SYSCFG->EXTICR[1]의 EXTI5 필드는 하나뿐

// 진단:
printf("EXTICR[1]: 0x%08lX\r\n", SYSCFG->EXTICR[1]);
// EXTI5 필드(비트 7:4)가 예상 포트 값인지 확인

// 해결: 핀 번호 충돌 없도록 설계 단계에서 GPIO 핀 재배치

문제 3: 바운싱으로 인한 과도한 ISR 진입

// 진단: ISR 진입 횟수 카운터로 확인
volatile uint32_t exti_count = 0;

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
    if (GPIO_Pin == GPIO_PIN_13)
    {
        exti_count++;
    }
}

// 버튼 1회 누름 후 exti_count 확인
// 1 이상이면 바운싱 발생 → 디바운싱 로직 추가

문제 4: EXTI9_5 또는 EXTI15_10 IRQ 핸들러에서 일부 핀 무시

// 원인: 공유 핸들러에서 사용 중인 핀을 모두 HAL 함수에 전달하지 않음

// 잘못된 예시:
void EXTI15_10_IRQHandler(void)
{
    HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_13);  // PIN_10, PIN_11 누락
    // PIN_10, PIN_11의 Pending 비트가 클리어되지 않아 폭주
}

// 올바른 예시:
void EXTI15_10_IRQHandler(void)
{
    HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_10);
    HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_11);
    HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_13);
}

10. 학습 정리

오늘 배운 내용

EXTI 구조

  • GPIO 핀과 NVIC 사이에서 엣지 변화를 감지하는 STM32 전용 컨트롤러
  • GPIO 핀 번호 = EXTI 라인 번호, 동일 번호의 다른 포트는 동시 사용 불가
  • SYSCFG->EXTICR로 어느 포트의 핀을 EXTI에 연결할지 선택

트리거와 모드

  • 상승/하강/양쪽 엣지 트리거 선택 가능
  • 인터럽트 모드: CPU ISR 실행, 이벤트 모드: 하드웨어 직접 신호 (Wakeup, 타이머 트리거 등)
  • Pending 비트는 1을 기록해야 클리어됨 (미클리어 시 인터럽트 폭주)

공유 IRQ 핸들러

  • EXTI5~9, EXTI10~15는 IRQ 핸들러를 공유
  • 사용 중인 모든 핀에 대해 HAL_GPIO_EXTI_IRQHandler() 호출 필요

디바운싱

  • 기계식 버튼은 반드시 디바운싱 처리 필요
  • 타이머 기반 또는 상태 머신 방식으로 구현
  • ISR에서는 플래그만 설정, 실제 처리는 메인 루프에서 수행

핵심 개념 요약

1. EXTI 설정 순서

// CubeMX 사용 시 자동 생성, 수동 설정 시 순서:
RCC->APB2ENR |= RCC_APB2ENR_SYSCFGEN;        // 1. SYSCFG 클럭
SYSCFG->EXTICR[n] |= PORT_CODE;              // 2. 포트 선택
EXTI->RTSR |= (1 << line) or EXTI->FTSR;    // 3. 트리거 설정
EXTI->IMR  |= (1 << line);                  // 4. 인터럽트 마스크 해제
NVIC_SetPriority(EXTIn_IRQn, priority);     // 5. 우선순위
NVIC_EnableIRQ(EXTIn_IRQn);                 // 6. NVIC 활성화

2. ISR 필수 처리

void EXTIn_IRQHandler(void)
{
    HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_n);    // Pending 클리어 + 콜백
}

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
    if (GPIO_Pin == GPIO_PIN_n)
    {
        flag = 1;    // 최소한의 처리만
    }
}

3. EXTI 라인 공유 제약

PA0, PB0, PC0 → 모두 EXTI Line 0 공유 → 하나만 선택 가능
PA13, PB13    → 모두 EXTI Line 13 공유 → 하나만 선택 가능
설계 시 GPIO 핀 번호가 겹치지 않도록 핀 배치 결정 필요

EXTI 설정 참고표

EXTI 라인IRQ 핸들러비고
0EXTI0_IRQHandler개별 전용
1EXTI1_IRQHandler개별 전용
2EXTI2_IRQHandler개별 전용
3EXTI3_IRQHandler개별 전용
4EXTI4_IRQHandler개별 전용
5 ~ 9EXTI9_5_IRQHandler5개 공유
10 ~ 15EXTI15_10_IRQHandler6개 공유
16PVD_IRQHandlerPVD 전용
17RTC_Alarm_IRQHandlerRTC Alarm 전용
18OTG_FS_WKUP_IRQHandlerUSB FS Wakeup
22RTC_WKUP_IRQHandlerRTC Wakeup Timer
profile
당신의 코딩 메이트

0개의 댓글