드디어 해야 할 것이 왔습니다… 안녕하세요! 지금까지 시간 제어는 _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개의 타이머가 있습니다:
타이머 | 비트 수 | 특징 | 주요 용도 |
---|---|---|---|
Timer0 | 8비트 | 간단, 빠른 오버플로우 | 시스템 틱, 간단한 타이밍 |
Timer1 | 16비트 | 높은 정밀도, PWM 지원 | 정밀 타이밍, 서보 제어 |
Timer2 | 8비트 | 비동기 모드 지원 | RTC, 절전 모드 |
기본 동작 원리
클럭 입력 → 프리스케일러 → 타이머 카운터 → 비교/오버플로우 → 인터럽트
16MHz → 분주비 → 0,1,2,3... → 감지 → ISR 실행
Timer0 기본 사용법
1. Timer0 설정 레지스터들
TCCR0B (Timer/Counter Control Register 0B)
c
// 클럭 소스 및 분주비 설정
TCCR0B |= (1 << CS02) | (1 << CS00); // 1024 분주
분주비 옵션:
CS02 | CS01 | CS00 | 분주비 | 주파수 | 오버플로우 주기 |
---|---|---|---|---|---|
0 | 0 | 0 | 정지 | – | – |
0 | 0 | 1 | 1 | 16MHz | 16µs |
0 | 1 | 0 | 8 | 2MHz | 128µs |
0 | 1 | 1 | 64 | 250kHz | 1.024ms |
1 | 0 | 0 | 256 | 62.5kHz | 4.096ms |
1 | 0 | 1 | 1024 | 15.625kHz | 16.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모터 제어
- 아날로그 출력 시뮬레이션
- 음성 및 톤 생성
타이머를 마스터했다면, 이제 더 정교한 아날로그 제어의 세계로 들어갈 시간입니다!
정확한 시간 제어가 가능해지면 마이크로컨트롤러 프로그래밍의 새로운 차원이 열립니다. 타이머 인터럽트를 활용해보세요!