※ 중요한 점 ※
UART 자체가 센서 패킷의 의미까지 정하지는 않는다.
명령 코드, 헤더, 길이, checksum/CRC, 응답 시간은 각 센서의 제조사가 별도의 프로토콜로 정의한다.
배선과 구현이 단순한 point-to-point 직렬 통신에 사용
| 사용 상황 | UART가 적합한 이유 | 대표 예 |
|---|---|---|
| 디버그 로그와 명령 콘솔 | PC terminal에서 바로 확인 가능 | printf, boot log, CLI |
| MCU와 통신 모듈 연결 | TX/RX/GND만으로 구현 가능 | GNSS, Bluetooth, Wi-Fi/LTE modem |
| 비교적 느린 센서 데이터 | 9600~115200 baud로 충분한 경우가 많음 | CO₂, 미세먼지, LiDAR |
| 펌웨어 다운로드와 부트로더 | 단순하고 널리 지원됨 | STM32 system bootloader |
| 두 MCU 간 통신 | 주소가 필요 없는 1:1 연결에 간편 | main MCU ↔ sub MCU |
반대로 다음 상황에서는 다른 인터페이스도 고려해야 한다.
STM32 TX ─────────> 센서 RX
STM32 RX <───────── 센서 TX
STM32 GND ────────── 센서 GND
TX와 RX를 서로 교차 연결하고 GND를 공통으로 묶는다. TX와 RX가 분리되어 있으므로 일반 UART는 동시에 송수신할 수 있는 full duplex 통신이다.
센서 전원 전압과 UART 신호 전압은 별개다. 예를 들어 센서 전원이 5 V이더라도 UART 신호는 3.3 V일 수 있다. 반드시 데이터시트의 I/O voltage를 확인해야 한다.

Idle(1) │ Start(0) │ D0 D1 D2 D3 D4 D5 D6 D7 │ Stop(1)
8N1은 8 data bits, No parity, 1 stop bit를 의미한다.115200 baud에서 한 bit 시간은 약 8.68 μs다. 8N1은 한 바이트를 보내는 데 start 1 + data 8 + stop 1, 총 10 bit가 필요하므로 이론적인 payload 상한은 다음과 같다.
115200 bit/s ÷ 10 bit/byte = 11,520 byte/s
실제 센서 패킷에는 헤더와 checksum 등이 포함되므로 유효 데이터율은 이보다 낮다.
송신 과정

- CPU가
USART_DR에 한 바이트를 기록한다.- USART 하드웨어가 start/data/stop bit를 붙여 TX로 직렬 전송한다.
TXE=1이면 data register가 비어 다음 바이트를 기록할 수 있다.- 마지막 stop bit까지 전송됐는지는
TC=1로 확인한다.
수신 과정

- USART가 RX의 start edge를 검출한다.
- bit 중앙을 oversampling해 값을 판정한다.
- 완성된 바이트를
USART_DR로 옮기고RXNE=1을 설정한다.- CPU가
USART_DR을 읽으면RXNE가 해제된다.- 이전 바이트를 읽기 전에 새 바이트가 오면
OREoverrun이 발생한다.
주요 오류 flag
SR을 읽은 뒤 DR을 읽는 순서를 사용한다.| 플래그 | SR 비트 | 의미 | 발생 조건 |
|---|---|---|---|
PE | bit 0 | Parity Error | 수신한 패리티 값이 계산 결과와 다를 때 |
FE | bit 1 | Framing Error | Stop bit가 정상적인 1로 감지되지 않을 때 |
NF (NE) | bit 2 | Noise Error | 수신 신호에서 잡음이 감지됐을 때 |
ORE | bit 3 | Overrun Error | 기존 데이터를 읽기 전에 다음 데이터가 도착했을 때 |
STM32F446에서 OVER8=0은 16배 oversampling, OVER8=1은 8배 oversampling이다. 16배 방식은 clock 오차 허용 범위가 더 넓고, 8배 방식은 더 높은 최대 baud rate를 허용한다. ONEBIT=0이면 bit 중앙의 세 sample을 majority vote해 잡음을 판정한다.
Asynchronous로 설정하면 실제 배선과 사용 방식은 일반 UART와 동일하다.STM32F446 기준 비교표
| 구분 | UART4, UART5 | USART1, USART2, USART3, USART6 |
|---|---|---|
| 정식 명칭 | Universal Asynchronous Receiver/Transmitter | Universal Synchronous/Asynchronous Receiver/Transmitter |
| 비동기 통신 | 지원 | UART와 동일 |
| 동기 통신 | 미지원 | 지원, master-only |
| 비동기 기본 배선 | TX, RX, GND | UART와 동일 |
| RTS/CTS 사용 시 배선 | TX, RX, RTS, CTS, GND | UART와 동일 |
| 동기 통신 배선 | 해당 없음 | TX, RX, CK, GND |
| 비동기 프레임 | start + data + parity(선택) + stop | UART와 동일 |
| 데이터 길이 | 8비트 또는 9비트 | UART와 동일 |
| Smartcard mode | 미지원 | 지원 |
| LIN mode | 지원 | UART와 동일 |
| IrDA | 지원 | UART와 동일 |
| DMA | 지원 | UART와 동일 |
| 단선 Half-Duplex | 지원 | UART와 동일 |
| RTS/CTS | UART4, UART5 지원 | USART1, USART2, USART3, USART6 지원 |
| HAL 비동기 API | HAL_UART_* | UART와 동일 |
| HAL 동기 API | 해당 없음 | HAL_USART_* |
USART가 언제나 UART보다 빠르다는 뜻은 아니다. 최대 baud rate는 peripheral clock과 oversampling 설정에 의해 결정된다.
또 하나의 주의점이 있다. STM32F4에서 parity를 켜면 parity bit가 word length의 MSB 자리를 사용한다. 따라서 8 data + parity가 필요하면 9-bit word length와 parity를 선택해야 한다. 8-bit word length + parity는 실제로 7 data + parity가 된다.
HAL은 register bit와 clock 계산을 driver가 대신 관리한다. HAL_Init()과 SystemClock_Config()를 먼저 실행한 뒤 아래 초기화를 호출한다.
#include "stm32f4xx_hal.h"
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
UART_HandleTypeDef huart2;
bool hal_uart2_init(uint32_t baud)
{
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_USART2_CLK_ENABLE();
GPIO_InitTypeDef gpio = {0};
gpio.Pin = GPIO_PIN_2 | GPIO_PIN_3;
gpio.Mode = GPIO_MODE_AF_PP;
gpio.Pull = GPIO_PULLUP;
gpio.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
gpio.Alternate = GPIO_AF7_USART2;
HAL_GPIO_Init(GPIOA, &gpio);
huart2.Instance = USART2;
huart2.Init.BaudRate = baud;
huart2.Init.WordLength = UART_WORDLENGTH_8B;
huart2.Init.StopBits = UART_STOPBITS_1;
huart2.Init.Parity = UART_PARITY_NONE;
huart2.Init.Mode = UART_MODE_TX_RX;
huart2.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart2.Init.OverSampling = UART_OVERSAMPLING_16;
return HAL_UART_Init(&huart2) == HAL_OK;
}
bool uart_write_bytes(const uint8_t *data, size_t len, uint32_t timeout_ms)
{
if (len > UINT16_MAX) return false;
return HAL_UART_Transmit(&huart2, (uint8_t *)data,
(uint16_t)len, timeout_ms) == HAL_OK;
}
bool uart_read_byte(uint8_t *data, uint32_t timeout_ms)
{
return HAL_UART_Receive(&huart2, data, 1, timeout_ms) == HAL_OK;
}
CubeMX가 이미 MX_USART2_UART_Init()과 HAL_UART_MspInit()을 생성했다면 위 초기화 전체를 중복해서 넣지 말고, 생성된 huart2를 사용해 송수신 wrapper(read, write)만 추가한다.
GPIO, 클럭, Baud rate, 프레임 설정 등의 기본 초기화는 CubeMX가 생성한 MX_USART2_UART_Init() 사용
따라서 이후 예제에서는 USART2->SR과 USART2->DR을 이용한 송수신 과정만 직접 구현
#include "stm32f4xx_hal.h"
#include <stddef.h>
#include <stdint.h>
/* 1바이트를 DR에 기록 */
static void USART2_WriteByte(uint8_t data)
{
/* DR이 비워질 때까지 대기 */
while ((USART2->SR & USART_SR_TXE) == 0U) {
}
USART2->DR = data;
}
/* 바이트 배열 송신 */
void USART2_SendBytes(const uint8_t *data, size_t length)
{
for (size_t i = 0; i < length; i++) {
USART2_WriteByte(data[i]);
}
/* 마지막 Stop bit까지 전송 완료 대기 */
while ((USART2->SR & USART_SR_TC) == 0U) {
}
}
/* 문자열 송신 */
void USART2_SendString(const char *str)
{
while (*str != '\0') {
USART2_WriteByte((uint8_t)*str);
str++;
}
while ((USART2->SR & USART_SR_TC) == 0U) {
}
}
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART2_UART_Init();
USART2_SendString("Hello UART!\r\n");
uint8_t command[] = {0xFF, 0x01, 0x86, 0x00};
USART2_SendBytes(command, sizeof(command));
while (1) {
}
}
실제 프로그램에서는 무한 대기 대신 SysTick 기반 timeout을 추가하고, PE/FE/NF/ORE를 확인해야 한다. STM32F4에서 오류를 정리하는 기본 순서는 SR을 읽고 이어서 DR을 읽는 것이다.
NUCLEO-F446RE의 기본 solder bridge 상태는 SB13/SB14 ON, SB62/SB63 OFF다. 따라서 PA2/PA3의 USART2는 on-board ST-LINK Virtual COM Port에 연결되고 Arduino D1/D0 및 Morpho header 쪽에서는 끊겨 있다.
PA2/PA3를 실제 센서 header로 사용하려면 보드 revision을 확인한 뒤 공식 지침에 따라 다음처럼 변경해야 한다.
SB13/SB14: OFF
SB62/SB63: ON
납땜 변경 없이 기본 VCP를 유지하고 싶다면 USART2는 debug console로 남겨두고 센서는 USART1 또는 USART3에 연결하는 구성이 편하다.