I2C 보고 SPI 봤으니 이번엔 UART다. 사실 셋 중에 제일 처음 만나게 되는 게 이 녀석인데, "시리얼 모니터에 글자 찍기"가 바로 UART다. 근데 앞에 둘이랑 결정적으로 다른 점이 하나 있다. 클럭선이 없다.

클럭선이 없다고?

I2C엔 SCL, SPI엔 SCK 라는 클럭선이 있었다. 이게 "지금이 1비트를 읽을 타이밍이야" 하고 박자를 딱딱 찍어주는 선이었다. 그래서 둘 다 동기식(synchronous) 통신이라고 불렀다.

그런데 UART는 그 클럭선이 없다. 그래서 비동기(asynchronous)다. 이름부터가 그렇다. UART = Universal Asynchronous Receiver/Transmitter. 우리가 흔히 "시리얼 통신", "RS-232" 라고 부르는 게 다 이거다. "유아트" 라고 읽으면 된다.

여기서 바로 드는 의문. 클럭이 없는데 대체 언제가 1비트인지 어떻게 알아? 이게 UART의 핵심이라 뒤에서 따로 짚는다. 결론만 먼저 말하면, 양쪽이 통신 속도를 미리 약속해두는 방식으로 해결한다.

선은 딱 2개

UART는 데이터선이 2개다. (GND 빼고)

  • TX : Transmit. 내가 내보내는(송신) 선.
  • RX : Receive. 내가 받는(수신) 선.

그리고 배선할 때 여기서 한 번 다들 삽질한다. TX는 TX끼리, RX는 RX끼리 연결하는 게 아니다. 내가 내보낸 걸 상대는 받아야 하니까, X자로 교차해서 이어줘야 한다. 내 TX → 상대 RX, 상대 TX → 내 RX. 여기에 GND만 공통으로 하나 이어주면 끝이다.

UART 결선도. 내 TX는 상대 RX로 교차 연결. 클럭선이 없다는 게 핵심이다.

SPI처럼 전이중(full-duplex)이라 TX/RX가 따로 있어서 보내기와 받기를 동시에 할 수 있다. 그리고 SPI/I2C랑 다르게 마스터/슬레이브 개념이 없다. 그냥 양쪽이 대등하게 각자 TX로 쏘고 RX로 받는다. 처음엔 TX끼리 이어놓고 "왜 글자가 안 나오지" 하고 한참 헤맸다 ㅡㅡ;;;

그래서 I2C / SPI랑 뭐가 다른데?

앞에 둘을 봤으니 표로 비교하는 게 제일 빠르다.

  I2C SPI UART
클럭선 있음 (SCL) 있음 (SCK) 없음 (비동기)
선 개수 2선 4선 2선 (TX, RX)
역할 구분 마스터/슬레이브 마스터/슬레이브 대등 (없음)
통신 방향 반이중 전이중 전이중
속도 약속 클럭이 알아서 클럭이 알아서 보드레이트로 미리

핵심은 역시 클럭선의 유무다. 클럭이 없는 대신 UART는 다른 방법으로 박자를 맞춘다. 그게 보드레이트다.

클럭 없이 어떻게 비트를 나누나 — 보드레이트

클럭선이 없으니 "지금 읽어" 하고 알려주는 신호가 없다. 그럼 어떻게 하냐면, 양쪽이 1비트에 쓸 시간을 미리 똑같이 약속해둔다. 이 약속한 속도가 보드레이트(baud rate)다. 단위는 bps(초당 비트).

  • 흔히 쓰는 값: 9600, 115200 bps.
  • 1비트에 쓰는 시간 = 1 / 보드레이트. 예를 들어 9600bps면 1비트가 약 104μs.

즉 받는 쪽은 클럭을 볼 필요 없이 "신호가 시작되면 정해진 시간 간격으로 뚝뚝 끊어서 비트를 읽으면 된다." 양쪽이 같은 보드레이트로 맞춰놨으니 타이밍이 딱딱 들어맞는 거다.

그럼 "신호가 시작되는 순간"은 어떻게 아느냐? 여기서 프레임 얘기가 나온다.

프레임 구조 (8N1)

평소에 아무것도 안 보낼 때, UART 라인은 HIGH를 유지한다. 이걸 idle 상태라고 한다. 그러다 데이터를 보낼 때 다음 순서로 한 묶음(프레임)을 쏜다.

  • 시작비트 (1개) : 라인을 HIGH에서 LOW로 뚝 떨어뜨린다. "이제 시작한다!"는 신호. 받는 쪽은 이 떨어지는 순간을 보고 "아 시작이구나" 하고 타이머를 돌린다.
  • 데이터비트 (보통 8개) : 진짜 데이터. 여기서 주의할 게, LSB first다. 즉 낮은 자리 비트부터 나간다. (SPI는 보통 MSB first였는데 UART는 반대라 헷갈린다.)
  • 패리티비트 (옵션) : 에러 체크용. 안 쓰는 경우가 더 많다.
  • 정지비트 (1~2개) : 라인을 다시 HIGH로 올려서 "한 프레임 끝" 표시.

이 설정을 짧게 "8N1" 이라고 적는다. 8비트 데이터, No 패리티, 정지비트 1개. 제일 흔한 설정이다.

문자 'A'(0x41)를 보내는 프레임. idle(HIGH) → 시작(LOW) → 데이터 8비트 → 정지(HIGH).

위 그림은 문자 'A'를 보내는 예시다. 'A'는 아스키로 0x41 = 0b01000001인데, LSB first라서 실제 라인에는 1, 0, 0, 0, 0, 0, 1, 0 순서(b0→b7)로 나간다. 비트 순서가 눈으로 보던 이진수랑 뒤집혀서 나가는 게 처음엔 좀 어지럽다.

제일 흔한 삽질: 보드레이트 안 맞음

UART 하다 보면 거의 무조건 한 번은 겪는 증상이 있다. 시리얼 모니터에 글자가 ????? 이렇게 깨진 문자로 나오는 거다.

원인은 십중팔구 양쪽 보드레이트가 안 맞아서다. 한쪽은 9600으로 보내는데 받는 쪽은 115200으로 읽으면, 1비트 시간이 서로 다르니까 비트를 엉뚱한 지점에서 끊어 읽는다. 그러니 글자가 와장창 깨진다. 클럭선이 없어서 순전히 시간 약속에 의존하는 UART의 대가인 셈이다ㅋㅋ. 글자 깨지면 제일 먼저 양쪽 보드레이트부터 확인하자.

코드로 보자 (AVR)

AVR에선 하드웨어 UART가 내장돼 있어서 레지스터만 세팅하면 된다. SPI 때 비트뱅잉으로 직접 두드린 것보단 오히려 편하다. 레지스터 이름부터 정리하면,

  • UBRRH / UBRRL : 보드레이트 설정값(16비트)을 상/하위로 나눠 넣는다.
  • UDR : 데이터 레지스터. 여기에 쓰면 송신, 여기서 읽으면 수신.
  • UCSRA : 상태 확인. UDRE(송신버퍼 비었나), RXC(수신 완료됐나) 비트를 본다.
  • UCSRB : TXEN(송신 켜기), RXEN(수신 켜기).
  • UCSRC : 프레임 형식(데이터 비트 수, 패리티, 정지비트) 설정. 여기서 8N1을 잡는다.

먼저 초기화다. 보드레이트값(UBRR)은 공식이 정해져 있다. UBRR = F_CPU / (16 × baud) − 1.

void UART_init(unsigned long baud)
{
    // 보드레이트 계산 : UBRR = F_CPU/(16*baud) - 1
    unsigned int ubrr = F_CPU / (16UL * baud) - 1;

    UBRRH = (unsigned char)(ubrr >> 8);   // 상위 8비트
    UBRRL = (unsigned char)(ubrr);         // 하위 8비트

    // 송신(TXEN), 수신(RXEN) 둘 다 켠다
    UCSRB = (1 << TXEN) | (1 << RXEN);

    // 프레임 형식 : 8비트 데이터, 패리티 없음, 정지 1비트 (8N1)
    UCSRC = (1 << UCSZ1) | (1 << UCSZ0);
}

이제 한 글자 보내기. 송신 버퍼가 빌 때까지 기다렸다가 UDR에 쓰면 된다. 버퍼가 아직 차 있는데 덮어쓰면 데이터가 날아가니까 UDRE 비트를 확인하는 거다.

void UART_putc(char c)
{
    // 송신 버퍼가 빌 때까지 대기 (UDRE = 1 되면 비었다는 뜻)
    while ( !(UCSRA & (1 << UDRE)) );

    UDR = c;   // 버퍼 비었으니 이제 쓴다 -> 하드웨어가 알아서 쏴준다
}

받기는 반대다. 수신이 완료될 때까지 기다렸다가 UDR을 읽는다. 한 프레임이 다 들어오면 RXC 비트가 1이 된다.

char UART_getc(void)
{
    // 데이터가 들어올 때까지 대기 (RXC = 1 되면 도착했다는 뜻)
    while ( !(UCSRA & (1 << RXC)) );

    return UDR;   // 도착했으니 읽어서 돌려준다
}

패턴이 SPI 때랑 비슷하다. 플래그 비트를 while로 기다렸다가 → 준비되면 데이터 레지스터를 건드린다. 송신은 "버퍼 빌 때까지", 수신은 "올 때까지" 기다리는 것만 다르다.

문자열 출력 예시

한 글자짜리를 만들었으니 문자열은 그냥 반복이다. 시리얼 모니터에 Hello 찍는 정도는 이걸로 끝난다.

void UART_puts(const char *s)
{
    while (*s)              // 널 문자('\0') 만날 때까지
        UART_putc(*s++);    // 한 글자씩 보낸다
}

void main(void)
{
    UART_init(9600);            // 보드레이트 9600으로 시작
    UART_puts("Hello UART!\r\n");

    while (1)
    {
        char c = UART_getc();   // 뭔가 들어오면
        UART_putc(c);           // 그대로 되돌려 보낸다 (에코)
    }
}

PC 시리얼 모니터를 열 때 보드레이트를 꼭 9600으로 맞춰야 글자가 제대로 보인다. (안 맞추면 아까 그 ????? 향연이다.) 위 while 루프는 받은 글자를 그대로 돌려보내는 에코인데, 통신이 잘 되는지 확인할 때 딱 좋다.

정리하면

  • UART = 비동기 직렬통신. I2C/SPI와 달리 클럭선이 없다.
  • 선은 TX, RX 2개 + GND. X자로 교차 연결(내 TX → 상대 RX).
  • 클럭이 없는 대신 보드레이트로 1비트 시간을 미리 약속한다. (1비트 = 1/보드레이트)
  • 프레임 = 시작비트(LOW) + 데이터 8비트(LSB first) + (패리티) + 정지비트(HIGH). 흔히 8N1.
  • 글자 깨지면(?????) 제일 먼저 양쪽 보드레이트를 의심하자.

클럭선 없이 시간 약속만으로 데이터를 주고받는다는 게 처음엔 신기했는데, 프레임 구조를 보고 나니 납득이 됐다. I2C, SPI, UART까지 봤으니 기본적인 통신 3종은 얼추 정리된 느낌이다. 다음엔 통신 말고 ADC(아날로그 값 읽기)를 정리해봐야겠다.

반응형

'프로그래밍 > 마이컴' 카테고리의 다른 글

PWM (펄스폭 변조)  (0) 2026.07.19
ADC (아날로그 읽기)  (0) 2026.07.19
SPI 통신  (0) 2026.07.19
ATMEGA32L External Interrupt  (0) 2013.08.14
I2C 통신  (0) 2013.08.14

+ Recent posts