前言
UART(Universal Asynchronous Receiver Transmitter)是嵌入式開發最常見的通訊介面之一。雖然現在有 SPI、I2C、RS485、CAN 等協定百花齊放,但 UART 仍然是除錯、GPS 模組、藍牙模組、工業感測器溝通的主要手段。很多初學者以為 UART 只是「隨便接一接就能通」,實際上 baud rate 誤差、中斷設計、資料遺失等問題經常讓專案卡關。這篇文章會從訊號層級一路講到 STM32 與 ESP32 的實戰程式碼。
一、UART 基本原理
1.1 訊號架構
UART 使用兩條資料線進行全雙工通訊:
- TX(Transmit):發送資料
- RX(Receive):接收資料
兩端交叉連接:A 的 TX 接 B 的 RX,A 的 RX 接 B 的 TX。
UART 是非同步通訊——沒有獨立的時脈線。雙方靠約定好的 baud rate(每秒傳輸的位元數)取得同步。
1.2 UART 幀格式
一個完整的 UART 幀包含以下部分:
- Idle(空閒):線路為高電位(邏輯 1)
- Start Bit(起始位元):拉低線路(邏輯 0),持續 1 個位元時間
- Data Bits(資料位元):5~9 位元,預設為 8 位元,LSB first(最低有效位在前)
- Parity Bit(可選同位元):None / Even / Odd
- Stop Bit(停止位元):拉高線路,持續 1、1.5 或 2 個位元時間
最常見的配置是 8N1:8 資料位元、無同位、1 停止位元。
Start Bit: 0
Data: D0 D1 D2 D3 D4 D5 D6 D7 (LSB first)
Stop Bit: 1
圖 1:UART 8N1 幀格式 — 從 Start Bit 開始,依序傳送 D0~D7,最後以 Stop Bit 結束。
如果啟用同位檢查(Even/Odd Parity),資料位元與同位位元中 1 的總數會維持一致值。Even Parity 使總數為偶數,Odd Parity 使總數為奇數。
1.3 Baud Rate 與誤差
Baud rate 直接影響通訊可靠性。理論上雙方 baud rate 必須完全一致,但實務上允許 ±2%~±3% 的誤差。常見速率:
| Baud Rate | 位元時間 | 適用場景 |
|---|---|---|
| 300 | 3.33 ms | GPS / 舊設備 |
| 1200 | 833 μs | 工業長距離 |
| 9600 | 104 μs | 標準工業 / 感測器 |
| 115200 | 8.68 μs | 除錯 / STM32 預設 |
| 921600 | 1.09 μs | ESP32 高吞吐 |
| 2000000 | 0.5 μs | 高速傳輸(ESP32) |
圖 2:9600 / 115200 / 921600 bps 的單個位元時間差異。速率越高,每個位元越窄,對時脈精度的要求也越高。
實務重點:當使用非標準 baud rate(如 250000、500000)時,務必確認 MCU 的 UART clock 是否能整除出目標值。STM32 的 USART 時脈公式為:
USART_DIV = UART_CLK / (8 × (2 - OVER8) × BaudRate)
OVER8=0 為 16 倍 oversampling,OVER8=1 為 8 倍。如果除不盡,誤差會累積導致資料錯誤。
二、硬體流量控制(RTS/CTS)
當接收端來不及處理資料時,需要能通知發送端暫停傳送。這就是流控制的功能:
- RTS(Request To Send):接收端輸出,通知對方「我可以接收」
- CTS(Clear To Send):發送端輸入,收到 RTS 確認後才發送
圖 3:RTS/CTS 硬體流控制 — 當 RX buffer 快滿時,接收端拉高 RTS,發送端將 CTS 拉高後暫停傳送,直到 buffer 清空。
在 STM32 中啟用硬體流控制只需一行:
// HAL_UART_Init 中的 UART_HandleTypeDef
huart1.Init.HwFlowCtl = UART_HWCONTROL_RTS_CTS;
在 ESP32 Arduino 中:
Serial2.begin(115200, SERIAL_8N1, RX_PIN, TX_PIN, false, 256);
// 最後一個參數為 invert 信號,不表示 RTS/CTS
// ESP32 硬體 RTS/CTS 需用 Serial2.setPins(RX, TX, RTS, CTS)
三、STM32 UART 實作(HAL Library)
3.1 基本初始化
#include "stm32f4xx_hal.h"
UART_HandleTypeDef huart2;
void MX_USART2_UART_Init(void)
{
huart2.Instance = USART2;
huart2.Init.BaudRate = 115200;
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;
HAL_UART_Init(&huart2);
}
3.2 輪詢模式發送
HAL_UART_Transmit(&huart2, (uint8_t *)"Hello UART\r\n", 14, HAL_MAX_DELAY);
3.3 中斷模式接收
#define BUF_SIZE 256
uint8_t rx_buffer[BUF_SIZE];
uint16_t rx_index = 0;
void start_reception(void)
{
HAL_UART_Receive_IT(&huart2, &rx_char, 1);
}
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART2)
{
rx_buffer[rx_index++] = rx_char;
if (rx_index >= BUF_SIZE) rx_index = 0;
// 收到換行字元時觸發處理
if (rx_char == '\n')
{
process_line(rx_buffer, rx_index);
rx_index = 0;
}
HAL_UART_Receive_IT(&huart2, &rx_char, 1);
}
}
3.4 DMA 模式收發
uint8_t tx_data[] = "DMA Transfer Test\r\n";
uint8_t rx_dma_buffer[128];
// 發送
HAL_UART_Transmit_DMA(&huart2, tx_data, sizeof(tx_data));
// 接收(IDLE line 中斷 + DMA)
HAL_UART_Ex_ReceiveToIdle_DMA(&huart2, rx_dma_buffer, sizeof(rx_dma_buffer));
__HAL_UART_ENABLE_IT(&huart2, UART_IT_IDLE);
// 回呼
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
if (huart->Instance == USART2) {
HAL_UART_Transmit_DMA(&huart2, rx_dma_buffer, Size);
HAL_UART_Ex_ReceiveToIdle_DMA(&huart2, rx_dma_buffer, 128);
}
}
DMA + IDLE 中斷是最實用的 UART 接收模式:一次接收任意長度的封包,不需預先知道資料長度。
四、ESP32 UART 實作
4.1 Arduino 框架
#define RX2_PIN 16
#define TX2_PIN 17
void setup()
{
Serial.begin(115200);
Serial2.begin(115200, SERIAL_8N1, RX2_PIN, TX2_PIN);
Serial2.setTimeout(50);
}
void loop()
{
if (Serial2.available())
{
String line = Serial2.readStringUntil('\n');
line.trim();
Serial2.print("Echo: ");
Serial2.println(line);
Serial.println("RX: " + line);
}
}
4.2 ESP-IDF 框架
#include "driver/uart.h"
#include "driver/gpio.h"
#define UART_PORT UART_NUM_2
#define TXD_PIN GPIO_NUM_17
#define RXD_PIN GPIO_NUM_16
#define BUF_SIZE 1024
void uart_init(void)
{
const uart_config_t config = {
.baud_rate = 115200,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
.source_clk = UART_SCLK_APB,
};
uart_param_config(UART_PORT, &config);
uart_set_pin(UART_PORT, TXD_PIN, RXD_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
uart_driver_install(UART_PORT, BUF_SIZE * 2, 0, 0, NULL, 0);
}
void app_main(void)
{
uart_init();
uint8_t buf[256];
while (1)
{
int len = uart_read_bytes(UART_PORT, buf, sizeof(buf)-1, pdMS_TO_TICKS(100));
if (len > 0)
{
buf[len] = '\0';
uart_send((char *)buf, len);
}
vTaskDelay(pdMS_TO_TICKS(10));
}
}
4.3 ESP32 UART 事件佇列(ESP-IDF)
QueueHandle_t uart_queue;
uart_driver_install(UART_PORT, BUF_SIZE * 2, BUF_SIZE * 2, 20, &uart_queue, 0);
void uart_event_task(void *pvParameters)
{
uart_event_t event;
uint8_t data[256];
while (1) {
if (xQueueReceive(uart_queue, &event, portMAX_DELAY))
{
switch (event.type) {
case UART_DATA:
uart_read_bytes(UART_PORT, data, event.size, portMAX_DELAY);
break;
case UART_FIFO_OVF:
uart_flush_input(UART_PORT);
break;
case UART_BUFFER_FULL:
uart_flush_input(UART_PORT);
break;
case UART_PARITY_ERR:
// 同位檢查錯誤
break;
case UART_FRAME_ERR:
// 幀錯誤
break;
default:
break;
}
}
}
}
五、實戰陷阱與除錯技巧
5.1 Baud Rate 誤差計算
以 STM32F407 @ 84 MHz APB2 clock 為例,目標 115200 bps:
USARTDIV = 84000000 / (16 × 115200) = 45.5729
整數部分 = 45, 小數部分 = 0.5729 × 16 ≈ 9
實際 = 84000000 / (16 × (45 + 9/16)) = 115226
誤差 = (115226 - 115200) / 115200 × 100% = 0.023%
0.023% 相當安全。但目標為 250000 bps 時:
USARTDIV = 84000000 / (16 × 250000) = 21.0 → 精確
黃金法則:先查 MCU 的 UART clock,再用公式驗算目標 baud rate 是否整除。
5.2 硬體接線常見雷區
- TX/RX 交叉:很多人初次接 UART 把 TX-TX、RX-RX 連在一起,什麼都收不到
- 共地:兩端必須共用 GND,否則 floating 電位會造成隨機亂碼
- 電壓位準:STM32 通常是 3.3V,某些 GPS 模組用 5V,必須加電平轉換
- 閒置腳位:未使用的 UART 腳位建議外部 pull-up 避免 floating 觸發中斷
5.3 資料遺失的排查
如果發送長封包時丟資料,檢查順序:
- Buffer 太小 → 加大 RX buffer
- 中斷優先級不足 → 檢查 NVIC 優先權設置
- 沒有啟用 FIFO → STM32 USART 有 16 byte FIFO,啟用後減少中斷頻率
- 流控制未啟用 → 對方發太快,接收端來不及處理
- DMA 配置錯誤 → DMA 循環模式 / 增量位址不正確
六、拓展:UART 的變體與應用
6.1 RS-232 vs TTL UART
| 特性 | TTL UART | RS-232 |
|---|---|---|
| 邏輯 1 | 3.3V / 5V | -3V ~ -15V |
| 邏輯 0 | 0V | +3V ~ +15V |
| 最大距離 | ~1m | ~15m |
| 常見應用 | MCU 內部通訊 | 工業電腦、DB9連接器 |
| 電平轉換 | 不需要 | MAX3232 晶片 |
6.2 自訂協定
裸發資料容易出錯,建議在 UART 上層封裝簡單協定:
[Header 0xAA][Length][Payload...][CRC16][Footer 0x55]
typedef struct {
uint8_t header; // 0xAA
uint8_t length;
uint8_t payload[256];
uint16_t crc;
uint8_t footer; // 0x55
} __attribute__((packed)) UartPacket;
接收端以狀態機解析:
typedef enum {
WAIT_HEADER,
WAIT_LENGTH,
WAIT_PAYLOAD,
WAIT_CRC,
WAIT_FOOTER,
} ParserState;
ParserState state = WAIT_HEADER;
void parse_byte(uint8_t byte)
{
switch (state)
{
case WAIT_HEADER:
if (byte == 0xAA) {
state = WAIT_LENGTH;
}
break;
// ... 其餘狀態同理
}
}
七、總結
UART 看似簡單,但從 baud rate 計算、中斷設計、DMA 配置到自訂協定,每個環節都能影響最終穩定度。幾個學習重點:
- 訊號層級:理解 Start Bit、Data Bit、Stop Bit 的時序
- 速率匹配:計算 baud rate 誤差,確保在 ±2% 以內
- 接收策略:輪詢 < 中斷 < DMA+IDLE(由低到高效)
- 容錯設計:加上 CRC、超時重傳、狀態機解析
- 硬體考慮:共地、電壓位準、接線極性
不論是 STM32 還是 ESP32,UART 的原理都是一致的——理解原理後,換平台只是換 API 而已。

圖 4:UART 常見配置格式一覽表與 baud rate 對照。
文章評論