atmega128, atmega32의 USART 통신(Queue를 이용하는 방식) - 2편
이글의 전부 또는 일부, 사진, 소스프로그램 등은 저작자의 동의 없이는 상업적인 사용을 금지합니다. 또한, 비상업적인 목적이라하더라도 출처를 밝히지 않고 게시하는 것은 금지합니다.
(본 글은 2017.12.02.에 필자의 다른 티스토리 http://avrlab.tistory.com에 적었던 것을 옮겨왔습니다.)
2. 데이터 송신
데이터를 송신하는 과정은 다음과 같이 간단합니다.
1) 데이터 레지스터가 비었는지 확인한다.
2) 데이터 레지스터에 데이터를 출력한다.
UCSR0A 레지스터의 UDRE(USART Data Register Empty) 비트를 보면 데이터 레지스터(UDR: USART Data Register)가 비었는지 아닌지를 확인할 수 있습니다. 즉, 이 비트가 0이면 데이터 레지스터가 비어 있는 상태가 아니며, 이 비트가 1이면 데이터 레지스터가 비어 있는 상태 입니다. 즉 다음과 같이하여 UDRE가 1이 될때 까지 대기하다가, UDRE가 1이되면 데이터 레지스터 UDR에 출력합니다.
(ATMEGA128 USART0)
;================================================= ; PARAMETER : AL ;================================================= USART0_CHAR: SBIS UCSR0A,UDRE0 RJMP USART0_CHAR OUT UDR0,AL RET
atmega128의 USART1은 UCSR1A와 UDR1이 63보다 크기 때문에 SBIS 명령과 OUT 명령을 사용할 수 없습니다. 다음과 같이 SBRS 명령과 LDS, STS 명령을 사용하여 데이터를 송신합니다.
(ATMEGA128 USART1)
;================================================= ; PARAMETER : AL ;================================================= USART1_CHAR: PUSH AH USART1_CHAR_WAIT: LDS AH,UCSR1A SBRS AH,UDRE1 RJMP USART1_CHAR_WAIT STS UDR1,AL POP AH RET
(ATMEGA32)
;================================================= ; PARAMETER : AL ;================================================= USART_CHAR: SBIS UCSRA,UDRE RJMP USART_CHAR OUT UDR,AL RET
3. 데이터 수신
① Queue 사용의 필요성
데이터를 수신하는 편에서 보면 상대방이 언제 데이터를 보낼는지 예측할 수가 없고, 설령 데이터를 보내는 시간이 약속 되어 있다하더라도 정해진 시간에 정확하게 데이터를 보낼는지를 알 수가 없습니다. 이런 의미에서 보면 수신측에서는 다른 일을 하다가, 데이터가 도착했을 때에 인터럽트를 발생시켜서 데이터를 수신해 놓고, 적절한 시기에 적절한 방법으로 처리하는 것이 합리적입니다.
이렇게 인터럽트로 수신 데이터를 처리할 경우에, 연속적으로 두 개 이상의 데이터가 들어 온다면 일부의 데이터를 잃어 버리는 경우가 발생할 수 있습니다. 이를 예방하려면 데이터가 들어오는 즉시 일정한 장소에 저장해 놓고, 하나씩 꺼내어 쓰는 방법을 사용하는 것이 좋을 것입니다. 이렇게 데이터들을 저장하고 사용할 때에 queue를 이용할 필요가 있습니다.
② Queue
일반적으로 queue는 일정 영역의 메모리를 확보해 놓고, 수신되는 데이터들을 순서대로 저장해 놓았다가 사용하는 방법으로 stack과 queue를 사용할 수 있습니다. stack은 일반적으로 후입선출(LIFO: Last In First Out)을 많이 사용하지만, queue는 후입선출과 선입선출(FIFO: First In First Out) 모두 가능합니다. 일반적인 serial 통신에서는 queue를 선입선출 방법으로 사용합니다.
다음은 queue를 선입선출로 사용하는 일반적인 방법입니다.
1) Queue로 사용할 메모리를 확보합니다.
2) Queue를 관리하는데 필요한 변수들을 정의하고 초기화합니다.
전형적인 변수로는 들어오는 데이터를 저장할 queue 상의 위치, 꺼내 갈 데이터의 queue 상의 위치, 현재 queue에 있는 데이터의 수 등입니다. 본 글에서는 들어오는 데이터의 queue 상의 위치를 변수 QUEUE_HEAD로, 꺼내 갈 데이터의 queue 상의 위치를 변수 QUEUE_TAIL로, 현재 queue에 있는 데이터의 수 DATASU_INQUEUE로 정의하여 사용합니다.
초기화시에는 들어 온 데이터가 없으므로 QUEUE_HEAD, QUEUE_TAIL, DATASU_INQUEUE 모두 0입니다.
3) Queue에 데이터가 하나 들어 온 경우를 가정합니다.
보통은 데이터를 넣는 작업을 put으로 표현합니다. 데이터를 하나 넣으면 다음 데이터를 저장할 위치는 1이 되어야 합니다. 즉 QUEUE_HEAD는 1이 되어야 합니다. 이와 함께 queue에 데이터가 하나 있게 되므로 DATASU_INQUEUE도 1로 해야 합니다. 아직 데이터를 꺼내 간 적은 없으므로 FIFO 원리에 의해 앞으로 꺼내 가야할 데이터는 아직 queue의 맨 앞에 있는 데이터이므로 QUEUE_TAIL은 0을 유지합니다.
4) Queue에 데이터가 하나 더 들어 왔다고 가정합니다. QUEUE_HEAD는 2가 되고, DATASU_INQUEUE도 2가 되어야 합니다. 아직 데이터를 꺼내 간 적이 없기 때문에 QUEUE_TAIL은 계속 0입니다.
5) Queue에서 데이터를 하나 꺼내 간다고 가정합니다.
데이터를 꺼내 가는 작업을 get으로 표현합니다. 데이터를 하나 꺼내 간 후에는 다음에 꺼내 갈 데이터를 가리키는 QUEUE_TAIL의 값은 1이 되어야 합니다. 또 데이터를 꺼내 갔기 때문에 queue에 남아 있는 데이터 수를 1 감소시켜서 DATASU_INQUEUE는 1로 해야합니다. QUEUE_HEAD는 새로 들어온 데이터가 없으므로 그대로 2의 값을 갖습니다.
위 사항들을 정리하면 queue는 다음과 같이 운영합니다.
1) QUEUE_HEAD, QUEUE_TAIL, DATASU_INQUEUE를 모두 0으로 초기화 한다.
2) 데이터가 하나 들어오면 QUEUE_HEAD와 DATASU_INQUEUE를 1식 증가 시킨다.
3) 데이터를 하나 꺼내 가면 QUEUE_TAIL은 1 증가시키고, DATASU_INQUEUE는 1감소 시킨다.
기본적으로는 위와 같은 원리에 따라 queue를 운영하지만 몇 가지 더 고려해야할 상황이 있습니다.
첫째, QUEUE_HEAD와 QUEUE_TAIL을 증가시키지만 queue의 범위를 벗아나지 않게 해야 합니다. 즉, QUEUE_HEAD나 QUEUE_TAIL이 queue의 끝에 도달하면 다음 값은 queue의 처음을 가리키도록 0으로 해야 합니다.
둘째, queue에 데이터가 하나도 없을 때에 데이터를 가져가려고 시도할 때에 어떻게 해야 할까 입니다. DATASU_INQUEUE의 값이 0일 때 데이터를 가져가려고 시도하면, 데이터가 도달할 때까지 기다리는 방법, 에러 표시를 하고 되돌아 가는 방법 등 프로그램에 따라 적절한 조치를 취해 주어야 합니다. 데이터가 도달할 때까지 기다리는 방법은 상대방과의 연결이 끊이진 경우에는 무한 대기 상태에 빠지므로 이를 대비한 적절한 조치가 있어야 합니다.
세째, queue가 꽉 찼는데 계속 데이터가 들어오는 경우에는 어떻게 할 것인가를 고려해야 합니다. queue가 꽉 찼는지 여부는 DATASU_INQUEUE의 값이 queue의 크기와 같은지 여부를 판단하면 됩니다. 이 경우에 상대방에게 전송 중지 요청을 하는 방법, 그냥 버리는 방법 등 적절한 방안을 취해야 합니다.
③ QUEUE 운영 사례
atmega128 USART0를 이용한 queue 사용 예입니다. atmega128 USART1의 경우와 atmega32 USART의 경우도 다를 바가 없기 때문에 반복하여 설명하지 않습니다.
1) QUEUE 정의
#define QUEUE_SIZE 128 .DSEG QUEUE0: .BYTE QUEUE_SIZE DATASU_INQUEUE0: .DW 1 QUEUE0_HEAD: .DW 1 QUEUE0_TAIL: .DW 1
128바이트 크기의 QUEUE0를 선언하고, 이 queue를 운영하기 위해 필요한 변수 DATASU_INQUEUE0, QUEUE0_HEAD, QUEUE0_TAIL 등의 변수를 정의하였습니다. 이 경우에는 queue의 크기가 128바이트이므로 나머지 세 변수를 DB로 정의해도 상관이 없습니다. 다만 나중에 queue의 크기를 256바이트 이상으로 늘릴 필요가 있을 때에 매크로 QUEUE_SIZE만 바꾸어 주면 되도록하기 위해서 DW로 정의하였습니다.
2) USART0_INIT 함수에 queue 관리 변수 초기화 루틴 추가
queue 관리 변수들을 초기화 하는 루틴을 USART0_INIT 함수에 추가합니다.
USART0_INIT: PUSH AL LDI AL,UCSRA0_VALUE OUT UCSR0A,AL ; asynchronous mode LDI AL,HIGH(UBRR0_VALUE) STS UBRR0H,AL LDI AL,LOW(UBRR0_VALUE) OUT UBRR0L,AL LDI AL,((1 << UCSZ01) | (1 << UCSZ00)) ; AL,0x06 STS UCSR0C,AL ; N,8,1 LDI AL,((1 << RXCIE0) | (1 << RXEN0) | (1 << TXEN0)) ; AL,0x98 OUT UCSR0B,AL CLR AL STS QUEUE0_HEAD,AL STS QUEUE0_HEAD + 1,AL STS QUEUE0_TAIL,AL STS QUEUE0_TAIL + 1,AL STS DATASU_INQUEUE0,AL STS DATASU_INQUEUE0 + 1,AL POP AL RET
앞의 글에서 만든 USART0_INIT 함수에 CLR AL 이후의 내용을 추가했습니다.
3) USART0_Rx_Complete 인터럽트 서비스 루틴
간단히 주석으로 설명을 첨부하였습니다.
;==================================================== ; USART0 인터럽트 처리 루틴 ;==================================================== __USART0_Rx_Complete: PUSH AL IN AL,SREG PUSH AL __USART0_Rx_Complete_WAIT: // 수신 완료 대기 SBIS UCSR0A,RXC0 RJMP __USART0_Rx_Complete_WAIT IN AL,UDR0 PUSH AH PUSH XL PUSH XH PUSH ZL PUSH ZH LDS XL,DATASU_INQUEUE0 // 큐가 넘치면 버린다. LDS XH,DATASU_INQUEUE0 + 1 LDI ZL,LOW(QUEUE_SIZE) LDI ZH,HIGH(QUEUE_SIZE) CP XL,ZL CPC XH,ZH BRSH __USART0_Rx_Complete_QUIT ADIW X,1 // 큐에 있는 데이터 수를 늘리고, STS DATASU_INQUEUE0,XL STS DATASU_INQUEUE0 + 1,XH LDI ZL,LOW(QUEUE0) // 데이터를 큐에 데이터 저장 LDI ZH,HIGH(QUEUE0) LDS XL,QUEUE0_HEAD LDS XH,QUEUE0_HEAD + 1 ADD ZL,XL ADC ZH,XH ST Z,AL ADIW X,1 // QUEUE0_HEAD를 1 증가 시키고 LDI ZL,LOW(QUEUE_SIZE) // 큐의 범위를 벗어나면 0으로 LDI ZH,HIGH(QUEUE_SIZE) CP XL,ZL CPC XH,ZH BRLO __USART0_Rx_Complete_WRAP CLR XL CLR XH __USART0_Rx_Complete_WRAP: STS QUEUE0_HEAD,XL STS QUEUE0_HEAD + 1,XH __USART0_Rx_Complete_QUIT: POP ZH POP ZL POP XH POP XL POP AH POP AL OUT SREG,AL POP AL RETI
위의 USART0_Rx_Complete 함수가 하는 일을 간단히 설명하겠습니다.
i) SREG 레지스터의 값과 사용할 레지스터들의 값을 스택으로 대피시킨다.
ii) UCSR0A 레지스터의 RXC0 값을 읽어서 수신이 완료될 때까지 기다린다.
iii) UDR0로부터 데이터를 읽은다.
iv) DATASU_INQUEUE0가 꽉찼으면 방금 수신한 데이터를 버리고 리턴한다.
v) DATASU_INQUEUE0가 꽉찼차지 않았으면 데이터를 큐에 넣는다.
vi) QUEUE0_HEAD를 1 증가시킨다.
vii) QUEUE0_HEAD가 QUEUE_SIZE보다 크면 QUEUE_HEAD의 값을 0으로 한다.
Viii) SREG를 포함한 모든 레지스터의 값을 원상 복귀시키고 리턴한다.
USART0 포트에 데이터가 수신되면 이 인터럽트 서비스 루틴이 호출되도록 설정해야 합니다. ATMEGA128의 경우 USART0 Rx Complete Interrupt는 19번째 인터럽트이므로 프로그램 영역 주소 36((19 - 1) * 2, 16진수로는 0x24)에서 위 함수 USART0_Rx_Complete로 JMP하도록하면 됩니다. 예를 들면 다음과 같습니다.
.EQU USART0_RX_COMPLETE_ADDRESS = ((19 - 1) * 2) .ORG USART0_RX_COMPLETE_ADDRESS JMP USART0_Rx_Complete
4) QUEUE에서 데이터를 가져가는 함수
다음은 queue에서 데이터를 하나 꺼내가는 함수입니다.
이 함수에서는 queue에 데이터가 없을 때에는 USART_TIMEOUT_FLAG의 값을 증가시키다가 USART_TIMEOUT_FLAG의 값이 USART_WAIT_TIME과 같아지면
이 함수를 호출한 특에서는 변수 USART_TIMEOUT_FLAG를 검사해서 이 변수의 값이 0이면 정상적으로 데이터가 수신된 것으로 판단하고, 그렇지 않으면 데이터가 수신되지 않은 것으로 판단하여야 합니다.
나머지 부분은 위의 USART0_Rx_Complete 인터럽트 서비스 루틴에서 설명한 내용과 같습니다.
////////////////////////////////////////////////// // PARAM NONE // RETURN AL // CHANGED NONE ////////////////////////////////////////////////// GETDATA_FROM_QUEUE0: PUSH XL PUSH XH PUSH ZL PUSH ZH CLR AL STS USART_TIMEOUT_FLAG,AL GETDATA_FROM_QUEUE0_WAIT: // 큐가 비었는지 검사 LDS XL,DATASU_INQUEUE0 LDS XH,DATASU_INQUEUE0 + 1 TST XL BRNE GETDATA_FROM_QUEUE0_1 TST XH BRNE GETDATA_FROM_QUEUE0_1 CPI AL,USART_WAIT_TIME // 큐가 비었으면 USART_WAIT_TIME BRLO GETDATA_FROM_QUEUE0_WAIT_MORE // 만큼 기다리면서 LDS XL,USART_TIMEOUT_FLAG // USART_TIMEOUT_FLAG을 증가시킴 INC XL STS USART_TIMEOUT_FLAG,XL RJMP GETDATA_FROM_QUEUE0_QUIT GETDATA_FROM_QUEUE0_WAIT_MORE: RCALL DELAY_1MS INC AL RJMP GETDATA_FROM_QUEUE0_WAIT GETDATA_FROM_QUEUE0_1: CLR AL STS USART_TIMEOUT_FLAG,AL LDS XL,DATASU_INQUEUE0 // 큐에 있는 문자 수를 줄이고, LDS XH,DATASU_INQUEUE0 + 1 SBIW X,1 STS DATASU_INQUEUE0,XL STS DATASU_INQUEUE0 + 1,XH LDI ZL,LOW(QUEUE0) // QUEUE_TAIL을 1 증가시킨 후에 LDI ZH,HIGH(QUEUE0) LDS XL,QUEUE0_TAIL LDS XH,QUEUE0_TAIL + 1 ADD ZL,XL ADC ZH,XH LD AL,Z ADIW X,1 LDI ZL,LOW(QUEUE_SIZE) LDI ZH,HIGH(QUEUE_SIZE) CP XL,ZL CPC XH,ZH BRLO GETDATA_FROM_QUEUE0_NO_WRAP CLR XL // 큐테일이 큐를 벗어나면 0으로 초기화 CLR XH GETDATA_FROM_QUEUE0_NO_WRAP: STS QUEUE0_TAIL,XL STS QUEUE0_TAIL + 1,XH GETDATA_FROM_QUEUE0_QUIT: POP ZH POP ZL POP XH POP XL RET
atmega128의 USART1은 위 두 함수의 0을 모두 1로 바꾸어 주면됩니다. 다만, USART1의 레지스터들은 모두 63보다 크기 때문에 SBIS 명령 대신에 레지스터로 읽어서 SBRS 명령을 써야하고, OUT 명령이나 IN 명령 대신에 STS 명령과 LDS 명령을 쓰면됩니다.
atmega32의 경우에는 위 두 함수에서 사용한 0을 모두 지우고 사용하면 됩니다.
이상으로 1편과 2편에 걸친 atmega128과 atmega32를 대상으로 한 데이터 수신 인터럽트와 queue를 사용한 serial 통신에 관한 설명을 모두 마칩니다. atmega32의 serial 통신 내용은 USART가 하나인 다른 모든 avr에 공통으로 적용할 수 있고, atmega128과 관련된 설명은 USART가 두 개인 모든 avr에 공통으로 적용할 수 있습니다.