티스토리 뷰

입문

인터럽트 - 시작과 끝

Just4Fun 2016. 4. 7. 21:12

AVR에서 인터럽트가 발생하였을 때 이를 처리하기 위한 ISR 함수는 ISR()이라는 매크로를 사용하면 된다고 앞의 글에서 설명하였다.  UART RX 인터럽트가 발생하였을 때 이를 처리하기 위하여 ISR(USART_RX_vect)라고 정의해 주면 되었다.

그러나, AVR 프로그램을 처음 시작하는 프로그래머는 어디서 이런 정보를 얻어야 되는지 몰라서 무척 당황하게 된다.  물론 요즘은 인터넷 검색으로 'AVR 인터럽트'라고 치면 많은 정보를 쉽게 찾을 수 있겠지만, 이렇게 찾아낸 것들은 주로 많이 사용하는 인터럽트 몇개의 경우에 한정된다.

만약 Analog Comparator에서 발생하는 인터럽트를 사용하려면 ISR() 안에 무엇을 넣어야 되는지 빨리 못찾을수가 있다.

답부터 얘기하면 Atmel Studio가 설치되어 있는 폴더를 찾으면 된다.  물론 이런것들은 많은 프로젝트를 수행하면서 얻게 되는 노하우이기때문에 많은 경험을 쌓는수밖에 없다.

C:\Program Files (x86)\Atmel\Studio\7.0\toolchain\avr8\avr8-gnu-toolchain\avr\include\avr 에 보면 각 AVR CPU 종류별로 헤더파일이 만들어져 있는것을 볼 수 있다.  다른 버전의 Atmel Studio의 경우 폴더 구조가 약간 다를수도 있는데 toolchain 폴더를 찾으면 그 다음부터는 동일하다.

이 폴더에서 ATmega328P에 해당되는 "iom328p.h" 파일을 열어서 뒷부분을 보면 각 인터럽트 벡터별 이름을 찾을 수 있다.  Analog Comparator 인터럽트의 경우는 "ANALOG_COMP_vect"를 사용하면 되는것으로 나와있다.

그러나, 매번 이런식으로 헤더파일을 찾는것은 은근히 귀찮고 짜증나는 일이다.  이런 경우 다음과 같은 방법으로 해결할 수 있다.  _VECTOR() 매크로를 사용하면 된다.

ISR(_VECTOR(23))

다음과 같은 코드를 작성하여 컴파일 한 후 실행파일이 어떻게 변화 되었는 지 확인해 보자.


UART RX 인터럽트는 정상적으로 동작되고, 25번 라인에 새로운 ISR이 등록된 것을 볼 수 있다.


이제 본론으로 돌아와서, 이번 글에서는 UART RX 인터럽트가 발생하는 순간부터 끝날때까지 AVR안에서는 어떤 일들이 일어나는지 설명하도록 하겠다.

인터럽트 - UART에서 만든 프로그램이 시작되면 컴퓨터의 UART 터미널 화면에 "UART RX Interrupt Test Program."이라는 배너 메시지를 출력한 후 loop() 함수를 무한 반복 실행하게 된다.  loop()함수 안에는 아무런 코드가 들어 있지 않으므로 계속 제자리 뛰기를 하고 있는 중이다.

AVR이 계속 제자리 뛰기를 하고 있는 중에 컴퓨터의 키보드에서 "a"키를 누르면 'a'에 해당되는 0x61이 컴퓨터의 COM port로 들어간다.  COM port에서는 1바이트 데이터를 LSB에서부터 차례로 한 비트씩 UART 프로토콜에 맞추어 내 보낸다.

AVR의 UART receiver 블럭에서는 1 비트씩 들어오는 데이터를 샘플링하여 1 바이트 데이터로 복원한다.  이 데이터는 UDR 레지스터에 보관이 된다.  1바이트 데이터가 다 들어오면 UCSRA의 RXC 비트를 1로 만든다.  그리고 UCSRB레지스터에 RXCIE 비트가 1로 되어 있으면 CPU로 UART RX 인터럽트 신호를 보낸다.

AVR의 SREG 레지스터의 'I' 비트가 set 되어 있으면 인터럽트 모드로 들어가게 된다.

먼저 현재 수행중인 명령어가 끝날때까지 기다렸다가 명령어 수행이 완료되면 다음에 수행되어야할 명령어의 주소를 스택포인터 레지스터가 가리키는 위치에 저장한다.  AVR의 Flash 메모리 크기가 128KB 보다 작으면 2바이트 주소값이 저장되고, flash 메모리 크기가 128KB보다 크면 3바이트 주소가 저장된다.

그 다음엔 SREG 레지스터의 'I'를 clear한다.  'I' 비트가 clear 되었으므로 다른 인터럽트가 발생하더라도 CPU는 이를 무시하게 된다.  즉, 인터럽트 모드에 돌입하게 되면 다른 인터럽트 발생이 억제 되는 것이다.  그러나 ISR안에서 'I' 비트를 set하게 되면 다시 인터럽트가 발생할 수 있다.  인터럽트 안에서 다시 인터럽트가 발생하는 것을 nested interrupt라고 한다.  이 때에는 ISR을 아주 세심하게 만들어야 한다.  인터럽트가 발생하면 인터럽트가 발생할 시점의 상태로 되돌아 와야 하는데 nested interrupt를 사용하게 되면 이 부분이 상당히 처리하기가 어렵게 된다.  따라서 가급적 nested interrupt를 사용하지 않는것이 좋다.

SREG레지스터의 'I'비트가 clear 된 후에 UART RX 인터럽트 벡터 주소에서 명령어를 실행 한다.

  48:	0c 94 53 00 	jmp	0xa6	; 0xa6 <__vector_18>

__vector_18이라고 되어 있는 심볼을 찾아서 점프하는 코드가 들어 있다.  __vector_18에는 다음과 같은 코드가 있다.

5번 줄에서부터 UART RX ISR이 시작된다고 보면 된다.  ISR의 앞 부분에는 ISR을 수행하는데 사용되는 범용 레지스터를 스택에 저장하는 코드가 나온다.  이렇게 하는 이유는 인터럽트가 발생하기 전에 프로그램이 수행되는데 필요한 정보가 범용 레지스터에 들어 있는데, 이런 정보가 ISR을 수행할 때 다른 값으로 변경되기 때문이다.  ISR이 끝난후 원래 상태대로 되돌리려면 인터럽트가 발생하는 시점의 레지스터들을 모두 스택에 저장해 두어야 한다.

AVR에서는 'r1' 레지스터는 항상 '0'의 값을 가지고 있는것으로 간주한다.  따라서 굳이 r1레지스터를 저장할 필요는 없지만 여기에서는 그냥 r1레지스터도 스택에 저장되는것으로 나온다.

6~8 라인은, 일단 r0레지스터를 스택에 저장한 후 0x3F레지스터의 값을 r0에 읽어와서 이 값도 스택에 저장한다.  0x3F레지스터는 이미 앞의 글에서 설명을 하였다.  바로 SREG 레지스터이다.   SREG 레지스터에는 명령어가 수행된 후 그 결과를 알려주기 위한 여러가지 상태 플래그 정보들이 있다.  그러므로 이 값들도 ISR끝날때 원래대로 돌려주기 위하여 스택에 저장하여야 한다.

9번 라인은 r1레지스터의 값을 0으로 만드는 코드이다.  r1레지스터는 항상 0의 값을 가지고 있어야 한다.

10~21라인은 나머지 레지스터의 값을 스택에 저장하는 코드들이다.

25~44라인이 ISR 코드이다.  UDR 레지스터에서 값을 읽어 printf()를 이용하여 다시 UART TX로 내보내는 코드가 들어 가는 것이다.

그런데 여기서 한가지 의문점이 생길것이다.  AVR에는 r0~r31까지 모두 32개의 범용 레지스터가 있는데, 10~21라인에서 저장하는 레지스터는 그 중 일부분이다.  나머지 레지스터는 왜 스택에 저장하지 않는것인가?

AVR은 32개의 범용 레지스터를 효과적으로 사용하기 위하여 사용 용도를 구별하여 정해 놓았다.  ATMEL에서 만든 문서중에 "Mixing Assembly and C with AVRGCC"에 다음과 같은 내용에 대한 설명이 있다.

위의 표에서 r2~r17, r28,r29레지스터는 "call-saved"라고 되어 있다.  이 말은 어떤 함수에서 이 레지스터를 사용하려면 반드시 사용하기 전에 스택에 저장해 두었다가 함수가 끝나기 전에 다시 원래대로 돌려 놓아야 한다는 뜻이다.  그렇다는 얘기는 인터럽트가 발생하였을 때 이를 처리하기 위한 ISR에서 이들 레지스터를 사용하더라도 ISR이 끝날때는 원래대로 돌려줄 것이기 때문에 굳이 인터럽트 모드 초반 부분에서는 따로 저장해둘 필요가 없게 되는 것이다.  그러나 초반에 스택에 저장하였다가 나중에 인터럽트가 끝나기 전에 다시 복구 시켜도 전혀 관계없다. 안전을 위해서 그렇게 하는것도 좋은 방법일 수 있다.  단지, 스택에 레지스터를 저장하는 그 만큼의 시간이 필요하다는것은 염두에 두어야 한다.

r18~r27, r30(표에 오타가 있다. r0이 아니고 r30이다), r31은 "call-used"라고 되어 있다.  이 말은 함수내에서 이 레지스터가 필요할 때 아무런 제약없이 사용할 수 있다는 뜻이 된다.  그러므로 ISR안에서도 필요한 경우 따로 저장해 둘 필요없이 사용해도 된다.  그렇다는 얘기는 인터럽트가 발생하기 직전에 수행되던 함수내에서 사용하고 있던 레지스터 값들이 변경될 수 있게된다.  따라서 이 레지스터들은 미리 스택에 저장해 두었다가 인터럽트 모드가 끝나기전에 원래 값들로 되돌려 주어야 하는것이다.

46~57은 ISR이 다 끝나고 난 뒤, 스택에 저장되어 있던 "call-used"레지스터들을 원래 값으로 복원 시켜주는 코드이다.  스택은 LIFO구조이므로 저장했던 순서의 반대로 읽어 와야 된다.

58~60은 저장했던 SREG 레지스터 값을 원래대로 복원하고 r0도 원래값으로 돌려 놓는 것이다.

마지막으로 r1레지스트도 원래 값대로 복원한다.

인터럽트 모드의 마지막 명령어는 항상 reti로 끝나야 된다.

reti가 수행되면 SREG 레지스터의 'I'비트가 set 된다.  다시 인터럽트가 발생되도록 만드는 것이다.

SREG의 'I' 비트를 set 한 다음에는 인터럽트가 발생하는 순간에 수행 되었어야 할 명령어의 주소를 스택에서 읽어 들여 그 위치로 점프한다.  그러면 인터럽트 때문에 멈추어져 있던 프로그램이 다시 원래대로 수행을 재개하게 되는 것이다.

이로써 인터럽트가 발생하는 시점부터 끝날때까지의 과정을 자세히 살펴보았다.


입문 과정 목차

'입문' 카테고리의 다른 글

타이머/카운터 - OC(Output Compare) match  (3) 2016.04.09
타이머/카운터 - TCNT  (1) 2016.04.09
인터럽트 - UART  (0) 2016.04.06
인터럽트 - 개념  (1) 2016.04.04
UART 온도계  (0) 2016.04.03
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2025/04   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30
글 보관함