0x6A Logbook

0x6A Logbook
Shi6a的筆記本
  1. 首頁
  2. 程式開發
  3. 正文

ESP32 藍牙傳統 SPP(Serial Port Profile)完整教學:BT Classic 通訊、RFCOMM 框架、主從架構與 HC-05 相容

2026 年 6 月 21 日 14點熱度 0人點贊 0條評論

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

Bluetooth Classic vs BLE 比較

藍牙傳統協定棧

BT Classic 協定棧 — SPP Profile 架構

SPP 位於協定棧最上層:

  • Radio:79 個 1 MHz 頻道,以 1600 hops/s 跳頻
  • Baseband:管理 ACL 非同步無連線邏輯傳輸
  • Link Manager:配對、認證、加密、功率控制
  • HCI:主機控制器介面(ESP32 內部使用,不開放給用戶)
  • L2CAP:邏輯鏈結控制與適配協定,封裝/分段/重組
  • RFCOMM:序列埠模擬協定,支援 RS-232 控制訊號
  • SDP:服務發現協定,查詢遠端裝置支援的 Profile
  • SPP:序列埠規範,定義 RFCOMM 如何模擬序列埠

SPP 連線流程

SPP 連線建立流程

  1. Inquiry(查詢):ESP32 發送查詢請求,掃描附近裝置(約 10~30 秒)
  2. Page(分頁):找到目標裝置後,發送 Page 訊息建立 ACL 連線
  3. SDP Query:查詢遠端裝置是否支援 SPP Profile
  4. RFCOMM Connect:建立 RFCOMM 通道(虛擬序列埠)
  5. Data Stream:雙向序列資料傳輸開始

RFCOMM 資料框架

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  (最高安全)

效能測試

SPP 吞吐量 vs 封包大小

從測試數據可以總結:

  • 最大吞吐量:約 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 指令相容層 + 相同腳位配置
標籤: 教學
最後更新:2026 年 6 月 21 日

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