0x6A Logbook

0x6A Logbook
Shi6a的筆記本
  1. 首頁
  2. 正文

UART 通訊協定完整教學:從原理到 STM32/ESP32 實作

2026 年 5 月 24 日 14點熱度 0人點贊 0條評論

前言

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 幀包含以下部分:

  1. Idle(空閒):線路為高電位(邏輯 1)
  2. Start Bit(起始位元):拉低線路(邏輯 0),持續 1 個位元時間
  3. Data Bits(資料位元):5~9 位元,預設為 8 位元,LSB first(最低有效位在前)
  4. Parity Bit(可選同位元):None / Even / Odd
  5. 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

UART 8N1 幀格式

圖 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)

不同 Baud Rate 的位元時間比較

圖 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 確認後才發送

RTS/CTS 硬體流控制

圖 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 硬體接線常見雷區

  1. TX/RX 交叉:很多人初次接 UART 把 TX-TX、RX-RX 連在一起,什麼都收不到
  2. 共地:兩端必須共用 GND,否則 floating 電位會造成隨機亂碼
  3. 電壓位準:STM32 通常是 3.3V,某些 GPS 模組用 5V,必須加電平轉換
  4. 閒置腳位:未使用的 UART 腳位建議外部 pull-up 避免 floating 觸發中斷

5.3 資料遺失的排查

如果發送長封包時丟資料,檢查順序:

  1. Buffer 太小 → 加大 RX buffer
  2. 中斷優先級不足 → 檢查 NVIC 優先權設置
  3. 沒有啟用 FIFO → STM32 USART 有 16 byte FIFO,啟用後減少中斷頻率
  4. 流控制未啟用 → 對方發太快,接收端來不及處理
  5. 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 配置到自訂協定,每個環節都能影響最終穩定度。幾個學習重點:

  1. 訊號層級:理解 Start Bit、Data Bit、Stop Bit 的時序
  2. 速率匹配:計算 baud rate 誤差,確保在 ±2% 以內
  3. 接收策略:輪詢 < 中斷 < DMA+IDLE(由低到高效)
  4. 容錯設計:加上 CRC、超時重傳、狀態機解析
  5. 硬體考慮:共地、電壓位準、接線極性

不論是 STM32 還是 ESP32,UART 的原理都是一致的——理解原理後,換平台只是換 API 而已。

UART 常見配置與速率

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

標籤: RS485 工業通訊
最後更新:2026 年 5 月 24 日

shi6a

這個人很懶,什麼都沒留下

點贊

文章評論

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回覆

COPYRIGHT © 2026 0x6A Logbook. ALL RIGHTS RESERVED.

Theme Kratos Made By Seaton Jiang