이번 글… 정말 열심히 쉽게 알아볼 수 있는 내용으로 적었습니다. 최선을 다했어요. ㅠㅠ
안녕하세요! 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의 인터럽트 우선순위 (높은 것부터):
- RESET – 시스템 리셋
- INT0 – 외부 인터럽트 0
- INT1 – 외부 인터럽트 1
- PCINT0 – 핀 변화 인터럽트 0
- PCINT1 – 핀 변화 인터럽트 1
- PCINT2 – 핀 변화 인터럽트 2
- WDT – 워치독 타이머
- TIMER2_COMPA – Timer2 Compare Match A
- TIMER2_COMPB – Timer2 Compare Match B
- 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
)을 사용할 수 있다 - 재진입 문제를 인식하고 해결할 수 있다
- 인터럽트와 메인 루프 간의 안전한 데이터 공유를 구현할 수 있다
다음 단계
인터럽트를 마스터했다면:
- 6편의 타이머 코드를 다시 읽어보세요 – 이제 완전히 이해될 겁니다!
- 외부 인터럽트로 즉각적인 버튼 반응 시스템을 만들어보세요
- 실시간 시스템의 기초를 닦을 수 있습니다
인터럽트는 마이크로컨트롤러가 진짜 똑똑해지는 순간입니다. 이제 CPU가 여러 일을 동시에 처리할 수 있어요!
인터럽트를 마스터하면 마이크로컨트롤러가 단순한 순차 처리기에서 실시간 멀티태스킹 시스템으로 진화합니다!