보충 2) 인터럽트 완전 정복 – 마이크로컨트롤러의 멀티태스킹

이번 글… 정말 열심히 쉽게 알아볼 수 있는 내용으로 적었습니다. 최선을 다했어요. ㅠㅠ

안녕하세요! 6편에서 갑자기 등장한 인터럽트(Interrupt)ISR(Interrupt Service Routine) 때문에 당황하셨을 수도 있습니다. 인터럽트는 마이크로컨트롤러 프로그래밍에서 가장 중요하면서도 어려운 개념 중 하나입니다. 이번에는 인터럽트를 완전히 정복해보겠습니다!

이 글의 목표

  • 인터럽트가 정확히 무엇인지 일상 생활 비유로 이해
  • 왜 인터럽트가 필요한지 _delay_ms()와 비교해서 파악
  • ISR 작성 규칙과 주의사항 완전 숙달
  • volatile 키워드의 필요성과 사용법 이해
  • 다양한 인터럽트 종류와 우선순위 파악

인터럽트란 무엇인가? – 일상 생활 비유

인터럽트 = 긴급 전화

상황: 당신이 집에서 책을 읽고 있는데 갑자기 전화가 울렸습니다.

평상시 상황:
책 읽기(main 함수) → 계속 진행 → 끝까지 읽기

인터럽트 상황:
책 읽기 → [전화 울림!] → 책갈피 끼우기 → 전화 받기(ISR) → 통화 끝 → 책갈피 찾기 → 책 읽기 재개

핵심 포인트:

  • 책 읽기 = main() 함수의 일반적인 작업
  • 전화 울림 = 인터럽트 발생 (하드웨어 이벤트)
  • 책갈피 = CPU가 현재 상태를 저장
  • 전화 받기 = ISR(Interrupt Service Routine) 실행
  • 책 읽기 재개 = main() 함수로 돌아감

왜 이런 복잡한 방식을 사용할까?

인터럽트 없는 세상 (Polling 방식)

c

while(1) {
    // 중요한 작업들
    update_display();
    process_user_input();
    save_data();
    
    // 전화가 왔는지 계속 확인 (비효율적!)
    if (phone_ringing()) {
        answer_phone();
    }
    
    // 문자가 왔는지도 확인
    if (message_arrived()) {
        read_message();
    }
    
    // 알람 시간인지도 확인
    if (alarm_time()) {
        ring_alarm();
    }
    
    // ... 확인할 것이 너무 많다!
}

문제점:

  • CPU가 계속 “확인”만 하느라 바쁨
  • 긴급한 일도 다음 확인 순서까지 기다려야 함
  • 전력 낭비가 심함

인터럽트가 있는 세상

c

// 평상시에는 중요한 일만 처리
void main() {
    while(1) {
        update_display();      // 화면 업데이트
        process_user_input();  // 사용자 입력 처리
        save_data();          // 데이터 저장
        
        // 긴급한 일은 인터럽트가 알아서 처리!
    }
}

// 전화가 오면 자동으로 실행
ISR(PHONE_INTERRUPT) {
    answer_phone();  // 즉시 전화 받기
}

// 알람 시간이 되면 자동으로 실행  
ISR(ALARM_INTERRUPT) {
    ring_alarm();   // 즉시 알람 울리기
}

장점:

  • 평상시에는 중요한 작업에 집중
  • 긴급한 일은 즉시 처리
  • 전력 효율성 극대화

마이크로컨트롤러에서 인터럽트

인터럽트가 발생하는 순간들

c

// 타이머가 오버플로우될 때
ISR(TIMER0_OVF_vect) {
    // 정확히 16.384ms마다 자동 실행!
}

// 버튼이 눌렸을 때 (외부 인터럽트)
ISR(INT0_vect) {
    // 버튼 누르는 즉시 실행! (디바운싱 필요없음)
}

// 시리얼 데이터가 도착했을 때
ISR(USART_RX_vect) {
    // 데이터 받는 즉시 실행!
}

인터럽트 없이 타이머 사용한다면?

c

// 비효율적인 polling 방식
void main() {
    while(1) {
        // 타이머 확인을 위해 계속 레지스터를 읽어야 함
        if (TCNT0 >= 250) {  
            TCNT0 = 0;
            led_toggle();
        }
        
        // 다른 작업들... 하지만 타이밍이 부정확해짐
        other_work();  // 이 작업이 오래 걸리면 타이머 체크가 늦어짐
    }
}

문제점:

  • other_work()가 오래 걸리면 타이머 체크가 늦어짐
  • 정확한 타이밍 보장 불가
  • CPU가 계속 확인 작업에 시간 소모

ISR 작성의 황금 규칙

규칙 1: 빠르게 끝내라 (Keep It Short and Simple)

c

// 잘못된 ISR - 너무 오래 걸림
ISR(TIMER0_OVF_vect) {
    printf("Timer overflow occurred\n");  // 시리얼 출력은 느림!
    _delay_ms(100);                       // 절대 금지!
    complex_calculation();                // 복잡한 계산도 금지!
    
    // 다른 인터럽트들이 블록됨!
}

// 올바른 ISR - 빠르고 간단
volatile uint8_t timer_flag = 0;

ISR(TIMER0_OVF_vect) {
    timer_flag = 1;  // 플래그만 설정하고 즉시 종료
}

void main() {
    while(1) {
        if (timer_flag) {
            timer_flag = 0;
            
            // 여기서 복잡한 작업 수행
            printf("Timer overflow occurred\n");
            complex_calculation();
        }
        
        other_tasks();
    }
}

규칙 2: 전역 변수는 volatile로 선언하라

c

// 잘못된 선언
uint8_t counter = 0;

ISR(TIMER0_OVF_vect) {
    counter++;  // ISR에서 변경
}

void main() {
    while(1) {
        if (counter >= 100) {  // 컴파일러가 최적화로 인해 읽지 못할 수 있음!
            counter = 0;
            do_something();
        }
    }
}

// 올바른 선언  
volatile uint8_t counter = 0;  // volatile 키워드 필수!

ISR(TIMER0_OVF_vect) {
    counter++;
}

void main() {
    while(1) {
        if (counter >= 100) {  // 항상 메모리에서 최신값 읽음
            counter = 0;
            do_something();
        }
    }
}

규칙 3: 재진입(Reentrancy) 문제 주의

c

// 위험한 코드 - 데이터 충돌 가능
volatile uint16_t shared_data = 0;

ISR(TIMER0_OVF_vect) {
    shared_data++;  // 16비트 변수는 2번의 명령으로 처리됨
}

void main() {
    uint16_t local_copy;
    
    while(1) {
        local_copy = shared_data;  // 읽는 도중 인터럽트 발생 시 문제!
        
        if (local_copy >= 1000) {
            shared_data = 0;
            process_data();
        }
    }
}

// 안전한 코드 - 원자적 접근
#include <util/atomic.h>

volatile uint16_t shared_data = 0;

ISR(TIMER0_OVF_vect) {
    shared_data++;
}

void main() {
    uint16_t local_copy;
    
    while(1) {
        // 인터럽트를 잠시 차단하고 안전하게 읽기
        ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
            local_copy = shared_data;
        }
        
        if (local_copy >= 1000) {
            ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
                shared_data = 0;
            }
            process_data();
        }
    }
}

volatile 키워드 완전 이해

volatile이 필요한 이유

c

volatile uint8_t button_pressed = 0;

ISR(INT0_vect) {
    button_pressed = 1;  // 외부 인터럽트에서 변경
}

void main() {
    while(1) {
        if (button_pressed) {     // ← 컴파일러 최적화 관점
            button_pressed = 0;
            handle_button();
        }
        
        other_work();
    }
}

컴파일러 최적화의 함정

c

// volatile 없을 때 컴파일러가 보는 관점:
void main() {
    while(1) {
        if (button_pressed) {  // "main에서 button_pressed를 변경한 적이 없네?"
            // ...               // "그럼 한 번 false면 계속 false겠구나"
        }                      // → 최적화: 조건 검사 생략!
        
        other_work();
    }
}

// volatile 있을 때:  
void main() {
    while(1) {
        if (button_pressed) {  // "volatile이니까 매번 메모리에서 읽어야지"
            // ...               // → 항상 최신값 확인
        }
        
        other_work();
    }
}

volatile 사용 가이드라인

c

// volatile이 필요한 경우
volatile uint8_t isr_flag;              // ISR에서 변경되는 플래그
volatile uint16_t timer_counter;        // ISR에서 증가하는 카운터
volatile uint8_t adc_result;           // ADC 인터럽트에서 업데이트

// volatile이 불필요한 경우  
uint8_t local_variable;                // 로컬 변수
const uint8_t constants = 100;         // 상수
uint8_t main_only_variable;           // main에서만 사용하는 변수

ATmega328P의 인터럽트 종류

주요 인터럽트 벡터들

c

// 외부 인터럽트
ISR(INT0_vect) {        // PD2 핀의 외부 인터럽트
    // 버튼, 센서 신호 등
}

ISR(INT1_vect) {        // PD3 핀의 외부 인터럽트
    // 또 다른 외부 신호
}

// 핀 변화 인터럽트
ISR(PCINT0_vect) {      // PORTB 핀들의 변화 감지
    // 여러 핀 동시 모니터링
}

// 타이머 인터럽트들
ISR(TIMER0_OVF_vect) {  // Timer0 오버플로우
    // 기본 시스템 틱
}

ISR(TIMER0_COMPA_vect) { // Timer0 Compare Match A
    // 정확한 시간 간격
}

ISR(TIMER1_OVF_vect) {  // Timer1 오버플로우 (16비트)
    // 긴 시간 간격
}

// 통신 인터럽트들
ISR(USART_RX_vect) {    // UART 수신 완료
    // 시리얼 데이터 받음
}

ISR(USART_TX_vect) {    // UART 송신 완료  
    // 시리얼 데이터 전송 완료
}

// ADC 인터럽트
ISR(ADC_vect) {         // ADC 변환 완료
    // 아날로그 값 읽기 완료
}

인터럽트 우선순위

ATmega328P의 인터럽트 우선순위 (높은 것부터):

  1. RESET – 시스템 리셋
  2. INT0 – 외부 인터럽트 0
  3. INT1 – 외부 인터럽트 1
  4. PCINT0 – 핀 변화 인터럽트 0
  5. PCINT1 – 핀 변화 인터럽트 1
  6. PCINT2 – 핀 변화 인터럽트 2
  7. WDT – 워치독 타이머
  8. TIMER2_COMPA – Timer2 Compare Match A
  9. TIMER2_COMPB – Timer2 Compare Match B
  10. TIMER2_OVF – Timer2 오버플로우 … (계속)

중요: 높은 우선순위 인터럽트는 낮은 우선순위 인터럽트를 중단시킬 수 있습니다!

인터럽트 디버깅과 트러블슈팅

자주 발생하는 문제들

문제 1: 인터럽트가 실행되지 않음

c

// 실수 1: sei() 호출 안함
void main() {
    TIMSK0 |= (1 << TOIE0);  // 인터럽트 마스크 설정
    // sei();  ← 이 줄이 없으면 인터럽트 안됨!
    
    while(1) {
        // 아무리 기다려도 인터럽트 안 일어남
    }
}

// 올바른 코드
void main() {
    TIMSK0 |= (1 << TOIE0);
    sei();  // 전역 인터럽트 활성화 필수!
    
    while(1) {
        // 이제 인터럽트 발생함
    }
}

문제 2: 인터럽트 벡터명 오타

c

// 실수 2: 벡터명 오타
ISR(TIMER0_OVERFLOW_vect) {  // 잘못된 이름!
    // 이 함수는 절대 호출되지 않음
}

// 올바른 벡터명
ISR(TIMER0_OVF_vect) {  // 정확한 이름
    // 올바르게 호출됨
}

문제 3: 무한 인터럽트 발생

c

// 실수 3: 인터럽트 플래그 클리어 안함
ISR(TIMER0_COMPA_vect) {
    // 인터럽트 플래그가 자동으로 클리어되지 않는 경우
    // OCF0A 비트를 수동으로 클리어해야 함
    
    do_something();
    // TIFR0 |= (1 << OCF0A);  ← 이 줄이 없으면 무한 반복!
}

디버깅 도구들

인터럽트 카운터로 동작 확인

c

volatile uint32_t interrupt_count = 0;
volatile uint8_t debug_flag = 0;

ISR(TIMER0_OVF_vect) {
    interrupt_count++;
    
    // 1초마다 디버그 정보 출력
    if (interrupt_count % 61 == 0) {  // 61 * 16.384ms ≈ 1초
        debug_flag = 1;
    }
}

void main() {
    // 타이머 설정...
    sei();
    
    while(1) {
        if (debug_flag) {
            debug_flag = 0;
            printf("Interrupts: %lu\n", interrupt_count);
        }
    }
}

인터럽트 실행 시간 측정

c

ISR(TIMER0_OVF_vect) {
    PORTB |= (1 << PB0);   // ISR 시작 표시
    
    // 실제 ISR 작업
    timer_flag = 1;
    
    PORTB &= ~(1 << PB0);  // ISR 끝 표시
}
// 오실로스코프로 PB0 핀을 보면 ISR 실행 시간을 측정 가능

실전 인터럽트 활용 패턴

패턴 1: 다중 타이머 시스템

c

volatile uint16_t ms_counter = 0;
volatile uint8_t task1_flag = 0;
volatile uint8_t task2_flag = 0;
volatile uint8_t task3_flag = 0;

ISR(TIMER0_OVF_vect) {
    TCNT0 = 256 - 250;  // 16ms 정확히
    ms_counter += 16;
    
    // Task1: 100ms 주기
    static uint16_t task1_timer = 0;
    task1_timer += 16;
    if (task1_timer >= 100) {
        task1_timer = 0;
        task1_flag = 1;
    }
    
    // Task2: 250ms 주기  
    static uint16_t task2_timer = 0;
    task2_timer += 16;
    if (task2_timer >= 250) {
        task2_timer = 0;
        task2_flag = 1;
    }
    
    // Task3: 1000ms 주기
    static uint16_t task3_timer = 0;
    task3_timer += 16;
    if (task3_timer >= 1000) {
        task3_timer = 0;
        task3_flag = 1;
    }
}

void main() {
    // 타이머 설정...
    sei();
    
    while(1) {
        if (task1_flag) {
            task1_flag = 0;
            handle_fast_task();     // 100ms 작업
        }
        
        if (task2_flag) {
            task2_flag = 0;  
            handle_medium_task();   // 250ms 작업
        }
        
        if (task3_flag) {
            task3_flag = 0;
            handle_slow_task();     // 1초 작업
        }
        
        // 기타 작업들...
    }
}

패턴 2: 버튼 인터럽트 + 소프트웨어 디바운싱

c

volatile uint8_t button_event = 0;
volatile uint32_t last_press_time = 0;

ISR(INT0_vect) {
    // 하드웨어적으로는 즉시 반응하지만
    // 디바운싱은 소프트웨어에서 처리
    button_event = 1;
    last_press_time = system_ms;  // 현재 시간 저장
}

void main() {
    // 외부 인터럽트 설정...
    sei();
    
    while(1) {
        if (button_event) {
            // 50ms 디바운싱 시간 확인
            if ((system_ms - last_press_time) >= 50) {
                button_event = 0;
                
                // 버튼이 여전히 눌려있는지 확인
                if (!(PIND & (1 << PD2))) {
                    handle_button_press();
                }
            }
        }
        
        other_tasks();
    }
}

인터럽트 마스터 체크리스트

다음 항목들을 모두 이해했다면 인터럽트를 마스터한 것입니다:

기본 개념

  • 인터럽트가 무엇인지 일상 생활 비유로 설명할 수 있다
  • Polling 방식 대비 인터럽트의 장점을 설명할 수 있다
  • ISR이 언제 어떻게 실행되는지 이해한다

실전 기술

  • sei()의 필요성을 이해한다
  • ISR은 짧고 빠르게 작성해야 한다는 것을 안다
  • volatile 키워드를 정확히 사용할 수 있다
  • 인터럽트 벡터명을 정확히 작성할 수 있다

고급 활용

  • 여러 인터럽트를 조합해서 사용할 수 있다
  • 인터럽트 우선순위를 고려해서 설계할 수 있다
  • 인터럽트 관련 버그를 스스로 디버깅할 수 있다

안전한 코딩

  • 원자적 접근(ATOMIC_BLOCK)을 사용할 수 있다
  • 재진입 문제를 인식하고 해결할 수 있다
  • 인터럽트와 메인 루프 간의 안전한 데이터 공유를 구현할 수 있다

다음 단계

인터럽트를 마스터했다면:

  1. 6편의 타이머 코드를 다시 읽어보세요 – 이제 완전히 이해될 겁니다!
  2. 외부 인터럽트로 즉각적인 버튼 반응 시스템을 만들어보세요
  3. 실시간 시스템의 기초를 닦을 수 있습니다

인터럽트는 마이크로컨트롤러가 진짜 똑똑해지는 순간입니다. 이제 CPU가 여러 일을 동시에 처리할 수 있어요!


인터럽트를 마스터하면 마이크로컨트롤러가 단순한 순차 처리기에서 실시간 멀티태스킹 시스템으로 진화합니다!

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

이 사이트는 Akismet을 사용하여 스팸을 줄입니다. 댓글 데이터가 어떻게 처리되는지 알아보세요.