타이머 기초와 정확한 시간 제어) _delay_ms()를 넘어서

드디어 해야 할 것이 왔습니다… 안녕하세요! 지금까지 시간 제어는 _delay_ms() 함수에만 의존해왔습니다. 하지만 실제 마이크로컨트롤러 프로그래밍에서는 타이머를 사용해서 더 정밀하고 효율적인 시간 제어를 합니다. 이번 편에서는 ATmega328P의 타이머 시스템을 배워보겠습니다!

이번 편의 목표

  • _delay_ms()의 한계점 이해
  • ATmega328P 타이머 시스템 개요
  • Timer0를 이용한 기본 타이밍 제어
  • 오버플로우 인터럽트 활용
  • 정밀한 시간 측정과 실시간 시계 구현

_delay_ms()의 한계점

현재까지 사용한 방식의 문제점

c

while(1) {
    LED_ON();
    _delay_ms(1000);    // CPU가 1초간 아무것도 하지 못함
    LED_OFF();
    _delay_ms(1000);    // 또 1초간 대기
}

문제점들:

  • CPU 낭비: 대기 시간 동안 다른 작업 불가
  • 정확도 한계: 컴파일러 최적화에 따라 시간이 부정확할 수 있음
  • 인터럽트 방해: 인터럽트 발생 시 타이밍이 어긋남
  • 복수 타이밍 불가: 여러 개의 서로 다른 주기를 동시에 처리 어려움

타이머 사용 시 장점

c

// 메인 루프는 계속 다른 일을 할 수 있음
while(1) {
    if (timer_flag) {
        timer_flag = 0;
        LED_TOGGLE();
    }
    
    // 다른 중요한 작업들 수행 가능
    check_buttons();
    update_display();
    process_serial_data();
}

ATmega328P 타이머 시스템 개요

타이머 종류와 특징

ATmega328P에는 3개의 타이머가 있습니다:

타이머비트 수특징주요 용도
Timer08비트간단, 빠른 오버플로우시스템 틱, 간단한 타이밍
Timer116비트높은 정밀도, PWM 지원정밀 타이밍, 서보 제어
Timer28비트비동기 모드 지원RTC, 절전 모드

기본 동작 원리

클럭 입력 → 프리스케일러 → 타이머 카운터 → 비교/오버플로우 → 인터럽트
16MHz    →    분주비    →   0,1,2,3... →      감지        →    ISR 실행

Timer0 기본 사용법

1. Timer0 설정 레지스터들

TCCR0B (Timer/Counter Control Register 0B)

c

// 클럭 소스 및 분주비 설정
TCCR0B |= (1 << CS02) | (1 << CS00);  // 1024 분주

분주비 옵션:

CS02CS01CS00분주비주파수오버플로우 주기
000정지
001116MHz16µs
01082MHz128µs
01164250kHz1.024ms
10025662.5kHz4.096ms
101102415.625kHz16.384ms

TIMSK0 (Timer Interrupt Mask Register 0)

c

// 오버플로우 인터럽트 활성화
TIMSK0 |= (1 << TOIE0);

TCNT0 (Timer/Counter 0 Register)

c

// 현재 카운터 값 읽기/쓰기
uint8_t current_count = TCNT0;
TCNT0 = 100;  // 카운터 값 설정

2. 첫 번째 타이머 프로그램

c

#include <avr/io.h>
#include <avr/interrupt.h>

volatile uint8_t timer_overflow_count = 0;

// Timer0 오버플로우 인터럽트 서비스 루틴
ISR(TIMER0_OVF_vect) {
    timer_overflow_count++;
}

int main(void) {
    // LED 출력 설정
    DDRB |= (1 << PB5);
    
    // Timer0 설정
    TCCR0B |= (1 << CS02) | (1 << CS00);  // 1024 분주
    TIMSK0 |= (1 << TOIE0);               // 오버플로우 인터럽트 활성화
    
    sei();  // 전역 인터럽트 활성화
    
    while(1) {
        // 약 61번의 오버플로우 = 1초 (61 * 16.384ms ≈ 1초)
        if (timer_overflow_count >= 61) {
            timer_overflow_count = 0;
            PORTB ^= (1 << PB5);  // LED 토글
        }
    }
    
    return 0;
}

정밀한 1초 타이머 만들기

정확한 계산 방법

16MHz 클럭, 1024 분주비 사용 시:

  • 타이머 주파수 = 16,000,000 ÷ 1024 = 15,625Hz
  • 1틱당 시간 = 1 ÷ 15,625 = 64µs
  • 오버플로우까지 틱 수 = 256개 (8비트)
  • 오버플로우 주기 = 256 × 64µs = 16.384ms

더 정확한 1초 타이머

c

#include <avr/io.h>
#include <avr/interrupt.h>

volatile uint16_t millisecond_counter = 0;
volatile uint8_t second_flag = 0;

ISR(TIMER0_OVF_vect) {
    // 미리 계산된 값으로 카운터 초기화 (더 정확한 타이밍)
    TCNT0 = 256 - 250;  // 250틱 = 16ms 정확히
    
    millisecond_counter += 16;
    
    if (millisecond_counter >= 1000) {
        millisecond_counter = 0;
        second_flag = 1;
    }
}

int main(void) {
    DDRB |= (1 << PB5);
    
    // Timer0 설정 (64 분주로 변경)
    TCCR0B |= (1 << CS01) | (1 << CS00);  // 64 분주
    TIMSK0 |= (1 << TOIE0);
    TCNT0 = 256 - 250;  // 초기값 설정
    
    sei();
    
    while(1) {
        if (second_flag) {
            second_flag = 0;
            PORTB ^= (1 << PB5);
        }
        
        // 메인 루프에서 다른 작업 수행 가능
    }
    
    return 0;
}

다중 타이머 시스템

여러 개의 서로 다른 주기 처리

c

#include <avr/io.h>
#include <avr/interrupt.h>

// 다양한 타이머 카운터들
volatile uint16_t ms_counter = 0;
volatile uint8_t led1_timer = 0;    // 500ms 주기
volatile uint8_t led2_timer = 0;    // 300ms 주기  
volatile uint8_t led3_timer = 0;    // 200ms 주기

// 플래그들
volatile uint8_t led1_flag = 0;
volatile uint8_t led2_flag = 0;
volatile uint8_t led3_flag = 0;

ISR(TIMER0_OVF_vect) {
    TCNT0 = 256 - 250;  // 16ms 정확히
    ms_counter += 16;
    
    // LED1: 500ms 주기
    led1_timer += 16;
    if (led1_timer >= 500) {
        led1_timer = 0;
        led1_flag = 1;
    }
    
    // LED2: 300ms 주기
    led2_timer += 16;  
    if (led2_timer >= 300) {
        led2_timer = 0;
        led2_flag = 1;
    }
    
    // LED3: 200ms 주기
    led3_timer += 16;
    if (led3_timer >= 200) {
        led3_timer = 0;
        led3_flag = 1;
    }
}

int main(void) {
    // LED들 출력 설정
    DDRB |= (1 << PB5) | (1 << PB4) | (1 << PB3);
    
    // Timer0 설정
    TCCR0B |= (1 << CS01) | (1 << CS00);
    TIMSK0 |= (1 << TOIE0);
    TCNT0 = 256 - 250;
    
    sei();
    
    while(1) {
        if (led1_flag) {
            led1_flag = 0;
            PORTB ^= (1 << PB5);  // 500ms 주기로 토글
        }
        
        if (led2_flag) {
            led2_flag = 0;
            PORTB ^= (1 << PB4);  // 300ms 주기로 토글
        }
        
        if (led3_flag) {
            led3_flag = 0;  
            PORTB ^= (1 << PB3);  // 200ms 주기로 토글
        }
        
        // 다른 작업들...
    }
    
    return 0;
}

실시간 시계 구현

시:분:초 시계 만들기

c

#include <avr/io.h>
#include <avr/interrupt.h>
#include <stdio.h>

typedef struct {
    uint8_t hours;
    uint8_t minutes;
    uint8_t seconds;
} time_t;

volatile time_t current_time = {12, 0, 0};  // 초기 시간 12:00:00
volatile uint8_t time_update_flag = 0;
volatile uint16_t ms_counter = 0;

ISR(TIMER0_OVF_vect) {
    TCNT0 = 256 - 250;
    ms_counter += 16;
    
    if (ms_counter >= 1000) {  // 1초마다
        ms_counter = 0;
        
        current_time.seconds++;
        if (current_time.seconds >= 60) {
            current_time.seconds = 0;
            current_time.minutes++;
            
            if (current_time.minutes >= 60) {
                current_time.minutes = 0;
                current_time.hours++;
                
                if (current_time.hours >= 24) {
                    current_time.hours = 0;
                }
            }
        }
        
        time_update_flag = 1;
    }
}

// 시간을 이진수로 LED에 표시하는 함수
void display_time_on_leds(void) {
    // 초의 하위 6비트를 6개 LED로 표시 (0~59초)
    PORTB = current_time.seconds & 0x3F;
    
    // 또는 시/분/초를 다른 방식으로 표현
    // PORTB = (current_time.hours << 3) | (current_time.minutes >> 3);
}

int main(void) {
    DDRB = 0xFF;  // 포트 B 전체 출력
    
    // Timer0 설정
    TCCR0B |= (1 << CS01) | (1 << CS00);
    TIMSK0 |= (1 << TOIE0);
    TCNT0 = 256 - 250;
    
    sei();
    
    while(1) {
        if (time_update_flag) {
            time_update_flag = 0;
            display_time_on_leds();
            
            // 시리얼로 시간 출력 (SimulIDE에서 확인)
            printf("%02d:%02d:%02d\n", 
                   current_time.hours, 
                   current_time.minutes, 
                   current_time.seconds);
        }
        
        // 버튼으로 시간 설정 등 다른 기능들...
    }
    
    return 0;
}

스톱워치 구현

정밀한 타이밍 측정

c

#include <avr/io.h>
#include <avr/interrupt.h>

typedef struct {
    uint8_t minutes;
    uint8_t seconds;  
    uint8_t centiseconds;  // 1/100초 단위
} stopwatch_t;

volatile stopwatch_t stopwatch = {0, 0, 0};
volatile uint8_t stopwatch_running = 0;
volatile uint8_t display_update_flag = 0;

ISR(TIMER0_OVF_vect) {
    TCNT0 = 256 - 160;  // 10ms 정확히 (1/100초)
    
    if (stopwatch_running) {
        stopwatch.centiseconds++;
        
        if (stopwatch.centiseconds >= 100) {
            stopwatch.centiseconds = 0;
            stopwatch.seconds++;
            
            if (stopwatch.seconds >= 60) {
                stopwatch.seconds = 0;
                stopwatch.minutes++;
                
                if (stopwatch.minutes >= 100) {  // 99분 59초 99까지
                    stopwatch.minutes = 99;
                    stopwatch.seconds = 59;
                    stopwatch.centiseconds = 99;
                    stopwatch_running = 0;  // 자동 정지
                }
            }
        }
        
        display_update_flag = 1;
    }
}

void check_buttons(void) {
    static uint8_t last_start_button = 1;
    static uint8_t last_reset_button = 1;
    
    uint8_t start_button = (PINC & (1 << PC0)) ? 1 : 0;
    uint8_t reset_button = (PINC & (1 << PC1)) ? 1 : 0;
    
    // 시작/정지 버튼
    if (last_start_button && !start_button) {
        _delay_ms(20);
        stopwatch_running = !stopwatch_running;
    }
    
    // 리셋 버튼  
    if (last_reset_button && !reset_button) {
        _delay_ms(20);
        stopwatch.minutes = 0;
        stopwatch.seconds = 0;
        stopwatch.centiseconds = 0;
        stopwatch_running = 0;
        display_update_flag = 1;
    }
    
    last_start_button = start_button;
    last_reset_button = reset_button;
}

void display_stopwatch(void) {
    // 초를 이진수로 LED 표시
    PORTB = stopwatch.seconds;
    
    // 또는 전체 시간을 압축해서 표시
    // uint16_t total_centiseconds = stopwatch.minutes * 6000 + 
    //                               stopwatch.seconds * 100 + 
    //                               stopwatch.centiseconds;
    // PORTB = (total_centiseconds >> 8) & 0xFF;
}

int main(void) {
    DDRB = 0xFF;                      // LED 출력
    DDRC &= ~((1 << PC0) | (1 << PC1)); // 버튼 입력
    PORTC |= (1 << PC0) | (1 << PC1);   // 풀업 활성화
    
    // Timer0 설정 (64 분주)
    TCCR0B |= (1 << CS01) | (1 << CS00);
    TIMSK0 |= (1 << TOIE0);
    TCNT0 = 256 - 160;
    
    sei();
    
    while(1) {
        check_buttons();
        
        if (display_update_flag) {
            display_update_flag = 0;
            display_stopwatch();
            
            printf("%02d:%02d.%02d %s\n", 
                   stopwatch.minutes,
                   stopwatch.seconds, 
                   stopwatch.centiseconds,
                   stopwatch_running ? "RUN" : "STOP");
        }
    }
    
    return 0;
}

고급 타이머 기법

1. Compare Match 사용

c

// OCR0A에 비교값 설정
OCR0A = 124;  // 125틱마다 인터럽트 (8ms)

// CTC 모드 설정
TCCR0A |= (1 << WGM01);

// Compare Match 인터럽트 활성화  
TIMSK0 |= (1 << OCIE0A);

ISR(TIMER0_COMPA_vect) {
    // 정확히 8ms마다 실행
}

2. 타이머 체인

c

// Timer0으로 밀리초, Timer1으로 초 단위 처리
volatile uint16_t milliseconds = 0;
volatile uint8_t seconds = 0;

ISR(TIMER0_OVF_vect) {
    milliseconds++;
    if (milliseconds >= 1000) {
        milliseconds = 0;
        seconds++;
    }
}

응용 과제

과제 1: 다기능 타이머

  • 카운트다운 타이머
  • 랩 타임 기능이 있는 스톱워치
  • 알람 시계

과제 2: 정밀한 PWM 생성

타이머를 이용해서 소프트웨어 PWM을 구현해보세요.

과제 3: 주파수 측정기

외부 신호의 주파수를 측정하는 시스템을 만들어보세요.

다음 편 예고

다음 편에서는 **PWM(Pulse Width Modulation)**을 배워보겠습니다:

  • 하드웨어 PWM vs 소프트웨어 PWM
  • LED 밝기 조절과 페이드 효과
  • 서보모터와 DC모터 제어
  • 아날로그 출력 시뮬레이션
  • 음성 및 톤 생성

타이머를 마스터했다면, 이제 더 정교한 아날로그 제어의 세계로 들어갈 시간입니다!


정확한 시간 제어가 가능해지면 마이크로컨트롤러 프로그래밍의 새로운 차원이 열립니다. 타이머 인터럽트를 활용해보세요!