CS/마이크로프로세서

[마이크로프로세서] 타이머/카운터 및 예제 (1)

nowkoes 2023. 11. 28. 23:30

본문

레지스터

 

 이제 레지스터들을 이용하여 타이머/카운터를 제어하는 시간을 가져보도록 하자. 실습을 하기 전, 관련 레지스터들에 대해 알아보자. 참고로 8비트 타이머/카운터를 제어할 예정이므로 타이머/카운터0를 기준으로 설명하도록 하겠다. 


TCCR0 레지스터

 

 

 TCCRn 레지스터동작 모드, 프리스케일러의 분주비를 설정하는 레지스터다. 일반적으로 동작 모드는 WGM01과 WGM00의 값으로 결정된다. 즉, 2개의 비트를 사용하므로 총 4가지 모드가 있다는 것을 알 수 있다.

 

 

 여기서 설정한 모드에 따라 비교 일치 출력 모드의 값의 의미, 즉 OCn 핀의 동작이 바뀐다. 예를 들어 타이머/카운터0에서 일반 모드 혹은 CTC 모드에서 (COM01, COM00) = (1,1)이면 비교 일치 시 OC0 출력을 1로 설정하지만, 고속 PWM 모드에서는 (COM01, COM00) = (1,1)이면 비교 일치 시 출력을 1로 설정하고, BOTTOM에서 OC0 출력으로 0으로 설정하는 반전 비교 출력 모드로 설정한다. 

 

 

 다음은 각 모드에 따른 OC0 핀의 동작이다. Table 53은 일반 모드와 CTC 모드에 대한 비교 출력 모드, Table 54는 고속 PWM 모드에 대한 비교 출력 모드, Table 55는 위상 정정 PWM 모드에 대한 비교 출력 모드다. 

 

 

 또한 하위 3개 비트 CS00, CS01, CS02에 값에 따라 분주비를 선택할 수 있다. 


TCNT0 레지스터

 

 TCNTn 레지스터계수 값을 저장하는 레지스터로서, 읽기 및 쓰기가 가능하다. 이때 값을 수정함으로써 계수의 시작점, 즉 인터럽트 주기를 조정할 수 있다. 예를 들어 8비트 타이머/카운터에서 TCNT0를 10으로 설정한다면, 0부터 255까지 계수하는 것이 아닌 10부터 255까지 총 246번을 카운트한다. 


TIMSK 레지스터

 

 TIMSK 레지스터타이머/카운터의 인터럽트 허용 여부를 개별적으로 설정하는 레지스터다. TOIEn 비트가 1로 설정되고 SREG.I 비트(상태 레지스터의 인터럽트 플래그)가 1로 설정되면, 타이머/카운터의 오버플로우 인터럽트를 허용하고, OCIEn 비트가 1로 설정되면 타이머/카운터n의 출력 비교 인터럽트를 허용한다. 예를 들어, TOIE0인 0번 비트와 OCIE0인 1번 비트가 각각 1로 쓰여있다면, 타이머/카운터0의 오버플로우와 출력 비교 인터럽트가 허용된다.


TIFR 레지스터

 

 TIFR 레지스터타이머/카운터의 인터럽트 발생 상태를 저장하는 레지스터다. 사용자 입장에서 크게 다룰 일은 없지만, 인터럽트가 발생했을 때 해당 레지스터의 값이 자동으로 변경된다는 사실은 체크해 두면 좋을 것 같다. 즉, 인터럽트 서비스 루틴 실행 시 자동으로 0으로 클리어되며, 비트에 1을 쓰는 경우엔 설정되어 있던 플래그를 소프트웨어적으로 클리어한다. 

 

 이 외에도 클럭 관련 설정을 담당하는 ASSR과 특수 입출력 기능을 담당하는 SFIOR 레지스터가 있지만, 당장은 다루지 않을 것이므로 생략하겠다. 


실습(1) 타이머/카운터를 이용한 FND 동적 구동

#include <avr/io.h>
#include <util/delay.h>

unsigned char segmentTable[15] 
= {0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F, 0x77, 0x7C, 0x39, 0x5E, 0x79, 0x71};

int main(void)
{
	DDRC = 0xFF; // FND 포트 출력 설정
	DDRG = 0x0F; // FND Select 출력 설정
	
	while(1)
	{
		for(int i = 0; i < 4; i++)
		{
			PORTG = 0x01 << i;
			PORTC = segmentTable[4 - i];
			_delay_ms(1);
		}
	}
	
	return 0;
}

 해당 코드는 디지털 입출력 제어 시간에 배웠던 FND를 동적으로 출력하여 디스플레이에 1,2,3,4를 띄우는 코드다. 1ms가 지날 때마다 출력되는 부분을 타이머/카운터의 오버플로우 인터럽트를 이용하여 해당 코드를 수정해 보자. 

 

 

 먼저 클럭 주기와 인터럽트 주기를 조정하기 위해 분주비를 설정해야 한다. 일반적으로 원하는 인터럽트 주기와 비슷한 최대 주기의 분주비를 선택하므로, 1ms와 비슷한 분주비 64를 선택하였다. 조금 더 정밀하게 계산해 보면 1ms 오버플로우 주기를 달성하기 위해서 타이머가 256번 카운트하는 동안 1ms가 경과해야 한다. 따라서 원하는 클럭 주기는 전체 시간을 타이머의 최대 카운트 값으로 나눈 값이므로 다음과 같이 수식이 나온다.

 

 

 따라서 우리가 원하는 시스템 클럭 주기가 3.90625us이므로, 16MHz를 사용하는 ATmega128의 시스템 클럭 주기 0.0625us를 나눠주면 분주비를 계산할 수 있다.

 

 

 프리스케일러의 분주비를 64로 설정하면, 클럭 주기는 0.0625us * 64 = 4us가 되고, 인터럽트 주기는 4us * 256 = 1.024ms가 된다. 하지만 이는 정확한 1ms 주기가 아니므로, 시간이 경과할수록 오차가 누적된다. 이를 해결하기 위해 오버플로우 주기 1ms와 분주비로 설정한 클럭 주기 4us를 나눠주면 250이라는 값이 나온다. 즉, 256개가 아닌 250개를 계수하였을 때 인터럽트를 발생하게 하면 된다. 

 

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

unsigned char segmentTable[15] 
= {0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F, 0x77, 0x7C, 0x39, 0x5E, 0x79, 0x71};

 FND에 출력할 정수를 담아 놓은 segmentTable을 초기화하고, 입출력에 관련된 avr/io.h, 인터럽트와 관련된 avr/interrupt.h을 전처리한다. 

 

int main(void)
{
	DDRC = 0xFF; 
	DDRG = 0x0F;
	
	TCCR0 = TCCR0 & ~ (1 << CS02) & ~ (1 << CS01) & ~ (1 << CS00); 
	TIMSK = TIMSK | (1 << TOIE0); 
	sei(); 
	
	TCCR0 = TCCR0 | (1 << CS02); 
	
	while(1)
	{
		
	}
	
	return 0;
}

 우선 main 함수에서 설정한 레지스터 값들을 확인해 보자.

 

1. DDRC와 DDRG는 FND 출력을 위해 설정한 레지스터다.

2. TCCR0 레지스터에서 분주비만 사용할 것이므로 하위 3비트 CS02, CS01, CS00만 사용한다는 의미에서 우선적으로 초기화하였다.

3. 그리고 오버플로우 인터럽트를 허가하기 위한 TIMSK를 설정해 주었다.

4. 전역 인터럽트 허가 (SREG.I)를 위해 sei() 함수를 사용하였다.

5. 마지막으로 TCCR0에 분주비 64에 해당하는 (CS02, CS01, CS00) = (1, 0, 0)을 설정해 주면 된다.

 

ISR(TIMER0_OVF_vect)
{
	static unsigned char index = 0;
	
	PORTG = 0x01 << index; 
	PORTC = segmentTable[4 - index];
	
	index = (index == 3) ? 0 : index + 1;
	
	TCNT0 = 6;
}

 

 TIMER0의 오버플로우를 처리하는 인터럽트 서비스 루틴이다. 타이머/카운터0의 오버플로우 인터럽트 인자는 TIMER0_OVF_vect다.

 

1. 동적 구동하기 위한 변수 index를 static으로 선언해 주자. static 키워드정적 변수로서, 함수 내부에서 선언되었을 때 선언된 함수 내에서만 사용 가능한 지역 변수를 생성한다. 즉, 전역 변수처럼 사용하기 위해 해당 키워드를 사용하였다. 좀 더 자세한 설명을 원한다면 해당 링크의 static 키워드 부분을 확인하자.

2. 1ms마다 오버플로우가 발생했을 때 PORTG와 PORTC의 값을 변경하여 동적 구동을 구현하였다.

3. index의 값이 3이 되었을 때 다시 0으로 초기화하고, 그렇지 않으면 한 개씩 늘리도록 하였다.

4. 마지막으로 250까지의 계수를 위해 TCNT0의 값을 256 - 250 = 6으로 설정하였다.

 

총합본

더보기
#include <avr/io.h>
#include <avr/interrupt.h>

unsigned char segmentTable[15] 
= {0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F, 0x77, 0x7C, 0x39, 0x5E, 0x79, 0x71};

ISR(TIMER0_OVF_vect)
{
	static unsigned char index = 0;
	
	PORTG = 0x01 << index; 
	PORTC = segmentTable[4 - index];
	
	index = (index == 3) ? 0 : index + 1;
	
	TCNT0 = 6;
}

int main(void)
{
	DDRC = 0xFF; 
	DDRG = 0x0F;
	
	TCCR0 = TCCR0 & ~ (1 << CS02) & ~ (1 << CS01) & ~ (1 << CS00); 
	TIMSK = TIMSK | (1 << TOIE0); 
	sei(); 
	
	TCCR0 = TCCR0 | (1 << CS02); 
	
	while(1)
	{
		
	}
	
	return 0;
}
반응형