본문 바로가기

AVR/AVR 연습, Tutorial

AVR의 타이머/카운터 인터럽트 응용 LED시프트 순차점멸하기 - AVR 연습하기


이론 정리 다음에는 항상 실제 실험을 해왔고, 타이머/카운터 인터럽트 이론을 정리했으니 이제 

실습입니다. 타이머/카운터로 AVR 내부에서 정확히 재서 1초를 만드는 프로그램을 짜 보았습니다.

첫번째 실험은 LED로 해봤지만 타이머/카운터 인터럽트는 활용도가 높은 만큼 여러가지를 제어해볼려고 합니다.^



연습 하드웨어 회로는?




다시 처음으로 돌아와 버렸네요..! 

LED전류제한용 저항330옴과 LED 그리고 제가 만든 AVR 모듈이 사용된 회로입니다.

모듈이 없다면 브레드보드에 TINY2313으로 구성해서 사용해도 되는 초간단 회로이지요.

회로는 너무 많이 봐서 지겨우니 새로운 소스코드를 보도록 할까요!

동작은 기본제공되는 delay함수를 사용하지 않고 타이머 인터럽트로 delay함수를 만들어서

정확한 1초로 어떤 동작을 실행하는 것 입니다.


CodeVisionAVR 용 소스코드


#include <tiny2313.h>    // Tiny2313의 입출력 관련 헤더파일을 포함합니다.


int count;    // 전역변수 Count를 선언합니다.


interrupt [TIM0_OVF] void timer0_ovf_isr(void) // 중요.!! TCNT0 값 넘침(Overflow)발생시 여기로 점프

{

    if(count>50)    // Count값이 50보다 크면

        count=0;    // Count값을 0으로 만듭니다.

    else    // 아니라면

        count++;    // Count값을 1 증가합니다.


    TCNT0=0x64;    // TCNT0 값을 10진수로 100, 16진수로 64로 초기화 합니다.

}


void timer_delay(void)    // timer_delay라는 함수를 직접 만듭니다.

{

    count=0;    // Count값을 0으로 만들고

    while(count<=50){}    // Count값이 50일 때 까지 아무동작도 하지않습니다.

}


void main(void)    // main() 함수를 선언 합니다.

{

unsigned char LED=1;    // 지역변수 LED를 선언하고 1을 넣습니다. (이 변수는 main에서만 사용이 가능합니다.)

  

PORTB=0x00;    // 포트B의 초기값을 논리0 (0V)로 출력합니다.

DDRB=0xFF;    // 포트B의 모든포트를 출력으로 설정합니다.


TCCR0A=0x00;    // 타이머인터럽트 설정레지스터A의 설정값을 일반모드(Overflow)모드로 설정

TCCR0B=0x05;    // 타이머인터럽트 설정레지스터B의 설정값을 1024 분주비로 설정합니다.

TCNT0=0x64;    // TCNT0의 초기값을 10진수로 100, 16진수로 64로 설정합니다.


TIMSK=0x02;    // 타이머인터럽트의 TOIE0를 설정 Overflow를 허용합니다.


#asm("sei")    // 전체 모든인터럽트를 허용합니다.


while (1)    // 아래 구문을 무한반복합니다.

      {

        if(LED==0x80)    // LED변수 값이 0x80과 같다면

        {        

            PORTB=0x80;    // 포트B의 7번핀(0x80)에 논리1 (5V) 출력합니다.

            timer_delay();    // timer_delay를 불러 Count값 50일 때까지 위 상태를 유지합니다.

            PORTB=0x00;    // 포트B의 전체 비트에 논리0 (0V)를 출력합니다.

            LED=1;    // LED변수를 다시 1로 초기화합니다.                        

        }

        else    // LED변수값이 0x80이 아니라면

        {                                 

            PORTB=LED;    // 포트B에 변수 LED값을 출력합니다.

            timer_delay();    // timer_delay를 불러 Count값 50일 때까지 위 상태를 유지합니다.

            LED=LED<<1;    // LED변수를 1비트 왼쪽으로 시프트해서 LED변수에 저장합니다.

        }    

      }

}


프로그램을 설명드리면, 먼저 #include부분과 전역변수 선언문을 먼저 실행하고 난 후에 다음으로 

main함수부터 동작을 시킵니다. main함수를 만난 결과 B포트를 출력으로 설정하고, B포트의 초기값은 0 으로 설정 후에 타이머인터럽트 설정레지스터 A와 B를 각각 설정후에 TCNT0 값을 10진수 99으로 초기값을 설정해 줍니다.

그러면 앞 포스팅에서 설명했듯이 8Mhz를 사용중이면 0.000000125초 (0.125 마이크초)가 AVR로 입력되고 인터럽트 설정레지스터 B에 의해 1024 분주 되게 되면 0.000128 (128 마이크로 초)

가 입력되게 됩니다.

이때 AVR은 TCNT0값이 256이 되면 오버플로우가 발생하게 되는데요.

내가 원하는 20ms (밀리초)를 만들기 위해서는 TCNT0값이 156번만 카운트를 한후에 오버플로우가 발생되어야 합니다. 따라서 256-156을 한 100를 초기값으로 입력해주면 0.000128초 마다 

TCNT0값이 1씩 증가되고, 256이 되면 interrupt [TIM0_OVF] void timer0_ovf_isr(void) 이 부분으로 점프 해서 인터럽트의 {} 를 한 번 처리합니다.

그러면 20ms (밀리초)마다 저 부분으로 점프 하게 되는 것이겠죠. 그런데 또 인터럽트 처리부분에 전역변수로 Count를 만들어 두었습니다. 왜? Count값을 50으로 해두었을까요?

156을 AVR에서 카운트해서 20ms 마다 인터럽트로 점프를 하면 또 Count값으로 20ms 마다 1씩 증가해서 50을 세는 겁니다. 그러면 20ms * 50 = 1초가 되게 되죠.

이 때 만들어진 1초를 while문으로 50이 될때까지 아무 동작도 하지않도록 해서 정확한 1초 딜레이로 사용하는 것입니다.


원래 시프트 연산자만 사용해서 프로그램을 간단하게 만들어 볼려고 노력했으나, 동작이 안되서 if문으로 돌렸네요.. 복잡해도 이해해 주셔요.! 저의 한계입니다.


AVR Studio 용 소스코드


#include <avr/io.h>    // AVR의 기본 입출력 관련 헤더파일을 포함합니다.

#include <avr/interrupt.h>    // 인터럽트 관련 헤더파일을 포함합니다.


volatile int count;    // 전역변수 Count를 선언합니다.


ISR(TIMER0_OVF_vect)    // 중요.!! TCNT0 값 넘침(Overflow)발생시 여기로 점프

{

    if(count>50)    // Count값이 50보다 크면

        count=0;    // Count값을 0으로 만듭니다.

    else    // 아니라면

        count++;    // Count값을 1증가합니다.


    TCNT0=0x64;    // TCNT0값을 10진수 100, 16진수 64로 초기화합니다.

}


void timer_delay(void)    // timer_delay라는 함수를 만듭니다.

{

    count=0;    // Count값을 0으로 만들고

    while(count<=50){}    // Count값이 50이 될때까지 아무런 동작도 하지않습니다.

}


int main(void)    // main함수를 선언합니다.

{

unsigned char LED=1;    // 지역변수 LED를 선언하고 1를 넣습니다. (이 변수는 main에서만 사용이 가능합니다.)

  

PORTB=0x00;    // 포트B의 초기값을 논리0 (0V)로 출력합니다.

DDRB=0xFF;    // 포트B의 모든포트를 출력으로 설정합니다.


TCCR0A=0x00;    // 타이머인터럽트 설정레지스터A의 설정값을 일반모드(Overflow)모드로 설정

TCCR0B=0x05;    // 타이머인터럽트 설정레지스터B의 설정값을 1024 분주비로 설정합니다.

TCNT0=0x64;    // TCNT0의 초기값을 10진수로 100, 16진수로 64로 설정합니다.


TIMSK=0x02;    // 타이머인터럽트의 TOIE0를 설정 Overflow를 허용합니다.   


sei();    // 전체 모든 인터럽트를 허용합니다.


while (1)    // 아래 구문을 무한 반복합니다.

      {

        if(LED==0x80)    // LED변수 값이 0x80과 같다면

        {        

            PORTB=0x80;    // 포트B의 7번핀(0x80)에 논리1 (5V) 출력합니다.

            timer_delay();    // timer_delay를 불러 Count값 50일 때까지 위 상태를 유지합니다.

            PORTB=0x00;    // 포트B의 전체 비트에 논리0 (0V)를 출력합니다.

            LED=1;    // LED변수를 다시 1로 초기화합니다.          

        }

        else    // LED변수값이 0x80이 아니라면

        {                                 

            PORTB=LED;    // 포트B에 변수 LED값을 출력합니다.

            timer_delay();    // timer_delay를 불러 Count값 50일 때까지 위 상태를 유지합니다.

            LED=LED<<1;    // LED변수를 1비트 왼쪽으로 시프트해서 LED변수에 저장합니다.

        }    

      }


return 0;    // main함수에 0을 리턴합니다.

}


직접만든 함수가 복잡한 것 같은가요? timer_delay()라는 함수를 만들어서 복잡해진 것 같지만

사실 timer_delay()에 있는 내용

count=0;

while(count<=50){} 을 while(1)의 무한 반복구문에 있는 timer_delay(); 자리에 넣은것과 같습니다.


 if(LED==0x80)

        {        

            PORTB=0x80;

            count=0;

    while(count<=50){}

            PORTB=0x00;

            LED=1;   

        }

        else

        {                                 

            PORTB=LED;

            count=0;

    while(count<=50){}

            LED=LED<<1;

        }    


그리고 한가지 더 코드비전소스코드와 Studio 소스코드의 뭔가 다른점을 찾으셨나요?

일반적인 다른점이 아닌것은 바로 스튜디오에서의 전역변수 선언부분 volatile int count; 입니다.

코드비전에서는 int count;로 선언했는데 스튜디오는 왜? volatile를 사용했을까요?

코드비전의 경우는 실제 상용 판매되는 프로그램이라서 그런지 컴파일러가 변수에 대한 최적화를 사용에 맞게 잘 되는 것 같습니다. 하지만 GCC기반으로 동작되는 Studio에서는 인터럽트에서 단일 동작이 아닌 계속 변하는 변수의 경우에는 선언할 때 앞에 volatile를 선언하지않으면 컴파일러가 변수에 대한 최적화를 실행합니다.

ISR(TIMER0_OVF_vect)

{

    if(count>50)

        count=0;

    else

        count++; 

변수는 일단 위에서 선언되었다고 보고, 가령 이 부분에서 count값을 계속해서 50까지 세어줍니다.

저는 AVR이 하나하나 세서 50일 때 원하는 동작을 보여 주기를 원합니다.

하지만 컴파일러는 그렇지 않습니다.

어차피 하나하나 세어봤자 0으로 초기화 할껀데 뭐하러 셀까??  컴파일러는 메모리를 줄이기 위해서 나름 최적화를 진행합니다. count=0 으로 최적화를 해버린 것이죠.

그럼 제가 50일 때 동작하라고 시켜둔 프로그램은 정상으로 동작할까요? 아니겠죠!!

여기까지 제가 이해한 내용입니다.^^


프로그래밍 후 동작은?




거의 1초 단위로 오차없이 왼쪽으로 시프트 되는 모습입니다.

기존의 delay_ms를 써도 비슷하게 동작될텐데요. 딜레이함수의 경우에는 명령어처리 속도에 의존해서 시간을 지연하기 때문에 프로그램이 길어진다던가 하면 정확도가 크게 떨어진다더군요.

무선 송수신이나 적외선 리모컨신호를 받을 때도 시간이 정확해야 수신감도가 크게 오릅니다.

이건 LightDrive를 만들면서 알았죠.! 부족한 점이나 보충 할 내용이 있다면 댓글을 달아주세요.!!

저도 모르는 부분 알면 좋습니다.^