ESP32 藍牙傳統 SPP(Serial Port Profile)
ESP32 作為雙模藍牙晶片,同時支援 Bluetooth Classic(BR/EDR)與 Bluetooth Low Energy(BLE)。其中藍牙傳統 SPP 是最實用的功能之一:它將無線藍牙連線模擬成一個虛擬序列埠,讓原本使用有線 UART 的設備可以直接轉為無線通訊。
藍牙傳統 SPP 廣泛應用於:
- Arduino 無線燒錄與序列監控
- GPS 接收器資料串流
- 醫療設備(血壓計、體重計)資料傳輸
- 工業 PLC 無線除錯
- 取代 HC-05/HC-06 藍牙模組
Bluetooth Classic vs BLE

藍牙傳統協定棧
SPP 位於協定棧最上層:
- Radio:79 個 1 MHz 頻道,以 1600 hops/s 跳頻
- Baseband:管理 ACL 非同步無連線邏輯傳輸
- Link Manager:配對、認證、加密、功率控制
- HCI:主機控制器介面(ESP32 內部使用,不開放給用戶)
- L2CAP:邏輯鏈結控制與適配協定,封裝/分段/重組
- RFCOMM:序列埠模擬協定,支援 RS-232 控制訊號
- SDP:服務發現協定,查詢遠端裝置支援的 Profile
- SPP:序列埠規範,定義 RFCOMM 如何模擬序列埠
SPP 連線流程
- Inquiry(查詢):ESP32 發送查詢請求,掃描附近裝置(約 10~30 秒)
- Page(分頁):找到目標裝置後,發送 Page 訊息建立 ACL 連線
- SDP Query:查詢遠端裝置是否支援 SPP Profile
- RFCOMM Connect:建立 RFCOMM 通道(虛擬序列埠)
- Data Stream:雙向序列資料傳輸開始
RFCOMM 資料框架
Arduino 程式設計
藍牙 SPP 從裝置(SerialToSerialBT)
// ESP32 藍牙 SPP 從裝置(最簡範例)
// 手機可透過 Serial Bluetooth Terminal App 連線
#include "BluetoothSerial.h"
BluetoothSerial SerialBT;
void setup() {
Serial.begin(115200);
SerialBT.begin("ESP32_SPP_Device"); // 藍牙名稱
Serial.println("藍牙 SPP 已啟動,等待連線...");
}
void loop() {
// 讀取藍牙接收的資料,轉發到硬體序列埠
if (SerialBT.available()) {
char c = SerialBT.read();
Serial.write(c);
}
// 讀取硬體序列埠,轉發到藍牙
if (Serial.available()) {
char c = Serial.read();
SerialBT.write(c);
}
delay(5);
}
藍牙 SPP 主裝置(掃描並連線)
// ESP32 藍牙 SPP 主裝置 — 掃描並自動連線
#include "BluetoothSerial.h"
BluetoothSerial SerialBT;
String remoteName = "HC-05";
bool connected = false;
void setup() {
Serial.begin(115200);
SerialBT.begin("ESP32_Master");
Serial.println("開始掃描藍牙裝置...");
}
void loop() {
if (!connected) {
// 掃描並連線
BTScanResults *results = SerialBT.discover(15); // 掃描 15 秒
if (results) {
for (int i = 0; i < results->getCount(); i++) {
BTAdvertisedResult *device = results->getDevice(i);
if (device->getName().indexOf(remoteName) >= 0) {
Serial.printf("找到 %s (%s),正在連線...
",
device->getName().c_str(),
device->getAddress().toString().c_str());
if (SerialBT.connect(device)) {
connected = true;
Serial.println("連線成功!");
}
break;
}
}
delete results;
}
if (!connected) {
Serial.println("未找到目標裝置,10 秒後重試...");
delay(10000);
}
} else {
// 已連線,資料透傳
if (SerialBT.available()) {
Serial.write(SerialBT.read());
}
if (Serial.available()) {
SerialBT.write(Serial.read());
}
// 檢查連線狀態
if (!SerialBT.connected()) {
Serial.println("連線中斷!");
connected = false;
delay(1000);
}
}
}
指定 MAC 位址連線(跳過掃描)
// ESP32 藍牙 SPP — 直接連線到指定 MAC 位址
#include "BluetoothSerial.h"
BluetoothSerial SerialBT;
const char* remoteAddress = "00:11:22:33:44:55"; // 目標裝置 MAC
void connectToKnownDevice() {
// 從 MAC 字串建立位址物件
uint8_t addr[6];
sscanf(remoteAddress, "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx",
&addr[0], &addr[1], &addr[2],
&addr[3], &addr[4], &addr[5]);
if (SerialBT.connect(addr)) {
Serial.println("連線成功!");
} else {
Serial.println("連線失敗!");
}
}
自訂 UUID 與多重連線
// ESP32 藍牙 SPP — 自訂 UUID 與多重連線
#include "BluetoothSerial.h"
BluetoothSerial SerialBT;
// 自訂 SPP UUID(預設為 0x1101)
// 可改用自訂 UUID 避免與其他 SPP 應用衝突
void customUUID() {
// 使用 SerialBT API 設定自訂 UUID
// SerialBT.registerSDPServiceRecord(uuid, name);
// UUID 格式: "00001101-0000-1000-8000-00805F9B34FB"
}
// 監控連線事件
void setup() {
Serial.begin(115200);
// 註冊連線回呼
SerialBT.register_callback([](esp_spp_cb_event_t event,
esp_spp_cb_param_t *param) {
switch (event) {
case ESP_SPP_SRV_OPEN_EVT: // 用戶端連入
Serial.printf("用戶端連線: %s
",
param->srv_open.rem_bda->address[0]); // 注意:此為簡化
break;
case ESP_SPP_CLOSE_EVT: // 連線中斷
Serial.println("連線中斷");
break;
default:
break;
}
});
SerialBT.begin("ESP32_SPP_Advanced");
}
ESP-IDF 原生 API
// ESP-IDF 藍牙 SPP 範例
#include "esp_spp_api.h"
static void esp_spp_cb(esp_spp_cb_event_t event,
esp_spp_cb_param_t *param) {
switch (event) {
case ESP_SPP_SRV_OPEN_EVT:
ESP_LOGI("SPP", "Client Connected");
break;
case ESP_SPP_DATA_IND_EVT:
// 收到資料,回呼中處理
ESP_LOGI("SPP", "Data received: %d bytes",
param->data_ind.len);
// 回傳資料
esp_spp_write(param->data_ind.handle,
param->data_ind.len,
param->data_ind.data);
break;
case ESP_SPP_CLOSE_EVT:
ESP_LOGI("SPP", "Client Disconnected");
break;
default:
break;
}
}
void app_main(void) {
esp_bt_controller_mem_release(ESP_BT_MODE_BLE); // 僅使用 BT Classic
esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
esp_bt_controller_init(&bt_cfg);
esp_bt_controller_enable(ESP_BT_MODE_CLASSIC_BT);
esp_bluedroid_init();
esp_bluedroid_enable();
esp_spp_register_callback(esp_spp_cb);
esp_spp_init(ESP_SPP_MODE_CB);
// 啟動 SPP 服務(自訂名稱)
esp_spp_start_srv(ESP_SPP_SEC_NONE, ESP_SPP_ROLE_SLAVE,
0, "ESP32_SPP_IDF");
ESP_LOGI("SPP", "SPP 服務已啟動,等待連線...");
}
藍牙配對與安全
// ESP32 藍牙 SPP 配對模式設定
#include "esp_bt_device.h"
void setupSPPWithPairing() {
// 設定配對 PIN 碼
esp_bt_pin_type_t pin_type = ESP_BT_PIN_TYPE_FIXED;
esp_bt_pin_code_t pin_code;
memcpy(pin_code, "1234", 4); // 固定 PIN 碼
esp_bt_gap_set_pin(pin_type, 4, pin_code);
// 或使用變數 PIN(使用者輸入)
// esp_bt_pin_type_t pin_type = ESP_BT_PIN_TYPE_VARIABLE;
// esp_bt_gap_set_pin(pin_type, 0, NULL);
}
// 連線安全模式
// ESP_SPP_SEC_NONE = 0 (無安全)
// ESP_SPP_SEC_ENCRYPT = 1 (加密)
// ESP_SPP_SEC_AUTH = 2 (認證)
// ESP_SPP_SEC_AUTH_ENCRYPT = 3 (認證 + 加密)
// ESP_SPP_SEC_MODE4_LEVEL4 = 4 (最高安全)
效能測試

從測試數據可以總結:
- 最大吞吐量:約 85 KB/s(封包大小 672 bytes)
- 最佳封包大小:256~512 bytes(效能/延遲平衡)
- 最小封包 (16 bytes):僅 18 KB/s(封包頭部開銷佔比過高)
- 延遲:0.9~7.9 ms,取決於封包大小
- 實際限制:L2CAP MTU = 672 bytes,RFCOMM 最大資料承載
HC-05 相容模式
// ESP32 完全相容 HC-05 AT 指令模式(遷移用)
// HC-05 使用 AT 指令設定,ESP32 原生不支援
// 但可自行實作 AT 指令解析器
void processATCommand(String cmd) {
if (cmd == "AT") {
SerialBT.println("OK");
} else if (cmd.startsWith("AT+NAME")) {
String name = cmd.substring(7);
SerialBT.end();
SerialBT.begin(name.c_str());
SerialBT.println("OKsetname");
} else if (cmd.startsWith("AT+PSWD")) {
String pwd = cmd.substring(7);
SerialBT.println("OKsetPSWD");
} else if (cmd == "AT+ROLE") {
SerialBT.println("+ROLE: 0"); // Slave mode
} else {
SerialBT.println("ERROR");
}
}
多工應用:藍牙 + Wi-Fi 共存
// ESP32 藍牙 + Wi-Fi 同時運行
// ESP32 的 Bluetooth 和 Wi-Fi 共用同一個 2.4 GHz 天線
// 硬體協調器自動切換時槽
#include <WiFi.h>
#include "BluetoothSerial.h"
BluetoothSerial SerialBT;
WiFiServer server(80);
void setup() {
Serial.begin(115200);
// 同時啟用 Wi-Fi 和藍牙
WiFi.begin("SSID", "PASSWORD");
SerialBT.begin("ESP32_DualMode");
server.begin();
Serial.println("Wi-Fi + BT Classic 同時運行");
}
void loop() {
// 藍牙資料處理
if (SerialBT.available()) {
String data = SerialBT.readString();
Serial.printf("[BT] %s
", data.c_str());
}
// Wi-Fi HTTP 伺服器處理
WiFiClient client = server.available();
if (client) {
// 處理 HTTP 請求
}
delay(5);
}
雙模藍牙:BT Classic + BLE 共存
// ESP32 雙模藍牙範例(BT Classic SPP + BLE 同時)
// 注意:需要足夠的記憶體(建議使用 PSRAM)
// 使用 Arduino 框架時,BluetoothSerial 預設使用 BT Classic
// BLE 使用 BLEDevice / BLEServer 函式庫
// 兩者可同時初始化
// 同時啟用:
// BluetoothSerial SerialBT; // BT Classic SPP
// BLEDevice::init("ESP32_BLE"); // BLE
// 記憶體注意事項:
// - BT Classic + BLE 同時啟用需要約 1.2 MB ROM + 200 KB RAM
// - 如果 OOM,請關閉不需要的功能(例如 BT Classic 的 A2DP)
// - 在 ESP-IDF menuconfig 中可以精細控制
// 天線共用:
// ESP32 內建 RF 交換器,BT 和 Wi-Fi 共用天線
// 不需要外部切換電路
實務注意事項
| 問題 | 原因 | 解法 |
|---|---|---|
| 連線不穩定 | 2.4 GHz 干擾(Wi-Fi、微波爐) | 增加重傳機制,使用跳頻 |
| 配對失敗 | PIN 碼不一致 | 設定固定 PIN 碼,或使用 SSP |
| iOS 無法連線 | iOS 不支援 SPP(MFi 限制) | 改用 BLE,或使用 MFi 認證晶片 |
| Android 需位置權限 | Android 6+ 藍牙掃描需位置權限 | 在 App 中請求 ACCESS_FINE_LOCATION |
| Wi-Fi 斷線 | BT + Wi-Fi 共存干擾 | 降低藍牙連線間隔,使用共存模式 |
| ESP32 重啟 | 藍牙堆疊記憶體不足 | 增加 FreeRTOS heap,關閉不需要的 BT Profile |
| HC-05 無法連線 | HC-05 預設為從裝置模式 | 確保 HC-05 角色設定正確 |
| 資料遺失 | RFCOMM 流量控制未設定 | 啟用 RTS/CTS 硬體流控 |
常見應用
| 應用 | 主/從 | 資料流量 | 備註 |
|---|---|---|---|
| 無線序列監控 | 從 | 低 (< 1 KB/s) | 取代 USB 線,方便除錯 |
| Arduino 無線燒錄 | 從 | 中 (~10 KB/s) | 需搭配 ESP32 燒錄工具 |
| GPS 資料串流 | 主 | 低 (~4800 bps) | NMEA 0183 協定 |
| 工業感測器資料收集 | 主 | 中 (~115200 bps) | Modbus RTU over SPP |
| 音頻串流 (APTX) | 從 | 高 (~300 KB/s) | 使用 A2DP Profile,非 SPP |
| HC-05 替代 | 主/從 | 可設定 | 完全相容,功能更強 |
總結
ESP32 的藍牙傳統 SPP 是 IoT 開發中非常實用的功能,特別是作為有線 UART 的無線替代方案。與市面上常見的 HC-05/HC-06 模組相比,ESP32 內建藍牙不僅成本更低、功耗更優,而且可以同時運行 Wi-Fi + BT Classic + BLE 三種模式。
選型參考:
- 最簡序列透傳:BluetoothSerial + SerialBT.begin()(一行初始化)
- 主裝置主動連線:SerialBT.discover() + SerialBT.connect()
- 雙模共存:BT Classic SPP + BLE 同時運行
- 工業應用:ESP-IDF esp_spp_api + 加密配對 + 流量控制
- HC-05 遷移:AT 指令相容層 + 相同腳位配置
文章評論