BLE 这玩意儿刚上手的时候我也觉得挺玄乎。手机一搜就能连上发几个字节过去灯就亮了——看起来简单但真到自己写代码的时候各种 UUID、特征值、通知、写入权限一堆名词砸过来很容易懵。这篇内容就是把我自己从零折腾 ESP32 蓝牙 BLE 的过程完整梳理一遍从环境搭建到手机 APP 控制每一步都讲清楚为什么这么做、坑在哪里。不管你是刚买了一块 ESP32 开发板的新手还是从 Arduino 转过来想试试 MicroPython 的老玩家跟着走一遍基本都能跑通。1. 先搞清楚 BLE 到底是怎么回事1.1 蓝牙经典 vs BLE别被名字骗了很多人第一次接触 ESP32 蓝牙以为蓝牙就是蓝牙连上就能传数据。实际上蓝牙协议分两大流派经典蓝牙Bluetooth Classic和低功耗蓝牙BLE。它们虽然都叫蓝牙但底层协议完全不同互相之间不能直接通信。经典蓝牙就是我们以前用来连蓝牙耳机、蓝牙音箱的那种特点是持续连接、传输速率高但功耗也大。BLE 是后来为了物联网场景设计的特点是连接建立快、功耗极低适合传感器数据采集、遥控指令这类小数据量场景。ESP32 同时支持这两种模式但你不能在一个应用里同时用经典蓝牙和 BLE 做同一件事。对于手机 APP 控制 ESP32 这个场景BLE 是更合适的选择——手机端不需要配对码连接速度快而且 iOS 和 Android 对 BLE 的支持都很成熟。注意ESP32-S3 只支持 BLE不支持经典蓝牙。如果你买的是 S3 系列就不用纠结选哪个了。1.2 GATT 协议BLE 通信的数据组织方式BLE 通信的核心是GATTGeneric Attribute Profile。你可以把它想象成一个数据库里面有很多张表每张表有行有列。具体来说Service服务一个服务就是一组相关功能的集合。比如一个设备信息服务里可能包含设备名称、厂商名称、固件版本等。Characteristic特征值服务下面的具体数据项。每个特征值有唯一的 UUID、有值、有权限读、写、通知等。Descriptor描述符特征值的附加信息比如单位、格式等。手机 APP 和 ESP32 之间的通信本质上就是手机去读写 ESP32 上某个特征值的过程。ESP32 把数据放在特征值里手机来读或者手机往特征值里写数据ESP32 收到后执行相应操作。1.3 为什么选 Nordic UART ServiceBLE 的 UUID 分两种16 位短 UUID 和 128 位长 UUID。16 位 UUID 是蓝牙技术联盟分配给标准服务的比如电池服务、心率服务等。但如果你要自定义服务就得用 128 位 UUID。Nordic UART ServiceNUS是 Nordic 公司定义的一套自定义服务虽然不是官方标准但在创客圈子里用得非常多几乎成了事实标准。它包含两个特征值RX 特征值手机往这里写数据ESP32 接收TX 特征值ESP32 往这里发数据手机接收通知用 NUS 的好处是很多现成的手机 BLE 调试 APP 都内置了对它的支持你不需要自己写手机端代码就能快速测试。后面我会推荐几个好用的 APP。2. 开发环境搭建MicroPython 还是 Arduino2.1 两条路线的选择逻辑ESP32 开发主要有两条路线Arduino 框架和MicroPython。两者各有优劣选哪个取决于你的背景和需求。Arduino 框架用 C 编写性能好、资源占用低适合对实时性要求高的场景。但代码相对繁琐编译烧录周期长。MicroPython 用 Python 语法开发速度快交互式 REPL 可以即时调试特别适合快速原型验证。对于零基础学 ESP32 蓝牙这个目标我强烈建议从 MicroPython 入手。原因很简单你不需要理解编译工具链、不需要处理复杂的 C 语法写几行 Python 代码就能看到效果。等把 BLE 的通信逻辑摸清楚了再转 Arduino 也不迟。2.2 MicroPython 固件烧录步骤如果你拿到的是全新 ESP32 开发板里面通常没有 MicroPython 固件需要先烧录。步骤如下第一步安装 esptoolpip install esptool第二步下载固件去 MicroPython 官网下载对应你芯片型号的固件文件.bin 格式。ESP32 和 ESP32-S3 的固件不一样别下错了。第三步擦除 Flashesptool.py --chip esp32 --port /dev/ttyUSB0 erase_flashWindows 上端口号可能是 COM3、COM4 之类的在设备管理器里能看到。第四步烧录固件esptool.py --chip esp32 --port /dev/ttyUSB0 --baud 460800 write_flash -z 0x1000 esp32-20230426-v1.20.0.bin烧录完成后用串口工具连接开发板按一下复位键如果看到提示符说明 MicroPython 已经跑起来了。提示烧录时如果一直失败检查 USB 线是不是只能充电不能传数据的那种。这个坑我踩过换了三根线才发现是线的问题。2.3 Thonny IDE 配置写 MicroPython 代码我推荐用Thonny。它轻量、免费、自带串口终端对新手非常友好。安装好 Thonny 后在工具→选项→解释器里选择MicroPython (ESP32)端口选你开发板对应的串口。配置好后点确定如果底部 Shell 窗口出现就说明连接成功了。Thonny 的好处是你可以直接在 Shell 里敲代码测试不用每次都保存文件再运行。比如你想测试一个引脚电平直接敲from machine import Pin然后操作就行即时看到结果。3. 写代码让 ESP32 变成一个 BLE 设备3.1 最小可运行 BLE 代码先来看一段最精简的 BLE 广播代码让 ESP32 能被手机搜到import bluetooth from ble_advertising import advertising_payload ble bluetooth.BLE() ble.active(True) name ESP32_BLE payload advertising_payload(namename) ble.gap_advertise(100000, adv_datapayload) print(BLE 已启动设备名, name)这段代码做了三件事初始化 BLE 模块、构建广播数据、开始广播。gap_advertise的第一个参数是广播间隔单位是微秒100000 就是 100 毫秒。间隔越短手机搜到的速度越快但功耗也越高。ble_advertising这个模块不是 MicroPython 内置的需要你自己创建一个ble_advertising.py文件。这个文件在 MicroPython 官方示例里能找到主要功能是把设备名、UUID 等信息打包成 BLE 广播要求的二进制格式。3.2 注册 NUS 服务光能广播还不够手机连上之后要能读写数据就需要注册服务。下面是注册 Nordic UART Service 的代码from micropython import const import struct _IRQ_CENTRAL_CONNECT const(1) _IRQ_CENTRAL_DISCONNECT const(2) _IRQ_GATTS_WRITE const(3) _FLAG_READ const(0x0002) _FLAG_WRITE_NO_RESPONSE const(0x0004) _FLAG_WRITE const(0x0008) _FLAG_NOTIFY const(0x0010) _UART_UUID bluetooth.UUID(6E400001-B5A3-F393-E0A9-E50E24DCCA9E) _UART_TX ( bluetooth.UUID(6E400003-B5A3-F393-E0A9-E50E24DCCA9E), _FLAG_READ | _FLAG_NOTIFY, ) _UART_RX ( bluetooth.UUID(6E400002-B5A3-F393-E0A9-E50E24DCCA9E), _FLAG_WRITE | _FLAG_WRITE_NO_RESPONSE, ) _UART_SERVICE (_UART_UUID, (_UART_TX, _UART_RX))这里定义了三个 UUID服务本身的 UUID、TX 特征值 UUID、RX 特征值 UUID。注意 TX 和 RX 的权限设置TX 是READ | NOTIFY因为 ESP32 要通过它主动通知手机RX 是WRITE | WRITE_NO_RESPONSE因为手机要往这里写数据。3.3 处理连接和断开事件BLE 是事件驱动的你需要注册一个中断处理函数来响应连接、断开、写入等事件def _irq(self, event, data): if event _IRQ_CENTRAL_CONNECT: conn_handle, _, _ data self._connections.add(conn_handle) print(手机已连接) elif event _IRQ_CENTRAL_DISCONNECT: conn_handle, _, _ data self._connections.discard(conn_handle) print(手机已断开) self._advertise() elif event _IRQ_GATTS_WRITE: conn_handle, attr_handle data data self._ble.gatts_read(attr_handle) print(收到数据, data)连接事件里记录连接句柄断开事件里移除句柄并重新开始广播否则手机断开后就搜不到设备了写入事件里读取手机发来的数据。3.4 完整代码整合把上面的片段整合成一个完整的类方便复用import bluetooth import struct from micropython import const from ble_advertising import advertising_payload class BLEUART: def __init__(self, ble, nameESP32_BLE): self._ble ble self._ble.active(True) self._ble.irq(self._irq) ((self._tx_handle, self._rx_handle),) self._ble.gatts_register_services((_UART_SERVICE,)) self._connections set() self._payload advertising_payload(namename) self._advertise() def _irq(self, event, data): # 事件处理逻辑 pass def _advertise(self): self._ble.gap_advertise(100000, adv_dataself._payload) def send(self, data): for conn_handle in self._connections: self._ble.gatts_notify(conn_handle, self._tx_handle, data)这个类封装了初始化、广播、发送数据的功能。实例化之后调用send()方法就能往手机发数据。4. 手机端 APP 选择与调试4.1 推荐几款好用的 BLE 调试 APP手机端不需要写代码用现成的 BLE 调试工具就行。以下是我实测过的几款APP 名称平台特点nRF ConnectiOS/AndroidNordic 官方出品功能最全支持 NUSLightBlueiOS/Android界面简洁适合快速测试BLE DebuggerAndroid轻量启动快Serial Bluetooth TerminalAndroid支持自定义 UUID适合串口透传nRF Connect 是我最常用的它能自动识别 Nordic UART Service直接给你一个类似串口终端的界面输入文字点发送就能传到 ESP32。4.2 连接与数据收发测试打开 nRF Connect扫描设备找到你设置的设备名比如 ESP32_BLE点击连接。连接成功后展开服务列表找到 Nordic UART Service你会看到两个特征值带向上箭头图标的 TX 特征值点击旁边的三个点选择Enable notifications这样 ESP32 发来的数据就能显示出来。带向下箭头图标的 RX 特征值点击旁边的向上箭头图标可以输入要发送的数据。在 RX 特征值里输入 hello点发送。如果 ESP32 的串口终端打印出 收到数据bhello说明通信链路已经打通了。4.3 常见连接问题排查手机搜不到设备先确认 ESP32 的串口有没有报错广播是否正常启动。如果代码跑起来了但搜不到可能是广播间隔太长改成 50000 试试。连上了但发数据没反应检查 RX 特征值的权限是否包含 WRITE以及中断处理函数里是否正确读取了数据。iOS 上搜不到iOS 对 BLE 广播数据格式要求比较严格确保advertising_payload函数正确打包了数据。另外 iOS 会缓存设备信息如果改了设备名可能需要在手机设置里忽略旧设备再重新搜索。5. 实战用手机控制 LED 亮灭5.1 硬件连接拿一个 LED 和一个电阻LED 长脚接 ESP32 的 GPIO2短脚经过电阻接 GND。GPIO2 是 ESP32 开发板上通常自带 LED 的引脚如果你用的是自带 LED 的板子这一步可以省略。5.2 代码实现在之前 BLEUART 类的基础上加入 LED 控制逻辑from machine import Pin led Pin(2, Pin.OUT) def _irq(self, event, data): if event _IRQ_GATTS_WRITE: conn_handle, attr_handle data cmd self._ble.gatts_read(attr_handle).decode().strip() if cmd on: led.value(1) self.send(LED 已打开) elif cmd off: led.value(0) self.send(LED 已关闭) else: self.send(未知命令 cmd)手机端在 RX 特征值里发送 onLED 点亮同时手机收到 LED 已打开 的通知。发送 offLED 熄灭。5.3 扩展思路这个框架可以扩展到很多场景把 LED 换成继电器就能控制家电换成电机驱动就能做小车读取传感器数据后通过 TX 特征值发给手机就是一个无线传感器节点。如果你想做更复杂的交互可以定义一套简单的协议比如用 JSON 格式传输数据{cmd:led,value:1}。ESP32 端解析 JSON手机端也按同样格式发送。这样扩展性更好加新功能不用改通信层。6. 踩过的坑和实用技巧6.1 广播数据长度限制BLE 广播数据最大 31 字节超过就会报错。设备名太长、UUID 太多都可能导致超限。解决办法是把不必要的信息放到扫描响应里或者缩短设备名。6.2 连接间隔与功耗的平衡BLE 连接建立后手机和 ESP32 会协商一个连接间隔。间隔越短响应越快但功耗越高。对于 LED 控制这种场景间隔设成 100ms 左右就够了。如果做电池供电的项目可以适当加大间隔。6.3 数据粘包问题BLE 的写入操作不保证每次写入的数据边界和你的预期一致。如果手机快速连续发送多条命令ESP32 可能一次收到多条拼接在一起的数据。解决办法是在协议里加分隔符比如每条命令以\n结尾ESP32 端按行解析。6.4 断开后自动重连手机离开范围后 BLE 会断开ESP32 需要重新开始广播否则手机回来搜不到设备。在断开事件里调用_advertise()是必须的这个坑我一开始没注意调试了半天以为是手机的问题。6.5 MicroPython 内存限制ESP32 的 MicroPython 可用内存有限如果代码里大量使用字符串拼接、频繁创建大对象可能会触发内存不足错误。建议把固定字符串定义为常量避免在中断处理函数里做复杂操作。7. 从 BLE 控制到完整项目7.1 加入传感器数据上报把 LED 控制扩展成传感器数据上报只需要在 TX 特征值上定期发送数据。比如接一个 DHT11 温湿度传感器每隔几秒读取一次通过send()方法发给手机。手机端 nRF Connect 开启通知后就能看到实时数据。7.2 多服务注册一个 BLE 设备可以注册多个服务。比如你既要做 UART 透传又要做 OTA 固件升级可以分别注册两个服务。gatts_register_services接受一个服务列表返回的句柄也是按顺序排列的。7.3 与 WiFi 共存ESP32 的蓝牙和 WiFi 共用射频模块同时开启时会有一定的性能影响。如果项目里既要 BLE 配置又要 WiFi 联网建议分时使用BLE 只在配网阶段开启配网完成后关闭 BLE启动 WiFi。7.4 低功耗优化如果项目用电池供电BLE 的功耗优化很关键。主要手段包括增大广播间隔、增大连接间隔、减少通知频率、在空闲时进入 light sleep 模式。实测下来优化得当的话一颗 18650 电池可以撑好几周。我在实际项目里最深的体会是BLE 调试一定要有耐心。手机端和 ESP32 端的日志都要看很多时候问题出在你想不到的地方——比如手机 APP 缓存了旧的 GATT 表或者广播数据格式不对导致 iOS 直接忽略。多试几款 APP多打日志问题总能定位到。