반응형

[다시 쓰는 8051 따라하기] 디지탈 시계를 만들기 위한 기초작업
+------------------------------------------------------+
게시장소 : 개인블로그와 다음카페 로봇자작천국
작 성 자 : Timy(me^^;)
작 성 일 : 2004.11.3
문    의 : 다음카페 http://cafe.daum.net/tinyrobo 혹은
           개인블로그 http://electoy.cafe24.com/blog
+------------------------------------------------------+
[시작]

## 디지탈 시계를 만들기 위한 기초작업

인터럽트를 이용한 타이머로 정확한 시간에 신호보내기

인터럽트를 이용하지 않고 일반 딜레이함수를 사용할 경우, 어쩔수 없는 시간 지연이 생기게 된다. 적어도 인터럽트가 걸리는 순간, 일반 프로그램은 실행이 정지되고 인터럽트가 우선 실행되게 된다. 이전에 만든 딜레이함수는 인터럽트를 쓰지 않고 사용할 수 있게 작성되었다. 그래서 일반적인 경우에는 대충 맞게 쓸수 있지만 정확한 시간을 맞출수는 없다.

그래서 이번엔 타이머 0를 사용해서 필요한 시간을 만드는 방법을 보도록 하겠다.
우선 기본적으로 8051 에서의 인터럽트는 S5P2 라고 알려져 있다. 이것은 앞에서 한번 언급했었는데 인터럽트가 걸리는 순간은 10번째 오실레이터 시간이라고 했다. 8051에서 가장 작은 단위의 명령이 수행되는데 걸리는 시간은 12 OSC 타임이다. 즉 12번 오실레이터가 깜박여야 작은 명령 하나가 실행된다는 의미다. 이렇게 12번 오실레이터가 신호를 보내는 시간은 오실레이터의 주파수에 좌우한다.

TY51 에서 사용한 오실레이터는 24MHz 짜리를 사용했다. 즉 한번 오실레이터가 신호를 보낼때 걸리는 시간은 1/24M 라는 의미다. 1초에 24M 번 진동하는 오실레이터가 1번 진동에 걸리는 시간은 역수다(이건 중학교 산수도 아니다. 초등학교 산수다 ^^).

그래서 1번 진동에 걸리는 시간은 1/24M , 여기에 12번 진동해야 한번 명령이 실행된다고 했으니까 명령을 수행하는데 걸리는 시간단위는 여기에 12를 곱해주면 된다. 즉 12*1/24M 가 된다. 이것을 계산하면 0.5 us 가 계산된다. 즉, 8051 에서 24MHz OSC 를 달았을때 하나의 명령은 0.5 us 가 소요된다는 의미다. 물론 명령중에 조금 복잡한 명령은 이런 시간의 두배 에서 네배까지 걸리는 명령도 있다. 즉, 가장 간단한 명령은 0.5 us 그보다 복잡한 명령은 1 us, 가장 복잡한 명령은 2 us 가 걸리는 셈이다. 여기서 가장 작은 명령에 걸리는 시간을 Machine Time (혹은 Cycle) 라고 부른다. 가장 기초적인 명령 하나를 처리하는데 걸리는 시간이 사실 마이크로 프로세서에서는 가장 중요한 것이니까 말이다...
 
자, 그러면 이제 조금 더 머리를 쓰자. 12번 오실레이터가 진동하는데 (발작하는데) 걸리는 시간이 0.5 us 라고 했다. 그러면 인터럽트를 건다고 했을때 인터럽트 체크의 간격은 얼마가 될까? 인터럽트도 역시 1 Machine Cycle 에 해당하는 0.5 us 마다 한번씩 체크하게 된다. 10번째 오실레이터 진동때 인터럽트를 검사한다고 했다. 그러니까 인터럽트도 매 10번째 진동을 체크하는거니까 12번마다 하는 셈이다.

그리고 이것은 S5P2 라고도 표현한다. 지난번에 링크에 걸어둔 자료에서 찾았다. 12번의 오실레이터의 진동을 6번의 사이클로 나누고 6번의 사이클을 각각 두번의 펄스로 나눴다. 그래서 5번째 사이클의 2번째 펄스라는 의미로 10번째 오실레이터 진동의 s5p2 로 표현한다. 이것을 8051에서는 그다지 큰 의미는 없어보인다. 다만 앞으로 유사품들을 보게 되면 체크를 S5P2 말고 다른 곳에서 하는 칩들이 있다. 그때는 어느 곳을 먼저 체크하는지 살필 필요가 있겠다.

사설이 길었다. 이렇게 인터럽트체크간격도 0.5 us 라는 것을 알았다. 이제 그것을 이용해서 정확한 시간을 보내는 방법을 한번 알아보자.

인터럽트에서 타이머와 카운터에 대해서 지난번에 간단히 짚고 넘어갔다. 사용법에 대해서는 전혀 언급을 하지 않았다. 이제 그걸 볼 시간이 되었다.

8051에서 기본적으로 타이머/카운터는 두개 제공된다. 두개를 다 써도 되고 하나만 써도 되고 안써도 된다. 8052에서는 (AT89S52) 타이머가 하나 더 추가되었다. 타이머를 하나 더 쓸수 있다는 말이다. 이건 꽤 장점이 있다. 나중에 해보면 타이머 하나 더 있는게 왜 좋은지 알 것이다.

타이머와 카운터는 같은 것이다. 보통 카운터는 외부 핀에 연결시켜서 그 핀에 신호가 오면 인터럽트를 거는 방식으로 사용된다. 카운터를 이용한 예제는 워낙 널렸으니 별로 소개할 마음도 없다. ^^ 나중에 프로그램중에 또 나오면 그때 보자.

타이머는 외부 핀 입력대신 오실레이터에서 나오는 신호를 입력 받는 것이다. 그러니까 외부 입력을 아주 정교하게 정확하게 하고 있는 것이나 마찬가지다. 버튼 키를 하나 두고 거기에 외부 입력으로 누를때마다 신호를 받아서 인터럽트를 처리하는 것이 카운터고 버튼 키 대신에 내부에 있는 오실레이터에서 나오는 주기적이고 정확한 신호를 받아서 처리하는 것이 바로 타이머다. 그러니까 카운터와 타이머는 사실 다른 것이 아니다.

타이머와 카운터를 구별짓는 것은 그래서 단 하나 TMOD 의 3번째와 7번째 비트로 처리한다. 3번째 비트는 타이머0 를 쓸 것인지 카운터0를 쓸 것인지 묻는 것이고, 7번째 비트는 타이머1을 쓸 것인지 카운터1을 쓸 것인지 묻는 것이다. 우리는 타이머를 쓰니까 TMOD 의 3번째와 7번째 비트는 0을 주겠다. 그리고 타이머를 쓸때 동작 모드가 네종류가 있다.

모드0는 13비트 프리스케일러 모드라 불리는 것으로 MCS48 호환을 위해 만들어 둔 것이다. 사용할 일이 없다. 많이 쓰는 것은 모드 1과 모드2이다.
모드1은 16비트로 기능하는 타이머인데, 8051의 레지스트는 8비트인데 16비트를 쓰기 위해 카운터 레지스트인 TH0 TL0 TH1 TL1 을 각각 합쳐서 사용하는 것이다. 그러니까 타이머0이세넌 TH0와 TL0 를 쓴다. 여기서 셀수 있는 범위는 16비트이므로 0부터 0xFFFF 까지, 즉 65536 을 셀수 있다. 8비트에서 셀수 있는 범위가 0부터 0xFF 즉, 256 까지인것에 비해서 상당히 큰 수를 셀수 있다.

모드는 TMOD 레지스트를 써서 결정해 준다.

TMOD : [[GATE] [C/T] [M1] [M0]]TIMER_1 | [[GATE] [C/T] [M1] [M0]]TIMER_0

GATE 는 0으로 둔다. 타이머 카운터를 소프트웨어적으로 구동한다.
C/T 는 0이면 TIMER 로 동작한다. 1이면 COUNTER, 여기서는 0으로 한다.
M1, M0 는 두 비트가 합해서 의미를 가진다. 0 0 은 모드0, 0 1 은 모드I, 1 0 은 모드II, 1 1 은 모드III 이다. 각 모드의 특징은 아래를 봐라.

여기서 TMOD 는 0x22 로 설정해 둔다.

사실 인터럽트가 너무 자주 걸리는 것도 좋은 것이 아니다. 그래서 16비트로 사용하는 모드1을 쓰는 것이 추천되고 있는 듯 하다.

하지만 여기서는 모드2를 사용하겠다. 모드2는 또 다른 특징이 있는데, 모드1이 16비트를 사용해서 많은 숫자를 가질수 있다는 장점이 있다면 모드2는 초기값을 설정해서 가질수 있다는 장점이 있다.

이게 무슨 말인고하니 모드2에서는 셀수 있는 용량은 8비트로 0부터 255까지 밖에 되지 않는다. 하지만 처음 시작할때 초기 값을 0부터 시작하는 것이 아니라 0부터 255까지 중 어느 것이든 선택해서 시작할 수 있다는 것이다.

다시말해 모드1에서는 0부터 0xFFFF 까지 세고, 그 다음번 셀때 0xFFFF + 1 범위를 넘게 된다. 이것을 오버플로우라고 하는데 16비트의 범위를 넘게 되는 순간 인터럽트는 그 순간을 알려주고 인터럽트 루틴을 수행하게 된다. 인터럽트 루틴이란 일종의 프로그램이다. 인터럽트가 걸리는 순간, 바로 실행되도록 만들어둔 프로그램이라고 생각하면 된다. 이를테면 TV 보다가 초인종이 울리면 나가서 문을 열어주듯이 인터럽트가 걸리면 그때 필요한 일을 해주게 된다.

16비트를 쓰는 모드1에서는 0부터 0xFFFF 까지 가고, 거기서 1 증가되면 오버플로우가 되면서 인터럽트 루틴이 수행된다. 그러니까 0부터 0xFFFF 까지는 매 머신사이클마다 인터럽트가 오버플로우 되었는지만 체크하고 아무것도 안하고 있는 것이다. 그러다가 넘는 순간 숨겨뒀던 프로그램을 실행시키는 것이다.

그런덴 모드2에서는 처음 시작 위치를 설정할 수 있다. 그러니까 TH0 에 어떤 값을 두면 그 값이 초기값이 되는 것이다. 8비트 타이머니까 8비트짜리 레지스트하나면 사실 된다. 그래서 TL0 레지스트를 사용한다. 모드2에서는 TL0 레지스트에 TH0 레지스트안에 있는 값을 복사해 넣는다. 그리고 거기서 1씩 증가시켜간다. 증가시키다가 오버플로우가 되는 지점, 즉, 0xFF 에서 1이 더해지는 시점에 TL0 의 값을 0으로 돌리는 대신 TH0 에 저장되어 있던 값을 넣어주는 것이다.

여기서 우리는 TH0 에 56 이라는 값을 넣어주었다. (이것은 10진수다 ^^). 왜 56이냐면...
우선 0xFF 까지가 한계라는 것을 말했다. 즉, 0부터 0xFF 까지, 256개의 범위에서 56을 빼면 200 이라는 숫자가 나온다. TL0 가 56부터 시작하면 256까지 가서 다시 0 대신 56이 될때까지 정확하게 200번 인터럽트를 체크하는 단계를 거치게 된다. 즉, 200번 * 1 머신 타임 = 200 * 0.5 us, 그래서 100us 라는 시간이 나온다.

TH0 에 56을 넣고, 타이머0를 8비트 재설정모드 (모드2)로 하면 이런 장점이 있는 것이다.
그 다음에 인터럽트 서비스 루틴내에 unsigned char 형 변수 1씩 증가시키는 루틴을 둔다. 변수는 global 로 설정해 둔다. 그러면 인터럽트가 걸릴때마다 , 즉 TL0 값이 56에서 출발해서 0xFF 에서 다시 56 이 될 때마다, 설정된 변수는 1씩 증가한다. 이 변수가 100을 넘으면 변수에서 100을 빼준다.

이렇게 해주면 변수가 0부터 100까지 증가하는 동안 걸리는 시간은 100 * 100us = 10 ms 인 셈이다.
이 시간은 정확하다. 그리고 만약 인터럽트를 이용해서 걸리는 시간이 부정확하다면 다른 곳에 문제가 있는 셈인데, 그것은 설정이 잘못되었을 경우가 가장 많고 - 실수했다는 말이다. ^^ -, 다음으로는 인터럽트의 Priority 관계를 이해하지 못해서 생긴 경우도 발생할 수 있다.

인터럽트는 한번에 하나만 발생하란 법이 없다. 두개 이상이 동시에 발생할 수 있다. 그때 어떻게 할 것인가? 타이머0를 사용하고 있는데 그때 동시에 타이머1에 사건이 생겼다. 그러면서 시리얼로 신호가 들어오는 시리얼인터럽트까지 걸렸다. 그러면 어떻게 될까? 그래서 인터럽트간에는 우선순위가 있다.

인터럽트 우선순위는 이전에 말해줬다. 찾아보기 바란다.
어쨌거나 타이머0는 우선순위가 꽤 높다. 그래서 사실 어떤 것도 신경써주지 않아도 된다. 별도로 뭘하지만 않으면 말이다. 그래도 걱정이 된다면 우선순위를 높여주는 명령을 주자. 이 명령을 주면 우선순위가 낮던 녀석도 우선순위가 높아질 수 있다.

여기 쓰이는 레지스터는 IP 레지스터다. IP 는 다음과 같이 되어 있다.

[-] [-] [PT2] [PS] [PT1] [PX1] [PT0] [PX0]

여기서 맨 아래쪽 [PX0] 가  외부 인터럽트 /INT0 인터럽트 프라이어티 비트과 [PT0] 가 타이머0 인터럽트 프라이어티다. 여기서 내가 PRIORITY 를 높여주고 싶은 것만 1로 체크해주면 된다. 다른 것은 그냥 있던 대로 두고 [PT0] 비트만 1로 설정하자. 그러면 TIMER0 의 Priority 가 최고로 높아진다. 다른 인터럽트가 걸려도 우선적으로 Timer0 인터럽트 루틴이 먼저 수행된다. 그러면 시간이 지연되거나 디지탈시계가 늦어지는 불상사는 없어질 것이다.

사용법은 간단하다.

IP   = 0x02;

라고 적어주면 된다. 우선 설명은 SDCC 환경에서 한다. 사실 어셈블러에서 컴파일해도 비슷하다. 그건 스스로 해보기를 바란다. ^^ (귀찮아서 그런다.. ㅜ.ㅜ)

다음, 또 봐야 할 것은 인터럽트 가능 레지스터 IE (Interrupt Enable Register) 이다. 다음과 같다.

[EA] [-] [ET2] [ES] [ET1] [EX1] [ET0] [EX0]

위와 같이 8개의 비트다. 맨 왼쪽 것 [EA]는 0을 넣으면 전체 인터럽트가 금지되고, 1이 되면 아래쪽에 있는 6개의 비트가 가진 값을 사용하게 된다. [EA]가 0이면 아래쪽에 어떤 값이 있어도 무시되고 인터럽트를 쓰지 않는다는 말이다.
여기서 우리는 [ES] [ET1] [ET0]이렇게 세개의 비트를 1로 설정하고 나머지는 0으로 두겠다. ES 는 Enable Serial 의 약자로 시리얼인터럽트를 사용하게 하겠다는 것이고, ET1 과  ET0 는 타이머1과 타이머0를 사용가능하게 하겠다는 의미다. 현재  ET2 와 EX0 EX0 는 쓰지 않는다. 그 비트들은 0으로 설정한다.

IE = 0x9A; //(= 1001 1010 B)

이제 TCON 레지스트만 보면 된다. 다음과 같다.

[TF1] [TR1] [TF0] [TR0] [?] [?] [?] [?]

TF1, TF0 는 타이머1과 0의 오버플로우 플래그다. 사용자가 설정하는 값이 아니다. 인터럽트를 체크하면서 하드웨어적으로 자동 세트되고, 오버플로우 발생시 인터럽트 서비스 루틴(ISR : 앞으로 ISR 로 부르겠다.. 쓰기가 귀찮아져서.. ^^), 을 실행시킨 후 다시 0으로 자동 소거된다.

TR1, TR0 는 Timer1 or 0 run control bit 로 타이머를 On/Off 시킬때 소프트웨어적으로 할수 있도록 설정하는 것이다. TR0 와 TR1 을 각각 1로 설정해준다. 나머지 비트는 건드릴 필요 없고, TCON 은 비트별로 제어가 가능하므로

TR0 = 1;
TR1 = 1;

이렇게 해주면 된다.

이제 설정할 것은 끝난 셈이다.


1. 초기화 : 인터럽트 설정

   IE   = 0x9A;                // Interrrupt Enable : Serial, Timer0, Timer1
   IP   = 0x02;                // priority Timer0;
   TMOD = 0x22;                // timer mode initialize. T0 & T1 8bit Timer Set
   TH0  = 56;                // 100us timer set
   TH1  = 243;                // TIMER1 FOR SERIAL COMMUNICATION
   EA   = 0x9A;                // enable interrupt
   ET0  = 1;                // interrupt overflow
   ET1  = 1;
   TR0  = 1;                   // start counting the timer0  
   TR1  = 1;

2. ISR (Interrupt Service Routine) 내 처리

   count ++ ;
  
3. 메인 함수

인터럽트 설정, 초기화
무한루프 {
        (카운터 >= 100) 이면
                { P_1 을 반전시킴
                카운터 = 카운터 - 100 }
  }
  
위와 같은 형식으로 프로그램을 짜면 된다. 한가지 주의 할 것은 , ISR 내에는 가능하면 프로그램을 간소화시켜야 한다. 정상적인 main() 함수가 실행되다가 인터럽트가 걸려서 ISR을 호출하여 그 안에 있는 프로그램을 호출시킬때 너무 긴 프로그램이 실행되면 여기서야 별 문제가 없겠지만, 다른 인터럽트가 걸리는 등 문제가 발생하고 머리가 복잡해 질수 있다. 가능하면 ISR 내에서는 int 타입의 변수를 쓰지 말것을 권한다. 그리고 어셈명령으로 복잡해서 두 머신 사이클이 필요한 루틴은 사용하지 않는 것이 좋다. priority 가 더 높은 인터럽트가 걸렸을때 이미 두 머신 사이클이 필요한 루틴이 수행중이면 우선순위의 인터럽트와 이미 수행중인 루틴 사이에 문제가 발생한다.

그럼, 여기까지 짠 프로그램을 한번 보도록 하자.

10 ms 마다 P2 에 달려있는 LED 가 옆으로 한칸씩 이동해가면 점멸하는 프로그램이다. 반전시간이 정확한 것이 장점이다. ^^ 사실 너무 빨라서 눈으로 보면 옆으로 엄청난 속도로 이동하는 것을 느낄수 있다.^^
여기 이 루틴을 이용해서 디지탈 시계를 제작해 보도록 하자.


//------------------------- 프로그램 시작 -----------------------------//

#include "at89x52.h"

unsigned char count = 0 ;

void init_T0 (void)        // 24MHz X-TAL
{
   IE   = 0x9A;                // Interrrupt Enable : Serial, Timer0, Timer1
   IP   = 0x02;                // priority Timer0;
   TMOD = 0x22;                // timer mode initialize. T0 & T1 8bit Timer Set
   TH0  = 56;                // 100us timer set
   TH1  = 243;                // TIMER1 FOR SERIAL COMMUNICATION
   IE   = 0x9A;                // enable interrupt
   ET0  = 1;                // interrupt overflow
   ET1  = 1;
   TR0  = 1;                        // start counting the timer0  
   TR1  = 1;
}

void T0_int (void) interrupt 1 using 1  // Using Bank1 (can use 0 to 3)
{
   count ++ ;
}

void main(void)
{
   init_T0 () ;                     // initialize timer
   P2 = 1;

   while(1) {
        if (count >= 100) {
            P2 = (P2 << 1);
            if (P2==0) P2 = 1;
            count = count - 100;
            }   // 10ms = 100 * 100 us
   }
}

//------------------------- 프로그램 끝 -----------------------------//


[끝]
+------------------------------------------------------+
반응형

+ Recent posts