UART

2023. 3. 6. 14:12AVR ATmega128

1.UART

UART는 비동기 직렬 통신 방식 중 하나이다. I2C와 비교해 보면, 데이터를 보낼 때 클록을 사용하지 않고, 통신에 데이터 선을 한 개만 사용해 수신과 송신을 한 번에 할 수 없는 I2C와 달리, TxD(송신), RxD(수신) 두 개의 선을 사용해 송수신을 동시에 할 수 있다. 이렇게 송신과 수신을 동시에 할 수 있는 통신 방법을 Full Duplex(전이중) 통신이라고 한다. 

AVR은 비동기와 동기 통신 모두를 지원하는 USART를 사용한다. 

  UART I2C SPI
통신 방식 비동기 동기 동기
사용 핀 Rxd, Txd SDA, SCK MISO, MOSI, CLK
송수신 여부 전이중 반이중 전이중
연결 가능한 디바이스 1:1 1:N 1:N
통신속도 표시 BaudRate / bps bps bps

데이터시트에 AVR의 USART의 특징이 나와 있다. 

• Supports Serial Frames with 5, 6, 7, 8, or 9 Data Bits and 1 or 2 Stop Bits

• Odd or Even Parity Generation and Parity Check Supported by Hardware

그중 이 두 개의 특징을 잘 알아야 하는데, 비동기 통신을 사용하기 때문에 통신하는 기기끼리 미리 데이터 비트의 개수 등을 정해놓을 필요가 있다.

2. UART연결과 통신 프로토콜

Tera term의 설정창

MCU에서 데이터를 보낼 때, 다른 기기에서는 데이터를 받아야 하므로 Rx(수신) - Tx(송신)으로 연결해야 한다.

UART에서는 동기를 진행하는 클럭이 없고, 통신하는 기기간의 동작 클럭이 다를 수 있기 때문에 서로 간의 보레이트(Baudrate), 통신에 사용하는 데이터 비트수, 패리티 비트 여부, 스탑비트 개수 등을 맞춰주어야 한다. 서로 다른 통신 속도를 사용하면 아래 나와있는 데이터비트를 읽는 타이밍이 달라지기 때문에 오동작할 수 있다. 

TxD(전송)기준 타이밍 다이어그램

시작 신호 이후 데이터를 보내게 되는데 보통은 대부분 1Byte 단위로 데이터를 보내기 때문에 8비트를 사용한다. P는 패리티 비트인데, 데이터 송수신 중 오류를 검출하기 위한 비트이다. 짝수 또는 홀수 패리티 비트를 사용할 수 있다. 요즘은 데이터 송수신의 안정성이 많이 좋아졌기 때문에 패리티 비트도 잘 사용하지 않는다. 이후 1 또는 2바이트의 스탑 신호 이후 데이터를 또 송신할 경우 IDLE에서 LOW신호를 보내 다시 데이터를 보내고, 데이터 송수신이 끝날경우 IDLE에서 HIGH 신호를 보내 데이터 통신을 끝내게 된다. 

 

3. AVR의 UART 

AVR의 UART관련 하드웨어이다. 크게 제어단, 수신단, 송신단으로 나눌 수 있다. 

먼저 제어단에는 송신 속도를 제어하는 UBRR 레지스터가 있고, 패리티 비트, 인터럽트 설정 여부 등등 기타 설정을 제어하는 UCSRx 레지스터가 있다. 

송/수신단에는 UDR이라는 레지스터가 있다. 코딩할 때 이름이 같아 헷갈릴 수 있는데 같은 이름을 가지는 서로 다른 레지스터이다. 송/수신 시에 설정한 만큼의 데이터 비트가 들어오면 해당 데이터 비트가 Shift register를 통해 각각 UDR로 쉬프트 되게 된다. 

 

3-1 보레이트 설정

보레이트 공식
UBRR 설정값

1번 표는 보레이트에 따른 UBRR의 계산 공식이고, 2번 표는 많이 사용하는 클록인 16 Mhz 기준으로 UBRR설정값이 나와있는 표이다. 해당 표의 UBRR값을 기입하면 보레이트가 설정된다. 

 

3-2 레지스터 설정

UDRn은 같은 주소에 공유된 레지스터이고, 각각 송/수신에 사용되는 버퍼이다. 

송신시에는 TXBn의 데이터가, 수신 시에는 RxBn의 데이터가 사용된다.

RxCn, TxCn은 각각 송, 수신이 완료되면 set 되는 비트이며, 송수신 완료 인터럽트에 사용될 수 있다. 

UDREn은 UDRn의 송신 버퍼(TxBn)의 데이터가 비어있을 때 set 되는 비트이다. 이 값이 1이면 송신 버퍼가 비어 송신할 준비가 되었다는 의미이다.

FEn은 수신 버퍼에 오류가 있으면 set 되는 비트이다.

DORn은 수신 버퍼가 가득 차 있으면 set되는 비트이다. 

UPEn은 패리티 비트 오류가 있으면 set되는 비트이다.

위의 3개 비트들은 레지스터를 설정할 때 0으로 만들고 설정해야 한다. 

U2Xn은 2배모드 설정 비트이다. 1이면 2배 모드이다.

MPCMn은 다중통신 설정 모드이다. 이 비트를 set 하면 다중 통신 모드가 활성화된다

 

RXCIEn, TXCIEn은 송수신 인터럽트 활성화 비트이다. 1로 set되면 송수신 완료 인터럽트를 사용 가능해진다.

UDRIEn은 데이터 레지스터가 비면 인터럽트를 발생시킨다. 

RXENn, TXENn은 송수신 모드 활성화 모드이다. 각 비트들을 활성화시키면 수신과 송신을 사용할 수 있다. 

UCSZn2은 사용할 데이터의 비트를 설정하는 비트이다. UCSRnC 레지스터의 비트들과 같이 사용해 데이터의 비트 길이를 설정할 수 있다. 

RXB8n, TXB8n은 9비트 데이터 모드를 사용할 때 9번째 비트가 저장되는 비트이다. 

7번 비트는 사용하지 않는다. 

UMSELn은 동기 모드를 사용할지 비동기 모드를 실행할지 선택하는 비트이다. 1이면 동기, 0이면 비동기 모드이며 기본값은 비동기 모드이다. 

UPMn1,0는 패리티모드를 설정하는 비트이다. 

보통은 패리티 비트를 사용하지 않기 때문에 굳이 설정할 필요는 없다. 

USBSn은 Stop비트를 선택하는 비트이다. 0이면 1비트, 1이면 2비트이다. 

UCSZn1,0UCSRnB에 있는 UCSZn2와 같이 데이터의 비트를 설정하는 비트이다.

대부분 8비트를 사용하므로, UCSZn2는 0으로, 나머지는 1로 설정하면 된다. 

UCPOLn은 동기 모드일때 데이터가 클록의 어느 에지일 때 바뀌는지 설정하는 비트이다. 비동기 모드일 때는 사용하지 않는다. 

 

UBRRnH는 보레이트를 설정하는 레지스터이며, 12비트를 사용한다. UBRR는 위의 표를 참조해서 작성하면 된다. 

 

4. AVR-> PC 송신 코드 작성

가장 많이 사용하는 설정인 송수신 사용, 9600보레이트, 데이터 길이 8비트, 패리티 비트 없음, 스탑비트 1비트를 사용한다. 시리얼 모니터는 아두이노 IDE의 시리얼 모니터를 사용하였다. 

void UART0_init()
{
	//8bit data, 패리티 사용안함, 1비트 Stop bit (기본값)
	UCSR0B |= 1<<RXEN0 | 1<<TXEN0; //송수신 사용
	UCSR0A |= 1<<U2X0; //2배 모드 사용
	UBRR0L = 207; 9600 Baudrate
}
void UART0_Transmit( unsigned char data )
{
	/* Wait for empty transmit buffer */
	while ( !( UCSR0A & (1<<UDRE0)) );
	/* Put data into buffer, sends the data */
	UDR0 = data;
}
unsigned char UART0_Receive()
{
	/* Wait for data to be received */
	while ( !(UCSR0A & (1<<RXC0)) );
	/* Get and return received data from buffer */
	return UDR0;
}
int main(void)
{
    /* Replace with your application code */
	UART0_init();
    while (1) 
    {
		UART0_Transmit('H');
		UART0_Transmit('E');
		UART0_Transmit('L');
		UART0_Transmit('L');
		UART0_Transmit('O');
		UART0_Transmit('\n');
		_delay_ms(1000);
    }
}

이 코드대로 실행하면 

잘 실행되는 것을 알 수 있다. 

 

문자열을 출력하려면 문자열을 끝에는 \0이 나온다는 것을 이용해

void UART0_print(char *str)
{
	for (int i = 0; str[i]; i++)
	{
		UART0_Transmit(str[i]);
	}
}

int main(void)
{
    /* Replace with your application code */
	UART0_init();
    while (1) 
    {
		UART0_print("hello\n");
		_delay_ms(500);
    }
}

이 코드를 작성하면 문자열을 출력할 수 있다. 

역시 잘 나오는 모습을 볼 수 있다.

5 PC->AVR 수신 코드

int main(void)
{
    /* Replace with your application code */
	UART0_init();
	char rxData;
    while (1) 
    {
		rxData = UART0_Receive();
		UART0_print("Received Data : ");
		UART0_Transmit(rxData);
		UART0_Transmit('\n');
		_delay_ms(10);
		
    }
}

 

main()에 수신받은 데이터를 한 바이트씩 출력하는 코드를 추가해 주면 역시 잘 작동되는 것을 알 수 있다. 하지만 문제가 있다. 위 코드를 실행하게 되면 여러 문자를 출력할 때 나오지 않는 값이 생길 것이다. 이는 AVR의 버퍼가 다 차서 데이터를 더이상 받지 못하거나, 다른 코드를 실행할 때 PC에서받은 데이터를 놓치기 때문이다. 이때는 수신완료 인터럽트를 사용해 인터럽트 발생시마다 사용자가 임의로 만든 버퍼에 저장하게 하면 다른 코드를 실행하던 중이라도 UART의 데이터를 놓치지 않을 것이다. 

void UART0_init()
{
	//rx,tx사용, 2배모드 사용, 9600 Baudrate
	//8bit data, 패리티 사용안함, 1비트 Stop bit (기본값)
	//수신완료 인터럽트 사용
	UCSR0B |= 1<<RXEN0 | 1<<TXEN0 | 1<<RXCIE0; 
	UCSR0A |= 1<<U2X0; 
	UBRR0L = 207; //16 : 115200, 207 : 9600
}

따라서 init() 함수에 수신완료 인터럽트가 발생되게 수정해준다. 인터럽트를 사용할때 상단에 #include <avr/interrupt.h>를 추가해줘야한다. 

uint8_t uart0ReceiveBuff[100];
uint8_t uartRxCompleteFlag= 0;
ISR(USART0_RX_vect)
{
	uint8_t rx0Data = UDR0;
	static uint8_t uartRx0Tail = 0; //버퍼의 인덱스를 부를 때 tail이라는 말을 많이 사용

	if(rx0Data == '\n') //문장이 끝났음을 알려주는 문자는 보통 \n사용
	{
		uart0ReceiveBuff[uartRx0Tail] = rx0Data;
		uartRx0Tail++;
		uart0ReceiveBuff[uartRx0Tail] = 0; //문자열의 마지막에는 \0이 나오기 때문에 문자열로 만들어주기 위해 0을 넣어줌 \0 = 0;
		uartRx0Tail = 0;//문자열이 끝나면 버퍼위치 초기화
		uartRxCompleteFlag = 1;
	}
	else 
	{
		uart0ReceiveBuff[uartRx0Tail] = rx0Data;
		uartRx0Tail++;
	}
	
	
}

사용자 버퍼를 만들고, 수신받은 데이터를 버퍼에 저장하고, 버퍼를 문자열처럼 사용하는 코드이다. 

문자열을 사용할 때는 보통 문자열이 끝낱다는 것을 알리기 위해 \n을 사용하기 때문에 해당 문자가 들어오면 버퍼를 출력한후 버퍼를 초기화한다. 

Tail을 초기화하기 전 1증가 후 0을 넣는 이유는 내부적으로 문자열의 끝에는 NULL문자가 나오기 때문에 NULL문자를 추가해 문자열로 처리하기 위함이다. 

int main(void)
{
    /* Replace with your application code */
	UART0_init();
	SREG |= 0x80; //글로벌 인터럽트 허용
	char rxData;
    while (1) 
    {
		UART0_print("Hello AVR!\n");
		if(uartRxCompleteFlag){
			uartRxCompleteFlag = 0;
		UART0_print("Received Data : ");
		UART0_print(uart0ReceiveBuff);
		}
		_delay_ms(1000);
    }
}

 

결과

 

아두이노 시리얼 모니터로 Arduino라는 문자열을 보내면, 인터럽트 내부에서 버퍼에 'A' 'r' 'd' 'u' 'i' 'n' 'o' '\n' 가 사용자 버퍼에 들어간다. \n이 나왔으므로, ISR에서 Tail을 1증가시킨 후 NULL문자를 넣는다. 그러면 이 버퍼를 내부적으로 문자열로 활용할 수 있다.  그림으로 표현하면 아래와 같다. 

개행문자가 나오면 ISR에서 개행문자의 다음 문자를 NULL문자로 채운다. 

NULL이 들어가면 tail을 초기위치로 되돌린후 버퍼를 사용한 뒤, 다음 문자가 들어오면 tail위치부터 다시 기록한다. 

이제 송수신이 가능한 코드를 완성하였다.

 

 

'AVR ATmega128' 카테고리의 다른 글

GPIO  (0) 2023.03.07
UART 활용  (0) 2023.03.06
I2C(TWI) LCD  (1) 2023.03.03
초음파센서 드라이브 만들기  (0) 2023.03.03
AVR초음파 센서  (0) 2023.03.03