반응형

마이크로프로세서를 사용할 때 타이머라는 것을 사용하게 된다. 필자가 알기로는 타이머가 없는 마이크로프로세서는 없다. 마이크로프로세서에서 타이머는 당연히 있어야 하는 것이다.

그렇다면 타이머라는 것이 무엇인가?
우선 지금 우리는 신호등이라는 것을 만들고 있다. 신호등을 제대로 만들기 위해서는 최소한 불이 켜지고 꺼지는 시간을 정확히 할 수 있어야 한다.

보통 일정시간동안 LED에 불이 켜져 있도록 하기 위해서 일반적으로 딜레이함수라는 것을 사용하게 되는데, 이 딜레이 함수는 정확한 시간을 낼 수가 없다.

딜레이 함수는 다음과 같이 만들어진다.

 

delay()
{
    int i;
    for(i=0;i<1000;i++) {
    }
}

 

이런 식으로 함수 내부에서 아무런 일도 하지 않으면서 그냥 시간을 죽이는 그런 함수다. 이걸로 LED 에 불을 일정시간 켜두기 위해서는

 

LED 켠다;
딜레이함수 실행;
LED 끈다;

 

이런 식으로 프로그램을 짠다. 그러면 일정시간 LED 가 켜져있다가 꺼지게 된다. 하지만 문제는 이 딜레이함수는 정확한 시간을 계산하기가 꽤 어렵다는 점이다.

그래서 8051 에서 사용하게 되는 것이 타이머라는 것이다.

이 타이머를 알기 위해서는 먼저 인터럽트라는 개념을 조금 알아둘 필요가 있다. 인터럽트는 중요한 일이 발생하면 하던 일을 잠시 멈추고 중요한 일을 먼저하는 것을 말한다. 인터럽트가 발생했다는 말은 지금 하던 일보다 아주~ 중요한 일이 발생해서 하던 일을 잠시 중단하고 그 일(인터럽트를 건 일)을 해야만 하게 되었다는 뜻이다.

무슨 일이기에 그렇게 중요한 일일까? ^^ 그거야 나도 모르지, 다만 8051 을 만든 사람들은 인터럽트란 걸 만들어 뒀고, 그걸 잘 이용하면 우리는 아주 편하게 8051 을 사용할 수 있다는 뜻이다.

이렇게 생각하면 된다. 내가 전자렌지에 컵라면을 넣고 끓이고 있다. 그런데 생각해보니 시간을 잘못 설정했다. 3분을 설정해 둬야 하는데 30분을 설정해 뒀다. 이걸 어떻게 하나? 하지만 우리는 별로 걱정하지 않는다. 그냥 문을 열면 30분으로 설정되어 있지만 전자렌지는 알아서 꺼진다. 인터럽트가 걸린게다. 문을 여는 곳에 센서를 부착해서 문을 여는 인터럽트가 걸리면 어떤 일을 하다가로 즉시로 중지하도록 설정을 해 둔 것이다.

이 인터럽트하고 타이머가 무슨 상관일까? 어떤 일을 하다가 스위치가 또깍하고 걸리면 인터럽트가 걸리게 되는데, 이 스위치를 눈에 보이지 않는 것으로 해보자. 그러니까 TY52 에는 24MHz 짜리 오실레이터가 있다. 그건 1초에 2천4백만번 켜지고 꺼지기를 반복한다는 의미다. (되게 자주 한다... ^^;)

이때 켜지고 꺼지는 것을 한 번의 스위치 온, 오프로 생각할 수 있다. 타이머는 이렇게 오실레이터에 의한 스위치 온, 오프의 갯수를 일정하게 세는 것이다.

그러니까 1초에 2천4백만 번을 깜박일 때 1번 타이머가 걸리게 된다고 생각해보자. 이 타이머는 정확히 1초에 한번 걸리게 된다. 다시 말해 1초에 한번 어떤 신호를 줄 수 있다는 말이다. 이것을 인터럽트라고 하고, 이때 우리는 어떤 일을 하라고 8051 에게 명령할 수 있다. 이것을 인터럽트루틴, 보통 ISR(Interrupt Service Routine)이라고 부르는 것이다.

 

설명이 장황했다.

8051 에는 타이머0 부터 타이머2 까지 있다.
여기서는 타이머 0번을 사용해서 필요한 시간을 만드는 방법을 보도록 하겠다.

우선 기본적으로 8051 에서의 인터럽트는 S5P2 라고 알려져 있다. 이것이 무슨 소린고 하니(몰라도 된다^^) 8051의 인터럽트가 걸리는 순간은 10번째 오실레이터 시간이라는 의미다. 8051에서 가장 작은 단위의 명령이 수행되는데 걸리는 시간은 12 OSC 타임(12번 오실레이터가 깜박이는 시간)이다. 이걸 머신사이클이라고도 부른다. 즉 12번 오실레이터가 깜박여야 작은 명령 하나가 실행된다는 의미다. 이렇게 12번 오실레이터가 신호를 보내는 시간은 오실레이터의 주파수에 좌우한다. 이 12번중 5번째 State 의 2번째 Pulse, 즉 10번째 신호때 인터럽트가 발생된다.

 

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

그래서 1번 진동에 걸리는 시간은 1/24M , 여기에 12번 진동해야 한번 명령이 실행된다고 했으니까 명령을 수행하는데 걸리는 시간단위는 여기에 12를 곱해주면 된다. 즉 12*1/24M 가 된다. 이것을 계산하면 0.5 us 가 계산된다. 즉, 8051 에서 24MHz OSC 를 달았을 때 가장 작은 하나의 명령(이를테면 nop 같은 것)은 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을 주겠다. 그리고 타이머를 쓸때 동작 모드가 네종류가 있다. TMOD가 뭔지는 아직 설명하지 못하겠다. 우선 공부하고 싶은 사람을 위해서 알려줄 것은 SFR 이라는 특별한 레지스터를 사용하여 인터럽트를 설정한다는 것이다. 레지스터의 이름중에 TMOD 라는 것이 있고, 이걸 사용해서 타이머를 설정한다.

 

모드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]]| [[GATE] [C/T] [M1] [M0]]
          TIMER_1       |          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 는 모드1 로 설정해 둔다.

 

Timer0 : [0], [0]-Timer, [10]-모드2

TMOD &= 0xf0;
TMOD |= 0x01;

 

이렇게하면 TMOD 의 앞 4개 비트는 이전 값을 그대로 가지고 있으면서 뒤의 4개 비트는 0001이 된다.

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

여기서도 모드1을 사용하겠다.

모드1에서는 셀 수 있는 용량은 16비트로 0부터 0xffff 까지 65536개의 값을 가질 수 있다. 초기값은 0이지만 그 값은 TH0, TL0 값을 조절해서 설정할 수 있다.

 

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

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

 

여기서 약간의 트릭을 써서 TH0 와 TL0 값을 넣어줌으로 인터럽트가 걸리는 간격을 65536 에서 20000 으로 변화시켜줄 수 있다.

그러니까, TH0 와 TL0 값을 0에서 시작하는 대신 65536-20000 에서 시작하는 것이다. 이 값을 계산하면 45535가 되고, 16진수로 0xB1DF가 된다. 즉, TH0 에 0xBF 를 TL0에 0xDF 를 넣어주면 된다.

 

물론 이 값은 인터럽트 루틴초기에도 넣어주어야 한다.

왜 2만이라는 값을 넣었을까? 이런 생각을 했다면 잘하고 있는 것이다. ^^

 

우선 오실레이터 1회 진동시간은 1/24M sec 다. 그때 8051 머신사이클은 12배를 해주면 된다(12회 진동시 1번). 즉, 매회 인터럽트 체크에 걸리는 시간은 1/24M * 12 즉, 1/2M sec 가 된다. 계산하면 0.5us 가 된다. 여기에 2만을 곱하면 10ms 가 된다. 즉, 타이머 인터럽트가 정확히 10ms 마다 걸리는 것이다. 이 10ms 를 가지고 내가 원하는 시간을 만들어 낼 수 있다.

그리고, 주의할 것이 있다. 인터럽트는 한번에 하나만 발생하란 법이 없다. 두개 이상이 동시에 발생할 수 있다. 그때 어떻게 할 것인가? 타이머 인터럽트를 두개 사용할 수도 있다. 8051 에는 인터럽트가 5개 걸릴 수 있다. 타이머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 환경에서 한다. 사실 어셈블러에서 컴파일해도 비슷하다. 그건 스스로 해보기를 바란다. ^^ (귀찮아서 그런다.. ㅜ.ㅜ) 여기서는 타이머0 하나만 사용하므로 IP 레지스터를 사용하지 않는다.

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

 

[EA] [-] [ET2] [ES] [ET1] [EX1] [ET0] [EX0]
1 0 0 0  0 0 1 0 (2진수)
= 0x82

 

위와 같이 8개의 비트다. 맨 왼쪽 것 [EA]는 0을 넣으면 전체 인터럽트가 금지되고, 1이 되면 아래쪽에 있는 6개의 비트가 가진 값을 사용하게 된다. [EA]가 0이면 아래쪽에 어떤 값이 있어도 무시되고 인터럽트를 쓰지 않는다는 말이다.

 

IE = 0x82;

 

라고 해도 되지만 보통

 

EA = 1;
ET0 = 1;

 

이렇게 사용하기도 한다. EA 와 ET0 는 IE 레지스터 안의 개별 비트의 이름이다.

여기서 우리는 [EA], [ET0] 이렇게 두개 비트를 1로 설정하고 나머지는 0으로 두겠다.

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

 

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

 

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

 

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

 

TR0 = 1;

 

이렇게 해주면 된다.

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


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

 

TMOD &= 0xF0;
    /* Timer 0 mode 2 counter with software gate */
TMOD |= 0x01;
    /* GATE0=0; C/T0#=0; M10=0; M00=1; */
TH0 = 0xB1; /* 초기화 */
TL0 = 0xDF; /* 초기화 */
ET0=1;      /* enable timer0 interrupt */   
EA=1;       /* enable interrupts */
TR0=1;      /* Timer0 interrupts Start*/


2. ISR (Interrupt Service Routine) 내 처리

 

TH0 = 0xB1; /* 초기화 */
TL0 = 0xDF; /* 초기화 */
count++ ;

 

3. 메인 함수

 

인터럽트 설정, 초기화
무한루프 {
    (카운터 >= 100) 이면 
    { P_1 을 반전시킴 
    카운터 = 카운터 - 100 }
}

 

위와 같은 형식으로 프로그램을 짜면 된다. 한가지 주의 할 것은 , ISR 내에는 가능하면 프로그램을 간소화시켜야 한다. 정상적인 main() 함수가 실행되다가 인터럽트가 걸려서 ISR을 호출하여 그 안에 있는 프로그램을 호출시킬 때 너무 긴 프로그램이 실행되면 여기서야 별 문제가 없겠지만, 다른 인터럽트가 걸리는 등 문제가 발생하고 머리가 복잡해 질수 있다. 가능한 인터럽트 함수는 짧고 간략하게 만들라.

 

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

10 ms 마다 P1_1 이 반전되는 프로그램이다. 반전시간이 정확한 것이 장점이다. ^^

[숙제] 여기 이 루틴을 이용해서 신호등의 신호주기를 일정하게 만들어주는 프로그램을 짜보자. 우선 일정한 시간 간격으로 LED 가 점멸하도록 타이머를 사용해서 프로그램을 짜보자.


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

/*
타이머 0, 모드1 사용, 10ms
*/

#include <AT89X52.H>

#define HIGH10MS    0xB1
#define LOW10MS 0xDF

unsigned int ui_cnt;
unsigned char uc_cnt;

void main(void)
{
    TMOD &= 0xF0;
    TMOD |= 0x01;
    TH0 = HIGH10MS;
    TL0 = LOW10MS;

    ET0=1;
    EA=1;
    TR0=1;
   
    ui_cnt = uc_cnt = 0;
   
    while(1) {
        if(ui_cnt>100) {
            ui_cnt-=100;
            P1_3=!P1_3;
           
            if(uc_cnt>10) {
                uc_cnt=0;
                P1_4=!P1_4;
                }

            uc_cnt++;              
            }   
    }
}


void it_timer0(void) interrupt 1 /* interrupt address is 0x000b */
{
    TH0 = HIGH10MS;
    TL0 = LOW10MS;
    ui_cnt++;
    P1_2=!P1_2;
}

 

여기까지 한번 잘 살펴보고 소스를 응용해서 신호등 프로그램을 짜보기를 바랍니다. ^^

참고로 인터럽트 루틴은

 

void it_timer0(void) interrupt 1
{

    내용;

}

 

이렇게 짜면 됩니다.

 

it_timer 는 내가 만드는 인러럽트서비스루틴(ISR)함수의 이름입니다. 스스로 알아서 정해주면 됩니다.

맨 앞의 void 는 이 ISR 이 전달하는 값이 없다는 것을 나타내고, ()안의 void 는 따로 다른 곳에서 전달받는 값이 없다는 것을 나타냅니다.

 

마지막의 interrupt 1 은 1번 인터럽트 발생시 이 함수를 실행시키라는 의미입니다. 인터럽트는 8051에는 5개가 있고, AT89S52 는 여기에 하나가 추가되어 6개가 있습니다. 즉 interrupt 0 부터 5까지 사용이 가능하고, 각각은 다음과 같습니다.

 

인터럽트 번호 : 이름 - 인터럽트 벡터주소
Interrupt 0 : External 0 - 0x0003
Interrupt 1 : Timer 0 - 0x000B
Interrupt 2 : External 1 - 0x0013
Interrupt 3 : Timer 1 - 0x001B
Interrupt 4 : Serial - 0x0023
Interrupt 5(8052만) : Timer 2 - 0x002B

 

SDCC 메뉴얼 33페이지에 보면 ISR 에 대해 나와 있습니다. interrupt 1 뒤에 using 1 이란 것이 붙을 수도 있습니다. using 은 레지스터뱅크 사용을 사용자가 직접 지정해 주는 것이고, using 키워드를 사용하지 않으면 컴파일러가 알아서 뱅크를 지정해 줍니다. C로 컴파일할때는 굳이 using 키워드를 사용할 필요가 없습니다.

조금 길었습니다. ^^ 그리고 내용도 조금 쉽지는 않을 듯 하네요. ^^ 건투를 빕니다. ^^

 

          --------------------------------------
               Timy의 전자카페
               기분좋은 하루되세요. ^^
               http://www.electoy.net
          --------------------------------------

반응형

+ Recent posts