[임베디드/C] UART시리얼 통신
시리얼 통신
마이크로컨트롤러의 경우, 핀 수가 많지않기 때문에 일반적으로 병렬보다는 직렬통신을 많이 한다. 그 중, 가장 흔히 사용하는 것은 UART시리얼 통신. 시리얼 통신 방법에는 UART말고도 SPI, I2C 등의 방법을 흔히 사용하고 그 이외에도 많이 있다고는 하나 일단 UART를 알아보자.
UART
쉽게 설명하기 위해 예를 들어보자.
컴공과 출신인 김이병, 드디어 첫근무. 새벽 2시쯤 한 통의 전화가 걸려온다.
감사합니다, XXX 통신정비반 이병 김이병입니다. 머스타드일까요?
힘내라, 김이병! 컴퓨터로 코딩할때와는 다르게 실제 데이터는 전압으로 찍힌다(아날로그값)!!.
출처: https://m.jjang0u.com/board/view/military/13148383
그렇다면 마이크로컨트롤러의 경우는?
전송시에 0을 GND, 1을 VCC로. 수신시 반대로 변환하여 사용한다
‘8개의 LED중 홀수번째 것만 켜’라는 경우에 우리는 ‘10101010’을 상상하지만, 보내는 쪽에서는 하나의 비트를 전달하는 데 몇 초를 써야하지? 받는 쪽에서는 몇 초에 한 번씩 찍어야 한 비트씩 받을 수 있을까? 라는 어려움이 있을 것이다.
그 외에도, 김이병은
- 수많은 비트 속에서 어느 것이 데이타고 어느 것이 발신음일까.
- 넘기는 과정에서 잘못된 데이타가 왔는지는 어떻게 알 수 있을까.
- 어느게 RX고, 어디가 TX일까. 어느 타이밍에 회선을 찍어야 될까. 같은 다양한 고민을 하게 될 것이다.
우물쭈물하다가 점검시간인 5분을 넘겨버린 김이병. 잔뜩 화가난 간부가 김이병에게 사수를 불러오라고 말한다. 마이크로컨트롤러 상병님? 주무시는데 죄송합니다. 당직사령이 전화받으랍니다ㅠㅠ
UART통신에 쓰는 레지스터
UART 데이타 전송을 위한 레지스터의 구조는 다음과 같다.
마이크로컨트롤러와 실제 입출력장치를 제어하기 위해서는 그에 맞는 레지스터를 조작해야 한다. 마찬가지로 tx, rx, baud rate 등 UART시리얼 통신의 환경설정에 관여하는 모든 것들은 그에 맞는 레지스터에 적절한 값을 넣어 조작한다.
- UCSRnA
- UCSRnB
- UCSRnC
- UBRRnH와 UBRRnL
- UDRn
상기 6개 레지스터가 우리의 타깃이다.
UCSRnA
통신의 상태를 확인하고 과정을제어하기 위한 레지스터 중 하나이다
. | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
name | RXCn | TXCn | UDREn | FEn | DRn | UPEn | U2Xn | MPCMn |
ReadorWrite | R | R/W | R | R | R | R | R/W | R/W |
InitNum | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
7번비트부터 순서대로
- RXCn: receive complete, 수신버퍼(UDRn)에 읽지 않은 문자가 있을 때 1이고 비어있을 때 0이 된다.
- TXCn: transmit complete, 전송 시프트 레지스터에서 데이타를 송신하고 송신버퍼(UDRn)도 비어있을 때 1이 된다.
- UDREn: USART data register empty, 송신버퍼(UDRn)가 비어 데이터를 받을 준비가 되어있을 때 1이 된다.
- FEn: Frame Error: 프레임 수신에 오류가 발생할 때 1이 된다.
- DORn: data overrun error, 수신 버퍼가 가득 찬 상태에서 수신 시프트 레지스터에 새로운 문자가 수신되고 다시 그 다음 문자의 시작비트가 검출되는 상황(오버런)일 때 1이 된다.
- UPEn: USART parity error, 페리티 오류시 1이 된다.
- U2Xn: double the USART transmission speed, 비동기 전송모드에서만 사용되며 2배속일때 1, 1배속일 때 0이 된다.
- MPCMn: multiprocessor communication mode, 1개의 마스터 프로세서가 여러개의 슬레이브 프로세서에 특정 어드레스를 전송함으로써 1개의 슬레이브만을 지정하여 데이터를 전송하는 멀티프로세서 통신모드에서 1이 된다.
UCSRnB
데이터의 송수신을 가능하게 하도록 설정하기 위해 사용한다. 디폴트로 UART통신의 송수신이 금지되어있다.
. | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
name | RXCIEn | TXCIEn | UDRIEn | RXENn | TXENn | UCSZn2 | RXB8n | TXB8n |
ReadorWrite | R/W | R/W | R/W | R/W | R/W | R/W | R/W | R/W |
InitNum | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
- RXENn: RX enable, UART 수신기의 수신기능을 활성화한다.
- TXENn: TX enable, UART 송신기의 송신기능을 활성화한다.
UCSRnC
통신에서 사용하는 데이터 형식 및 통신방법을 결정하기 위해 사용한다.
. | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
name | - | UMSELn | UPMn1 | UPMn0 | USBSn | UCSZn1 | UCSZn0 | UCPOLn |
ReadorWrite | R/W | R/W | R/W | R/W | R/W | R/W | R/W | R/W |
InitNum | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 0 |
- UMSELn: 동기 또는 비동기 통신모드를 지정한다. 비동기일때 0이다.
- UPMn0,1: 페리티모드 설정이다.
- USBSn: 정지비트의 수를 설정한다. 0이 1비트, 1이 2비트이다.
- UCSZn0,1: 데이터 비트의 수를 결정한다.
- UCPOLn: 클록의 극성을 지정하는 비트로 동기모드에서만 사용한다. 비동기일 때는 0의 값이다.
UBRRnH, UBRRnL
2개의 8비트 레지스터를 조합한 16비트 레지스터이다. baud rate를 12비트로 표현하며 UBRRnH의 상위 4비트는 사용되지 않는다.
. | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
UBRRnH | - | - | - | - | UBRRn[11] | UBRRn[10] | UBRRn[9] | UBRRn[8] |
UBRRnL | UBRRn[7] | UBRRn[6] | UBRRn[5] | UBRRn[4] | UBRRn[3] | UBRRn[2] | UBRRn[1] | UBRRn[0] |
ReadorWrite | R/W | R/W | R/W | R/W | R/W | R/W | R/W | R/W |
InitNum | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
UDRn
송수신된 데이터가 저장되는 버퍼레지스터다. 송신에 쓰는 TXB 레지스터와 수신에 쓰는 TXB 레지스터로 구성되어있으나 동일한 입출력 주소를 사용하기 때문에 쓰면 TX, 읽으면 RX가 된다.
바이트 단위 데이터 송수신
#define F_CPU 16000000L
// 16MHz 클럭 표시, L은 실수표시. ATmega128을 사용한다고 가정.
#include <avr/io.h>
#include <util/delay.h>
void UART1_init(void);
void UART1_transmit(char data);
unsigned char UART1_receive(void);
void UART1_init(void)
{
UBRR1H = 0x00;
UBRR1L = 2007; //9,600 baud rate 설정(UBRRnH,L레지스터)
//2배속모드 설정(UCSRnA 레지스터의 U2Xn비트)
//_BV(n) 매크로는 n번째에 1을 넣어줌.
UCSR1A |= _BV(U2X1);
// UCSRnC 레지스터. 비동기, 데이터 8비트, 패리티 없음, 정지비트 1비트
UCSR1C |= 0x06;
UCSR1B |= _BV(RXEN1);
UCSR1B |= _BV(TXEN1); //송수신 가능
}
void UART_transmit(char data)
{
//UDR1 송신버퍼가 비어 데이터를 받을 수 있을 때까지 루프
while(!(UCSR1A & (1 << UDRE1)) );
UDR1 = data; // 데이타 전송
}
unsigned char UART1_receive(void)
{
//UDR1 수신버퍼에 읽지않은 문자가 들어올 때까지 루프
while(!(UCSR1A & (1 << RXC1));
return UDR1; //수신된 데이타 반환
}
int main(void)
{
UART1_init();
while(1)
{
UART1_transmit(UART1_receive());
}
return 0;
}
문자열 출력
void UART1_print_string(char *str)
{
for(int i = 0; str[i]; i++)
UART1_transmit(str[i]);
}
널문자를 만날 때까지 바이트 단위 전송을 반복하면 된다.
1바이트 크기 정수 출력
//uint8_t는 부호없는 8비트 정수형 의미
void UART1_print_1_byte_number(uint8_t n)
{
char numString[4] = "0";
int i, index = 0;
if(n >0) {
for(i = 0; n!= 0; i++) {
numString[i] = n % 10 + '0';
n /= 10;
//자리수 기준으로 한자리씩 문자로 변환
}
numString[i] = '\0';
index = i - 1;
}
//거꾸로 넣었으므로 거꾸로 출력
for(i = index; i >= 0; i--)
UART1_transmit(numString[i]);
}
위 코드에서는 끊어서 여러번 나누어 변환시키는 과정이 있으나, sprintf함수를 쓰면 저렇게 하지 않아도 된다.
void UART1_print_1_byt_number(uint8_t n)
{
char numString[4] = "0";
sprintf(numString, "%d", n);
UART1_print_string(numString);
}
위 코드는 역순출력도 필요없다. 단, sprintf함수가 있는 stdio.h파일을 포함시켜야 하는데, 실행파일의 크기가 눈에 띄게 커진다. 함수 하나 쓴다고 크기를 그만큼 키우는 건 비효율적이다. 하지만, 실수출력하려면 어쩔 수 없이 저렇게 쓸 수 밖에 없긴 하다.
문자열, 1바이트 정수 출력이 가능한 UART통신 예제
#include <avr/io.h>
void UART1_init(void)
{
UBRR1H = 0x00;
UBRR1L = 207;
UCSR1A |= _BV(U2X1);
UCSR1C |= 0x06;
UCSR1B |= _BV(RXEN1);
UCSR1B |= _BV(TXEN1);
}
void UART1_transmit(char data)
{
while(!(UCSR1A & (1 << UDRE1)));
UDR1 = data;
}
unsigned char UART1_receive(void)
{
while(!(UCSR1A & (1 << RXC1)));
return UDR1;
}
void UART1_print_string(char *str)
{
for(int i = 0; str[i]; i++)
UART1_transmit(str[i]);
}
void UART1_print_1_byte_number(uint8_t n)
{
char numString[4] = "0";
int i, index = 0;
if(n >0) {
for(i = 0; n!= 0; i++) {
numString[i] = n % 10 + '0';
n /= 10;
}
numString[i] = '\0';
index = i - 1;
}
for(i = index; i >= 0; i--)
UART1_transmit(numString[i]);
}
가변 길이 문자열 수신
만약 마이크로컨트롤러가 컴퓨터로부터 데이터를 받는 경우, 가변 길이 문자가 전송될 수 있다. 이런 경우, 수신 데이터 버퍼와 그에 맞는 인덱스를 할당하고 리눅스의 경우 ‘\n’, 윈도의 경우 ‘\n\r’가 수신될 때, 플래그를 세워서 그 플래그에 맞는 if문 안에 출력이라든가, 출력장치를 제어한다 등의 코드를 짜넣으면 된다.
마이크로 컨트롤러에서 UART통신으로 printf, scanf사용하기
printf함수와 scanf함수는 표준입출력 스트림으로 데이터를 출력, 입력한다. UART통신으로 연결된 컴퓨터의 터미널을 표준입출력장치로, 전달되는 데이터를 스트림 형태로 바꿔주면 우리가 흔히 생각하는 printf와 scanf가 된다.
FILE OUTPUT = FDEV-SETUP_STREAM(UART1_transmit, NULL, _FDEV_SETUP_WRITE);
FILE INPUT = FDEV-SETUP_STREAM(UART1_receive, NULL, _FDEV_SETUP_READ);
이후, main함수에서 위에서 정의한 입출력 객체를 연결한다.
int main(void)
{
stdout = &OUTPUT;
stdin = &INPUT;
//생략
끝
댓글남기기