资讯动态

ESP32蓝牙开发实战:BLE与经典蓝牙SPP从入门到避坑

发布时间:2026/9/20 16:08:41 来源:尧图企业网站定制
1. 蓝牙通信在ESP32项目中的定位与整体设计思路1.1 为什么联网篇要单独讲蓝牙很多刚接触ESP32的朋友会有个疑问ESP32明明自带Wi-Fi为什么还要折腾蓝牙我刚开始做物联网项目时也这么想直到实际落地几个产品后才明白蓝牙和Wi-Fi在ESP32的项目里根本不是替代关系而是互补关系。Wi-Fi适合大数据量、需要接入路由器的场景比如上传温湿度到云端、拉取OTA固件而蓝牙尤其是BLE低功耗蓝牙适合近距离、低功耗、配网引导、参数配置这类场景。举个最常见的例子一个智能温控器出厂时不知道用户家的Wi-Fi密码这时候就得靠手机App通过蓝牙把Wi-Fi的SSID和密码传给ESP32ESP32拿到后再去连Wi-Fi。这就是典型的“蓝牙配网”流程也是米家、涂鸦这类生态里几乎标配的做法。所以这一讲的核心是把ESP32的蓝牙能力讲透怎么在ESP-IDF框架下初始化蓝牙协议栈、怎么用VSCode把代码烧进去、怎么让手机App能搜到设备并收发数据。学完这一讲你应该能独立做出一个“手机蓝牙控制ESP32板载LED”的最小可用Demo并理解背后的分层结构为后面做蓝牙配网、蓝牙Mesh、甚至接入米家生态打基础。1.2 ESP32蓝牙的两种形态经典蓝牙与BLE在动手写代码之前必须先把概念理清楚否则后面看API会一头雾水。ESP32的蓝牙硬件支持两种模式经典蓝牙Bluetooth ClassicBR/EDR就是我们平时连耳机、连音箱那种支持SPP串口透传、A2DP音频等协议。优点是吞吐量大、协议成熟缺点是功耗高配对流程相对重。低功耗蓝牙BLEBluetooth Low Energy为物联网而生功耗极低用“广播连接”的方式通信数据以GATT服务Service和特征值Characteristic的形式组织。手机App读写特征值就能和ESP32交换数据。ESP32的蓝牙控制器是双模的理论上可以同时跑经典蓝牙和BLE但不能同时跑BLE和Wi-Fi——这是很多新手踩的第一个大坑。因为BLE和Wi-Fi共用同一个2.4GHz射频前端ESP-IDF里如果同时初始化会出现射频资源冲突表现为Wi-Fi连不上或者蓝牙搜不到。官方给出的方案是分时复用coexistence但配置复杂、稳定性一般。我的建议是配网阶段用BLE配网成功后关掉BLE再开Wi-Fi这样最稳。1.3 整体方案选型为什么用ESP-IDF而不是Arduino网上搜“ESP32蓝牙”一大半教程是Arduino框架的几行代码就能跑起来。那为什么我们这套系列坚持用ESP-IDF原因有三个第一可控性。Arduino把蓝牙协议栈封装得太深你想改广播间隔、改MTU大小、加自定义GATT服务几乎无从下手。而ESP-IDF暴露了完整的Bluedroid协议栈API从GAP到GATT每一层都能调。第二工程化。ESP-IDF基于CMake构建配合VSCode插件可以做到组件化管理、多目标编译、串口监视一体化适合做正经产品而不是玩具。第三生态兼容。后面如果要接入米家Mesh、做OTA、跑FreeRTOS多任务ESP-IDF是唯一顺畅的路径。Arduino在这些场景下会非常别扭。代价就是学习曲线陡。但这一讲我会把每一步拆开讲让你少走弯路。1.4 开发环境确认清单在写蓝牙代码前先确认你的环境是齐的。我见过太多人卡在环境上代码没问题却烧不进去。清单如下项目要求检查方式ESP-IDF版本v5.0及以上推荐v5.1终端执行idf.py --versionVSCode最新稳定版帮助-关于ESP-IDF插件Espressif IDF官方插件扩展面板搜索开发板ESP32系列ESP32-WROOM-32最通用板子丝印数据线支持数据传输的USB线能识别串口即可串口驱动CP210x或CH340设备管理器看端口注意ESP32-S2、S3、C3这些型号的蓝牙能力不同S2没有蓝牙C3只有BLE没有经典蓝牙。选板子时务必确认别买回来发现跑不了经典蓝牙SPP。环境这块如果还没配好建议先回去看本系列前面的环境搭建篇这里不再展开。下面直接进入代码层面。2. 蓝牙协议栈核心概念与ESP-IDF分层解析2.1 从手机搜到设备到收发数据中间发生了什么很多人写蓝牙代码是“抄一段能跑就行”结果一出问题就抓瞎。要真正掌握得先理解一次完整的BLE通信流程。我把它拆成五个阶段广播AdvertisingESP32作为外设Peripheral周期性在三个广播信道37、38、39上发送广播包包里带着设备名、服务UUID等信息。手机作为中心设备Central扫描这些信道就能发现ESP32。扫描与发现Scanning手机扫描到广播包后解析出设备地址和名称显示在App列表里。连接Connection用户点击连接双方协商连接参数连接间隔、从机延迟、超时时间建立一条点对点链路。服务发现Service Discovery连接后手机读取ESP32的GATT数据库知道它有哪些服务、每个服务下有哪些特征值、每个特征值的读写权限。数据交互Read/Write/Notify手机通过写特征值下发命令通过读或订阅通知Notify获取ESP32上报的数据。理解这五步你就知道代码里每一段在干什么了。ESP-IDF的API基本就是围绕这五步组织的。2.2 ESP-IDF蓝牙协议栈的分层结构ESP-IDF的蓝牙栈叫Bluedroid也有NimBLE可选后面讲区别它的分层从上到下大致是应用层你写的业务代码注册回调、处理事件。GATT/GAP层GATT管服务与特征值GAP管广播、扫描、连接。Bluedroid核心层协议栈主体处理L2CAP、ATT、SM安全管理等。HCI层主机控制器接口连接软件栈和硬件控制器。控制器层ESP32芯片内的蓝牙硬件跑射频和链路层。应用层通过回调函数和事件与协议栈交互。比如广播启动成功会触发ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT手机写入特征值会触发ESP_GATTS_WRITE_EVT。你写的代码本质上就是一堆事件处理器的集合。2.3 Bluedroid与NimBLE怎么选ESP-IDF提供两套BLE主机栈对比项BluedroidNimBLE支持经典蓝牙是否内存占用较大约100KB较小约50KBAPI风格事件回调多略繁琐更简洁生态成熟度高资料多较新适用场景需要经典蓝牙或双模纯BLE、资源紧张我的建议很直接如果你要做SPP串口透传、要连经典蓝牙设备只能用Bluedroid如果只做BLE配网、BLE传感器NimBLE更省资源。本讲为了覆盖全面以Bluedroid为主因为它是默认配置也是大多数教程和产品的选择。2.4 关键术语速查在正式写代码前把几个高频术语用大白话解释一遍后面看到就不会懵GAP管“怎么被发现、怎么连上”是蓝牙的门面。GATT管“连上之后能干什么”是数据交互的规则。Service服务一组相关功能的集合比如“LED控制服务”。Characteristic特征值服务里的具体数据点比如“LED开关状态”。UUID每个服务和特征值的唯一标识128位或16位。自定义服务一般用128位随机UUID。NotifyESP32主动向手机推送数据不用手机轮询。MTU单次能传的最大字节数默认23字节可协商到517字节。这些概念在代码里会反复出现先混个脸熟。3. 从零搭建BLE外设完整实操流程3.1 工程创建与目录结构打开VSCode按F1调出命令面板输入ESP-IDF: New Project选择模板sample_project命名为ble_led_demo。生成后的目录结构如下ble_led_demo/ ├── CMakeLists.txt ├── main/ │ ├── CMakeLists.txt │ └── ble_led_demo.c └── sdkconfig打开main/CMakeLists.txt确保内容包含idf_component_register(SRCS ble_led_demo.c INCLUDE_DIRS .)蓝牙相关的组件在ESP-IDF里是内置的不需要额外声明依赖但需要在sdkconfig里开启蓝牙。用idf.py menuconfig进入配置Component config→Bluetooth→ 勾选BluetoothBluetooth→Bluetooth Host选Bluedroid - Dual-mode保存退出这一步很关键如果没开蓝牙编译时会报一堆undefined reference to esp_bt_...的错误。3.2 蓝牙初始化从控制器到主机蓝牙初始化分三步顺序不能乱// 第一步初始化NVS蓝牙协议栈需要用它存配对信息 esp_err_t 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); // 第二步释放经典蓝牙内存纯BLE场景并初始化控制器 ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT)); esp_bt_controller_config_t bt_cfg BT_CONTROLLER_INIT_CONFIG_DEFAULT(); ESP_ERROR_CHECK(esp_bt_controller_init(bt_cfg)); ESP_ERROR_CHECK(esp_bt_controller_enable(ESP_BT_MODE_BLE)); // 第三步初始化Bluedroid主机栈并注册GAP/GATT回调 ESP_ERROR_CHECK(esp_bluedroid_init()); ESP_ERROR_CHECK(esp_bluedroid_enable()); ESP_ERROR_CHECK(esp_ble_gap_register_callback(gap_event_handler)); ESP_ERROR_CHECK(esp_ble_gatts_register_callback(gatts_event_handler)); ESP_ERROR_CHECK(esp_ble_gatts_app_register(0));这里解释几个关键点。esp_bt_controller_mem_release是释放经典蓝牙占用的内存纯BLE项目一定要调能省下几十KB RAM。BT_CONTROLLER_INIT_CONFIG_DEFAULT()是默认控制器配置一般不用改。esp_ble_gatts_app_register(0)里的0是应用IDapp_id如果你要注册多个GATT应用就传不同的ID。实操心得NVS初始化失败是新手最常见的报错之一。如果你之前烧过别的固件NVS分区格式可能不兼容这时候nvs_flash_erase()就是救命的。我一般会在初始化失败时直接erase重来省得排查半天。3.3 GATT服务与特征值的定义接下来定义我们要暴露给手机的服务。这里设计一个“LED控制服务”包含一个可读写的特征值#define LED_SERVICE_UUID 0x00FF #define LED_CHAR_UUID 0xFF01 static uint8_t led_value 0; static const uint16_t primary_service_uuid ESP_GATT_UUID_PRI_SERVICE; static const uint16_t character_declaration_uuid ESP_GATT_UUID_CHAR_DECLARE; static const uint8_t char_prop_read_write ESP_GATT_CHAR_PROP_BIT_READ | ESP_GATT_CHAR_PROP_BIT_WRITE; static const uint16_t led_service_uuid LED_SERVICE_UUID; static const uint16_t led_char_uuid LED_CHAR_UUID; static uint8_t led_char_value[1] {0x00}; // GATT属性数据库 static const esp_gatts_attr_db_t gatt_db[HRS_IDX_NB] { // 服务声明 [IDX_SVC] { {ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)primary_service_uuid, ESP_GATT_PERM_READ, sizeof(uint16_t), sizeof(led_service_uuid), (uint8_t *)led_service_uuid} }, // 特征值声明 [IDX_CHAR] { {ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)character_declaration_uuid, ESP_GATT_PERM_READ, sizeof(uint8_t), sizeof(char_prop_read_write), (uint8_t *)char_prop_read_write} }, // 特征值UUID [IDX_CHAR_UUID] { {ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)led_char_uuid, ESP_GATT_PERM_READ, sizeof(uint16_t), sizeof(led_char_uuid), (uint8_t *)led_char_uuid} }, // 特征值数据 [IDX_CHAR_VAL] { {ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)led_char_uuid, ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE, sizeof(uint8_t), sizeof(led_char_value), led_char_value} }, };这段代码是GATT的核心。esp_gatts_attr_db_t是一个属性表每一项描述一个属性。ESP_GATT_AUTO_RSP表示协议栈自动响应读写请求不用你在回调里手动回。ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE表示这个特征值手机可以读也可以写。UUID这里用了16位的0x00FF和0xFF01方便调试。实际产品里建议用128位随机UUID避免和别的设备冲突。3.4 事件回调蓝牙的“神经中枢”ESP-IDF的蓝牙是事件驱动的所有交互都通过回调。先看GAP回调static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { switch (event) { case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT: esp_ble_gap_start_advertising(adv_params); break; case ESP_GAP_BLE_ADV_START_COMPLETE_EVT: if (param-adv_start_cmpl.status ESP_BT_STATUS_SUCCESS) { ESP_LOGI(TAG, 广播启动成功); } break; default: break; } }GAP回调主要处理广播相关事件。广播参数adv_params需要单独定义static esp_ble_adv_params_t adv_params { .adv_int_min 0x20, // 最小广播间隔 32*0.625ms 20ms .adv_int_max 0x40, // 最大广播间隔 40*0.625ms 40ms .adv_type ADV_TYPE_IND, .own_addr_type BLE_ADDR_TYPE_PUBLIC, .channel_map ADV_CHNL_ALL, .adv_filter_policy ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY, };广播间隔的计算单位是0.625ms0x20就是3232×0.62520ms。间隔越小越容易被搜到但功耗越高。配网场景建议20-40ms电池设备可以放到100ms以上。再看GATT回调这是数据交互的关键static void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) { switch (event) { case ESP_GATTS_REG_EVT: esp_ble_gatts_create_attr_tab(gatt_db, gatts_if, HRS_IDX_NB, 0); break; case ESP_GATTS_CREAT_ATTR_TAB_EVT: esp_ble_gatts_start_service(param-add_attr_tab.handles[IDX_SVC]); break; case ESP_GATTS_WRITE_EVT: if (param-write.handle led_char_handle) { led_value param-write.value[0]; gpio_set_level(LED_GPIO, led_value); ESP_LOGI(TAG, 收到写入: %d, led_value); } break; default: break; } }ESP_GATTS_WRITE_EVT是手机写入数据时触发的param-write.value就是手机发来的数据。这里我们直接把第一个字节当作LED状态写1点亮写0熄灭。3.5 广播数据与扫描响应配置光有广播参数还不够还得告诉手机“我叫什么、我能提供什么服务”。这就要配置广播数据和扫描响应数据static uint8_t adv_service_uuid128[32] { 0xfb, 0x34, 0x9b, 0x5f, 0x80, 0x00, 0x00, 0x80, 0x00, 0x10, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, }; static esp_ble_adv_data_t adv_data { .set_scan_rsp false, .include_name true, .include_txpower true, .min_interval 0x0006, .max_interval 0x0010, .appearance 0x00, .manufacturer_len 0, .p_manufacturer_data NULL, .service_data_len 0, .p_service_data NULL, .service_uuid_len sizeof(adv_service_uuid128), .p_service_uuid adv_service_uuid128, .flag (ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT), };include_name true让广播包里带上设备名手机扫描时就能直接看到。flag里的ESP_BLE_ADV_FLAG_GEN_DISC表示设备可被发现ESP_BLE_ADV_FLAG_BREDR_NOT_SPT表示不支持经典蓝牙纯BLE设备。配置完调用esp_ble_gap_set_device_name(ESP32_BLE_LED)设置设备名再调esp_ble_gap_config_adv_data(adv_data)应用广播数据。配置成功后会触发ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT在回调里启动广播。3.6 编译烧录与串口验证代码写完后在VSCode底部状态栏选择串口和芯片型号点击“火焰”图标编译再点“闪电”图标烧录。烧录完成后打开串口监视器波特率115200应该能看到I (xxx) BLE_LED: 广播启动成功这时候打开手机上的蓝牙调试App比如nRF Connect、LightBlue扫描就能看到ESP32_BLE_LED。连接后能看到UUID为0x00FF的服务里面有个0xFF01的特征值写1或0就能控制LED。注意如果搜不到设备先确认广播是否真的启动了看串口日志再确认手机蓝牙是否开启、是否在扫描。安卓手机有时需要开启定位权限才能扫描BLE设备这是系统限制不是代码问题。4. 经典蓝牙SPP串口透传实操4.1 SPP是什么什么时候用它SPPSerial Port Profile是经典蓝牙上的串口透传协议简单说就是“把蓝牙当串口用”。手机连上ESP32后双方可以像串口一样互发数据。它的优势是吞吐量大、延迟低、协议简单适合传文件、传音频流、做无线串口调试。什么时候用SPP而不是BLE我的经验是数据量大、需要持续传输、对功耗不敏感的场景用SPP配网、传小命令、电池供电的场景用BLE。比如你要做个蓝牙音箱、蓝牙打印机、无线串口模块SPP是首选。4.2 SPP初始化与回调注册SPP的初始化和BLE类似但用的是esp_spp_系列API// 初始化时启用经典蓝牙 ESP_ERROR_CHECK(esp_bt_controller_enable(ESP_BT_MODE_BTDM)); // 双模 // 注册SPP回调 ESP_ERROR_CHECK(esp_spp_register_callback(spp_callback)); esp_spp_cfg_t spp_cfg { .mode ESP_SPP_MODE_CB, .enable_l2cap_ertm true, .tx_buffer_size 0, }; ESP_ERROR_CHECK(esp_spp_enhanced_init(spp_cfg));注意这里控制器模式是ESP_BT_MODE_BTDM双模因为SPP属于经典蓝牙。如果你之前释放了经典蓝牙内存这里要改回来。4.3 SPP事件处理与数据收发SPP回调处理几个关键事件static void spp_callback(esp_spp_cb_event_t event, esp_spp_cb_param_t *param) { switch (event) { case ESP_SPP_INIT_EVT: esp_spp_start_srv(ESP_SPP_SEC_NONE, ESP_SPP_ROLE_SLAVE, 0, ESP32_SPP); break; case ESP_SPP_SRV_OPEN_EVT: ESP_LOGI(TAG, 手机已连接, handle%d, param-srv_open.handle); break; case ESP_SPP_DATA_IND_EVT: ESP_LOGI(TAG, 收到数据: %.*s, param-data_ind.len, param-data_ind.data); // 回显 esp_spp_write(param-data_ind.handle, param-data_ind.len, param-data_ind.data); break; case ESP_SPP_CLOSE_EVT: ESP_LOGI(TAG, 连接断开); break; default: break; } }ESP_SPP_DATA_IND_EVT是收到数据的事件param-data_ind.data是数据指针len是长度。这里做了个回显手机发什么就回什么方便验证链路。4.4 手机端连接与测试安卓手机可以用“蓝牙串口助手”类App苹果手机因为系统限制SPP支持较差建议用安卓测试。连接流程手机蓝牙设置里配对ESP32_SPP然后用App打开对应的串口通道就能收发数据了。实操心得SPP配对时如果提示PIN码默认一般是1234或0000。ESP-IDF里可以通过esp_bt_gap_set_pin自定义。另外SPP的吞吐量实测能到几百KB/s但受手机端App实现影响很大别指望跑满理论值。4.5 BLE与SPP的取舍对照表维度BLE经典蓝牙SPP功耗极低高吞吐量低几KB/s高几百KB/s连接速度快较慢手机兼容性iOS/安卓都好安卓好iOS差适用场景配网、传感器、小命令音频、文件、串口透传ESP-IDF复杂度中中选型时对着这张表看基本不会错。5. 常见问题排查与避坑经验实录5.1 编译与烧录阶段的坑问题一undefined reference to esp_bt_controller_init这是没在menuconfig里开蓝牙。进Component config → Bluetooth勾选重新编译。问题二烧录后串口无输出先确认波特率是115200再确认板子是否进入了下载模式。有些板子需要按住BOOT键再点烧录。另外如果之前烧过别的固件占用了串口先擦除flashidf.py erase-flash。问题三nvs_flash_init返回错误前面说过erase后重来。如果还不行检查分区表是否包含nvs分区。5.2 蓝牙连接阶段的坑问题一手机搜不到设备排查顺序串口日志确认广播是否启动 → 确认设备名是否设置 → 确认手机蓝牙和定位权限 → 换个调试App试试。我遇到过某品牌手机必须开定位才能扫BLE折腾半天。问题二能搜到但连不上多半是广播数据里的flag配置有问题。确认ESP_BLE_ADV_FLAG_GEN_DISC已设置。另外如果之前配对过手机可能缓存了旧信息去蓝牙设置里“忽略此设备”再重连。问题三连上后读写特征值失败检查GATT属性表里的权限位。如果特征值只设了ESP_GATT_PERM_READ手机写就会失败。另外确认handle是否正确param-write.handle要和特征值handle比对。5.3 数据交互阶段的坑问题一Notify收不到数据Notify需要手机先订阅写CCCD描述符。ESP-IDF里要手动添加CCCD属性并在ESP_GATTS_WRITE_EVT里判断是不是写CCCD然后调esp_ble_gatts_send_indicate发数据。问题二大数据分包BLE默认MTU是23字节实际可用20字节。超过就会分包。可以在连接后调esp_ble_gatt_set_local_mtu(517)协商大MTU但手机端也要支持。SPP没这个问题但要注意缓冲区大小。问题三Wi-Fi和蓝牙冲突前面强调过BLE和Wi-Fi不能同时跑。如果项目既要配网又要联网流程设计成BLE配网 → 拿到Wi-Fi信息 → 关BLE → 开Wi-Fi。ESP-IDF里用esp_bt_controller_disable()关蓝牙再初始化Wi-Fi。5.4 常见问题速查表现象可能原因解决方向编译报未定义引用未开蓝牙menuconfig开启搜不到设备广播未启动/权限查日志、开定位连不上flag配置/缓存改flag、忽略设备写失败权限位不对加WRITE权限Notify无效未订阅CCCD添加CCCD属性Wi-Fi蓝牙冲突射频共用分时复用吞吐量低MTU小/间隔大协商MTU、调间隔6. 进阶方向与项目扩展思路6.1 蓝牙配网从Demo到产品把这一讲的Demo改造成配网工具思路是BLE服务里加一个特征值手机写入Wi-Fi的SSID和密码JSON格式ESP32解析后调用esp_wifi_set_config连接连上后通过Notify回传IP地址然后关BLE开Wi-Fi。这套流程就是米家、涂鸦配网的简化版。6.2 接入米家Mesh的可行性ESP32接入米家Mesh本质是跑小米的Mesh协议栈需要厂商授权和特定的SDK个人开发者很难直接做。但可以走“蓝牙配网云端对接”的路线ESP32用BLE配网连上Wi-Fi后通过MQTT对接自己的云平台再通过云云对接接入米家。这是目前个人项目最现实的路径。6.3 与Micro-ROS结合如果你在做机器人项目ESP32可以作为Micro-ROS的节点通过蓝牙或Wi-Fi和ROS 2主机通信。蓝牙适合短距离调试Wi-Fi适合正式运行。Micro-ROS在ESP32上有官方支持配合VSCode的PlatformIO或ESP-IDF都能跑。6.4 低功耗优化电池设备要重点优化广播间隔、连接间隔、发射功率。广播间隔从20ms放到500ms功耗能降一个数量级。连接后调大从机延迟slave latency让ESP32大部分时间睡觉。实测下来优化后一颗纽扣电池能撑几个月。我个人在实际操作中的体会是蓝牙这块最容易被低估的就是“事件驱动”的思维方式。刚开始写总想着顺序执行结果到处是坑。后来想通了你只管注册回调剩下的交给协议栈事件来了处理就行。这个思路转变过来代码就顺了。另外调试蓝牙一定要有耐心串口日志是你的眼睛每一步都打日志出问题一眼就能定位。最后分享一个小技巧nRF Connect这个App一定要装它能把设备的GATT结构、广播包内容全展示出来比任何文档都直观。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价