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

2013.01.27 09:29




현재 군 복무중인 상태입니다.


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

실습입니다. 타이머/카운터로 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를 만들면서 알았죠.! 부족한 점이나 보충 할 내용이 있다면 댓글을 달아주세요.!!

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


BinGoon AVR/AVR 연습, Tutorial , , , , , ,

  1. Blog Icon
    김씌

    작성자님 동영상을 보니까 시간이 안맞는 듯 하네요

  2. 안녕하세요^ 또 달아주셨네요..ㅎ
    시간이 후반부에 약간씩 틀어지는 모습을 말하시는 것이라면...
    일단 하드웨어 (전자회로) 에서 8Mhz의 크리스탈 발진회로를 사용중이고, 앞 포스팅의 클럭분주 레지스터 설정에서 계산시에 8Mhz의 경우 20ms를 생성해낼 수 있는 레지스터값이 클럭/1024분주비였습니다.
    이 때 카운트해야하는 값 TCNT0레지스터 값이 156.25회가 나오는데요.
    여기서 나오는 소수점 차이가 카운트시 오차를 만들어내는 원인인 것 같습니다.

    두번째로 동영상과 실제 동작시간이 동일하지 않은 것을 말하시는 것이라면..
    스톱워치를 동영상 시작과 동시에 누른것이 아니고, 적당한 1초를 맞추어 누르고,
    시간 틀어짐이 없이 LED가 시프트 되는 것을 보여주기 위해서 중도에 스톱워치를 동작시켰 습니다.

  3. Blog Icon
    김씌

    아 그렇군요
    이해했습니다 ㅋ.ㅋ
    작성자님 그럼 이제 다음 포스팅은 무슨 내용으로 하실건가여?

  4. 블루투스 로봇을 완성하면, 타이머 인터럽트로 몇가지 더 해보고 통신이나, ADC글을 올릴까 합니다.
    마이크로 컨트롤러에서 타이머/카운터가 활용도가 가장높고 비중이 크기 때문에 좀 더 들어갈지도 모릅니다.^
    원하시는 포스팅 이라도?? 있으신가요??

  5. Blog Icon
    김씌

    dc모터나 스태핑 모터를 구동시키는
    소스가 궁금해요!

  6. 이번주나 다음주 중으로 DC모터 제어를 PWM포함해서 포스팅작업하겠습니다^
    자주찾아주셔서 감사합니다^

  7. Blog Icon
    김씌

    궁금한게 있는데요
    codevision 소스 인터럽트 부분에서 if(count<50)
    count=0;
    이거 지워도 동작에 이상없지안나여?
    아닌감.,.

  8. count 변수값을 최대 50으로 제한하기 위해서 조건문으로 비교후에 50이면 0으로 초기화되도록 프로그래밍 했습니다.
    인터럽트 부분에 count++ 만 사용해도 되지만 메모리에 어떤영향을 끼칠지 실험해보지 않아서 문제가 있을지 없을지는 실험해봐야 알 것 같습니다.
    예상은 코드비전에서는 문제없이 동작할 것 같습니다만
    스튜디오에서는 다음과 같은 문제점이 있을 것 같습니다.
    먼저.. count 변수 선언 앞에 volatile 선언으로 해당변수의 코드를 (메모리 사용량을 줄이기 위해서) 최적화를 실행하지않는데요.
    이 경우 다른 함수에서 변수를 사용하지 않는 경우에 변수값이 지속적으로 증가해서 메모리를 대폭 사용하는 문제점이 발생 할 것으로 예측합니다.
    저도 잘 모르는 것에 대한 댓글은 감사드립니다. ㅎ

    ps. 혹시 울산에 사시는 분이신가요?

  9. Blog Icon

    비밀댓글입니다

  10. 지적 감사드립니다.
    정정하도록 하겠습니다^

  11. Blog Icon
    avr초짜

    안녕하세요^^*

    궁금 한것이 있는데요

    while 문에서 timer_delay 안쓰면 1초 동작이 안되나요?

  12. 안녕하세요.^^
    연휴 관계로 답변이 늦어서 죄송합니다. while문으로 시간을 지정한 이유는 타이머, 카운터 값이 증가하는 동안 동작하는 방식으로 구성되었기 때문에 while문을 사용하였는데요, timer_delay함수가 timer_delay(void)로 void로 따로 입력되는 값 없이 무조건 1초로만 동작하도록 만든 함수입니다. 질문해 주신분이 원하는것에 따라서 timer_delay(int time) 같은 선언을 한 후 함수 내용을 수정해 주시면 timer_delay(5); 라는 식으로 입력했을 때 5초 동안 지연되도록 할 수 있습니다.^^

  13. Blog Icon
    한뜻

    안녕하세요^^초보라 헤메고 있네요~
    코드비젼 사용중입니다. 자작한 제 하드웨어2313헤드는 외부오실레이터 24.000짜리를 사용했습니다.
    시간계산이 안되어 외부 오실레이터를 빼고 내부로 쓰려니 에러증상이나오더라고요.. 별도로 코드비전에서 외부내부 설정을 바꿔줘야 하는지요? 그리고 그냥 외부 오실레이터를 사용하면 설정값을 바꿔줘야겠죠?
    현재 상태는 0.3초정도로 쉬프트 하고있네요..

  14. 연휴기간이라서 답변이 늦어 죄송합니다.
    질문주신 내부 외부 오실레이터 설정은 http://binworld.kr/47 포스팅에서 확인 할수 있듯이 퓨즈비트중 오실레이터 설정에 관련된 퓨즈비트를 변경하신후 적용해야 합니다. 따라서CodevisionAVR에서 퓨즈비트 설정을 변경하셔야, 내부 RC오실레이터 적용이 됩니다. 참조해주세요. 그리고 혹시 퓨즈비트를 잘못 설정하게 되면, AVR에 클럭이 들어가지않아서 AVR인공호흡이라는 강제로 XTAL1핀에 클럭을 넣어주는 과정을 거쳐야되니 주의하시기 바랍니다.

  15. Blog Icon
    한뜻

    추가 질문 프리스케일러 계산할때 방법이 어떻게 되는거죠??
    클럭그대로 사용시
    클럭/8
    클럭/64
    클럭/256
    클럭/1024
    앞의이론은 봤지만.. 방법을 모르겠어서 질문 드립니다..

  16. 안녕하세요. 역시 늦어서 죄송합니다.
    프리스케일러는 만약 AVR에 외부 크리스탈이 8Mhz라면 8백만번 반복해서 들어오는 신호가 한번 들어오는 시간은 1/8000000입니다. 계산하게 되면 0.000000125초가 되죠. 그런데 여기서 프리스케일러는 이 신호가 8번 들어올때 한번 신호가 입력된걸로 또는 64번 256번 1024번 들어왔을때 입력되도록 분주하는 하드웨어 입니다. 따라서 프리스케일러를 8로 하게되면 0.000000125초가 8번 들어왔을때 한번 입력되는 시간은 0.000000125 x 8 한 0.000001초가 되지요. 이제 이 값으로 타이머 오버플로우 기능을 사용하시게 되면 0.000001 x 256을 했을 때 시간에 Studio4 기준 ISR(TIMER_OVF_vect0)부분으로 점프해서 이부분을 처리하게 됩니다.

    혹시 더 부족한 부분있다면 댓글달아주세요.

  17. Blog Icon

    비밀댓글입니다

  18. Blog Icon

    비밀댓글입니다

티스토리 툴바