아두이노 하드웨어 타이머 원리 및 레지스터 레벨 최적화 가이드

1. 하드웨어 타이머의 핵심 개념과 필요성

아두이노 개발 시 흔히 사용하는 delay() 함수는 프로세서가 아무런 동작도 하지 않고 대기하게 만드는 '블로킹(Blocking)' 방식입니다. 반면, 하드웨어 타이머는 메인 루프(CPU)와 독립적으로 작동하는 내부 회로입니다. 이를 활용하면 특정 시간 간격으로 이벤트를 발생시키거나 정밀한 PWM 신호를 생성하면서도, 동시에 센서 데이터를 처리하거나 통신을 수행하는 멀티태스킹이 가능해집니다.

ATmega328P 칩(Arduino Uno/Nano의 심장)에는 세 개의 타이머가 내장되어 있습니다.

  • Timer0 (8비트): millis(), micros(), delay() 등 아두이노의 시간 관련 핵심 함수들이 사용합니다.
  • Timer1 (16비트): 정밀도가 높고 카운트 범위가 넓어 서보 모터 제어나 고정밀 타이밍에 적합합니다.
  • Timer2 (8비트): Timer0와 유사하지만 독립적으로 운용 가능하며, 주로 추가적인 PWM 출력이나 RTC 보조용으로 쓰입니다.

2. 타이머 동작을 결정하는 핵심 레지스터

타이머를 직접 제어하기 위해서는 레지스터(Register)라는 내부 메모리 공간에 특정 값을 설정해야 합니다. 주요 레지스터는 다음과 같습니다.

  • TCCRnA/B (제어 레지스터): 타이머의 동작 모드(Normal, CTC, PWM 등)와 클록 분주비(Prescaler)를 설정합니다. 분주기는 시스템 클록(16MHz)을 나누어 카운팅 속도를 조절하는 역할을 합니다.
  • TCNTn (카운터 레지스터): 실제 숫자가 올라가는 바구니 역할을 합니다. 설정된 클록에 따라 값이 1씩 증가합니다.
  • OCRnA/B (출력 비교 레지스터): 비교 기준값을 저장합니다. TCNTn의 값이 이 값과 일치하는 순간 '비교 일치(Compare Match)' 이벤트가 발생합니다.
  • TIMSKn (인터럽트 마스크 레지스터): 오버플로나 비교 일치 발생 시 CPU에 신호를 보낼지(인터럽트 발생 여부)를 결정합니다.

3. CTC 모드를 활용한 정밀 타이밍 구현

가장 많이 활용되는 방식은 CTC(Clear Timer on Compare Match) 모드입니다. 카운터가 0부터 시작해 OCRn 값에 도달하면 즉시 0으로 초기화되면서 인터럽트를 발생시키는 방식입니다.

타이머 설정 계산식

1초 간격으로 인터럽트를 발생시키고 싶다면 다음과 같이 계산합니다 (16MHz 클록, 1024 분주 기준):

목표값 = (시스템 클록 / (분주비 * 목표 주파수)) - 1
목표값 = (16,000,000 / (1024 * 1Hz)) - 1 = 15,624

레지스터 설정 코드 예시

void setupTimer1() {
  cli(); // 전체 인터럽트 일시 중지

  TCCR1A = 0; // TCCR1A 초기화
  TCCR1B = 0; // TCCR1B 초기화
  TCNT1  = 0; // 카운터 초기화

  // OCR1A에 계산된 비교값 저장 (1초 간격)
  OCR1A = 15624;

  // CTC 모드 활성화 (WGM12 비트 설정)
  TCCR1B |= (1 << WGM12);

  // 1024 분주비 설정 (CS12, CS10 비트 설정)
  TCCR1B |= (1 << CS12) | (1 << CS10);  

  // 타이머 비교 인터럽트 활성화
  TIMSK1 |= (1 << OCIE1A);

  sei(); // 전체 인터럽트 다시 허용
}

volatile int pulseCount = 0;
ISR(TIMER1_COMPA_vect) {
  // 1초마다 실행될 코드
  pulseCount++;
}

4. 하드웨어 구성 및 최적화 전략

타이머 인터럽트를 통해 외부 장치를 제어할 때는 하드웨어적 안정성도 고려해야 합니다.

출력 장치 연결 (LED 및 알람)

단순한 LED 표시등을 사용할 경우 적절한 제한 저항을 계산해야 합니다. 예를 들어 빨간색 LED(Vf=2.0V)를 5V 핀에 연결하고 20mA 전류를 흘리려면 약 150옴의 저항이 필요합니다. 청각적 알림이 필요하다면 부저(Buzzer)를 인터럽트 플래그와 연동하여 짧은 펄스로 구동할 수 있습니다.

입력 인터페이스 최적화

버튼이나 스위치를 통해 타이머 시간을 설정할 때는 '플로팅(Floating)' 현상을 방지해야 합니다. 아두이노의 INPUT_PULLUP 설정을 활용하여 내부 풀업 저항을 활성화하면 회로를 단순화하고 노이즈를 억제할 수 있습니다.

5. 소프트웨어 아키텍처 개선 및 디버깅

레지스터 레벨의 코딩은 성능을 극대화하지만, 관리하기 까다로울 수 있습니다. 다음은 실무에서 사용하는 최적화 팁입니다.

  • 인터럽트 서비스 루틴(ISR)의 경량화: ISR 내부에서 Serial.print()나 복잡한 계산을 수행하면 시스템 전체의 타이밍이 어긋날 수 있습니다. ISR에서는 상태 플래그(Flag)나 카운터 변수만 업데이트하고, 실제 무거운 로직은 loop() 함수에서 처리하도록 설계하십시오.
  • volatile 키워드 사용: ISR 내부와 메인 루프에서 공유하는 변수는 반드시 volatile로 선언하여 컴파일러의 원치 않는 최적화를 방지해야 합니다.
  • 상태 머신(State Machine) 도입: 타이머의 동작 상태(대기, 작동, 일시정지, 완료)를 열거형(enum)으로 정의하여 관리하면 복잡한 제어 로직도 가독성 있게 유지할 수 있습니다.

이러한 하드웨어 타이머 기법은 단순한 LED 깜빡임을 넘어, 고정밀 센싱 레이트 제어, 드론의 비행 제어 알고리즘, 복잡한 통신 프로토콜 구현 등 임베디드 개발의 고급 단계로 나아가기 위한 필수 관문입니다.

태그: Arduino ATmega328P Interrupt RegisterOptimization EmbeddedSystems

7월 29일 03:19에 게시됨