임베디드 학습 노트 목차

UART — 클럭 없이 약속으로 맞춘다

UART 는 선 두 개(TX·RX)로 통신한다. 클럭선이 없다. 그런데 어떻게 상대가 비트 경계를 아는가? — 양쪽이 같은 속도를 미리 약속하고, 시작 신호를 기준으로 각자 세는 것이다.

그래서 UART 문제의 거의 전부가 "속도가 안 맞는다" 로 귀결된다.


프레임 하나의 구조

유휴  시작    D0  D1  D2  D3  D4  D5  D6  D7  패리티  정지   유휴
HIGH   LOW   <---------- 데이터 (LSB 먼저) --------->  HIGH  HIGH
      _____
-----|     |---|---|---|---|---|---|---|---|-------|------------
      ^                                                ^
      하강 엣지로 시작을 알아챈다               1비트 이상 HIGH
  • 유휴 상태가 HIGH 다. 그래서 시작 비트는 LOW — 상태 변화로 시작을 감지할 수 있다
  • LSB 를 먼저 보낸다
  • 패리티는 선택이고 요즘은 대개 안 쓴다(상위 계층에서 CRC 를 쓰므로)
  • 정지 비트는 프레임 사이의 최소 간격이다. 데이터가 아니다

8N1 이라는 표기가 여기서 나온다 — 데이터 8비트 · 패리티 없음(None) · 정지 1비트.

수신 쪽은 어떻게 비트를 고르나

        시작 비트 하강 엣지 감지
             |
             v
      _______
-----|       |--- D0 ---|--- D1 ---|
     ^   ^                ^          ^
     |   └ 0.5 비트 기다려 중앙 확인   └ 1비트마다 중앙에서 샘플
     └ 여기서 카운터 시작

비트의 중앙에서 읽는다는 것이 요점이다. 경계에서 읽으면 엣지의 상승·하강 구간에 걸려 값이 불안정하다. 실제 하드웨어는 한 비트를 8~16회 오버샘플링해 다수결로 정하기도 한다.

여기서 속도 오차가 왜 치명적인지가 보인다. 매 비트마다 오차가 누적되므로, 8번째 비트에 가면 오차가 8배다.

보율 오차 2% 라면
  1비트 뒤: 2% 어긋남     (문제없다)
  8비트 뒤: 16% 어긋남    (아직 중앙 근처)
  10비트 뒤: 20% 어긋남   (경계에 닿기 시작)

허용 오차는 대략 ±2~3% 다. 그 이상이면 프레임 뒤쪽 비트부터 깨진다.

보율을 어떻게 만드나

BRR = 클럭 / 보율
// 84MHz APB2, 115200 bps
// 84,000,000 / 115,200 = 729.17
USART1->BRR = 729;              // 정수부만 쓰면
// 실제 보율 = 84,000,000 / 729 = 115,226 bps  → 오차 0.02%  ✓

STM32 는 BRR정수부 + 소수부(1/16) 로 나눠 담아 오차를 줄인다.

// 오차가 큰 조합의 예 — 클럭이 안 맞으면
// 8MHz, 115200 → 8,000,000 / 115,200 = 69.4
USART1->BRR = 69;               // 실제 = 115,942 → 오차 0.64%  (괜찮다)
USART1->BRR = 70;               // 실제 = 114,286 → 오차 0.79%  (괜찮다)

// 1MHz, 115200 → 8.68  ← 정수로 반올림하면 오차가 크다
// 9 → 111,111 (3.5% 오차)  ✗ 통신이 깨진다

낮은 클럭에서 높은 보율은 오차가 커진다. 그래서 통신 속도를 정할 때 "클럭으로 나눠 정수에 가까운 값" 을 고르는 것이 실무 관행이다 — 그래서 9600·115200 같은 수가 관습으로 굳었다.

// 실제 오차를 계산해 확인하는 습관
float actual = (float)clk / brr;
float err = (actual - target) / target * 100.0f;
printf("baud=%.0f err=%.2f%%\r\n", actual, err);

오차 한계는 왜 하필 2% 인가 — 직접 유도해 보자

"보율 오차는 2% 이내" 라는 규칙은 외우는 규칙이 아니라 샘플링 기하에서 나오는 수다.

수신기는 시작 비트의 하강 에지 한 번만 기준으로 잡고, 그 뒤로는 자기 클럭만 세며 각 비트의 중앙을 찍는다. 즉 에지에서 멀어질수록 오차가 누적된다.

        start   d0    d1    d2    d3    d4    d5    d6    d7   stop
에지 ─┐  ┌──── 각 비트 중앙에서 샘플 ────────────────────────┐
      └──┘  ↑     ↑     ↑     ↑     ↑     ↑     ↑     ↑     ↑
         0.5   1.5   2.5   ...                            9.5 비트째
                                          └ 여기가 가장 위험하다
  • 8N1 프레임은 start + 데이터 8 + stop = 10비트, 마지막 stop 비트의 중앙은 에지로부터 9.5 비트 뒤다.
  • 클럭 오차가 e 면 그 시점의 어긋남은 9.5 × e 비트다.
  • 샘플이 비트 밖으로 나가면 안 되므로 9.5 × e < 0.5e < 5.26% (수신기 단독 이론 한계).
  • 실제 STM32 는 16배 오버샘플링에서 7·8·9번째 샘플의 다수결을 쓴다. 창이 ±7/16 로 좁아져 9.5 × e < 7/16e < 4.6%.
  • 송신기와 수신기가 각각 오차를 갖는다. 둘을 더해 4.6% 를 넘으면 안 되므로 한쪽당 약 2.3%, 여유를 보면 한쪽당 2% 이내 가 실무 규칙이 된다.

이 유도를 알면 다음이 바로 설명된다.

조합 (16MHz)BRR실제 보율오차판정
9,6000x0683 (MANT 104, FRAC 3)9,598.1−0.02%안전
115,2000x008B (MANT 8, FRAC 11)115,107.9−0.08%안전
230,4000x0045 (MANT 4, FRAC 5)231,884.1+0.64%아직 여유
921,6000x0011 (MANT 1, FRAC 1)941,176.5+2.12%위험 — 상대 오차와 합하면 한계 초과

함정 — 921,600 처럼 분주비가 한 자리로 떨어지면 소수부 4비트로도 못 맞춘다. 분주비가 작을수록 FRAC 한 칸(1/16)의 상대 크기가 커지기 때문이다. 고속 통신에서 오차가 갑자기 커지는 이유가 이것이다.

여기에 내부 RC 발진기(±1%) 를 쓰면 그 자체로 예산의 절반을 먹는다. 그래서 고속 UART·CAN 은 외부 크리스털(±20ppm)을 요구한다 — 정확도가 아니라 오차 예산 문제다.

꼬리질문 대비 — "보율 오차 허용치를 왜 2% 로 잡나?"(위 유도: 9.5비트 누적 × 양쪽 오차). "왜 마지막 stop 비트에서 깨지나?"(에지에서 가장 멀어 누적 오차가 최대). 증상은 프레이밍 에러(FE) 로 나타난다 — stop 비트 자리에서 1 이 안 읽히기 때문이다.

초기화와 송수신

RCC->APB2ENR |= RCC_APB2ENR_USART1EN;
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;

// PA9=TX, PA10=RX → AF7
GPIOA->MODER  |= (0b10 << (9*2)) | (0b10 << (10*2));
GPIOA->AFR[1] |= (7 << ((9-8)*4)) | (7 << ((10-8)*4));

USART1->BRR = 729;                                  // 115200 @ 84MHz
USART1->CR1 = USART_CR1_TE | USART_CR1_RE | USART_CR1_UE;

// 송신 — TXE(송신 버퍼 비었나) 를 기다린다
void uart_putc(char c) {
    while (!(USART1->SR & USART_SR_TXE)) { }
    USART1->DR = c;
}

// 수신 — RXNE(받은 게 있나) 를 기다린다
char uart_getc(void) {
    while (!(USART1->SR & USART_SR_RXNE)) { }
    return USART1->DR;                              // 읽으면 RXNE 가 자동 클리어
}

TXETC 를 구분해야 한다.

  • TXE — 송신 버퍼가 비었다. 다음 바이트를 넣어도 된다
  • TC — 마지막 비트까지 선으로 나갔다
// RS-485 방향 전환은 TC 를 기다려야 한다
uart_write(buf, n);
while (!(USART1->SR & USART_SR_TC)) { }      // 완전히 나갈 때까지
gpio_set_de(0);                               // 이제 수신 모드로

TXE 만 보고 방향을 바꾸면 마지막 바이트가 잘린다. 반이중 통신에서 자주 겪는 문제다.

폴링은 실무에서 쓰지 않는다

// ✗ 이 함수가 반환할 때까지 아무것도 못 한다
char c = uart_getc();

115200bps 에서 한 바이트는 약 87µs 다. 그 사이에 다른 인터럽트를 처리해야 하므로 **인터럽트 + 링 버퍼**가 기본이다.

#define RB 256
static volatile uint8_t rb[RB];
static volatile uint16_t head, tail;

void USART1_IRQHandler(void) {
    uint32_t sr = USART1->SR;

    if (sr & USART_SR_ORE) {                 // 오버런 — 읽기 전에 다음이 왔다
        (void)USART1->DR;                     // 읽어서 플래그를 지운다
        overrun++;
        return;
    }
    if (sr & USART_SR_RXNE) {
        uint16_t next = (head + 1) & (RB - 1);
        if (next != tail) { rb[head] = USART1->DR; head = next; }
        else              { (void)USART1->DR; dropped++; }
    }
}

오버런(ORE)을 반드시 처리해야 한다. 안 지우면 그 플래그가 계속 서 있어 수신이 완전히 멈춘다 — "잘 되다가 갑자기 안 받아진다" 의 전형적인 원인이다.

오류 플래그 넷

플래그흔한 원인
ORE 오버런읽기 전에 다음 바이트가 왔다ISR 이 느리거나 폴링이 늦다
FE 프레이밍정지 비트 자리가 LOW 였다보율 불일치 · 선 잡음
NE 잡음오버샘플링 결과가 흔들렸다선이 길거나 접지가 나쁘다
PE 패리티패리티가 안 맞는다비트 오류

FE 가 계속 나오면 보율을 먼저 의심한다. 정지 비트를 못 찾는다는 것은 비트 경계 계산이 어긋났다는 뜻이다.

DMA — 대량 전송

// 송신을 DMA 로 넘기면 CPU 는 즉시 돌아온다
DMA2_Stream7->PAR  = (uint32_t)&USART1->DR;
DMA2_Stream7->M0AR = (uint32_t)txbuf;
DMA2_Stream7->NDTR = len;
DMA2_Stream7->CR  |= DMA_SxCR_MINC | (0b01 << DMA_SxCR_DIR_Pos) | DMA_SxCR_EN;
USART1->CR3 |= USART_CR3_DMAT;

수신에는 순환 DMA + 유휴 인터럽트가 강력하다.

// 길이를 모르는 패킷을 받을 때
USART1->CR1 |= USART_CR1_IDLEIE;             // 선이 조용해지면 인터럽트

void USART1_IRQHandler(void) {
    if (USART1->SR & USART_SR_IDLE) {
        (void)USART1->SR; (void)USART1->DR;   // IDLE 클리어는 SR→DR 순서로 읽는다
        uint16_t received = BUFLEN - DMA2_Stream5->NDTR;
        process(rxbuf, received);              // 한 패킷이 끝났다
    }
}

"패킷 끝을 어떻게 아나" 에 대한 하드웨어 답이다. 프로토콜에 길이 필드가 없어도 선이 조용해지는 순간을 하드웨어가 알려 준다.

실무에서 데는 곳

받는 쪽이 못 따라갈 때 상대에게 잠깐 멈추라고 알리는 방법을 흐름 제어라 한다. 신호선(RTS/CTS)을 쓰거나 약속된 문자를 주고받는데, 없으면 결국 오버런으로 데이터를 잃는다.

printf("hello\n");        // ✗ 터미널에서 계단 현상이 난다
printf("hello\r\n");      // ✓ CR+LF
✗ TX ── TX,  RX ── RX      (같은 것끼리 연결하면 안 된다)
✓ TX ── RX,  RX ── TX      (교차)
+ GND ── GND               (접지 공통이 없으면 기준이 없어 통신이 안 된다)

접지를 안 잇는 실수가 의외로 많다. UART 는 접지 기준의 전압으로 0/1 을 구분하므로, 기준이 다르면 아무 값도 못 읽는다.

3.3V MCU  ── 5V 장치      ✗ 5V 가 3.3V 핀에 들어가면 칩이 손상된다
                          → 레벨 시프터나 분압 저항이 필요하다
// USB-시리얼 변환기의 흔한 함정 — 드라이버가 보율을 마음대로 바꾸지는 않지만
// 터미널 설정(8N1, 흐름제어 없음)이 안 맞으면 글자가 깨진다

RS-232 는 UART 가 아니다. UART 는 논리 신호(0V/3.3V)이고 RS-232 는 ±12V 규격이다. 사이에 변환 칩(MAX232 등)이 필요하다. RS-485 는 차동 신호라 더 멀리 가고 잡음에 강하다.

전압거리다중 접속
UART (TTL)0/3.3V~1m1:1
RS-232±3~15V~15m1:1
RS-485차동~1200m다중(32+)

한눈에 정리

  • 클럭선이 없어 양쪽이 같은 보율을 약속한다. 그래서 문제의 대부분이 속도 불일치다
  • 유휴는 HIGH, 시작은 LOW — 상태 변화로 시작을 감지한다. LSB 먼저
  • 비트 중앙에서 샘플링하고, 오차가 매 비트 누적돼 허용 범위는 ±2~3%
  • 낮은 클럭 + 높은 보율은 오차가 커진다 — 실제 오차를 계산해 확인하는 습관
  • TXE(버퍼 비었다)와 TC(선으로 다 나갔다)는 다르다 — RS-485 방향 전환은 TC 를 기다린다
  • ORE 를 안 지우면 수신이 완전히 멈춘다 — "갑자기 안 받아진다" 의 원인
  • FE 가 계속 나오면 보율을 먼저 의심한다
  • 순환 DMA + IDLE 인터럽트가 가변 길이 패킷 수신의 정석
  • TX↔RX 교차, GND 공통 — 접지를 안 이으면 아무것도 안 된다
  • UART ≠ RS-232 — 전압 규격이 다르다. RS-485 는 차동이라 1200m 까지 간다

출처 — STMicroelectronics, RM0090 STM32F4 Reference Manual §30 USART(BRR·SR·CR1~3) · TIA/EIA-232-F(RS-232) · TIA/EIA-485-A(RS-485) 규격 · STMicroelectronics, AN3109 STM32 USART 활용

ADC — 아날로그를 숫자로SPI — 클럭을 같이 보내는 대신 선이 넷