ATmega128의 경우 2개의 8비트 타이머/카운터와 2개의 16비트 타이머/카운터를 가지고 있다.
이번 글에서는 Timer/Counter와 인터럽트 서비스 루틴(ISR) 사용법, 그리고 이를 이용해 delay 함수를 만들며 공부한 내용들을 정리해보려고 한다.
Timer/Counter란 기계 동작에 있어서 정확한 시간과 측정을 필요로 하는데, 이 때 쓰이는 방법이다.
- MCU 내부 클럭을 세는 장치
- 동기 모드 : 데이터 동기화를 위해 별도의 클럭 신호 전송
- 내부 클럭을 세어 일정시간동안 펄스를 만들어내거나 일정시간 뒤에 인터럽트를 발생
- 빠름
- 분주 가능 : 범위 내에서 클럭 선택 가능
- MCU 외부에서 입력되는 클럭을 세는 장치
- 비동기 모드 : 별도의 클럭을 사용하지 않고 데이터를 송수신하는 모드
- 외부핀(TOSC1,2 & T1,2,3)을 통하여 인간되는 펄스를 계수하여 동작
- 느림
- 분주 불가능 : 외부클럭 그대로 사용
그 외에 Timer/Counter를 공부하다보면 자주 나오는 개념들이 있었는데 그 개념들에 대해서도 짚고 넘어가보려 한다.
- Pulse Width Modulation의 약자로 한국어로는 펄스 폭 변조라 불린다.
- 1과 0의 구간이 제어되어 출력되는 구형파를 의미한다.
- 주로 DC 모터 또는 LED 빛의 세기를 제어할 때 사용한다.
- PWM 파형의 전체 주기 중에 1이 되는 시간의 비율을 의미한다.
- 단위 : %
- Duty Ratio = 1 출력시간 / PWM 파형 주기 * 100(%)
Max, Top, Bottom의 개념에 대해서 설명하려면 직관적으로 볼 수 있는 그림이 나을 것 같아서 열심히 아이패드로 그려왔다...ㅋㅋㅋㅋㅋㅋ
- Max : Timer/Counter가 가질 수 있는 최댓값
- Bottom : Timer/Counter가 가질 수 있는 최솟값
- Top : Timer/Counter가 도달할 수 있는 최댓값으로 사용자가 지정할 수 있으며, 오버플로우 값을 설정하거나 비교일치 레지스터를 설정함으로써 정할 수 있다.
앞서 말했듯이 ATmega128은 Timer/Counter0~Timer/Counter3까지 총 4개의 Timer/Counter을 가지고 있다.
Timer/Counter 0과 2, Timer/Counter 1과 3이 비슷한데, 우선 Timer/Counter 0&2에 대해서 먼저 공부했다.
- 8bit 업/다운 카운터이다.
- 2의 8승, 즉 256개의 펄스 계수가 가능하다.- 10bit Prescaler가 내장되어 있다.
- 오버플로우, 출력 비교 매치 인터럽트 소스를 제공한다.
이 때, Prescaler
가 무엇인지 몰라 알아보았는데, 고속의 클럭을 사용하여 타이머를 동작시킬 때, 문제가 생기는 것을 방지하기 위하여 클럭을 분주하여 더 느린 타이머를 구성한 것이다.
Timer/Counter0과 Timer/Counter2의 경우 설정 가능한 분주비에 차이가 있다.
Timer/Counter0
의 경우 가능한 분주비는 1, 8, 32, 64, 128, 256, 1024이며, Timer/Counter2
의 경우 분주비를 1, 8, 64, 256, 1024로 설정이 가능하다.
- 인터럽트 기능이 있다.
오버플로우 인터럽트
: 카운터의 값이 오버플로우 되는 경우 발생
출력 비교 인터럽트
: 카운터 값이 출력비교 레지스터의 값과 같게 되는 순간 발생
입력 캡쳐 인터럽트
: 외부로부터 트리거 신호에 의해서 카운터의 초기값을 입력캡쳐- 16비트, 즉 65536개의 펄스 계수가 가능하다.
- PWM 출력이 가능하다.
Timer/Counter 레지스터에 대해서 공부할 때는 각 bit 별로 무슨 역할을 하는지 하나하나 다 보기는 했었는데, datasheet에 상세히 잘 나와있을 뿐만 아니라, bit별로 무슨 역할을 하는지 외우면서 사용할 일이 전혀 없기 때문에 각 bit에 대해 공부한 것들은 생략하고, 레지스터가 무슨 역할을 하는지에 대해서만 정리해보았다.
- Timer/Counter 제어 레지스터
- 동작모드, Prescaler 등 Timer/Counter의 전반적인 동작형태 결정
- 강제 출력 비교, 파형 발생모드 설정, 비교매치 출력모드 설정, 분주비 설정이 가능하다.
TCCRnA
: 동작모드 설정, 비교출력 신호의 출력방식 설정TCCRnB
: 입력 캡쳐 관련 설정, 프리스케일러의 분주비 설정TCCRnC
: 비교출력 단자와 관련된 기능을 설정
- Timer/Counter n의 8비트 카운터 값을 저장하고 있는 레지스터
- 쓸 수도 있고, 읽을 수도 있음
- 자동으로 값이 갱신됨
- 16비트 구조이므로 8비트씩 2차례로 나누어 액세스
- Write 동작은 TCNTnH 수행 후 TCNTnL 수행
- Read 동작은 TCNTnL 수행 후 TCNTnH 수행
- Timer/Counter의 카운터 값이 TCNT와 비교하여 OC0 단자에 출력 신호를 발생하기 위한 8비트 값 저장
- 사용자가 설정
- Timer/Counter1,3의 경우 16bit의 값을 저장하며, CTC모드와 PWM 모드에서 사용
- 타이머 인터럽트 마스크 레지스터
- Timer/Counter가 발생하는 인터럽트를 개별적으로 허가하는 레지스터
- 타이머 인터럽트 플래그 레지스터
- Timer/Counter가 발생하는 인터럽트의 플래그를 저장하는 레지스터
- 비동기 상태 레지스터
- Timer/Counter0이 외부 클럭에 의하여 비동기 모드로 동작하는 경우 관련된 기능을 수행
- 특수기능 I/O 레지스터
- Timer/Counter들을 동기화 하는 것과 관련된 기능을 담당하는 레지스터
- 입력 캡쳐 레지스터
- ICPn 핀에서 이벤트가 발생했을 때, 그 당시의 TCNTn 레지스터 값을 ICRn 레지스터에 저장할 때 쓰임
datasheet를 보면 각 mode별 WGM
bit 설정값과, Top
값, OCR0를 변경했을 때 레지스터의 값이 업데이트 되는 시점
, 그리고 TOV0 플래그가 set 되는 시점
을 알 수 있다.
또한, datasheet를 보면 Timer/Counter의 동작모드별로 인터럽트 우선순위가 정해져 있어서 혹시나 여라 Timer/Counter을 같이 사용하게 될 경우 인터럽트 우선순위도 생각해주어야 한다.
- 일반적인 타이머 오버플로우 인터럽트 필요 시 사용
- 파형 출력X
- 상향 카운터
- 0x00~0xff 계수 동작 반복
- 카운터 도중 Clear X
- 오버플로우 인터럽트(OVF) : MAX = 0xff 값일 때 발생
- 클럭이 1번 생길 때마다 TCNT0이 1씩 증가하면서 카운트 됨
- 주파수 분주 기능으로 주로 사용
- 상향 카운터
- 0x00~OCR0 계수 동작 반복
- OCRnA의 레지스터에 원하는 값을 넣게되면 TCNT0이 OCRnA의 값만큼 증가된 후 비교매치 인터럽트 발생
- 사용자가 원하는 값으로 인터럽트를 일으킬 수 있음
- OCR0 == TCNT0이 되었을 때, TCNT0 = 0이 되므로 ISR 초기화 필요 X
CTC 모드의 경우 두 가지 모드가 있는데 4번 모드
로 설정해주면 OCRnA 값과 TCNT0 값이 일치할 때 인터럽트가 발생하게 되고, 12번 모드
로 설정해주면 ICRn 레지스터의 값과 TCNT0 값이 일치할 때 인터럽트가 발생하게 된다.
- TCNTn : 0x00~0xFF 까지 갔다가 overflow 되는 것 반복
- OCRn == TCNTn : OCn핀의 출력 값이 바뀜 (출력 값이 어떻게 바뀌느냐는 모드마다 다름)
비반전 비교 출력모드
: HIGH로 출력되다가, 비교매치에서 OCn핀에 0 출력, TCNTn이 0xFF에서 0x00으로 떨어질 때 다시 HIGH 출력
반전 비교 출력모드
: LOW로 출력되다가, 비교매치에서 OCn핀에 1출력, TCNTn이 0xFF에서 0x00으로 떨어질 때 다시 LOW 출력
- TCNTn : 0x00~0xFF로 갔다가 0x00으로 감소 (뚝 떨어지진 않음)
비반전 출력 모드
: TCNTn값이 상승할 때 TCNTn=OCRn일 때, OCn핀에 0 출력, TCNTn값이 하락할 때 일치하면 1 출력
반전 출력 모드
: 업카운트 중에 TCNTn=OCRn이면 OCn핀에 1 출력, 다운 카운트 중에 일치하면 0 출력
이번 세미나에서 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함수가 잘 작동함을 확인할 수 있다.