STM32 #13

홍태준·2026년 2월 21일

STM32

목록 보기
13/15
post-thumbnail

Week 3 Day 3: 타이머 인터럽트와 EXTI 동시 사용

학습 목표

  • STM32 타이머의 인터럽트 발생 원리와 구조를 이해한다
  • TIM 인터럽트와 EXTI 인터럽트의 차이점과 연동 방식을 파악한다
  • NVIC 우선순위 설정이 다중 인터럽트 처리에 미치는 영향을 이해한다
  • 타이머를 활용한 소프트웨어 디바운싱의 내부 동작을 이해한다
  • HAL 라이브러리를 통한 타이머 인터럽트 설정 및 EXTI 연동 구현 방법을 익힌다

STM32의 타이머(TIM)는 단순한 지연(Delay)이나 PWM 출력 외에도 주기적인 인터럽트를 발생시키는 시간 기반 이벤트 처리 장치로 사용됩니다. EXTI가 외부 신호의 비정기적인 엣지 변화를 감지한다면, 타이머 인터럽트는 정확한 시간 간격으로 CPU에 신호를 전달합니다. 두 인터럽트를 함께 사용할 때는 NVIC의 우선순위 설정과 ISR 실행 시간 관리가 핵심이 됩니다.


1. STM32 타이머 인터럽트 기본 구조

1.1 타이머 카운터와 업데이트 이벤트

타이머 카운터 동작 원리

TIM 카운터 동작 (업카운트 모드 기준):

CNT 레지스터: 0 → 1 → 2 → ... → ARR 값 도달
                                        │
                                        ▼
                              업데이트 이벤트(UEV) 발생
                                        │
                             ┌───────────┴───────────┐
                             ▼                     ▼
                    CNT = 0 으로 리셋          UIF 플래그 Set
                    (자동 재시작)        →     NVIC → CPU 인터럽트

ARR(Auto Reload Register): 카운터의 최대값 설정
PSC(Prescaler Register): 카운터 클럭 분주비 설정
CNT(Counter Register): 현재 카운터 값

타이머 인터럽트 주기 계산

타이머 클럭 주파수 = APB 클럭 / (PSC + 1)

인터럽트 주기 = (PSC + 1) * (ARR + 1) / APB 클럭

예시 (APB1 클럭 = 84MHz, 1ms 주기 인터럽트):
PSC = 83   → 타이머 클럭 = 84MHz / 84 = 1MHz (1us 단위)
ARR = 999  → 1000 카운트 후 UEV 발생 = 1ms 주기

1ms 주기 인터럽트:
(83 + 1) * (999 + 1) / 84,000,000 = 0.001초 = 1ms

STM32F4 타이머 분류

기본 타이머 (Basic Timer):
TIM6, TIM7
- 업카운트 전용
- 업데이트 인터럽트만 지원
- DAC 트리거 연결 가능

범용 타이머 (General Purpose Timer):
TIM2 ~ TIM5, TIM9 ~ TIM14
- 업/다운/센터얼라인 카운트
- 캡처/비교(CCR) 채널 보유
- 입력 캡처, 출력 비교, PWM, 인코더 모드 지원

고급 타이머 (Advanced Timer):
TIM1, TIM8
- 범용 기능 + 상보(Complementary) 출력
- 브레이크 입력, 데드타임 생성 지원
- 모터 제어에 특화

1.2 타이머 인터럽트 종류

TIM 인터럽트 플래그 (TIMx->SR 레지스터)

UIF  (Update Interrupt Flag)        : 업데이트 이벤트 (CNT = ARR 후 오버플로우)
CC1IF (Capture/Compare 1 Interrupt) : 채널 1 캡처/비교 일치
CC2IF                               : 채널 2 캡처/비교 일치
CC3IF                               : 채널 3 캡처/비교 일치
CC4IF                               : 채널 4 캡처/비교 일치
TIF  (Trigger Interrupt Flag)       : 트리거 이벤트
BIF  (Break Interrupt Flag)         : 브레이크 이벤트 (TIM1, TIM8 전용)

가장 일반적인 사용: UIF (주기적 인터럽트 = 타이머 오버플로우)

타이머 인터럽트 활성화 레지스터

TIMx->DIER (DMA/Interrupt Enable Register):
UIE  비트: 업데이트 인터럽트 활성화
CC1IE 비트: 채널 1 인터럽트 활성화
TIE  비트: 트리거 인터럽트 활성화

인터럽트 플래그 클리어:
TIMx->SR &= ~TIM_SR_UIF;  // UIF 클리어 (0을 써야 클리어, EXTI PR과 반대)
→ EXTI->PR은 1을 써서 클리어
→ TIMx->SR은 0을 써서 클리어 (비트 클리어 방식)

TIM의 인터럽트 플래그 클리어 방식은 EXTI의 Pending Register와 반대입니다. EXTI->PR에는 1을 기록하여 클리어하는 반면, TIMx->SR에는 0을 기록하여 클리어합니다. 두 방식을 혼동하면 플래그가 클리어되지 않아 인터럽트 폭주가 발생할 수 있습니다.


2. 타이머 인터럽트 레지스터 구성

2.1 주요 레지스터

타이머 설정 관련 레지스터

CR1  (Control Register 1)           : 카운터 방향, 버퍼 설정, 카운터 인에이블
CR2  (Control Register 2)           : 마스터 모드, 캡처/비교 제어
SMCR (Slave Mode Control Register)  : 슬레이브 모드, 외부 트리거 설정
DIER (DMA/Interrupt Enable Register): 인터럽트/DMA 요청 인에이블
SR   (Status Register)              : 인터럽트 플래그 (읽기 후 클리어)
EGR  (Event Generation Register)    : 소프트웨어로 이벤트 강제 발생
CNT  (Counter Register)             : 현재 카운터 값
PSC  (Prescaler Register)           : 클럭 분주비 (실제 적용은 UEV 시점)
ARR  (Auto Reload Register)         : 카운터 최대값 (버퍼 적용 가능)
CCRx (Capture/Compare Register)     : 채널별 캡처/비교값

레지스터 직접 조작 예시 (TIM2, 1ms 주기)

// 1. TIM2 클럭 활성화
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;

// 2. 카운터 정지 상태에서 설정
TIM2->CR1 &= ~TIM_CR1_CEN;

// 3. 분주비 설정 (84MHz / 84 = 1MHz)
TIM2->PSC = 83;

// 4. 오토 리로드값 설정 (1MHz / 1000 = 1kHz = 1ms)
TIM2->ARR = 999;

// 5. 카운터 초기화
TIM2->CNT = 0;

// 6. 업데이트 이벤트 강제 발생 (PSC, ARR 즉시 적용)
TIM2->EGR |= TIM_EGR_UG;

// 7. 업데이트 인터럽트 활성화
TIM2->DIER |= TIM_DIER_UIE;

// 8. NVIC 설정
NVIC_SetPriority(TIM2_IRQn, 5);
NVIC_EnableIRQ(TIM2_IRQn);

// 9. 카운터 시작
TIM2->CR1 |= TIM_CR1_CEN;

2.2 인터럽트 플래그 클리어

TIM 플래그 클리어 방법

// TIM ISR 내에서 UIF 클리어
void TIM2_IRQHandler(void)
{
    if (TIM2->SR & TIM_SR_UIF)     // UIF 플래그 확인
    {
        TIM2->SR &= ~TIM_SR_UIF;   // 0을 써서 클리어 (비트 클리어)
        // 처리 코드
    }
}

// HAL 사용 시 자동 처리
void TIM2_IRQHandler(void)
{
    HAL_TIM_IRQHandler(&htim2);    // 내부에서 플래그 클리어 후 콜백 호출
}

TIMx->SR 레지스터는 비트에 0을 기록하여 클리어하는 RC_W0(Read/Clear by Writing 0) 방식을 사용합니다. TIM2->SR &= ~TIM_SR_UIF 구문은 UIF 비트만 0으로 만들고 나머지 비트는 유지합니다. TIM2->SR = 0 처럼 전체를 0으로 쓰면 다른 채널의 플래그까지 모두 클리어되므로 주의해야 합니다.


3. HAL 라이브러리를 통한 타이머 인터럽트 설정

3.1 CubeMX 설정 절차

TIM 인터럽트 설정 순서

1. Timers → TIMx 선택
2. Mode:
   - Clock Source: Internal Clock
   - Channel 설정 (PWM, Capture 등 필요한 경우)
3. Configuration → Parameter Settings:
   - Prescaler: 분주비 입력 (PSC 값)
   - Counter Period: ARR 값 입력
   - Counter Mode: Up / Down / Center Aligned
   - Auto-Reload Preload: Enable (ARR 버퍼 사용 권장)
4. NVIC Settings:
   - TIMx global interrupt Enable 체크
   - Preemption Priority 설정
5. Code Generation → MX_TIMx_Init() 자동 생성

CubeMX 생성 코드 예시 (TIM2, 1ms 주기)

// MX_TIM2_Init() 내부 자동 생성
static void MX_TIM2_Init(void)
{
    TIM_ClockConfigTypeDef  sClockSourceConfig = {0};
    TIM_MasterConfigTypeDef sMasterConfig      = {0};

    htim2.Instance               = TIM2;
    htim2.Init.Prescaler         = 83;
    htim2.Init.CounterMode       = TIM_COUNTERMODE_UP;
    htim2.Init.Period            = 999;
    htim2.Init.ClockDivision     = TIM_CLOCKDIVISION_DIV1;
    htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
    HAL_TIM_Base_Init(&htim2);

    sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
    HAL_TIM_ConfigClockSource(&htim2, &sClockSourceConfig);

    sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
    sMasterConfig.MasterSlaveMode     = TIM_MASTERSLAVEMODE_DISABLE;
    HAL_TIMEx_MasterConfigSynchronization(&htim2, &sMasterConfig);
}

// main()에서 타이머 인터럽트 시작
HAL_TIM_Base_Start_IT(&htim2);  // IT: Interrupt 모드로 시작

3.2 HAL 타이머 인터럽트 관련 함수

타이머 제어 함수

// 타이머 인터럽트 시작/정지
HAL_StatusTypeDef HAL_TIM_Base_Start_IT(TIM_HandleTypeDef *htim);
HAL_StatusTypeDef HAL_TIM_Base_Stop_IT(TIM_HandleTypeDef *htim);

// 타이머 인터럽트 없이 시작 (폴링 모드)
HAL_StatusTypeDef HAL_TIM_Base_Start(TIM_HandleTypeDef *htim);
HAL_StatusTypeDef HAL_TIM_Base_Stop(TIM_HandleTypeDef *htim);

// ISR 내에서 호출 (플래그 클리어 + 콜백 호출)
void HAL_TIM_IRQHandler(TIM_HandleTypeDef *htim);

// 업데이트 인터럽트 콜백 (오버라이드하여 실제 처리 구현)
// __weak 선언이므로 재정의 가능
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim);

stm32f4xx_it.c 구성

// TIM2 전역 인터럽트 핸들러
void TIM2_IRQHandler(void)
{
    HAL_TIM_IRQHandler(&htim2);
}

// TIM3 전역 인터럽트 핸들러
void TIM3_IRQHandler(void)
{
    HAL_TIM_IRQHandler(&htim3);
}

콜백 함수 구현 (main.c 또는 별도 파일)

// 어떤 타이머에서 발생했는지 인스턴스로 구분
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM2)
    {
        // TIM2 업데이트 인터럽트 처리 (1ms 주기 태스크 등)
    }

    if (htim->Instance == TIM3)
    {
        // TIM3 업데이트 인터럽트 처리
    }
}

4. NVIC 우선순위와 다중 인터럽트 관리

4.1 NVIC 우선순위 구조

우선순위 레벨

STM32F4 기준: 4비트 우선순위 = 16단계 (0 ~ 15)
0이 가장 높은 우선순위, 15가 가장 낮은 우선순위

우선순위 구분:
Preemption Priority (선점 우선순위): 더 높은 우선순위 인터럽트가 현재 ISR을 중단하고 먼저 실행 가능
Sub Priority (서브 우선순위): 선점 우선순위가 같을 때 대기 중인 인터럽트의 처리 순서 결정

HAL 기본 설정: NVIC_PRIORITYGROUP_4 (선점 4비트, 서브 0비트)
→ 선점 우선순위 0 ~ 15, 서브 우선순위는 사용하지 않음

HAL 우선순위 그룹 종류

NVIC_PRIORITYGROUP_0 : 선점 0비트, 서브 4비트 (선점 없음)
NVIC_PRIORITYGROUP_1 : 선점 1비트, 서브 3비트
NVIC_PRIORITYGROUP_2 : 선점 2비트, 서브 2비트
NVIC_PRIORITYGROUP_3 : 선점 3비트, 서브 1비트
NVIC_PRIORITYGROUP_4 : 선점 4비트, 서브 0비트 (HAL 기본값)

HAL_Init() 내부에서 HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4) 자동 호출

선점(Preemption) 동작 원리

시나리오:
TIM2_IRQHandler 실행 중 (우선순위 5)
→ EXTI0_IRQHandler 발생 (우선순위 3)
→ TIM2_IRQHandler 일시 중단
→ EXTI0_IRQHandler 실행 (선점)
→ EXTI0_IRQHandler 종료
→ TIM2_IRQHandler 재개

반대 시나리오:
TIM2_IRQHandler 실행 중 (우선순위 3)
→ EXTI0_IRQHandler 발생 (우선순위 5)
→ EXTI0_IRQHandler 대기 (선점 불가)
→ TIM2_IRQHandler 종료
→ EXTI0_IRQHandler 실행

핵심: 숫자가 낮을수록 높은 우선순위 = 먼저 선점 가능

4.2 우선순위 설계 지침

인터럽트 우선순위 설계 예시

권장 우선순위 배치 (낮은 숫자 = 높은 우선순위):

0 ~ 1  : 시스템 크리티컬 (안전 인터럽트, Watchdog)
2 ~ 4  : 통신 인터럽트 (UART, SPI, I2C) - 데이터 손실 방지
5 ~ 7  : 센서 입력 EXTI - 실시간 신호 감지
8 ~ 10 : 주기 태스크 타이머 - 일반 주기 처리
11 ~ 15: 낮은 우선순위 태스크 (LED, 상태 표시 등)

주의:
HAL_GetTick()은 SysTick 인터럽트(기본 우선순위 15)에 의존
SysTick 우선순위보다 높은 ISR 내에서 HAL_Delay() 사용 금지
→ HAL_Delay()는 SysTick에 의존하므로 SysTick이 블로킹되면 무한 대기

SysTick과 사용자 인터럽트 관계

// HAL_Init() 내부에서 SysTick 설정
// 기본 우선순위: 15 (가장 낮음)

// 사용자 ISR에서 HAL_Delay() 사용 금지 예시
void TIM2_IRQHandler(void)  // 우선순위 5
{
    HAL_TIM_IRQHandler(&htim2);
}

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM2)
    {
        HAL_Delay(10);  // 위험: TIM2 ISR이 SysTick 선점 중이면 무한 대기
                        // HAL_GetTick()으로 비블로킹 처리로 대체해야 함
    }
}

5. 타이머 인터럽트와 EXTI 동시 사용

5.1 동작 흐름

타이머 + EXTI 병행 동작 구조

EXTI (비정기):
GPIO 핀 엣지 변화
        │
        ▼
EXTI_IRQHandler() 호출
        │
        ▼
HAL_GPIO_EXTI_Callback()
        │
        ▼
플래그 설정 (flag = 1)

TIM (정기):
카운터 오버플로우 (주기적)
        │
        ▼
TIM_IRQHandler() 호출
        │
        ▼
HAL_TIM_PeriodElapsedCallback()
        │
        ▼
플래그 확인 및 디바운싱 처리

메인 루프:
        ▼
플래그 소비 (실제 동작 수행)

5.2 타이머 기반 디바운싱 (타이머 인터럽트 활용)

디바운싱 구조 비교

방법 1 - HAL_GetTick() 기반:
EXTI 콜백에서 HAL_GetTick() 기록
메인 루프에서 경과 시간 확인
→ 메인 루프 의존적, 루프 주기가 빠를 때 불필요한 반복 발생

방법 2 - TIM 인터럽트 기반:
EXTI 콜백에서 디바운스 카운터 시작
TIM ISR에서 카운터 감소 및 만료 확인
→ 정확한 시간 제어, 메인 루프 독립적

타이머 인터럽트 기반 디바운싱 구현

// 전역 변수
volatile uint8_t  debounce_active  = 0;
volatile uint16_t debounce_counter = 0;
volatile uint8_t  button_event     = 0;

#define DEBOUNCE_TICKS  20U   // TIM 인터럽트 주기 1ms → 20ms 디바운스

// EXTI 콜백: 첫 엣지 감지 시 디바운스 카운터 시작
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
    if (GPIO_Pin == GPIO_PIN_13)
    {
        debounce_counter = DEBOUNCE_TICKS;  // 카운터 설정 (재설정 포함)
        debounce_active  = 1;               // 디바운스 진행 중 표시
    }
}

// TIM2 1ms 주기 ISR: 카운터 감소 및 만료 처리
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM2)
    {
        if (debounce_active && debounce_counter > 0)
        {
            debounce_counter--;

            if (debounce_counter == 0)
            {
                debounce_active = 0;

                // 안정 상태 확인
                if (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13) == GPIO_PIN_RESET)
                {
                    button_event = 1;  // 유효한 버튼 이벤트
                }
            }
        }
    }
}

// 메인 루프
int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_TIM2_Init();

    HAL_TIM_Base_Start_IT(&htim2);

    while (1)
    {
        if (button_event)
        {
            button_event = 0;
            HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);  // LED 토글
        }
    }
}

5.3 다중 버튼 디바운싱 구조

구조체 기반 다중 버튼 타이머 디바운싱

typedef struct
{
    GPIO_TypeDef *port;
    uint16_t      pin;
    uint16_t      debounce_counter;
    uint8_t       active;
    uint8_t       event_flag;
} Button_t;

#define DEBOUNCE_TICKS  20U
#define BUTTON_COUNT    3U

Button_t buttons[BUTTON_COUNT] =
{
    { GPIOA, GPIO_PIN_0,  0, 0, 0 },  // Button 1
    { GPIOB, GPIO_PIN_4,  0, 0, 0 },  // Button 2
    { GPIOC, GPIO_PIN_13, 0, 0, 0 },  // Button 3
};

// EXTI 콜백에서 핀에 해당하는 버튼 구조체 탐색 및 카운터 시작
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
    for (uint8_t i = 0; i < BUTTON_COUNT; i++)
    {
        if (buttons[i].pin == GPIO_Pin)
        {
            buttons[i].debounce_counter = DEBOUNCE_TICKS;
            buttons[i].active           = 1;
            break;
        }
    }
}

// TIM ISR에서 모든 버튼 디바운스 카운터 처리
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM2)
    {
        for (uint8_t i = 0; i < BUTTON_COUNT; i++)
        {
            if (buttons[i].active && buttons[i].debounce_counter > 0)
            {
                buttons[i].debounce_counter--;

                if (buttons[i].debounce_counter == 0)
                {
                    buttons[i].active = 0;

                    if (HAL_GPIO_ReadPin(buttons[i].port, buttons[i].pin)
                        == GPIO_PIN_RESET)
                    {
                        buttons[i].event_flag = 1;
                    }
                }
            }
        }
    }
}

// 메인 루프
int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_TIM2_Init();
    HAL_TIM_Base_Start_IT(&htim2);

    while (1)
    {
        if (buttons[0].event_flag) { buttons[0].event_flag = 0; /* 처리 */ }
        if (buttons[1].event_flag) { buttons[1].event_flag = 0; /* 처리 */ }
        if (buttons[2].event_flag) { buttons[2].event_flag = 0; /* 처리 */ }
    }
}

6. 타이머 인터럽트 주기 설계

6.1 태스크 주기와 타이머 설정

주기별 타이머 설정 예시 (APB1 = 84MHz)

1ms 주기:
PSC = 83,   ARR = 999    → 84MHz / 84 / 1000 = 1kHz

10ms 주기:
PSC = 839,  ARR = 999    → 84MHz / 840 / 1000 = 100Hz
또는
PSC = 83,   ARR = 9999   → 84MHz / 84 / 10000 = 100Hz

100ms 주기:
PSC = 8399, ARR = 999    → 84MHz / 8400 / 1000 = 10Hz
또는
PSC = 839,  ARR = 9999   → 84MHz / 840 / 10000 = 10Hz

주의:
PSC, ARR 모두 16비트 레지스터 = 최대값 65535
TIM2, TIM5는 32비트 카운터 예외 (ARR 최대 0xFFFFFFFF)

하나의 타이머로 다중 주기 태스크 처리

// TIM2: 1ms 기준 타이머
// 1ms, 10ms, 100ms 태스크를 하나의 타이머로 처리

volatile uint32_t tick_1ms   = 0;
volatile uint32_t tick_10ms  = 0;
volatile uint32_t tick_100ms = 0;

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM2)
    {
        tick_1ms++;

        if (tick_1ms % 10 == 0)   tick_10ms++;
        if (tick_1ms % 100 == 0)  tick_100ms++;
    }
}

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_TIM2_Init();
    HAL_TIM_Base_Start_IT(&htim2);

    uint32_t last_10ms  = 0;
    uint32_t last_100ms = 0;

    while (1)
    {
        if (tick_10ms != last_10ms)
        {
            last_10ms = tick_10ms;
            // 10ms 주기 태스크 (디바운싱 처리 등)
        }

        if (tick_100ms != last_100ms)
        {
            last_100ms = tick_100ms;
            // 100ms 주기 태스크 (상태 출력, 센서 읽기 등)
        }
    }
}

6.2 ISR 실행 시간 관리

ISR 내 처리 시간 원칙

ISR 내부 처리 시간 > 타이머 인터럽트 주기 → 인터럽트 누적 발생

예시:
TIM 인터럽트 주기: 1ms
ISR 내부 처리 시간: 2ms
→ 1ms 후 다시 UIF Set
→ 이전 ISR이 아직 실행 중
→ 선점 우선순위에 따라 재진입 또는 누락 발생

ISR 내 금지 사항:
1. HAL_Delay() 호출 (블로킹)
2. printf() 직접 호출 (UART 전송 대기)
3. 무거운 연산 (FFT, 행렬 연산 등)
4. 메모리 동적 할당 (malloc, free)

ISR 내 허용:
1. 플래그 변수 설정
2. 카운터 증감
3. 간단한 GPIO 조작
4. 링 버퍼에 데이터 적재

7. 타이머와 EXTI 연동 심화

7.1 입력 캡처 (Input Capture)

입력 캡처 개념

입력 캡처(Input Capture):
GPIO 엣지 발생 시 타이머 CNT 값을 CCR 레지스터에 자동 저장하는 기능

EXTI와의 차이:
EXTI: 엣지 발생 시 CPU 인터럽트 → 소프트웨어로 시간 기록
Input Capture: 엣지 발생 시 하드웨어가 CNT 값 자동 저장 → 정밀도 높음

활용:
- 신호 주파수 측정 (연속 상승 엣지 간격)
- 듀티 사이클 측정 (상승 ~ 하강 엣지 간격)
- 초음파 센서 거리 측정 (TRIG 출력 후 ECHO 펄스 폭 측정)

입력 캡처 설정 예시

TIM_IC_InitTypeDef sConfigIC = {0};

htim3.Instance               = TIM3;
htim3.Init.Prescaler         = 83;       // 1MHz 타이머 클럭
htim3.Init.CounterMode       = TIM_COUNTERMODE_UP;
htim3.Init.Period            = 0xFFFF;   // 최대 범위 사용
htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
HAL_TIM_IC_Init(&htim3);

sConfigIC.ICPolarity  = TIM_ICPOLARITY_RISING;   // 상승 엣지 캡처
sConfigIC.ICSelection = TIM_ICSELECTION_DIRECTTI; // 직접 입력
sConfigIC.ICPrescaler = TIM_ICPSC_DIV1;           // 매 엣지마다 캡처
sConfigIC.ICFilter    = 0;
HAL_TIM_IC_ConfigChannel(&htim3, &sConfigIC, TIM_CHANNEL_1);

HAL_TIM_IC_Start_IT(&htim3, TIM_CHANNEL_1);  // 캡처 인터럽트 시작

입력 캡처 콜백

uint32_t capture1 = 0;
uint32_t capture2 = 0;
uint8_t  capture_state = 0;

void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM3 && htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1)
    {
        if (capture_state == 0)
        {
            capture1      = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);
            capture_state = 1;
        }
        else
        {
            capture2      = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);
            capture_state = 0;

            // 두 캡처 값의 차이 = 신호 주기 (us 단위, 1MHz 기준)
            uint32_t period_us = (capture2 >= capture1)
                ? (capture2 - capture1)
                : (0xFFFF - capture1 + capture2 + 1);
        }
    }
}

7.2 타이머 트리거와 EXTI 이벤트 연결

EXTI 이벤트 모드로 타이머 트리거

// EXTI 이벤트 모드 + 타이머 외부 트리거 연결
// (하드웨어 직접 연결: EXTI 이벤트 → 타이머 시작)
// CubeMX에서 TIM Slave Mode → External Trigger 선택 후 필터, 극성 설정

// 소프트웨어 방식 (EXTI ISR에서 타이머 시작):
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
    if (GPIO_Pin == GPIO_PIN_0)
    {
        // 외부 신호 감지 후 타이머 시작
        __HAL_TIM_SET_COUNTER(&htim2, 0);      // CNT 초기화
        HAL_TIM_Base_Start_IT(&htim2);         // 타이머 시작
    }
}

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM2)
    {
        HAL_TIM_Base_Stop_IT(&htim2);          // 1회 후 정지
        timeout_event = 1;                     // 타임아웃 이벤트 발생
    }
}

8. 실전 예제

8.1 타이머 인터럽트 + EXTI 버튼 디바운싱 + LED 제어

전체 구현 예시

// 핀 구성:
// PC13: 사용자 버튼 (EXTI13, Falling Edge, 외부 풀업)
// PA5 : LED (Push-Pull 출력)
// TIM2: 1ms 주기 인터럽트 (디바운싱용)

volatile uint16_t debounce_cnt   = 0;
volatile uint8_t  debounce_run   = 0;
volatile uint8_t  button_event   = 0;

#define DEBOUNCE_TICKS  20U

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
    if (GPIO_Pin == GPIO_PIN_13)
    {
        debounce_cnt = DEBOUNCE_TICKS;
        debounce_run = 1;
    }
}

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM2)
    {
        if (debounce_run)
        {
            if (debounce_cnt > 0)
            {
                debounce_cnt--;
            }
            else
            {
                debounce_run = 0;

                if (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13) == GPIO_PIN_RESET)
                {
                    button_event = 1;
                }
            }
        }
    }
}

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

    HAL_TIM_Base_Start_IT(&htim2);

    while (1)
    {
        if (button_event)
        {
            button_event = 0;
            HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
        }
    }
}

8.2 긴 누름 / 짧은 누름 판별 (타이머 기반)

누름 시간 측정 구현

volatile uint32_t press_duration   = 0;
volatile uint8_t  press_counting   = 0;
volatile uint8_t  short_press_flag = 0;
volatile uint8_t  long_press_flag  = 0;

#define SHORT_PRESS_MAX_MS  500U
#define LONG_PRESS_MIN_MS   1000U

// EXTI Both Edge 설정 필요 (GPIO_MODE_IT_RISING_FALLING)
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_duration = 0;
            press_counting = 1;
        }
        else
        {
            // 상승 엣지: 해제, 카운터 정지 후 시간 판별
            press_counting = 0;

            if (press_duration < SHORT_PRESS_MAX_MS)
            {
                short_press_flag = 1;
            }
            else if (press_duration >= LONG_PRESS_MIN_MS)
            {
                long_press_flag = 1;
            }
        }
    }
}

// TIM2 1ms 주기 ISR
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM2)
    {
        if (press_counting)
        {
            press_duration++;
        }
    }
}

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

    HAL_TIM_Base_Start_IT(&htim2);

    while (1)
    {
        if (short_press_flag)
        {
            short_press_flag = 0;
            HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);   // LED 토글
        }

        if (long_press_flag)
        {
            long_press_flag = 0;
            HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET);  // LED 끄기 (리셋)
        }
    }
}

9. 디버깅

9.1 타이머 인터럽트 상태 확인

런타임 타이머 상태 출력

void print_timer_status(TIM_TypeDef *TIMx)
{
    printf("=== TIM Status ===\r\n");
    printf("CR1  : 0x%08lX\r\n", TIMx->CR1);
    printf("DIER : 0x%08lX\r\n", TIMx->DIER);
    printf("SR   : 0x%08lX\r\n", TIMx->SR);
    printf("CNT  : 0x%08lX\r\n", TIMx->CNT);
    printf("PSC  : 0x%08lX\r\n", TIMx->PSC);
    printf("ARR  : 0x%08lX\r\n", TIMx->ARR);
}

// 사용 예
print_timer_status(TIM2);

9.2 일반적인 실수와 해결

문제 1: 타이머 인터럽트가 발생하지 않음

// 원인 1: HAL_TIM_Base_Start_IT() 미호출 (Start() 만 호출)
// 원인 2: NVIC에서 TIM IRQ 미활성화

// 진단:
printf("DIER: 0x%08lX\r\n", TIM2->DIER);  // UIE 비트(bit 0) 확인
printf("CR1 : 0x%08lX\r\n", TIM2->CR1);   // CEN 비트(bit 0) 확인

// NVIC 활성화 확인:
uint32_t enabled = NVIC_GetEnableIRQ(TIM2_IRQn);
printf("TIM2 NVIC: %lu\r\n", enabled);    // 1이면 활성화

문제 2: 타이머 주기가 예상과 다름

// 원인: PSC, ARR 설정값 오류 또는 APB 클럭 주파수 착각

// 진단: 실제 클럭 주파수 확인
RCC_ClkInitTypeDef clk;
uint32_t latency;
HAL_RCC_GetClockConfig(&clk, &latency);
printf("APB1 CLK: %lu Hz\r\n", HAL_RCC_GetPCLK1Freq());
printf("APB2 CLK: %lu Hz\r\n", HAL_RCC_GetPCLK2Freq());

// TIM2 ~ TIM7 (APB1), TIM1, TIM8~TIM11 (APB2) 분류 확인
// APB1 분주비 != 1이면 타이머 클럭 = APB1 클럭 * 2 (HAL 자동 처리)

문제 3: ISR 내 HAL_Delay() 사용으로 시스템 멈춤

// 원인: TIM ISR 우선순위 < SysTick 우선순위일 경우
// TIM ISR이 SysTick을 선점 → SysTick 카운트 불가 → HAL_Delay() 무한 대기

// 해결: ISR 내 HAL_Delay() 제거, 비블로킹 방식으로 대체
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    // 잘못된 예:
    // HAL_Delay(10);

    // 올바른 예: 플래그만 설정
    task_flag = 1;
}

문제 4: 디바운스 타이머 중 EXTI 재발생으로 이중 처리

// 원인: 디바운스 카운터 진행 중에 EXTI가 다시 발생해 카운터 재설정
// 이 동작은 의도한 것 (바운싱 재시작)이므로 정상

// 실제 문제: 디바운스 완료 후 메인 루프에서 처리하기 전에
// 다시 EXTI 발생 → event_flag 이중 발생

// 해결: 디바운스 진행 중에는 EXTI 무시
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
    if (GPIO_Pin == GPIO_PIN_13 && !debounce_run)  // 진행 중이면 무시
    {
        debounce_cnt = DEBOUNCE_TICKS;
        debounce_run = 1;
    }
}

10. 학습 정리

오늘 배운 내용

타이머 인터럽트 구조

  • 카운터(CNT)가 ARR 값에 도달하면 업데이트 이벤트(UEV) 발생 후 UIF 플래그 Set
  • TIMx->SR 플래그는 0을 기록하여 클리어 (EXTI PR과 반대)
  • PSC와 ARR로 정확한 인터럽트 주기 설정 가능
  • ISR 내에서는 플래그 설정 등 최소한의 처리만 수행

NVIC 우선순위

  • 숫자가 낮을수록 높은 우선순위 (더 먼저 선점 가능)
  • 선점 우선순위가 같으면 서브 우선순위로 대기 순서 결정
  • SysTick 우선순위보다 높은 ISR 내에서 HAL_Delay() 사용 금지

타이머 + EXTI 연동

  • EXTI: 비정기 이벤트 감지 → 플래그 또는 카운터 설정
  • TIM ISR: 정기적으로 카운터 감소 및 만료 처리
  • 메인 루프: 이벤트 플래그 소비 및 실제 처리 수행
  • 입력 캡처(Input Capture)를 통해 신호 주기/폭을 하드웨어 수준에서 정밀 측정 가능

핵심 개념 요약

1. 타이머 인터럽트 주기 계산

인터럽트 주기 = (PSC + 1) * (ARR + 1) / APB 클럭

1ms 주기 예시 (84MHz APB1):
PSC = 83, ARR = 999
→ 84 * 1000 / 84,000,000 = 0.001s = 1ms

2. 플래그 클리어 방식 비교

EXTI->PR  : 해당 비트에 1 기록 → 클리어 (RC_W1)
TIMx->SR  : 해당 비트에 0 기록 → 클리어 (RC_W0)
HAL 사용 시 두 방식 모두 자동 처리

3. 타이머 디바운싱 흐름

// EXTI 콜백: 카운터 설정
debounce_cnt = DEBOUNCE_TICKS;
debounce_run = 1;

// TIM ISR: 카운터 감소 및 만료 처리
if (debounce_run && --debounce_cnt == 0)
{
    debounce_run = 0;
    if (GPIO 상태 확인) button_event = 1;
}

// 메인 루프: 이벤트 소비
if (button_event) { button_event = 0; /* 처리 */ }

4. ISR 설계 원칙

ISR 내 처리 시간 << 인터럽트 주기
ISR 내 블로킹 함수 호출 금지 (HAL_Delay, printf 등)
공유 변수는 volatile 선언 필수
ISR ↔ 메인 루프 간 데이터 교환은 플래그 또는 링 버퍼 사용

타이머 인터럽트 설정 참고표

타이머버스IRQ 핸들러비트 폭주요 용도
TIM1APB2TIM1_UP_TIM10_IRQHandler16비트고급 제어, 모터
TIM2APB1TIM2_IRQHandler32비트범용, 긴 타임베이스
TIM3APB1TIM3_IRQHandler16비트범용
TIM4APB1TIM4_IRQHandler16비트범용
TIM5APB1TIM5_IRQHandler32비트범용, 긴 타임베이스
TIM6APB1TIM6_DAC_IRQHandler16비트기본, DAC 트리거
TIM7APB1TIM7_IRQHandler16비트기본
TIM8APB2TIM8_UP_TIM13_IRQHandler16비트고급 제어
profile
당신의 코딩 메이트

0개의 댓글