UART 是 STM32 專案裡最常用、也最容易「看起來都設定對了卻沒有資料」的介面。遇到收不到資料、亂碼或偶發遺失時,先不要急著改程式;用固定順序排查,通常能很快縮小問題。

先確認接線與電氣條件

UART 的 TX 與 RX 必須交叉連接:開發板的 TX 接到對方 RX,開發板的 RX 接到對方 TX。同時,兩端一定要共地(GND)。

另一個常見問題是電壓位準。多數 STM32 GPIO 是 3.3V 邏輯,不能直接接 5V TTL 裝置的 TX;若對方輸出 5V,先確認該腳位是否 5V tolerant,或使用電平轉換器。

核對 UART 參數

兩端所有通訊參數都要一致。最常見組合是 115200 8N1

  • 鮑率:115200
  • 資料位元:8
  • 同位元檢查:None
  • 停止位元:1
  • 流量控制:None

只要停止位元或 parity 不一致,就可能出現可讀字元混雜亂碼的情況。先用 USB-to-UART 模組與序列埠工具驗證對方裝置是否真的以預期參數送資料。

檢查時脈是否真的符合預期

UART 的實際鮑率由 peripheral clock 推導。若 SystemClock_Config() 修改了 PLL、APB1 或 APB2 的分頻,UART 設定的 115200 可能已經不是實際的 115200。

建議在程式中確認 UART 使用的 peripheral clock,尤其是不同系列 STM32 對 APB 時脈與 timer/UART 的規則不完全相同。時脈錯誤時,終端機常會看到固定模式的亂碼。

先用最小化輪詢程式驗證

在導入 interrupt 或 DMA 前,先用阻塞式 API 驗證硬體路徑。這可以把問題切成「硬體與設定」或「非同步收發流程」。

uint8_t message[] = "UART ready\r\n";

HAL_UART_Transmit(&huart2, message, sizeof(message) - 1, 1000);

uint8_t rx;
if (HAL_UART_Receive(&huart2, &rx, 1, 1000) == HAL_OK) {
    HAL_UART_Transmit(&huart2, &rx, 1, 1000); // echo back
}

若連這段都沒有輸出,優先回到接線、GPIO alternate function、時脈和鮑率,而不是先懷疑 DMA。

Interrupt 模式的常見陷阱

使用 HAL_UART_Receive_IT() 時,接收完成 callback 只會觸發一次;若要持續接收,必須在 callback 裡重新啟動下一次接收。

static uint8_t rx_byte;

void uart_start_receive(void) {
    HAL_UART_Receive_IT(&huart2, &rx_byte, 1);
}

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    if (huart->Instance == USART2) {
        process_byte(rx_byte);
        HAL_UART_Receive_IT(&huart2, &rx_byte, 1);
    }
}

也要確認 USARTx_IRQHandler() 有呼叫 HAL_UART_IRQHandler(&huartx),且 NVIC 的 UART interrupt 已啟用。少了其中一項,callback 不會執行。

DMA 接收要處理資料邊界

DMA 很適合連續資料流,但它不知道一筆訊息在哪裡結束。實務上常搭配 UART idle line 偵測:當線路短暫閒置時,把目前收到的資料長度交給上層解析。

static uint8_t rx_buffer[256];

HAL_UARTEx_ReceiveToIdle_DMA(&huart2, rx_buffer, sizeof(rx_buffer));

void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t size) {
    if (huart->Instance == USART2) {
        parse_packet(rx_buffer, size);
        HAL_UARTEx_ReceiveToIdle_DMA(&huart2, rx_buffer, sizeof(rx_buffer));
    }
}

若使用 circular DMA,解析端要記錄上次讀取位置,避免同一段資料重複處理。對於含有明確封包頭、長度欄位與 checksum 的協定,建議把「接收資料」與「封包解析」拆成不同層,除錯會容易很多。

一份實用的排查順序

遇到 UART 異常時,我通常依照以下順序:

  1. TX/RX 是否交叉、兩端是否共地、位準是否安全。
  2. 用序列埠工具核對鮑率與 8N1 參數。
  3. 確認 GPIO alternate function、UART peripheral clock 與 NVIC。
  4. 用阻塞式 transmit/receive 做最小測試。
  5. 再檢查 interrupt callback 是否重新啟動接收。
  6. 最後才處理 DMA buffer、idle line 與封包邊界。

這套順序的核心是:先證明底層鏈路能通,再逐層加入非同步機制。比起一次改很多設定,這樣更容易找到真正的故障點。