BLE SPP 中心设备(Central)示例深度解析:用 esp-iot-solution 在 BLE 上构建虚拟串口
【免费下载链接】esp-iot-solutionEspressif IoT Library. IoT Device Drivers, Documentations and Solutions.项目地址: https://gitcode.com/GitHub_Trending/es/esp-iot-solution
导读
本文围绕 esp-iot-solution 仓库中 examples/bluetooth/ble_conn_mgr/ble_spp/spp_client 示例展开,深入讲解如何在 BLE(Bluetooth Low Energy)上实现串口仿真(SPP,Serial Port Profile)的中心设备(Central/Client)端。你将掌握:BLE SPP 自定义 Profile 的设计动机与 GATT 属性表结构、UART 与 BLE 之间的数据搬运任务与队列模型、MTU 协商与分片包协议,以及基于ble_conn_mgr组件从扫描连接到收发数据的完整实现原理,可直接据此搭建自己的"无线串口"应用或与手机 APP 对接。
一、背景:为什么 BLE 需要"自定义 SPP"
在经典蓝牙(BR/EDR)系统中,SPP(Serial Port Profile)是蓝牙 SIG 定义的标准 Profile,用于在蓝牙无线链路上仿真一个串口连接。但在 BLE 系统中,SIG没有定义一套被采纳的 SPP Profile,因此要在 BLE 上仿真串口,就必须将其实现为厂商自定义的 Profile(vendor-specific custom profile)。
本参考设计由两个 Demo 组成,分别运行在各自的端点上:
| Demo | 角色 | 仓库路径 |
|---|---|---|
| BLE SPP server(外围设备) | 提供 GATT 服务、广播 | spp_server |
| BLE SPP client(中心设备) | 被动扫描、连接、读写特征值 | spp_client |
这两个设备通过无线方式连接并交换数据,从而在空气中创建一条虚拟串行链路:服务器与客户端的每一个字节输入都可以被对方接收。示例中两端都使用UART 作为传输层,但这一设计可以很方便地改造适配其他串行协议(例如 SPI)——只要替换 UART 任务中的数据来源即可。
该厂商自定义 Profile 的核心实现位于 spp_client/main/app_main.c 与 spp_server/main/app_main.c。
二、初始化流程:UART 与 BLE 双通道就绪
服务器与客户端启动时都会先初始化 UART 和 BLE:
- 服务器(server):在 GATT 属性服务器中建立串口服务,连同标准的 GATT、GAP 服务一起注册;
- 客户端(client):对空中的 BLE 广播执行被动扫描,寻找 SPP 服务器并建立连接。
以客户端 app_main.c 的app_main()为例,初始化顺序清晰可见:
void app_main(void) { esp_ble_conn_config_t config = { .device_name = CONFIG_EXAMPLE_BLE_ADV_NAME, // 广播中的设备名,见 Kconfig.projbuild .broadcast_data = CONFIG_EXAMPLE_BLE_SUB_ADV // 后续广播数据 }; // 1. 初始化 NVS(蓝牙协议栈需要) ret = nvs_flash_init(); if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { ESP_ERROR_CHECK(nvs_flash_erase()); ret = nvs_flash_init(); } ESP_ERROR_CHECK(ret); // 2. 创建默认事件循环,注册 ble_conn_mgr 事件处理 esp_event_loop_create_default(); esp_event_handler_register(BLE_CONN_MGR_EVENTS, ESP_EVENT_ANY_ID, app_ble_conn_event_handler, NULL); // 3. 初始化 UART 并创建 UART 处理任务 ble_spp_uart_init(); // 4. 初始化并启动 BLE 连接管理 esp_ble_conn_init(&config); if (esp_ble_conn_start() != ESP_OK) { esp_ble_conn_stop(); esp_ble_conn_deinit(); esp_event_handler_unregister(BLE_CONN_MGR_EVENTS, ESP_EVENT_ANY_ID, app_ble_conn_event_handler); } }其中esp_ble_conn_config_t中的device_name与broadcast_data分别来自 Kconfig 选项CONFIG_EXAMPLE_BLE_ADV_NAME与CONFIG_EXAMPLE_BLE_SUB_ADV(定义于 Kconfig.projbuild,默认值分别为ESP_SPP_SERVER与SUB_ADV,可通过idf.py menuconfig修改)。
三、UART 初始化与任务/队列模型
ble_spp_uart_init()(spp_client/main/app_main.c)配置 UART0 并创建处理任务:
uart_config_t uart_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_RTS, // 硬件流控 .rx_flow_ctrl_thresh = 122, #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) .source_clk = UART_SCLK_DEFAULT, // IDF 5.0 及之后 #else .source_clk = UART_SCLK_APB, #endif }; // 安装 UART 驱动并取得事件队列(rx 缓冲 4096 字节,tx 缓冲 8192 字节) uart_driver_install(UART_NUM_0, 4096, 8192, 10, &spp_common_uart_queue, 0); uart_param_config(UART_NUM_0, &uart_config); uart_set_pin(UART_NUM_0, UART_PIN_NO_CHANGE, ...); // 创建 UART 处理任务,栈 4096 字节,优先级 8 xTaskCreate(ble_client_uart_task, "uTask", 4096, (void*)UART_NUM_0, 8, NULL);整个 SPP 应用用到的队列与任务如下(客户端):
- 队列
spp_common_uart_queue:存放从 UART 收到的数据消息(UART data messages received from the Uart); - 任务
ble_client_uart_task:处理 UART 数据。
对应地,服务器端使用ble_server_uart_task处理 UART。两个任务的核心逻辑相同:阻塞等待 UART 事件队列,收到UART_DATA事件后读取原始字节。
客户端 UART 任务如何发送数据
客户端的 ble_client_uart_task 在收到UART_DATA事件后:
- 按
event.size分配缓冲区,调用uart_read_bytes(UART_NUM_0, temp, event.size, portMAX_DELAY)读走数据; - 遍历
ATTRIBUTE_MAX_CONNECTIONS个连接槽位,对每个有效的attribute_handle[i]构造esp_ble_conn_data_t(UUID 类型为 16 位、UUID 值为GATT_SPP_CHR_UUID,即0xABF1); - 调用
esp_ble_conn_write(&inbuff)执行 GATT 写操作,成功则打印Write in uart task success!,每轮之间vTaskDelay(10)做节流。
其中连接槽位数量由协议栈决定:
#ifdef CONFIG_BT_NIMBLE_ENABLED #define ATTRIBUTE_MAX_CONNECTIONS CONFIG_BT_NIMBLE_MAX_CONNECTIONS #else #define ATTRIBUTE_MAX_CONNECTIONS CONFIG_BT_ACL_CONNECTIONS #endifattribute_handle[i]在ESP_BLE_CONN_EVENT_CONNECTED事件中被置为非零,表示该槽位可用。
服务器 UART 任务如何发送通知
服务器的 ble_server_uart_task 逻辑对称:收到 UART 数据后,对每个有效连接调用esp_ble_conn_notify(&inbuff)发送通知(notification),成功打印Notification sent successfully。
四、事件处理函数:事件驱动架构
BLE SPP 应用的核心事件处理函数:
- 服务器拥有一个特征值回调 + 一个事件处理函数:
esp_spp_chr_cb(const uint8_t *inbuf, uint16_t inlen, uint8_t **outbuf, uint16_t *outlen, void *priv_data, uint8_t *att_status); app_ble_conn_event_handler(void *handler_args, esp_event_base_t base, int32_t id, void *event_data);- 客户端拥有一个主事件处理函数:
app_ble_conn_event_handler(void *handler_args, esp_event_base_t base, int32_t id, void *event_data);客户端的 GATT 事件处理
客户端的app_ble_conn_event_handler(spp_client/main/app_main.c)监听BLE_CONN_MGR_EVENTS事件基,处理三类关键事件:
ESP_BLE_CONN_EVENT_CONNECTED:连接建立,置位attribute_handle[0] = 1;ESP_BLE_CONN_EVENT_DISCONNECTED:连接断开;ESP_BLE_CONN_EVENT_DATA_RECEIVE:收到服务器发来的数据。该事件的event_data为esp_ble_conn_data_t,代码先按 UUID 类型(16/32/128 位)打印 UUID,再用ESP_LOG_BUFFER_CHAR打印载荷;随后构造一个只读请求(data = NULL, data_len = 0),调用esp_ble_conn_read(&inbuff)对0xABF1特征值发起一次 GATT 读操作,成功则打印Read data success!及读回的内容(示例输出中可见读回的SPP_CHR)。
服务器的特征值回调
服务器的esp_spp_chr_cb(spp_server/main/app_main.c)是理解 GATT 服务端行为的入口:
inbuf == NULL→ 读操作:返回字符串SPP_CHR(示例输出中Callback for read);inbuf != NULL→ 写操作:将收到的数据原样拷贝到outbuf回显(示例输出中Callback for write);- 通过
*att_status = ESP_IOT_ATT_SUCCESS向 GATT 客户端返回 ATT 成功码。
回调函数类型即esp_ble_conn_cb_t,定义在 components/bluetooth/ble_conn_mgr/include/esp_ble_conn_mgr.h,outbuf指向的数据由连接管理组件负责释放。
五、分片包协议:MTU 不足时的数据分割
UART 收到数据后,数据会被投递到 UART 任务;在UART_DATA事件中取出原始数据,每次最大长度为 120 字节。数据如何跨空口发送取决于 MTU:
- 两个 ESP32 芯片互跑 Demo:BLE 连接建立后会交换 MTU(服务器 README 说明 MTU 会协商为 200 字节),因此每个包都可以直接发送,无需分片;
- 仅运行 ble_spp_server 且被手机连接:MTU 可能小于 123 字节,此时数据会被拆分为多个分片包依次发送。
分片包协议格式(每个分片包额外携带 4 字节头):
| 字节 | 含义 |
|---|---|
| 第 1~2 字节 | 固定为"##",标识这是一个分片包 |
| 第 3 字节 | 分片包总数(total number of the packets) |
| 第 4 字节 | 当前包的序号(current number of this packet) |
重要提示:如果你的手机 APP 需要与 ble_spp_server Demo 通信,必须检查并解析这一分片包结构,才能正确重组来自服务器的数据。
六、无线数据的收发路径与 GATT 属性表
发送(客户端 → 服务器)
- 客户端向服务器发送WriteNoRsp(无响应写)包;
- 服务器侧通过通知(notification)发送数据;
- 当 UART 收到数据时,UART 任务将其放入缓冲区,随后触发上述发送逻辑。
接收(服务器 → 客户端)
- 客户端在特征值回调中收到服务器数据(
ESP_BLE_CONN_EVENT_DATA_RECEIVE→esp_ble_conn_read)。
GATT 服务器属性表
该厂商自定义 Profile 的 GATT 属性表如下(服务器按此注册):
| 特征值 | UUID | 权限 |
|---|---|---|
| SPP_DATA_RECV_CHAR | 0xABF1 | READ & WRITE_NR |
| SPP_DATA_NOTIFY_CHAR | 0xABF2 | READ & NOTIFY |
| SPP_COMMAND_CHAR | 0xABF3 | READ & WRITE_NR |
| SPP_STATUS_CHAR | 0xABF4 | READ & NOTIFY |
对应的服务 UUID 为0xABF0(服务端源码中BLE_SVC_SPP_UUID16,客户端源码中GATT_SPP_SVC_UUID)。从服务器源码可见,服务注册通过查表驱动的方式完成——spp_nu_lookup_table定义特征值的名字、UUID 类型、访问标志(BLE_CONN_GATT_CHR_READ | WRITE | NOTIFY | INDICATE)与回调函数,再由esp_ble_conn_add_svc(&spp_svc)注册进 GATT 数据库。
客户端对端执行的 GATT 操作
本示例创建 GATT 客户端并执行被动扫描(示例日志中passive=1),如果设备广播了可连接属性与写特征值,则连接该外围设备。连接后对指定对端执行三类 GATT 操作:
- 发现所有服务、特征值和描述符(示例日志中依次出现
discover all services、discover all characteristics、discover all descriptors); - 发现完成后,从用户 UART 输入并写入特征值;
- (配合事件处理)读取特征值、接收通知。
七、如何编译、烧录与运行
环境与硬件要求
- 支持目标芯片:ESP32 / ESP32-C3 / ESP32-C2 / ESP32-S3;
- 硬件:一块搭载上述 SoC 的开发板 + 一根 USB 数据线(供电与烧录)。
设置目标芯片
在项目配置与构建之前,先用idf.py set-target设置正确的芯片目标:
idf.py set-target <chip_name>配置项目
idf.py menuconfig可配置项(位于Example Configuration菜单,见 Kconfig.projbuild):
EXAMPLE_BLE_ADV_NAME:广播中的设备名,默认ESP_SPP_SERVER;EXAMPLE_BLE_SUB_ADV:后续广播数据,默认SUB_ADV。
客户端示例默认通过 sdkconfig.defaults 启用 NimBLE 协议栈并将角色配置为中心设备:
CONFIG_BT_ENABLED=y CONFIG_BT_NIMBLE_ENABLED=y CONFIG_BLE_CONN_MGR_ROLE_CENTRAL=yESP32 系列芯片另通过 sdkconfig.defaults.esp32 将控制器模式锁定为 BLE-only(CONFIG_BTDM_CTRL_MODE_BLE_ONLY=y)。
构建、烧录并查看输出
idf.py -p PORT flash monitor(按Ctrl-]退出串口监视器。)
快速创建工程
也可以直接使用组件管理器一键创建示例工程:
idf.py create-project-from-example "espressif/ble_conn_mgr=*:spp_client" # BLE SPP 中心设备 idf.py create-project-from-example "espressif/ble_conn_mgr=*:spp_server" # BLE SPP 外围设备ble_conn_mgr组件本身的接入方式为:
idf.py add-dependency "espressif/ble_conn_mgr=*"八、运行日志解读:一次完整的连接与数据交换
以下是客户端在成功连接后控制台输出的典型日志(节选),结合前文的事件模型逐段解读:
I (382) blecm_nimble: BLE Host Task Started I (392) NimBLE: GAP procedure initiated: stop advertising. I (392) NimBLE: GAP procedure initiated: discovery; I (402) NimBLE: own_addr_type=0 filter_policy=0 passive=1 limited=0 filter_duplicates=1 I (412) NimBLE: duration=forever——BLE 主机任务启动,客户端进入被动扫描状态(passive=1)。
I (542) NimBLE: GAP procedure initiated: connect; I (542) NimBLE: peer_addr_type=0 peer_addr=84:f7:03:09:09:ca I (702) NimBLE: GATT procedure initiated: discover all services I (702) app_main: ESP_BLE_CONN_EVENT_CONNECTED——扫描到 SPP 服务器(地址84:f7:03:09:09:ca)并发起连接;连接成功后触发ESP_BLE_CONN_EVENT_CONNECTED,随即开始发现所有服务。
I (752) NimBLE: GATT procedure initiated: discover all characteristics; I (752) NimBLE: start_handle=1 end_handle=5 I (912) NimBLE: GATT procedure initiated: discover all characteristics; I (912) NimBLE: start_handle=6 end_handle=9 I (1102) NimBLE: GATT procedure initiated: discover all characteristics; I (1102) NimBLE: start_handle=10 end_handle=65535 I (1302) NimBLE: GATT procedure initiated: discover all descriptors; ... I (1592) blecm_nimble: Service discovery complete; rc=0, conn_handle=1——按句柄区间逐段发现特征值(start_handle=1..5、6..9、10..65535)与描述符,发现完成后rc=0表示成功。
I (2852) NimBLE: GATT procedure initiated: write; I (2852) NimBLE: att_handle=12 len=1 I (2922) app_main: Write in uart task success! ... I (7672) blecm_nimble: received notification; conn_handle=1 attr_handle=12 attr_len=1 I (7672) app_main: ESP_BLE_CONN_EVENT_DATA_RECEIVE I (7672) app_main: 44017 I (7672) app_main: Z I (7682) NimBLE: GATT procedure initiated: read; I (7772) blecm_nimble: characteristic read; conn_handle=1 attr_handle=12 len=7 value= I (7772) app_main: Read data success! I (7772) app_main: SPP_CHR——UART 输入通过esp_ble_conn_write写入特征值(att_handle=12,即 0xABF1 对应的句柄);随后收到服务器发来的通知,触发ESP_BLE_CONN_EVENT_DATA_RECEIVE,客户端打印收到的字节(44017、Z),并自动对特征值发起一次读操作,成功读回SPP_CHR。
九、源码级要点小结
- 分层架构:示例将
ble_conn_mgr作为 BLE 连接管理层(components/bluetooth/ble_conn_mgr),应用层只需注册事件回调、构造esp_ble_conn_data_t并调用esp_ble_conn_write / read / notify,无需直接接触 NimBLE 细节。注意该组件当前仅支持 NimBLE 协议栈。 - 双向通道不对称:客户端用 WriteNoRsp 上行、服务器用 Notification 下行,这是 BLE 数据通路最典型、吞吐最高的组合;要保证下行通知可达,对端需订阅特征值的 CCCD。
- 分片协议是"给手机 APP 的接口契约":120 字节/包、MTU 与
"##"四字节分片头共同构成了与第三方 APP 互通的边界条件,通信双方必须遵守同一约定。 - 连接数自适应:槽位数组大小随
CONFIG_BT_NIMBLE_MAX_CONNECTIONS(NimBLE)或CONFIG_BT_ACL_CONNECTIONS(Bluedroid)编译期决定,多连接场景下需关注槽位管理与句柄判空逻辑。 - 扩展性:SPP 的上行数据源、下行数据去向均集中在各自的 UART 任务中,替换为 SPI、I2C 或外接模块时只需改写任务内的收发代码,BLE 侧逻辑可原样复用。
以上内容与源码路径对应关系:客户端实现见 examples/bluetooth/ble_conn_mgr/ble_spp/spp_client/main/app_main.c,服务端实现见 examples/bluetooth/ble_conn_mgr/ble_spp/spp_server/main/app_main.c,BLE 连接管理 API 见 components/bluetooth/ble_conn_mgr/include/esp_ble_conn_mgr.h。实际运行时,建议使用两块 ESP32 开发板分别烧录 server 与 client 工程进行对测;若仅与手机通信,则手机侧需按本文第五节的协议格式解析分片数据。
【免费下载链接】esp-iot-solutionEspressif IoT Library. IoT Device Drivers, Documentations and Solutions.项目地址: https://gitcode.com/GitHub_Trending/es/esp-iot-solution
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考