Learning Objectives
- 說明 UART 的接線方式(TX 接 RX、共地),以及兩端鮑率必須相同的原因
- 由鮑率估算傳送一段資料需要多少時間
- 用 machine.UART 送出資料,並用 any()、readline() 在不卡住程式的情況下接收
- 解析 GPS 模組送來的 NMEA 語句,取出衛星數與經緯度
一個位元接一個位元地傳
上一課的 I2C 有一條時脈線,控制端用時脈告訴大家「現在讀這個位元」。UART(Universal Asynchronous Receiver-Transmitter) 沒有時脈線,是非同步的:雙方事先約定好速度,各自計時。GPS 模組、藍牙序列模組、指紋辨識模組, 還有你用 USB 線連到電腦的 REPL,都是 UART。
這一課要回答:
- UART 怎麼接線?兩端要約定什麼?
- MicroPython 怎麼送出與接收資料,而且不讓程式卡住?
- GPS 模組一直送來的一行行文字,要怎麼變成經緯度?
接線:交叉接、要共地
每一端都有 TX(傳送)與 RX(接收)。一端的 TX 要接到另一端的 RX,兩條線交叉:
ESP32 GPS 模組
GPIO17 (TX) ──────▶ RX
GPIO16 (RX) ◀────── TX
GND ─────────────── GND
3.3V ────────────── VCC(依模組規格)
GND 一定要接,雙方的電壓才有共同的參考點。另外要確認模組的訊號電壓:ESP32 的腳位是 3.3V, 接 5V 訊號的模組前要先看規格,必要時加電位轉換。
鮑率與資料框
鮑率(baud rate)是每秒傳幾個位元,雙方必須設成一樣,常見的有 9600、115200。 最常見的格式叫 8N1:每個位元組前面加一個起始位元、後面加一個停止位元,8 個資料位元、不用同位檢查, 所以每傳一個位元組要 10 個位元:
閒置(高) │起始│ b0 b1 b2 b3 b4 b5 b6 b7 │停止│ 閒置…
0 ← 8 個資料位元,低位在前 → 1
9600 鮑率每秒 9600 個位元,也就是每秒最多 960 個位元組。鮑率設錯的話,接收端在錯的時間點讀位元, 收到的就是一堆亂碼。
ESP32 的三組 UART
ESP32 有三組硬體 UART:UART0、UART1、UART2。依 MicroPython 文件,它們的預設腳位是:
| UART0 | UART1 | UART2 | |
|---|---|---|---|
| TX | GPIO1 | GPIO10 | GPIO17 |
| RX | GPIO3 | GPIO9 | GPIO16 |
- UART0 通常接到開發板的 USB 轉序列晶片,給 REPL 與上傳程式用,不要拿來接模組。
- UART1 的預設腳位常和開發板的快閃記憶體衝突,所以建立時最好明確寫出
tx=、rx=。 - 這一課用 UART2(TX=GPIO17、RX=GPIO16)。GPIO34~39 只能輸入,只能當 RX。
MicroPython 的 UART 會把收到的位元組先放進接收緩衝區,程式之後再讀:
| 函式 | 作用 |
|---|---|
uart.write(資料) |
送出字串或 bytes,回傳送出的位元組數 |
uart.any() |
緩衝區裡有幾個位元組(0 表示還沒有資料) |
uart.read() |
讀出緩衝區全部的資料(bytes);沒有資料時回傳 None |
uart.readline() |
讀到換行 \n 為止(包含換行);沒有資料時回傳 None |
收到的是 bytes,要當字串處理時用 .decode() 轉換。
逐步看:送出指令、讀回應答
很多序列模組用「AT 指令」設定:送出 AT 加換行,模組回 OK。模擬中的模組在 0.12 秒時回應。
注意第 7 行之後時間前進了約 4 毫秒(送 4 個位元組要花的時間),以及 reply 是 bytes 而不是字串。
逐步看:一行一行接收 GPS 資料
GPS 模組開機後會自己一直送資料,通常每秒一組。程式每 0.25 秒看一次緩衝區,有資料才讀一行, 沒有就去做別的事。這是第 07 課「不讓程式卡住」的寫法。
NMEA 語句:GPS 說的話
GPS 模組送出的文字遵守 NMEA 0183 格式:每一句以 $ 開頭、欄位用逗號分隔、* 後面是兩位十六進位的檢查碼,
最後以 \r\n 結束,一句最長 82 個字元。最常用的一種是 GGA(定位資料):
$GPGGA,023045.00,2458.0870,N,12111.4820,E,1,08,0.9,150.2,M,17.5,M,,*60
[0] [1] [2] [3] [4] [5][6][7] [8] [9]
| 索引 | 內容 | 這一句的值 |
|---|---|---|
| 0 | 語句種類(GP=GPS、GGA) | $GPGGA |
| 1 | UTC 時間 hhmmss.ss | 02:30:45(臺灣時間要加 8 小時) |
| 2、3 | 緯度 ddmm.mmmm、N/S | 24 度 58.0870 分,北緯 |
| 4、5 | 經度 dddmm.mmmm、E/W | 121 度 11.4820 分,東經 |
| 6 | 定位品質(0=尚未定位,1=GPS 定位) | 1 |
| 7 | 使用的衛星數 | 08 |
| 9 | 海拔(公尺) | 150.2 |
度分換算成度:2458.0870 是 24 度 58.0870 分,1 度=60 分,所以是 24 + 58.0870 ÷ 60 ≈ 24.9681 度。
地圖服務常用的就是這種十進位度數。
檢查碼:把 $ 和 * 之間的每個字元依序做 XOR(^),結果應該等於 * 後面的十六進位數字。
不相等就表示傳輸過程中有字元錯了,這一句要丟掉。
依 Waveshare 的模組說明,常見的 u-blox NEO-6M 模組預設輸出 NMEA、鮑率 9600。
逐步看:解析 GGA,算出經緯度
收到一句之後:轉成字串 → 用逗號切成串列 → 檢查是不是 GGA、有沒有定位 → 換算經緯度。注意 to_degrees 怎麼用 // 把度與分拆開。
小測驗:接線與鮑率
ESP32 和 GPS 模組用 UART 連接,下面哪一種接法正確?
鮑率 9600、格式 8N1(每個位元組 10 個位元),送出 96 個位元組大約要多久?
uart.any() 回傳 0 時呼叫 uart.readline(),會得到什麼?
小測驗:解析 NMEA
下面的程式會印出什麼?
line = "$GPGGA,023045.00,,,,,0,03,99.99,,,,,,*64"
f = line.split(",")
print(len(f), f[6], int(f[7]))
下面的程式會印出什麼?(緯度的度分換算)
v = float("2230.6000")
deg = int(v // 100)
print(round(deg + (v - deg * 100) / 60, 4))
下面的程式計算一句 NMEA 的檢查碼,會印出什麼?
body = "GPS"
c = 0
for ch in body:
c ^= ord(ch)
print("%02X" % c)
重點整理
- UART 沒有時脈線,雙方要設定相同的鮑率;8N1 每個位元組 10 個位元,9600 鮑率每秒約 960 個位元組。
- 接線:TX 接對方的 RX、RX 接對方的 TX,GND 一定要接;注意訊號電壓是 3.3V。
- ESP32 有三組 UART,UART0 留給 REPL;這一課用
UART(2, baudrate=9600, tx=17, rx=16)。 write()送出;any()先看緩衝區有沒有資料,再read()/readline(),收到的是 bytes,用decode()轉字串。- NMEA 語句用逗號分欄位、
*後面是 XOR 檢查碼;GGA 的緯度 ddmm.mmmm 要換算成十進位度數,沒定位時欄位是空的。
下一課讓 ESP32 連上 Wi-Fi,開始和網路上的服務交換資料。