STM32 #14

홍태준·2026년 2월 26일

STM32

목록 보기
14/15
post-thumbnail

Week 3 Day 4: 인터럽트 내 처리 시간 측정 및 최적화

학습 목표

  • ISR 실행 시간이 시스템 안정성에 미치는 영향을 이해한다
  • GPIO 토글, DWT 사이클 카운터, 타이머 캡처를 활용한 ISR 처리 시간 측정 방법을 익힌다
  • ISR 내 처리 시간 초과가 발생하는 원인과 증상을 진단할 수 있다
  • ISR 최적화를 위한 코드 구조 패턴(플래그 위임, 링 버퍼, 지연 처리)을 이해하고 적용한다
  • volatile, memory barrier, critical section의 역할과 올바른 사용법을 이해한다

ISR(Interrupt Service Routine)은 실행 시간이 짧을수록 시스템 전체의 응답성과 안정성이 높아집니다. ISR 내부에서 처리 시간이 길어지면 다른 인터럽트의 지연, 타이머 주기 누적 오류, 통신 데이터 손실 등 연쇄적인 문제가 발생합니다. 이번 강의에서는 ISR 처리 시간을 실제로 측정하는 방법과, 측정 결과를 바탕으로 ISR을 최소화하는 구조적 최적화 기법을 다룹니다.


1. ISR 처리 시간이 중요한 이유

1.1 ISR 지연이 시스템에 미치는 영향

인터럽트 지연 연쇄 구조

정상 동작:

TIM ISR (1ms 주기)
│←── 1ms ──→│←── 1ms ──→│←── 1ms ──→│
  ISR 실행     ISR 실행     ISR 실행
  (짧음)       (짧음)       (짧음)

문제 발생 (ISR 처리 시간 > 인터럽트 주기):

TIM ISR (1ms 주기)
│←──────── 1ms ────────→│
  ISR 실행 (2ms 소요)
                          ↑ 다음 UIF 이미 Set 됨
                          → 이전 ISR 종료 즉시 재진입 또는 누락

ISR 처리 시간 초과 증상

1. 타이머 인터럽트 누적
   - 1ms 주기 ISR이 2ms 실행 → 다음 UIF가 대기 → 재진입 반복
   - 결과: 시스템 전체가 ISR 실행에만 소비되는 인터럽트 폭주

2. 고우선순위 인터럽트에 의한 저우선순위 ISR 지연
   - 고우선순위 ISR이 길면 저우선순위 인터럽트가 장시간 대기
   - 결과: 실시간성 손실, 타이밍 의존 통신 프로토콜 오류

3. 메인 루프 실행 기회 감소
   - ISR이 CPU를 점유하는 시간이 늘수록 메인 루프 실행 빈도 감소
   - 결과: 상태 갱신, 표시 출력 등 메인 루프 의존 기능 지연

4. SysTick 왜곡
   - ISR이 SysTick보다 높은 우선순위로 SysTick을 선점하면
     HAL_GetTick() 카운트가 늦어짐
   - 결과: HAL_Delay(), 타임아웃 기반 로직 오동작

1.2 ISR 실행 시간의 허용 기준

허용 기준 계산 방법

기본 원칙:
ISR 처리 시간 ≤ 인터럽트 주기의 10 ~ 20%

예시:
TIM2 인터럽트 주기: 1ms = 1,000us
허용 ISR 처리 시간: 100 ~ 200us 이하

84MHz 클럭 기준:
1us = 84 사이클
100us = 8,400 사이클
→ ISR 내 명령어 총 사이클 수를 8,400 이내로 유지

통신 ISR (UART, SPI) 기준:
데이터 전송 주기 < ISR 처리 시간이 되면 버퍼 오버플로우 발생
→ 단순 데이터 수신 ISR은 수십 사이클 이내가 이상적

2. ISR 처리 시간 측정 방법

2.1 방법 1: GPIO 토글을 이용한 오실로스코프 측정

원리 및 구성

ISR 진입 시 GPIO HIGH → ISR 종료 시 GPIO LOW
오실로스코프로 HIGH 구간 펄스 폭 측정 = ISR 실행 시간

장점: 실시간 파형 관찰 가능, 최대/평균 측정 용이
단점: 오실로스코프 필요, 측정용 GPIO 핀 1개 소비

GPIO 토글 측정 구현

// 측정용 GPIO 초기화 (예: PA0, Push-Pull 출력)
// CubeMX 또는 직접 초기화:
void debug_gpio_init(void)
{
    __HAL_RCC_GPIOA_CLK_ENABLE();

    GPIO_InitTypeDef gpio = {0};
    gpio.Pin   = GPIO_PIN_0;
    gpio.Mode  = GPIO_MODE_OUTPUT_PP;
    gpio.Speed = GPIO_SPEED_FREQ_VERY_HIGH;  // 측정 정확도를 위해 최고 속도
    gpio.Pull  = GPIO_NOPULL;
    HAL_GPIO_Init(GPIOA, &gpio);

    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET);
}

// ISR 내 측정 삽입
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM2)
    {
        GPIOA->BSRR = GPIO_PIN_0;        // PA0 HIGH (ISR 시작)

        // --- 측정 대상 코드 ---
        debounce_counter++;
        // ...
        // ---------------------

        GPIOA->BSRR = GPIO_PIN_0 << 16;  // PA0 LOW (ISR 종료)
        // BSRR 상위 16비트에 쓰면 해당 핀 Reset (HAL_GPIO_WritePin보다 빠름)
    }
}

BSRR 레지스터 직접 접근 이유

HAL_GPIO_WritePin() 내부:
함수 호출 오버헤드 + 분기 + 레지스터 설정 = 수십 사이클 소요

GPIOA->BSRR 직접 접근:
단일 스토어 명령 = 1 ~ 2 사이클
→ 측정 오차 최소화를 위해 직접 접근 사용

BSRR(Bit Set/Reset Register) 구조:
상위 16비트: 해당 비트 위치의 핀을 Reset (LOW)
하위 16비트: 해당 비트 위치의 핀을 Set (HIGH)

예:
GPIOA->BSRR = (1 << 0);          // PA0 Set (HIGH)
GPIOA->BSRR = (1 << 0) << 16;    // PA0 Reset (LOW)
GPIOA->BSRR = (1 << 16);         // 동일하게 PA0 Reset

2.2 방법 2: DWT 사이클 카운터 (소프트웨어 측정)

DWT(Data Watchpoint and Trace) 개요

DWT: ARM Cortex-M 코어 내장 디버그 유닛
CYCCNT 레지스터: CPU 클럭 사이클을 카운트하는 32비트 자유 주행 카운터
활성화 후 읽기만 하면 현재까지 경과한 사이클 수를 반환

장점: 오실로스코프 불필요, printf로 결과 출력 가능
단점: 디버그 기능이므로 양산 코드에서는 비활성화 권장
     32비트 오버플로우 주의 (84MHz 기준 약 51초마다 오버플로우)

DWT CYCCNT 활성화 및 측정 구현

// DWT 활성화 함수
void DWT_Init(void)
{
    // CoreDebug: 디버그 컨트롤 블록 활성화
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;

    // CYCCNT 초기화 및 시작
    DWT->CYCCNT = 0;
    DWT->CTRL  |= DWT_CTRL_CYCCNTENA_Msk;
}

// 사이클 읽기 매크로
#define DWT_GET_CYCLE()   (DWT->CYCCNT)

// us 단위 변환 (84MHz 기준)
#define CYCLE_TO_US(c)    ((c) / 84U)

// ISR 내 측정 적용
volatile uint32_t isr_cycle_max = 0;
volatile uint32_t isr_cycle_last = 0;

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM2)
    {
        uint32_t t_start = DWT_GET_CYCLE();

        // --- 측정 대상 코드 ---
        // (실제 처리 로직)
        // ---------------------

        uint32_t t_end   = DWT_GET_CYCLE();
        uint32_t elapsed = t_end - t_start;  // 오버플로우에도 올바르게 계산됨

        isr_cycle_last = elapsed;
        if (elapsed > isr_cycle_max)
        {
            isr_cycle_max = elapsed;  // 최악의 경우(Worst Case) 기록
        }
    }
}

// 메인 루프에서 결과 출력
int main(void)
{
    HAL_Init();
    SystemClock_Config();
    DWT_Init();
    MX_GPIO_Init();
    MX_TIM2_Init();
    HAL_TIM_Base_Start_IT(&htim2);

    while (1)
    {
        // 1초마다 측정 결과 출력
        HAL_Delay(1000);
        printf("ISR last: %lu us, max: %lu us\r\n",
               CYCLE_TO_US(isr_cycle_last),
               CYCLE_TO_US(isr_cycle_max));
    }
}

DWT 오버플로우 처리

// 오버플로우가 발생해도 부호 없는 뺄셈은 올바른 경과 시간을 반환
// 예:
// t_start = 0xFFFFFFF0 (오버플로우 직전)
// t_end   = 0x00000010 (오버플로우 후)
// elapsed = 0x00000010 - 0xFFFFFFF0 = 0x00000020 = 32 사이클 (정확)

// 단, 측정 구간이 32비트 최대값(84MHz * 51초)을 초과하면 오류 발생
// ISR 측정 용도에서는 실질적으로 문제 없음

2.3 방법 3: 타이머 입력 캡처를 이용한 측정

입력 캡처 측정 구조

ISR 진입 → PA0 HIGH → TIM3 입력 캡처 (상승 엣지 CNT 저장)
ISR 종료 → PA0 LOW  → TIM3 입력 캡처 (하강 엣지 CNT 저장)
두 캡처 값의 차이 = ISR 실행 사이클 수

GPIO 토글 방법과 유사하지만, 오실로스코프 없이 소프트웨어로
캡처값을 직접 읽어 처리 가능 (DWT보다 하드웨어 정밀도 높음)

TIM3 입력 캡처 기반 측정 예시

// TIM3 채널 1: 상승 엣지 캡처 (ISR 진입)
// TIM3 채널 2: 하강 엣지 캡처 (ISR 종료)
// PA6 → TIM3_CH1, PA7 → TIM3_CH2 (AF 설정 필요)

uint32_t capture_rise  = 0;
uint32_t capture_fall  = 0;
uint32_t isr_duration  = 0;

void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM3)
    {
        if (htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1)
        {
            capture_rise = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);
        }
        else if (htim->Channel == HAL_TIM_ACTIVE_CHANNEL_2)
        {
            capture_fall = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_2);
            isr_duration = (capture_fall >= capture_rise)
                ? (capture_fall - capture_rise)
                : (0xFFFF - capture_rise + capture_fall + 1);
        }
    }
}

2.4 세 가지 측정 방법 비교

측정 방법           | 장점                          | 단점                        | 권장 사용처
--------------------|-------------------------------|-----------------------------|-----------------
GPIO + 오실로스코프 | 파형 직관적, 실시간 관찰 용이  | 장비 필요, 핀 1개 소비       | 초기 검증, 파형 분석
DWT CYCCNT          | 장비 불필요, printf 출력 가능  | 디버그 전용, 오버헤드 미미   | 개발 중 빠른 측정
TIM 입력 캡처       | 하드웨어 정밀도, 소프트웨어 활용| 타이머 1개 + GPIO 2개 소비  | 정밀 측정 필요 시

3. ISR 처리 시간 최적화 기법

3.1 기본 원칙: ISR 최소화 (Defer to Main Loop)

ISR에서 수행할 것과 수행하지 말 것

ISR 내 수행해야 하는 것:
1. 인터럽트 플래그 클리어 (HAL 사용 시 자동)
2. 하드웨어 레지스터 즉시 읽기 (UART 수신 데이터, ADC 결과 등)
3. 공유 변수 플래그 설정
4. 카운터 증감
5. 링 버퍼에 데이터 적재 (헤더/테일 포인터 이동)

ISR 내 절대 금지:
1. HAL_Delay() : SysTick 의존, 블로킹
2. printf()    : UART 전송 대기, 수십~수백 us 소요
3. malloc/free : 비결정적 실행 시간, 힙 단편화
4. 무거운 연산 : FFT, 행렬 연산, CRC 소프트웨어 계산
5. 긴 루프     : 반복 횟수가 데이터 의존적인 루프
6. HAL 고수준 함수: HAL_I2C_Master_Transmit 등 블로킹 통신

ISR 최소화 구조 패턴

// 나쁜 예: ISR 내 직접 처리
void USART2_IRQHandler(void)
{
    if (USART2->SR & USART_SR_RXNE)
    {
        uint8_t data = USART2->DR;

        // 수신 데이터 즉시 파싱 (ISR 내 긴 처리)
        if (data == 'A') {
            HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
            printf("Received A\r\n");   // 매우 위험
        }
        else if (data == 'B') {
            // 복잡한 상태 머신 처리...
        }
    }
}

// 좋은 예: ISR은 데이터 수집만, 처리는 메인 루프로 위임
#define RX_BUF_SIZE  64U

volatile uint8_t  rx_buf[RX_BUF_SIZE];
volatile uint8_t  rx_head = 0;
volatile uint8_t  rx_tail = 0;

void USART2_IRQHandler(void)
{
    if (USART2->SR & USART_SR_RXNE)
    {
        uint8_t data = USART2->DR;
        uint8_t next = (rx_head + 1) % RX_BUF_SIZE;

        if (next != rx_tail)  // 버퍼 풀이 아닌 경우만 적재
        {
            rx_buf[rx_head] = data;
            rx_head         = next;
        }
        // 버퍼 풀 시 데이터 손실 (또는 오버런 플래그 설정)
    }
}

// 메인 루프에서 처리
int main(void)
{
    // ...초기화...
    while (1)
    {
        while (rx_tail != rx_head)
        {
            uint8_t data = rx_buf[rx_tail];
            rx_tail = (rx_tail + 1) % RX_BUF_SIZE;

            // 여기서 데이터 파싱 및 처리 (시간 제약 없음)
            if (data == 'A') {
                HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
                printf("Received A\r\n");  // 메인 루프에서 안전
            }
        }
    }
}

3.2 volatile 키워드와 메모리 접근 순서

volatile이 필요한 이유

컴파일러 최적화 문제:
ISR과 메인 루프가 공유 변수를 사용할 때, 컴파일러는
변수가 ISR에 의해 변경될 수 있다는 사실을 모르고
변수를 레지스터에 캐시하거나 접근 순서를 변경할 수 있음

volatile 없이 컴파일러 최적화 결과 (예시):
while (flag == 0)  →  최적화 후: if (flag == 0) while(1) {}
{ /* 대기 */ }         (초기값 한 번만 읽고 루프 최적화)

volatile 선언 효과:
매 접근마다 실제 메모리에서 읽고 씀
컴파일러가 접근을 제거하거나 순서를 변경하지 않음

volatile 사용 규칙

// ISR과 메인 루프가 공유하는 변수는 반드시 volatile 선언
volatile uint8_t  button_event   = 0;
volatile uint16_t debounce_cnt   = 0;
volatile uint32_t isr_cycle_max  = 0;

// volatile 배열 (링 버퍼 등)
volatile uint8_t  rx_buf[64];
volatile uint8_t  rx_head = 0;
volatile uint8_t  rx_tail = 0;

// 구조체 전체를 volatile로 선언하는 경우
typedef struct {
    uint16_t counter;
    uint8_t  active;
    uint8_t  event;
} Button_t;

volatile Button_t btn;  // 구조체 전체 접근이 volatile

// 주의: volatile은 원자성(atomicity)을 보장하지 않는다
// 32비트 변수를 8비트 MCU에서 읽을 때 두 명령어로 분리될 수 있음
// STM32(32비트 ARM)에서는 32비트 이하 변수 단일 읽기/쓰기는 원자적

3.3 Critical Section (임계 구역)

Critical Section이 필요한 상황

문제 시나리오:
메인 루프에서 32비트 변수의 상위/하위 16비트를 각각 읽는 도중
ISR이 해당 변수를 수정하면 일관성이 깨질 수 있음

예시:
uint32_t counter = 0x0000FFFF;
메인 루프에서 읽기 시작: 하위 16비트 = 0xFFFF 읽음
→ 이 순간 ISR 발생, counter = 0x00010000 으로 변경
→ 상위 16비트 = 0x0001 읽음
→ 메인 루프가 읽은 값: 0x0001FFFF (실제값 0x00010000과 다름)

STM32(32비트 ARM Cortex-M)에서의 실제 적용 범위:
32비트 이하 단일 변수 읽기/쓰기: 원자적 (일반적으로 안전)
64비트 변수, 구조체 멤버 복수 접근, Read-Modify-Write: 비원자적 → 보호 필요

Critical Section 구현

// 방법 1: 인터럽트 전역 비활성화 (__disable_irq / __enable_irq)
uint32_t shared_data_copy;

// 인터럽트 비활성화 후 읽기
__disable_irq();
shared_data_copy = shared_data;  // 원자적으로 복사
__enable_irq();

// 복사한 값을 인터럽트 허용 상태에서 처리
process(shared_data_copy);

// 방법 2: PRIMASK를 이용한 상태 보존 방식 (더 안전)
uint32_t primask_state = __get_PRIMASK();  // 현재 인터럽트 상태 저장
__disable_irq();

// --- Critical Section 시작 ---
uint32_t val = shared_counter;
shared_counter = val + 1;
// --- Critical Section 종료 ---

__set_PRIMASK(primask_state);  // 이전 상태 복원 (이미 비활성화였다면 유지)

// 방법 3: HAL 매크로 사용
__HAL_LOCK(&handle);    // 단순 뮤텍스 (멀티코어 아님, 단일 스레드 보호용)
// 처리
__HAL_UNLOCK(&handle);

Critical Section 사용 지침

1. Critical Section은 최대한 짧게 유지
   - 인터럽트를 비활성화하는 시간이 길수록 실시간성 손실

2. Critical Section 내부에서 함수 호출 금지
   - 함수 내부에서 __enable_irq()를 호출하는 경우를 방지

3. 복사(copy) 후 처리 패턴 권장
   __disable_irq();
   local_copy = shared_var;  // 복사만 수행
   __enable_irq();
   process(local_copy);      // 인터럽트 허용 상태에서 처리

4. STM32 단일 변수 접근은 대부분 원자적
   - uint8_t, uint16_t, uint32_t 단순 대입: 보호 불필요
   - uint64_t, 구조체 멤버 복수 접근: 보호 필요

3.4 링 버퍼(Ring Buffer)를 이용한 ISR-메인 루프 데이터 전달

링 버퍼 구조

링 버퍼 (Circular Buffer):
고정 크기 배열을 원형으로 사용
head: 다음 데이터를 쓸 위치 (ISR이 이동)
tail: 다음 데이터를 읽을 위치 (메인 루프가 이동)

상태 판별:
head == tail → 버퍼 비어 있음 (Empty)
(head + 1) % SIZE == tail → 버퍼 꽉 참 (Full)
(SIZE - tail + head) % SIZE → 현재 데이터 개수

      tail      head
       ↓         ↓
[ ][ ][D][D][D][D][ ][ ]
  ↑                   ↑
index 0            index SIZE-1
(원형이므로 head가 끝에 도달하면 0으로 돌아감)

범용 링 버퍼 구현

#define RING_BUF_SIZE  128U  // 반드시 2의 거듭제곱 권장 (% 연산 최적화)

typedef struct
{
    uint8_t  buf[RING_BUF_SIZE];
    volatile uint8_t head;
    volatile uint8_t tail;
} RingBuf_t;

// 초기화
void ring_buf_init(RingBuf_t *rb)
{
    rb->head = 0;
    rb->tail = 0;
}

// 데이터 쓰기 (ISR에서 호출)
uint8_t ring_buf_push(RingBuf_t *rb, uint8_t data)
{
    uint8_t next = (rb->head + 1) % RING_BUF_SIZE;
    if (next == rb->tail)  return 0;  // 버퍼 풀, 쓰기 실패

    rb->buf[rb->head] = data;
    rb->head          = next;
    return 1;
}

// 데이터 읽기 (메인 루프에서 호출)
uint8_t ring_buf_pop(RingBuf_t *rb, uint8_t *data)
{
    if (rb->head == rb->tail)  return 0;  // 버퍼 비어 있음, 읽기 실패

    *data    = rb->buf[rb->tail];
    rb->tail = (rb->tail + 1) % RING_BUF_SIZE;
    return 1;
}

// 데이터 개수 확인
uint8_t ring_buf_count(RingBuf_t *rb)
{
    return (RING_BUF_SIZE + rb->head - rb->tail) % RING_BUF_SIZE;
}

// 사용 예
RingBuf_t uart_rx_buf;

void USART2_IRQHandler(void)  // ISR: 데이터 적재
{
    if (USART2->SR & USART_SR_RXNE)
    {
        ring_buf_push(&uart_rx_buf, (uint8_t)USART2->DR);
    }
}

int main(void)
{
    ring_buf_init(&uart_rx_buf);
    // ...초기화...
    while (1)
    {
        uint8_t data;
        while (ring_buf_pop(&uart_rx_buf, &data))
        {
            // 데이터 처리 (메인 루프에서 시간 제약 없이)
            process_byte(data);
        }
    }
}

3.5 ISR 내 연산 최적화

컴파일러 최적화 활용

// 조건부 컴파일을 통한 디버그/릴리즈 분리
#ifdef DEBUG
    #define ISR_DEBUG_GPIO_HIGH()   (GPIOA->BSRR = GPIO_PIN_0)
    #define ISR_DEBUG_GPIO_LOW()    (GPIOA->BSRR = GPIO_PIN_0 << 16)
#else
    #define ISR_DEBUG_GPIO_HIGH()   // 릴리즈에서 제거
    #define ISR_DEBUG_GPIO_LOW()
#endif

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM2)
    {
        ISR_DEBUG_GPIO_HIGH();
        // ...처리...
        ISR_DEBUG_GPIO_LOW();
    }
}

인라인 함수와 ISR 최적화

// __attribute__((always_inline)): 함수 호출 오버헤드 제거
static inline __attribute__((always_inline))
void debounce_tick(volatile uint16_t *counter, volatile uint8_t *active,
                   volatile uint8_t *event, GPIO_TypeDef *port, uint16_t pin)
{
    if (*active && *counter > 0)
    {
        (*counter)--;
        if (*counter == 0)
        {
            *active = 0;
            if (HAL_GPIO_ReadPin(port, pin) == GPIO_PIN_RESET)
            {
                *event = 1;
            }
        }
    }
}

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM2)
    {
        // 인라인 함수: 컴파일러가 함수 호출 없이 코드 삽입
        debounce_tick(&btn1_cnt, &btn1_active, &btn1_event, GPIOC, GPIO_PIN_13);
        debounce_tick(&btn2_cnt, &btn2_active, &btn2_event, GPIOA, GPIO_PIN_0);
    }
}

ISR 내 if-else 최소화

// 나쁜 예: 불필요한 조건 중첩
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM2)
    {
        if (debounce_active == 1)
        {
            if (debounce_counter != 0)
            {
                if (debounce_counter > 0)  // 중복 조건
                {
                    debounce_counter--;
                }
            }
        }
    }
}

// 좋은 예: 조건 간소화 및 조기 반환
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance != TIM2)  return;  // 조기 반환
    if (!debounce_active)        return;  // 조기 반환

    if (debounce_counter > 0)
    {
        debounce_counter--;
        return;
    }

    // counter == 0: 만료 처리
    debounce_active = 0;
    if (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13) == GPIO_PIN_RESET)
    {
        button_event = 1;
    }
}

4. ISR 최적화 패턴 심화

4.1 플래그 비트필드를 이용한 다중 이벤트 관리

비트필드 이벤트 플래그

// 여러 이벤트를 하나의 변수로 관리 (메모리 절약 + 원자적 클리어)
typedef union
{
    uint8_t all;
    struct
    {
        uint8_t btn1_pressed  : 1;
        uint8_t btn2_pressed  : 1;
        uint8_t uart_received : 1;
        uint8_t timer_tick    : 1;
        uint8_t reserved      : 4;
    } bits;
} EventFlags_t;

volatile EventFlags_t events = { .all = 0 };

// ISR에서 비트 설정
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
    if (GPIO_Pin == GPIO_PIN_13)  events.bits.btn1_pressed  = 1;
    if (GPIO_Pin == GPIO_PIN_0)   events.bits.btn2_pressed  = 1;
}

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM2)   events.bits.timer_tick = 1;
}

// 메인 루프에서 소비
int main(void)
{
    // ...초기화...
    while (1)
    {
        if (events.bits.btn1_pressed)
        {
            events.bits.btn1_pressed = 0;
            HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
        }
        if (events.bits.timer_tick)
        {
            events.bits.timer_tick = 0;
            // 주기 태스크 실행
        }
    }
}

4.2 지연 처리(Deferred Processing) 패턴

태스크 큐를 이용한 지연 처리

// ISR에서 실행할 작업 대신 "작업 요청"을 큐에 등록
// 메인 루프에서 큐를 소비하며 실제 처리 수행

typedef enum
{
    TASK_NONE = 0,
    TASK_BTN_PRESS,
    TASK_BTN_LONG_PRESS,
    TASK_UART_PROCESS,
    TASK_LED_TOGGLE,
} TaskID_t;

#define TASK_QUEUE_SIZE  16U

volatile TaskID_t task_queue[TASK_QUEUE_SIZE];
volatile uint8_t  task_head = 0;
volatile uint8_t  task_tail = 0;

// ISR에서 태스크 요청 등록
void task_push(TaskID_t task)
{
    uint8_t next = (task_head + 1) % TASK_QUEUE_SIZE;
    if (next != task_tail)
    {
        task_queue[task_head] = task;
        task_head             = next;
    }
}

// ISR: 플래그만 큐에 등록 (최소 처리)
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
    if (GPIO_Pin == GPIO_PIN_13)
    {
        task_push(TASK_BTN_PRESS);
    }
}

// 메인 루프: 큐에서 태스크를 꺼내 실제 처리
void task_dispatch(TaskID_t task)
{
    switch (task)
    {
        case TASK_BTN_PRESS:
            HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
            printf("Button pressed\r\n");
            break;
        case TASK_BTN_LONG_PRESS:
            printf("Long press detected\r\n");
            break;
        case TASK_LED_TOGGLE:
            HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
            break;
        default:
            break;
    }
}

int main(void)
{
    // ...초기화...
    while (1)
    {
        while (task_tail != task_head)
        {
            TaskID_t task = task_queue[task_tail];
            task_tail     = (task_tail + 1) % TASK_QUEUE_SIZE;
            task_dispatch(task);
        }
    }
}

4.3 ISR 재진입 방지

재진입 가능 ISR과 방지 방법

재진입(Re-entrancy) 문제:
- 동일한 ISR이 실행 중에 같은 인터럽트가 다시 발생하여
  ISR이 중첩 호출되는 현상
- 대부분의 임베디드 코드는 재진입을 고려하지 않으므로 오동작 발생

STM32에서 재진입 발생 조건:
- 선점 우선순위가 같은 인터럽트: 재진입 없음 (NVIC가 블로킹)
- 선점 우선순위가 더 높은 인터럽트에 의해 낮은 ISR이 중단:
  중단 후 재개되므로 재진입이 아님 (정상 선점)
- ISR 내에서 __enable_irq()를 명시적으로 호출한 경우:
  동일 ISR 재진입 가능 → 이 패턴을 피해야 함

재진입 방지 플래그 패턴

// ISR 내에서 인터럽트를 다시 활성화해야 하는 특수한 경우의 방어 코드
static volatile uint8_t isr_running = 0;

void TIM2_IRQHandler(void)
{
    if (isr_running)  return;  // 재진입 방지
    isr_running = 1;

    HAL_TIM_IRQHandler(&htim2);

    isr_running = 0;
}

5. 실전 예제

5.1 DWT 기반 ISR 처리 시간 프로파일러

전체 프로파일링 구현

// dwt_profiler.h

#ifndef DWT_PROFILER_H
#define DWT_PROFILER_H

#include "stm32f4xx_hal.h"

typedef struct
{
    uint32_t last_us;
    uint32_t max_us;
    uint32_t min_us;
    uint32_t count;
    uint32_t sum_us;
} ProfileResult_t;

void     profiler_init(void);
uint32_t profiler_start(void);
void     profiler_stop(uint32_t t_start, ProfileResult_t *result);
void     profiler_print(const char *name, const ProfileResult_t *result);

#endif
// dwt_profiler.c

#include "dwt_profiler.h"
#include <stdio.h>

#define CPU_FREQ_MHZ  84U

void profiler_init(void)
{
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
    DWT->CYCCNT       = 0;
    DWT->CTRL        |= DWT_CTRL_CYCCNTENA_Msk;
}

uint32_t profiler_start(void)
{
    return DWT->CYCCNT;
}

void profiler_stop(uint32_t t_start, ProfileResult_t *result)
{
    uint32_t elapsed_cycle = DWT->CYCCNT - t_start;
    uint32_t elapsed_us    = elapsed_cycle / CPU_FREQ_MHZ;

    result->last_us = elapsed_us;
    result->count++;
    result->sum_us += elapsed_us;

    if (elapsed_us > result->max_us) result->max_us = elapsed_us;
    if (result->count == 1 || elapsed_us < result->min_us) result->min_us = elapsed_us;
}

void profiler_print(const char *name, const ProfileResult_t *result)
{
    printf("[%s] last=%luus max=%luus min=%luus avg=%luus count=%lu\r\n",
           name,
           result->last_us,
           result->max_us,
           result->min_us,
           (result->count > 0) ? (result->sum_us / result->count) : 0,
           result->count);
}

프로파일러 적용 예시

ProfileResult_t tim2_profile = {0};

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM2)
    {
        uint32_t t = profiler_start();

        // --- 처리 코드 ---
        debounce_counter++;
        // -----------------

        profiler_stop(t, &tim2_profile);
    }
}

int main(void)
{
    profiler_init();
    // ...초기화...
    HAL_TIM_Base_Start_IT(&htim2);

    while (1)
    {
        HAL_Delay(5000);
        profiler_print("TIM2_ISR", &tim2_profile);
    }
}

5.2 최적화 전후 비교 예제

최적화 전: ISR 내 직접 처리

// ISR 처리 시간: 약 50 ~ 200us (printf 포함)

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM2)
    {
        // 디바운스 처리
        if (debounce_active)
        {
            debounce_counter--;
            if (debounce_counter == 0)
            {
                debounce_active = 0;
                if (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13) == GPIO_PIN_RESET)
                {
                    HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);  // GPIO 처리
                    printf("Button pressed!\r\n");           // UART 전송 (위험)
                    press_count++;
                }
            }
        }
    }
}

최적화 후: ISR 최소화, 메인 루프로 위임

// ISR 처리 시간: 약 1 ~ 3us

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance != TIM2)  return;
    if (!debounce_active)        return;
    if (--debounce_counter > 0)  return;

    debounce_active = 0;
    if (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13) == GPIO_PIN_RESET)
    {
        button_event = 1;  // 플래그만 설정
    }
}

// 메인 루프에서 실제 처리
int main(void)
{
    // ...초기화...
    while (1)
    {
        if (button_event)
        {
            button_event = 0;
            HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);  // 안전
            printf("Button pressed!\r\n");           // 안전
            press_count++;
        }
    }
}

6. 디버깅

6.1 ISR 처리 시간 초과 진단

증상별 원인 분석

증상 1: 시스템이 주기적으로 멈추거나 응답이 없어짐
원인: ISR 내 블로킹 함수 (HAL_Delay, printf 등) 사용
진단: ISR 내 함수 호출 목록 검토, GPIO 토글로 ISR 진행 여부 확인

증상 2: 타이머 인터럽트 주기가 점점 늘어남 (측정값이 기대값보다 큼)
원인: ISR 처리 시간 > 인터럽트 주기 → 인터럽트 누적
진단: DWT로 ISR 실행 시간 측정, ISR 처리 시간과 인터럽트 주기 비교

증상 3: UART 데이터 수신 오류 (글자 누락, 깨짐)
원인: UART RX ISR 처리 시간이 너무 길어 다음 바이트 수신 전에 DR 레지스터 미처리
진단: Overrun Error(ORE) 플래그 확인 (USART->SR & USART_SR_ORE)

증상 4: HAL_GetTick() 값이 실제 시간보다 느리게 증가
원인: ISR이 SysTick을 선점하여 SysTick 카운트 누락
진단: SysTick 우선순위 vs 해당 ISR 우선순위 비교

진단 코드 예시

// UART 오버런 에러 감지
void check_uart_errors(void)
{
    if (USART2->SR & USART_SR_ORE)
    {
        // 오버런: RX ISR 처리 속도 부족
        printf("UART ORE detected!\r\n");
        // 에러 클리어: SR 읽고 DR 읽기
        volatile uint32_t tmp = USART2->SR;
        tmp = USART2->DR;
        (void)tmp;
    }

    if (USART2->SR & USART_SR_FE)
    {
        printf("UART FE (Framing Error) detected!\r\n");
    }
}

// NVIC 우선순위 확인 출력
void print_nvic_priorities(void)
{
    printf("TIM2   priority: %lu\r\n", NVIC_GetPriority(TIM2_IRQn));
    printf("EXTI13 priority: %lu\r\n", NVIC_GetPriority(EXTI15_10_IRQn));
    printf("USART2 priority: %lu\r\n", NVIC_GetPriority(USART2_IRQn));
    printf("SysTick priority: %lu\r\n", NVIC_GetPriority(SysTick_IRQn));
}

6.2 일반적인 실수와 해결

문제 1: volatile 미선언으로 컴파일러 최적화에 의한 변수 무시

// 잘못된 예:
uint8_t button_event = 0;  // volatile 없음

int main(void)
{
    while (!button_event)  // 컴파일러가 상수 조건으로 최적화할 수 있음
    {
        // ISR에서 button_event = 1 로 변경해도 루프 탈출 안 될 수 있음
    }
}

// 올바른 예:
volatile uint8_t button_event = 0;

int main(void)
{
    while (!button_event)  // 매 반복마다 실제 메모리에서 읽음
    {
    }
}

문제 2: ISR 내 printf로 인한 시스템 정지

// 원인: printf → UART 전송 → 완료 대기 (수십~수백 us)
//       타이머 주기 < printf 전송 시간 → 인터럽트 폭주

// 해결: ISR에서 플래그 설정, 메인 루프에서 printf 호출
// 또는: 디버그 출력 전용 링 버퍼를 두고 ISR은 버퍼에만 적재

// 진단:
// ISR 진입 GPIO HIGH 후 printf 호출 → 오실로스코프로
// GPIO HIGH 구간이 수십 us 이상이면 printf가 원인

문제 3: ISR과 메인 루프 간 공유 변수의 비일관성

// 원인: 64비트 변수 또는 구조체를 atomic 없이 공유

// 예시 문제:
typedef struct {
    uint32_t high;
    uint32_t low;
} Time64_t;

volatile Time64_t timestamp;  // ISR에서 갱신

// 메인 루프에서 읽기:
uint32_t h = timestamp.high;  // ← 이 시점에 ISR이 개입하여 전체 변경
uint32_t l = timestamp.low;   // ← 위에서 읽은 high와 쌍이 맞지 않음

// 해결: Critical Section으로 보호
__disable_irq();
uint32_t h = timestamp.high;
uint32_t l = timestamp.low;
__enable_irq();

7. 학습 정리

오늘 배운 내용

ISR 처리 시간 측정

  • GPIO 토글 + 오실로스코프: 파형 직관적, 실시간 관찰
  • DWT CYCCNT: 장비 불필요, 코드에서 us 단위 측정
  • 세 가지 방법 중 개발 단계에서는 DWT가 가장 빠르게 적용 가능

ISR 최적화 원칙

  • ISR 내부에서는 플래그 설정, 카운터 증감, 버퍼 적재만 수행
  • 모든 처리는 메인 루프로 위임 (Defer to Main Loop)
  • HAL_Delay(), printf() 등 블로킹 함수는 ISR 내 절대 금지

공유 변수 안전 처리

  • ISR과 메인 루프가 공유하는 모든 변수에 volatile 선언 필수
  • 복수 멤버 또는 64비트 변수는 Critical Section으로 보호
  • Critical Section은 가능한 짧게 유지

링 버퍼와 지연 처리

  • ISR → 메인 루프 데이터 전달에 링 버퍼 사용
  • 태스크 큐 패턴으로 ISR 작업을 메인 루프로 분산

핵심 개념 요약

1. ISR 허용 처리 시간 기준

ISR 처리 시간 ≤ 인터럽트 주기의 10 ~ 20%
1ms 주기 → 최대 100 ~ 200us
84MHz 기준: 1us = 84 사이클

2. DWT CYCCNT 사용 패턴

CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CTRL        |= DWT_CTRL_CYCCNTENA_Msk;

uint32_t t = DWT->CYCCNT;
// 측정 대상 코드
uint32_t elapsed_us = (DWT->CYCCNT - t) / 84U;

3. ISR 최적화 구조

// ISR: 최소한만 처리
void ISR(void) {
    flag = 1;       // 플래그 설정
    buf[head] = d;  // 버퍼 적재
    head++;         // 포인터 이동
}

// 메인 루프: 실제 처리
while (1) {
    if (flag) { flag = 0; /* 처리 */ }
}

4. volatile과 Critical Section 적용 기준

volatile 적용: ISR과 메인 루프가 공유하는 모든 변수
Critical Section 적용:
  - 64비트 변수
  - 구조체 복수 멤버를 동시에 읽어야 하는 경우
  - Read-Modify-Write 시퀀스 보호 필요 시

ISR 최적화 체크리스트

항목확인 사항상태
volatile 선언ISR 공유 변수 모두 volatile 선언 여부
블로킹 함수 제거ISR 내 HAL_Delay, printf 부재 여부
플래그 위임실제 처리가 메인 루프에서 수행되는지 여부
처리 시간 측정DWT 또는 GPIO로 ISR 처리 시간 측정 여부
처리 시간 허용 범위ISR 처리 시간이 주기의 20% 이하인지 여부
Critical Section64비트/구조체 공유 접근 보호 여부
링 버퍼데이터 스트림 처리 시 링 버퍼 사용 여부
우선순위 검토SysTick 우선순위와 ISR 우선순위 관계 확인
profile
당신의 코딩 메이트

0개의 댓글