示例深度解析:用 esp-iot-solution 在 BLE 上构建虚拟串口)
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 示例展开深入讲解如何在 BLEBluetooth Low Energy上实现串口仿真SPPSerial Port Profile的中心设备Central/Client端。你将掌握BLE SPP 自定义 Profile 的设计动机与 GATT 属性表结构、UART 与 BLE 之间的数据搬运任务与队列模型、MTU 协商与分片包协议以及基于ble_conn_mgr组件从扫描连接到收发数据的完整实现原理可直接据此搭建自己的无线串口应用或与手机 APP 对接。一、背景为什么 BLE 需要自定义 SPP在经典蓝牙BR/EDR系统中SPPSerial Port Profile是蓝牙 SIG 定义的标准 Profile用于在蓝牙无线链路上仿真一个串口连接。但在 BLE 系统中SIG没有定义一套被采纳的 SPP Profile因此要在 BLE 上仿真串口就必须将其实现为厂商自定义的 Profilevendor-specific custom profile。本参考设计由两个 Demo 组成分别运行在各自的端点上Demo角色仓库路径BLE SPP server外围设备提供 GATT 服务、广播spp_serverBLE 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_tUUID 类型为 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_handlerspp_client/main/app_main.c监听BLE_CONN_MGR_EVENTS事件基处理三类关键事件ESP_BLE_CONN_EVENT_CONNECTED连接建立置位attribute_handle[0] 1ESP_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_cbspp_server/main/app_main.c是理解 GATT 服务端行为的入口inbuf NULL→ 读操作返回字符串SPP_CHR示例输出中Callback for readinbuf ! 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.houtbuf指向的数据由连接管理组件负责释放。五、分片包协议MTU 不足时的数据分割UART 收到数据后数据会被投递到 UART 任务在UART_DATA事件中取出原始数据每次最大长度为 120 字节。数据如何跨空口发送取决于 MTU两个 ESP32 芯片互跑 DemoBLE 连接建立后会交换 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_CHAR0xABF1READ WRITE_NRSPP_DATA_NOTIFY_CHAR0xABF2READ NOTIFYSPP_COMMAND_CHAR0xABF3READ WRITE_NRSPP_STATUS_CHAR0xABF4READ 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 客户端并执行被动扫描示例日志中passive1如果设备广播了可连接属性与写特征值则连接该外围设备。连接后对指定对端执行三类 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.projbuildEXAMPLE_BLE_ADV_NAME广播中的设备名默认ESP_SPP_SERVEREXAMPLE_BLE_SUB_ADV后续广播数据默认SUB_ADV。客户端示例默认通过 sdkconfig.defaults 启用 NimBLE 协议栈并将角色配置为中心设备CONFIG_BT_ENABLEDy CONFIG_BT_NIMBLE_ENABLEDy CONFIG_BLE_CONN_MGR_ROLE_CENTRALyESP32 系列芯片另通过 sdkconfig.defaults.esp32 将控制器模式锁定为 BLE-onlyCONFIG_BTDM_CTRL_MODE_BLE_ONLYy。构建、烧录并查看输出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_type0 filter_policy0 passive1 limited0 filter_duplicates1 I (412) NimBLE: durationforever——BLE 主机任务启动客户端进入被动扫描状态passive1。I (542) NimBLE: GAP procedure initiated: connect; I (542) NimBLE: peer_addr_type0 peer_addr84: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_handle1 end_handle5 I (912) NimBLE: GATT procedure initiated: discover all characteristics; I (912) NimBLE: start_handle6 end_handle9 I (1102) NimBLE: GATT procedure initiated: discover all characteristics; I (1102) NimBLE: start_handle10 end_handle65535 I (1302) NimBLE: GATT procedure initiated: discover all descriptors; ... I (1592) blecm_nimble: Service discovery complete; rc0, conn_handle1——按句柄区间逐段发现特征值start_handle1..5、6..9、10..65535与描述符发现完成后rc0表示成功。I (2852) NimBLE: GATT procedure initiated: write; I (2852) NimBLE: att_handle12 len1 I (2922) app_main: Write in uart task success! ... I (7672) blecm_nimble: received notification; conn_handle1 attr_handle12 attr_len1 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_handle1 attr_handle12 len7 value I (7772) app_main: Read data success! I (7772) app_main: SPP_CHR——UART 输入通过esp_ble_conn_write写入特征值att_handle12即 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_CONNECTIONSNimBLE或CONFIG_BT_ACL_CONNECTIONSBluedroid编译期决定多连接场景下需关注槽位管理与句柄判空逻辑。扩展性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.cBLE 连接管理 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),仅供参考