ISR(Interrupt Service Routine)은 실행 시간이 짧을수록 시스템 전체의 응답성과 안정성이 높아집니다. ISR 내부에서 처리 시간이 길어지면 다른 인터럽트의 지연, 타이머 주기 누적 오류, 통신 데이터 손실 등 연쇄적인 문제가 발생합니다. 이번 강의에서는 ISR 처리 시간을 실제로 측정하는 방법과, 측정 결과를 바탕으로 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(), 타임아웃 기반 로직 오동작
허용 기준 계산 방법
기본 원칙:
ISR 처리 시간 ≤ 인터럽트 주기의 10 ~ 20%
예시:
TIM2 인터럽트 주기: 1ms = 1,000us
허용 ISR 처리 시간: 100 ~ 200us 이하
84MHz 클럭 기준:
1us = 84 사이클
100us = 8,400 사이클
→ ISR 내 명령어 총 사이클 수를 8,400 이내로 유지
통신 ISR (UART, SPI) 기준:
데이터 전송 주기 < ISR 처리 시간이 되면 버퍼 오버플로우 발생
→ 단순 데이터 수신 ISR은 수십 사이클 이내가 이상적
원리 및 구성
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
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 측정 용도에서는 실질적으로 문제 없음
입력 캡처 측정 구조
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);
}
}
}
측정 방법 | 장점 | 단점 | 권장 사용처
--------------------|-------------------------------|-----------------------------|-----------------
GPIO + 오실로스코프 | 파형 직관적, 실시간 관찰 용이 | 장비 필요, 핀 1개 소비 | 초기 검증, 파형 분석
DWT CYCCNT | 장비 불필요, printf 출력 가능 | 디버그 전용, 오버헤드 미미 | 개발 중 빠른 측정
TIM 입력 캡처 | 하드웨어 정밀도, 소프트웨어 활용| 타이머 1개 + GPIO 2개 소비 | 정밀 측정 필요 시
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"); // 메인 루프에서 안전
}
}
}
}
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비트 이하 변수 단일 읽기/쓰기는 원자적
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, 구조체 멤버 복수 접근: 보호 필요
링 버퍼 구조
링 버퍼 (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);
}
}
}
컴파일러 최적화 활용
// 조건부 컴파일을 통한 디버그/릴리즈 분리
#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;
}
}
비트필드 이벤트 플래그
// 여러 이벤트를 하나의 변수로 관리 (메모리 절약 + 원자적 클리어)
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;
// 주기 태스크 실행
}
}
}
태스크 큐를 이용한 지연 처리
// 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);
}
}
}
재진입 가능 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;
}
전체 프로파일링 구현
// 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);
}
}
최적화 전: 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++;
}
}
}
증상별 원인 분석
증상 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));
}
문제 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();
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 시퀀스 보호 필요 시
| 항목 | 확인 사항 | 상태 |
|---|---|---|
| volatile 선언 | ISR 공유 변수 모두 volatile 선언 여부 | |
| 블로킹 함수 제거 | ISR 내 HAL_Delay, printf 부재 여부 | |
| 플래그 위임 | 실제 처리가 메인 루프에서 수행되는지 여부 | |
| 처리 시간 측정 | DWT 또는 GPIO로 ISR 처리 시간 측정 여부 | |
| 처리 시간 허용 범위 | ISR 처리 시간이 주기의 20% 이하인지 여부 | |
| Critical Section | 64비트/구조체 공유 접근 보호 여부 | |
| 링 버퍼 | 데이터 스트림 처리 시 링 버퍼 사용 여부 | |
| 우선순위 검토 | SysTick 우선순위와 ISR 우선순위 관계 확인 |