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.5 → e < 5.26% (수신기 단독 이론 한계).
- 실제 STM32 는 16배 오버샘플링에서 7·8·9번째 샘플의 다수결을 쓴다. 창이 ±7/16 로 좁아져 9.5 × e < 7/16 → e < 4.6%.
- 송신기와 수신기가 각각 오차를 갖는다. 둘을 더해 4.6% 를 넘으면 안 되므로 한쪽당 약 2.3%, 여유를 보면 한쪽당 2% 이내 가 실무 규칙이 된다.
이 유도를 알면 다음이 바로 설명된다.
| 조합 (16MHz) | BRR | 실제 보율 | 오차 | 판정 |
|---|---|---|---|---|
| 9,600 | 0x0683 (MANT 104, FRAC 3) | 9,598.1 | −0.02% | 안전 |
| 115,200 | 0x008B (MANT 8, FRAC 11) | 115,107.9 | −0.08% | 안전 |
| 230,400 | 0x0045 (MANT 4, FRAC 5) | 231,884.1 | +0.64% | 아직 여유 |
| 921,600 | 0x0011 (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 가 자동 클리어
}
TXE 와 TC 를 구분해야 한다.
- 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 | ~1m | 1:1 |
| RS-232 | ±3~15V | ~15m | 1: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 활용