이글의 전부 또는 일부, 사진, 소스프로그램 등은 저작자의 동의 없이는 상업적인 사용을 금지합니다. 또한, 비상업적인 목적이라하더라도 출처를 밝히지 않고 게시하는 것은 금지합니다.


(본 글은 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에 공통으로 적용할 수 있습니다.

블로그 이미지

엠쿠스

Microprocessor(STM32, AVR)로 무엇인가를 만들어 보고자 학습 중입니다.

,