[AVR] Timer/Counter

이여진·2023년 7월 24일
0

AVR

목록 보기
3/6
post-thumbnail

ATmega128의 경우 2개의 8비트 타이머/카운터와 2개의 16비트 타이머/카운터를 가지고 있다.

이번 글에서는 Timer/Counter인터럽트 서비스 루틴(ISR) 사용법, 그리고 이를 이용해 delay 함수를 만들며 공부한 내용들을 정리해보려고 한다.

 

📌 Timer/Counter 개념

Timer/Counter란 기계 동작에 있어서 정확한 시간과 측정을 필요로 하는데, 이 때 쓰이는 방법이다.

 

💡Timer

  • MCU 내부 클럭을 세는 장치
  • 동기 모드 : 데이터 동기화를 위해 별도의 클럭 신호 전송
  • 내부 클럭을 세어 일정시간동안 펄스를 만들어내거나 일정시간 뒤에 인터럽트를 발생
  • 빠름
  • 분주 가능 : 범위 내에서 클럭 선택 가능

💡Counter

  • MCU 외부에서 입력되는 클럭을 세는 장치
  • 비동기 모드 : 별도의 클럭을 사용하지 않고 데이터를 송수신하는 모드
  • 외부핀(TOSC1,2 & T1,2,3)을 통하여 인간되는 펄스를 계수하여 동작
  • 느림
  • 분주 불가능 : 외부클럭 그대로 사용

 

그 외에 Timer/Counter를 공부하다보면 자주 나오는 개념들이 있었는데 그 개념들에 대해서도 짚고 넘어가보려 한다.

🍀 PWM

  • Pulse Width Modulation의 약자로 한국어로는 펄스 폭 변조라 불린다.
  • 1과 0의 구간이 제어되어 출력되는 구형파를 의미한다.
  • 주로 DC 모터 또는 LED 빛의 세기를 제어할 때 사용한다.

 

🍀 Duty Ratio

  • PWM 파형의 전체 주기 중에 1이 되는 시간의 비율을 의미한다.
  • 단위 : %
  • Duty Ratio = 1 출력시간 / PWM 파형 주기 * 100(%)

 

🍀 Max/Top/Bottom

Max, Top, Bottom의 개념에 대해서 설명하려면 직관적으로 볼 수 있는 그림이 나을 것 같아서 열심히 아이패드로 그려왔다...ㅋㅋㅋㅋㅋㅋ

  • Max : Timer/Counter가 가질 수 있는 최댓값
  • Bottom : Timer/Counter가 가질 수 있는 최솟값
  • Top : Timer/Counter가 도달할 수 있는 최댓값으로 사용자가 지정할 수 있으며, 오버플로우 값을 설정하거나 비교일치 레지스터를 설정함으로써 정할 수 있다.

 

📌 Timer/Counter 0&2

앞서 말했듯이 ATmega128은 Timer/Counter0~Timer/Counter3까지 총 4개의 Timer/Counter을 가지고 있다.

Timer/Counter 0과 2, Timer/Counter 1과 3이 비슷한데, 우선 Timer/Counter 0&2에 대해서 먼저 공부했다.

🍀 Timer/Counter 0&2 공통점

  • 8bit 업/다운 카운터이다.
    - 2의 8승, 즉 256개의 펄스 계수가 가능하다.
  • 10bit Prescaler가 내장되어 있다.
  • 오버플로우, 출력 비교 매치 인터럽트 소스를 제공한다.

이 때, Prescaler가 무엇인지 몰라 알아보았는데, 고속의 클럭을 사용하여 타이머를 동작시킬 때, 문제가 생기는 것을 방지하기 위하여 클럭을 분주하여 더 느린 타이머를 구성한 것이다.

 

🍀 Timer/Counter 0&2 차이점

Timer/Counter0과 Timer/Counter2의 경우 설정 가능한 분주비에 차이가 있다.
Timer/Counter0의 경우 가능한 분주비는 1, 8, 32, 64, 128, 256, 1024이며, Timer/Counter2의 경우 분주비를 1, 8, 64, 256, 1024로 설정이 가능하다.

 

📌 Timer/Counter 1&3

  • 인터럽트 기능이 있다.
    오버플로우 인터럽트 : 카운터의 값이 오버플로우 되는 경우 발생
    출력 비교 인터럽트 : 카운터 값이 출력비교 레지스터의 값과 같게 되는 순간 발생
    입력 캡쳐 인터럽트 : 외부로부터 트리거 신호에 의해서 카운터의 초기값을 입력캡쳐
  • 16비트, 즉 65536개의 펄스 계수가 가능하다.
  • PWM 출력이 가능하다.

 

📌 Timer/Counter 레지스터

Timer/Counter 레지스터에 대해서 공부할 때는 각 bit 별로 무슨 역할을 하는지 하나하나 다 보기는 했었는데, datasheet에 상세히 잘 나와있을 뿐만 아니라, bit별로 무슨 역할을 하는지 외우면서 사용할 일이 전혀 없기 때문에 각 bit에 대해 공부한 것들은 생략하고, 레지스터가 무슨 역할을 하는지에 대해서만 정리해보았다.

💡TCCRn(n=0,2)

  • Timer/Counter 제어 레지스터
  • 동작모드, Prescaler 등 Timer/Counter의 전반적인 동작형태 결정
  • 강제 출력 비교, 파형 발생모드 설정, 비교매치 출력모드 설정, 분주비 설정이 가능하다.

💡TCCRnA,B,C(n=1,3)



  • TCCRnA : 동작모드 설정, 비교출력 신호의 출력방식 설정
  • TCCRnB : 입력 캡쳐 관련 설정, 프리스케일러의 분주비 설정
  • TCCRnC : 비교출력 단자와 관련된 기능을 설정

💡TCNTn(n=0,2)

  • Timer/Counter n의 8비트 카운터 값을 저장하고 있는 레지스터
  • 쓸 수도 있고, 읽을 수도 있음
  • 자동으로 값이 갱신됨

💡TCNTnH,L(n=1,3)

  • 16비트 구조이므로 8비트씩 2차례로 나누어 액세스
  • Write 동작은 TCNTnH 수행 후 TCNTnL 수행
  • Read 동작은 TCNTnL 수행 후 TCNTnH 수행

💡OCRn

  • Timer/Counter의 카운터 값이 TCNT와 비교하여 OC0 단자에 출력 신호를 발생하기 위한 8비트 값 저장
  • 사용자가 설정
  • Timer/Counter1,3의 경우 16bit의 값을 저장하며, CTC모드와 PWM 모드에서 사용

💡TIMSK, ETIMSK


  • 타이머 인터럽트 마스크 레지스터
  • Timer/Counter가 발생하는 인터럽트를 개별적으로 허가하는 레지스터

💡TIFR, ETIFR


  • 타이머 인터럽트 플래그 레지스터
  • Timer/Counter가 발생하는 인터럽트의 플래그를 저장하는 레지스터

💡ASSR

  • 비동기 상태 레지스터
  • Timer/Counter0이 외부 클럭에 의하여 비동기 모드로 동작하는 경우 관련된 기능을 수행

💡SFIOR

  • 특수기능 I/O 레지스터
  • Timer/Counter들을 동기화 하는 것과 관련된 기능을 담당하는 레지스터

💡ICRn(n=1,3)

  • 입력 캡쳐 레지스터
  • ICPn 핀에서 이벤트가 발생했을 때, 그 당시의 TCNTn 레지스터 값을 ICRn 레지스터에 저장할 때 쓰임

 

📌 Timer/Counter 동작모드

datasheet를 보면 각 mode별 WGM bit 설정값과, Top값, OCR0를 변경했을 때 레지스터의 값이 업데이트 되는 시점, 그리고 TOV0 플래그가 set 되는 시점을 알 수 있다.

또한, datasheet를 보면 Timer/Counter의 동작모드별로 인터럽트 우선순위가 정해져 있어서 혹시나 여라 Timer/Counter을 같이 사용하게 될 경우 인터럽트 우선순위도 생각해주어야 한다.

🍀 Normal Mode

  • 일반적인 타이머 오버플로우 인터럽트 필요 시 사용
  • 파형 출력X
  • 상향 카운터
  • 0x00~0xff 계수 동작 반복
  • 카운터 도중 Clear X
  • 오버플로우 인터럽트(OVF) : MAX = 0xff 값일 때 발생
  • 클럭이 1번 생길 때마다 TCNT0이 1씩 증가하면서 카운트 됨

🍀 CTC(Compare Timer on Compare Match) Mode

  • 주파수 분주 기능으로 주로 사용
  • 상향 카운터
  • 0x00~OCR0 계수 동작 반복
  • OCRnA의 레지스터에 원하는 값을 넣게되면 TCNT0이 OCRnA의 값만큼 증가된 후 비교매치 인터럽트 발생
  • 사용자가 원하는 값으로 인터럽트를 일으킬 수 있음
  • OCR0 == TCNT0이 되었을 때, TCNT0 = 0이 되므로 ISR 초기화 필요 X


CTC 모드의 경우 두 가지 모드가 있는데 4번 모드로 설정해주면 OCRnA 값과 TCNT0 값이 일치할 때 인터럽트가 발생하게 되고, 12번 모드로 설정해주면 ICRn 레지스터의 값과 TCNT0 값이 일치할 때 인터럽트가 발생하게 된다.

🍀 Fast PWM Mode

  • TCNTn : 0x00~0xFF 까지 갔다가 overflow 되는 것 반복
  • OCRn == TCNTn : OCn핀의 출력 값이 바뀜 (출력 값이 어떻게 바뀌느냐는 모드마다 다름)
    비반전 비교 출력모드 : HIGH로 출력되다가, 비교매치에서 OCn핀에 0 출력, TCNTn이 0xFF에서 0x00으로 떨어질 때 다시 HIGH 출력
    반전 비교 출력모드 : LOW로 출력되다가, 비교매치에서 OCn핀에 1출력, TCNTn이 0xFF에서 0x00으로 떨어질 때 다시 LOW 출력

🍀 Phase Correct PWM Mode

  • TCNTn : 0x00~0xFF로 갔다가 0x00으로 감소 (뚝 떨어지진 않음)
    비반전 출력 모드 : TCNTn값이 상승할 때 TCNTn=OCRn일 때, OCn핀에 0 출력, TCNTn값이 하락할 때 일치하면 1 출력
    반전 출력 모드 : 업카운트 중에 TCNTn=OCRn이면 OCn핀에 1 출력, 다운 카운트 중에 일치하면 0 출력

 

📌 인터럽트 서비스 루틴(ISR)

이번 세미나에서 Timer/Counter을 이용하여 delay 함수를 구현하기 위해 Timer/Counter0오버플로우 인터럽트Timer/Counter1비교매치 인터럽트를 사용하였기 때문에 이를 통한 인터럽트 서비스 루틴을 적어보려고 한다.

위에 나와있는 표가 datasheet에서 확인할 수 있는 인터럽트 벡터이다.

우선 Timer/Counter0의 오버플로우 인터럽트가 발생한 후 $0020 주소로 이동하고, 해당 ISR이 실행된다. 그 이후 ISR이 종료되면 원래 처리 중인 프로그램으로 복귀한다.

Timer/Counter1의 비교매치 인터럽트 발생과정도 동일하게 인터럽트 발생 > $0018 주소로 이동 > 해당 ISR 실행 > ISR 종료 후 원래 처리 중인 프로그램으로 복귀 의 순서로 진행된다.

 

🧷 코드

#define F_CPU 16000000UL
#define per_sec 1000
#include <avr/io.h>
#include <avr/interrupt.h>

void tc0_set();								//타이머/카운터0 설정 함수
void tc1_set();								//타이머/카운터1 설정 함수
void my_delay_tc0(int ms);			//타이머/카운터0 이용한 delay 함수
void my_delay_tc1(int ms);			//타이머/카운터1 이용한 delay 함수

volatile unsigned int cnt_tc0 = 0;		//타이머/카운터0 발생 횟수
volatile unsigned int cnt_tc1 = 0;		 //타이머/카운터1 발생 횟수

volatile변수를 사용해주었는데, 일반적으로 변수 데이터는 런타임에 RAM 영역에 저장되는데, ISR이 수행된 후 내부에서 값이 변경되므로 RAM에 저장된 데이터와 레지스터에 저장된 데이터가 서로 다를 수 있어 사용하게 되었다.

정리하자면, volatile변수를 사용하게 되면 실시간으로 변경되는 데이터 값을 ISR 외부에서 정확하게 읽어올 수 있게된다.

 

ISR(TIMER0_OVF_vect) //타이머0 오버플로 인터럽트 서비스 루틴
{
	TCNT0 =  256 - (F_CPU / per_sec / 64);
	cnt_tc0++; //타이머 발생횟수 1 증가
}

ISR(TIMER1_COMPA_vect) //타이머1 오버플로 인터럽트 서비스 루틴
{
	cnt_tc1++; //타이머 발생횟수 1 증가
}

위 사진과 같은 이유로 TCNT0 값을 설정하게 되었다.

 

void tc0_set()
{
	//Normal mode를 위한 설정
	TCNT0 =  256 - (F_CPU / per_sec / 64);
	TCCR0 = (1 << CS02) | (0 << CS01) | (0 << CS00);	//64분주
	TIMSK = (1 << TOIE0);
}
void tc1_set()
{
	//CTC mode를 위한 설정
	TCCR1A = (0 << COM1A1) | (1 << COM1A0) | (0 << WGM11) | (0 << WGM10);
	TCCR1B = (0 << WGM13) | (1 << WGM12) | (0 << CS12) | (1 << CS11) | (0 << CS10);
	//OCR1A레지스터로 TCNT의 TOP값 설정을 하려면 WGM13=0, WGM12=1, WGM11=0, WGM10=0
	//분주비를 8로 하였으므로 CS12=0, CS11=1, CS10=0
	OCR1A = 999;
	TIMSK = (1 << OCIE1A);
}

분주비 같은 경우 datasheet를 보면 공식이 나와있는데, 공식에 대입해보았을 때 나누어 떨어지는 분주비로 설정하게 되었다.

 

void my_delay_tc0(int ms)
{
	cnt_tc0 = 0;					//카운트 해주는 변수 0으로 초기화
	while(ms > cnt_tc0);
}

void my_delay_tc1(int ms)
{
	cnt_tc1 = 0;					//카운트 해주는 변수 0으로 초기화
	while(ms > cnt_tc1);
}

Timer/Counter 0과 1을 이용해서 만들어준 delay함수는 위와 같다.

 

int main(void)
{
	unsigned char led1, led2;               //LED점등 데이터가 저장될 8bit변수
	int i, j;

	DDRA = 0xff;                       //포트A를 출력으로 설정
	DDRB = 0xff;					   //파형 출력을 위해 포트B를 출력으로 설정
	tc0_set();	//타이머/카운터0 설정
	tc1_set();	//타이머/카운터1 설정

	sei();		//SREG 7번 bit 1로 설정 (Global Interrupt Enable)
	
	while (1)
	{
		PORTA = 0xff;
		my_delay_tc0(1000);
		
		for (i=0; i<8; i++)
		{
			led1 = 0xff >> 8-i;
			for (j=0; j<8-i; j++)
			{
				led2 = 0x80 >> j;
				PORTA = (~(led1 | led2));
				my_delay_tc0(1000);
			}
		}
		
		for (i=0; i<8; i++)
		{
			led1 = 0xff << 7-i;
			for (j=0; j<i+1; j++)
			{
				led2 = 0x80 >> i-j;
				PORTA = led1 & (0x00 ^ ~led2);
				my_delay_tc1(1000);
			}
		}
	}
}

main 함수 부분은 이 전글과 거의 동일한데, _delay_ms함수 대신에 Timer/Counter을 사용해 내가 만든 delay함수를 사용했다는 점비교매치모드 사용 후 파형 출력을 위해 포트를 열어준 부분, 인터럽트를 사용하기 위해 sei()함수를 사용해준 부분에 있어 차이가 있다.

Period = 1ms인것을 통해 Timer/Counter을 사용하여 내가 만든 delay함수가 잘 작동함을 확인할 수 있다.

 

profile
The sky is the limit

0개의 댓글