1. 蓝牙通信到底能做什么1.1 三个先用得上的典型场景先别急着敲代码想清楚蓝牙能干嘛后面学起来会顺很多。我做了这么多年嵌入式接触到的ESP32蓝牙项目里最常见的就三类。第一类是手机App和设备之间的数据透传。比如做一个环境监测站把温湿度、气压数据通过蓝牙发给手机做一个体感游戏手柄把陀螺仪数据传给手机做一个智能小车手机App控制前进后退。这种场景的特点是“手机为主设备为辅”手机屏幕上能看到数据、下发指令设备端负责采集和执行。BLE就是为这种场景量身定做的。第二类是设备接入蓝牙外设。ESP32可以连接蓝牙键盘、蓝牙鼠标、蓝牙耳机、蓝牙音箱、心率带、体脂秤。举个例子我见过有人用ESP32做一个跑步配速器蓝牙连接心率带实时读取心率数据超过阈值就震动提醒。这种情况下ESP32是主机外设是从机连接的建立和数据交互方式跟第一类正好相反。第三类是蓝牙信标和室内定位。把ESP32设置成广播模式手机App扫描到广播包根据RSSI信号强度估算距离实现展馆导览、商超推送、资产追踪。这类项目不用建立连接只需把数据放进广播包里发出去或者手机把数据广播出去由ESP32接收实现逻辑简单很多。这三个方向里透传是最适合入门的既有连接又有收发还能在手机上直观看到效果。本章的主线就走“设备端开启BLE服务手机App来连接收发数据”这条路把BLE的核心机制彻底跑通后面的场景就是换皮不换骨。1.2 先搞清楚BLE和经典蓝牙的区别ESP32最让人舒服的一点是它支持双模蓝牙也就是经典蓝牙和BLE同时支持。但零基础朋友很容易在这两个概念上犯迷糊我先把它们的关系理清楚。经典蓝牙也叫传统蓝牙就是以前手机连蓝牙耳机、蓝牙音箱用的那个协议特点是数据量大、传输速率高能传音频流但功耗也高。BLE的全称是低功耗蓝牙从蓝牙4.0开始引入它不是经典蓝牙的升级版而是一套全新的协议栈设计目标是用极低的功耗传输小批量数据。一个特别容易踩的坑是手机连ESP32的时候如果你的App里搜到的是“ESP32”这个名字得搞清楚走的是哪个协议。很多人做蓝牙App控制用的其实是BLE但思路还停留在经典蓝牙的SPP上结果调半天连不上。我后面会专门讲这两个协议在开发上的差异。简单记一句话BLE适合小数据量、低功耗、手机交互经典蓝牙适合音频、大数据量场景。2. 先弄懂ESP32的蓝牙底层能力2.1 不同型号ESP32的蓝牙配置差异很多初学者会在这里卡住同样是ESP32为什么有的能搜到蓝牙有的就是没有蓝牙选项原因很简单芯片型号不一样蓝牙能力也不一样。原版ESP32、ESP32-S3、ESP32-C3都带蓝牙但原版ESP32是双模也就是BR/EDR经典蓝牙和BLE都支持。ESP32-C3只支持BLE不支持经典蓝牙。ESP32-S3同样是只支持BLE。如果你看到某块板子写着“ESP32-WROOM-32”它基本就是双模的如果写着“ESP32-C3”、“ESP32-S3”之类的模组就只能用BLE。那怎么选型我个人的建议是新手入门优先选原版ESP32开发板因为生态最成熟、资料最多踩了坑也好搜到解决方案。做可穿戴或者电池供电的小设备选ESP32-C3这种单BLE芯片更合适成本和功耗都能降下来。至于ESP32-C5这种新芯片功耗控制做得更好蓝牙版本也更新但生态还在成长零基础阶段不必急着上手。顺带提一句蓝牙协议栈是硬件和软件配合的东西ESP32内部有一个专门的蓝牙控制器运行在芯片的射频模块上而上层的协议栈则由乐鑫的SDK提供。你用Arduino写蓝牙程序时看到的那一堆“BLEDevice”、“BLEServer”类的库函数本质上就是在调用这套协议栈的接口细节不用管知道这个关系就够了。2.2 BLE架构里必须会的5个基础概念用BLE开发躲不过GAP、GATT、UUID、服务和特征值这几个词。网上很多教程一上来就是术语轰炸我换一种方式讲用串口通信来类比。你在串口通信里要定波特率要用TX、RX两根线收发数据还要约定数据格式。BLE里也有类似的“约定”但它更复杂因为它要解决无线环境下“谁广播、谁扫描、谁连接、数据怎么表示”的问题。GAP是通用访问规范管的是“设备如何被发现”。设备广播自己的名字、服务UUID等消息其他设备扫描这些广播包这就完成了“发现”。你手机App里搜到的那些设备列表就是扫描GAP广播的结果。GATT是通用属性规范管的是“连接之后数据怎么组织”。GATT把数据抽象成服务Service和特征值Characteristic两层结构。你可以把服务理解成一个房间里的一排抽屉特征值就是抽屉里一个个带标签的小格子。手机要读设备端的温度就要找到温度这个服务下的温度特征值然后发起读操作。UUID就是这些服务和特征值的“身份证号”。蓝牙技术联盟规定了一些标准UUID比如电池电量服务的UUID是0x180F设备信息服务是0x180A。我们自己定义一个服务时一般用128位的UUID确保全球唯一。初学者自己写例程可以照抄官方例程里的那串“4fafc201-1fb5-459e-8fcc-c5c9c331914b”这是实验用的不用自己生成。广播、连接、服务、特征值、UUID这五个概念掌握住BLE的开发地图就有了。后面的代码基本都是在这个框架里打转。3. 开发环境准备Arduino IDE还是ESP-IDF3.1 Arduino IDE下快速搭好ESP32环境这一步很多人都会卡我详细写一遍。ESP32在Arduino IDE里不是默认支持的需要先把开发板管理器URL加进去。打开Arduino IDE进入“文件——首选项——附加开发板管理器网址”填上乐鑫官方的JSON地址https://espressif.github.io/arduino-esp32/package_esp32_index.json填好之后打开“工具——开发板——开发板管理器”搜索“esp32”找到“esp32 by Espressif Systems”这个条目点安装。这个过程会下载一整套工具链和编译工具时间长短取决于网络情况有时候会很久耐心等就行。装完后在“工具——开发板”菜单里就能看到ESP32系列了。选板子的时候注意市面上的开发板一般标注型号比如“ESP32-WROOM-32”、“ESP32-DevKitC”对应Arduino IDE里的“ESP32 Dev Module”或者“ESP32-WROOM-DA Module”。选错了可能编译报错或者串口连接异常。我这边的经验是绝大多数淘宝款ESP32开发板选“ESP32 Dev Module”都能跑。串口这块Windows系统一般会自动装好驱动打开设备管理器能看到“COM3”之类的端口号。如果你插上板子没有反应八成是CH340或者CP2102的驱动没装去官网下一个对应驱动装上就行。另外买板子的时候最好买带USB转串口芯片的版本否则还要外接一个USB转TTL模块对新手来说多一个环节就多一排坑。3.2 两种开发框架到底怎么选Arduino框架和ESP-IDF框架我简单做个对比方便你根据自己的情况选。Arduino框架适合新手抽象程度高、函数名直观写起来很接近普通的C语言程序官方库对BLE进行了封装像BLEDevice、BLEServer、BLEService这些类用起来非常方便几行代码就能启动一个BLE服务端。缺点是底层控制力弱有些高级功能不够灵活代码执行效率也比IDF框架略低一些。ESP-IDF是乐鑫官方的物联网开发框架代码更底层需要自己写事件循环、自己管理内存学习曲线陡峭得多。但它的优势也很明显可配置项丰富、对蓝牙协议栈的控制更细适合产品化开发、低功耗调优和需要精确定制协议的场景。零基础我推荐先用Arduino框架。理由很简单先跑通再优化理解原理以后再切IDF也不迟。很多人一上来就啃IDF的标准蓝牙示例结果被事件回调、任务调度这些东西绕晕了反而失去了学习的信心。不过我会在后面的章节里提一下两种框架下BLE相关的核心逻辑差异其实不大学通了Arduino版看IDF示例也能大致猜出七七八八。4. 零基础跑通第一个BLE透传例程4.1 准备一个手机端的蓝牙调试工具调试BLE手机端的工具必不可少。我常用的有两款一款叫nRF Connect是Nordic官方出的功能全面能扫描设备、看广播包、看服务列表、读写特征值安卓和iOS都有另一款叫LightBlue界面简洁适合快速验证。这两款选一个装就行我习惯用nRF Connect信息展示更完整。用App来调试的思路是这样的ESP32作为BLE服务端App作为客户端在App里能看到ESP32广播出来的设备名点击连接后能看到它开放的各个服务和特征值然后就能和ESP32收发数据了。这个过程和你在电脑上用串口助手调试单片机是一个思路只不过换成了无线方式多了一个“连接服务”的步骤。实操之前先确定你的ESP32开发板已经能跑起一个最简单的程序了比如点灯或者打印串口日志这样能保证编译烧录链路是通的。如果还没跑通过先回去把那一步补上否则中途出问题很难判断是哪边的问题。4.2 完整代码逐段解析下面这段代码是一个标准的BLE服务端例子实现了最基本的功能创建一个服务对外开放一个特征值支持读、写和通知三种操作。我逐段讲一下你理解了之后就可以自己改。先看头文件和全局变量#include BLEDevice.h #include BLEServer.h #include BLEUtils.h #include BLE2902.h BLECharacteristic *pCharacteristic NULL; bool deviceConnected false;这里引入了四个头文件。前三个是BLE的核心封装最后一个BLE2902.h是特征值描述符的声明作用稍后说。deviceConnected这个全局变量用来记录手机是否连接上了ESP32后面很多逻辑会依赖这个状态。接下来是服务端的连接回调类。当手机连接或断开时协议栈会调用对应函数class MyServerCallbacks : public BLEServerCallbacks { void onConnect(BLEServer* pServer) { deviceConnected true; Serial.println(设备已连接); } void onDisconnect(BLEServer* pServer) { deviceConnected false; Serial.println(设备已断开重新开始广播); pServer-getAdvertising()-start(); } };注意onDisconnect里有个关键动作重新启动广播。BLE设备断开连接后默认会停止广播如果手机App没断开前把设备从列表里删了下次想重新连接就搜不到了。所以要在断开回调里手动把广播再打开这是个常见的坑。然后是特征值的写回调。手机App往ESP32发数据时会触发这个函数class MyCallbacks : public BLECharacteristicCallbacks { void onWrite(BLECharacteristic *pCharacteristic) { std::string value pCharacteristic-getValue(); if (value.length() 0) { Serial.print(收到数据: ); for (int i 0; i value.length(); i) { Serial.print(value[i]); } Serial.println(); } } };这里有个小细节getValue()返回的是std::string类型不是char数组有些从C语言转过来的朋友容易在这里卡壳。其实可以把它当成字节数组用value[i]就是一个字节如果内容恰好是可打印ASCII码直接输出就能看到文字如果是二进制数据需要自己转成十六进制再打印。再看setup()函数的主体void setup() { Serial.begin(115200); BLEDevice::init(ESP32_BLE_Server); BLEServer *pServer BLEDevice::createServer(); pServer-setCallbacks(new MyServerCallbacks()); BLEService *pService pServer-createService(4fafc201-1fb5-459e-8fcc-c5c9c331914b); pCharacteristic pService-createCharacteristic( beb5483e-36e1-4688-b7f5-ea07361b26a8, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_NOTIFY ); pCharacteristic-setCallbacks(new MyCallbacks()); pCharacteristic-setValue(Hello from ESP32); pCharacteristic-addDescriptor(new BLE2902()); pService-start(); BLEAdvertising *pAdvertising pServer-getAdvertising(); pAdvertising-addServiceUUID(4fafc201-1fb5-459e-8fcc-c5c9c331914b); pServer-startAdvertising(); Serial.println(BLE服务已启动等待手机连接...); }BLEDevice::init(ESP32_BLE_Server)这行初始化BLE协议栈同时把设备广播名字设为ESP32_BLE_Server手机App里搜到的就是这个名字。创建服务和特征值这两步核心就是两串UUID。服务UUID对应前面说的“房间”特征值UUID对应“抽屉”。这里用的是两串固定的128位测试UUID你可以原样使用但正式项目里最好用在线UUID生成器生成自己的避免跟别人的服务冲突。特征值的属性用三个常量拼接PROPERTY_READ表示手机可以读这个特征值PROPERTY_WRITE表示手机能写数据进来PROPERTY_NOTIFY表示ESP32可以主动推送数据给手机。这三个属性不是互斥的可以根据需求自由组合。BLE2902这个描述符是给特征值加上“客户端特征配置”它的关键作用是让手机App能够订阅subscribe通知。如果不加这个描述符很多App点订阅通知会失败这是新手最容易忽略的一行代码我当初也是调了半天才发现少了它。再看loop()函数void loop() { if (deviceConnected) { static uint32_t lastTime 0; if (millis() - lastTime 2000) { lastTime millis(); std::string value Data: String(millis()).c_str(); pCharacteristic-setValue(value); pCharacteristic-notify(); Serial.println(已发送通知: String(value.c_str())); } } delay(10); }这个循环每2秒向手机推一条数据内容是当前的时间戳。这里用到了deviceConnected这个标志位确保只在手机连接时推送避免资源浪费。notify()就是“主动推送”的动作手机端只要订阅了这个特征值就能实时收到数据。如果你只想做“手机请求后设备返回数据”这种交互可以不写notify手机端直接读特征值就行。4.3 手机App连接和收发数据的完整流程代码烧录完成后打开串口监视器波特率设为115200应该能看到“BLE服务已启动等待手机连接...”的日志。接下来打开手机App的扫描界面设备列表里会出现ESP32_BLE_Server这个名字。点击连接之后App会进入服务列表界面。展开那个服务能看到一个可读可写可通知的特征值。这里跟串口调试器很像往特征值里写数据串口监视器里就能看到ESP32打印“收到数据: xxx”ESP32每2秒推送一次App的通知日志里也会滚出时间戳数据。第一次跑通这个流程的时候整套链路就通了。之后你可以把通知推送改成传感器真实数据比如用内部温度传感器或者外接的DHT11再把onWrite回调里的数据解析成控制指令比如开关灯、控制电机这样一个小项目的雏形就出来了。5. 经典蓝牙SPP和BLE玩法的横向扩展5.1 经典蓝牙SPP连接与串口透传如果你用的是原版ESP32这种双模芯片还能玩经典蓝牙的SPP协议。SPP的全称是串口通信配置文件它的使用体验比BLE还要直观设备端开启一个SPP服务手机上用“蓝牙串口助手”这类App连接连接成功后手机和ESP32之间就像串口线相连一样双向收发数据。SPP的开发在老一些的Arduino库里支持得不错ESP32示例里也有SerialToSerialBT这样的经典例程。几行代码就能把蓝牙变成一个无线串口#include BluetoothSerial.h BluetoothSerial SerialBT; void setup() { Serial.begin(115200); SerialBT.begin(ESP32_SPP); Serial.println(蓝牙串口已启动); } void loop() { if (SerialBT.available()) { Serial.write(SerialBT.read()); } if (Serial.available()) { SerialBT.write(Serial.read()); } }这个例程实际就是把硬件串口和蓝牙串口互相桥接电脑上用串口助手发数据到ESP32的物理串口再从手机蓝牙串口App里读出来。很多项目拿它来做无线调试非常方便。但要注意的是SPP在iOS设备上支持很弱因为苹果生态不允许普通App使用经典蓝牙SPP你得走MFi认证或者使用BLE。如果你的目标平台是安卓SPP完全没问题如果要做iOS应用老老实实选BLE。5.2 蓝牙键盘、蓝牙音频这些方向需要知道什么ESP32还可以做成蓝牙键盘、蓝牙鼠标这类输入设备这属于蓝牙HID人机接口设备协议。Arduino环境下有BLEKeyboard、BLEMouse这类第三方库实现起来非常快。我自己做过一个演示项目用ESP32模拟蓝牙键盘连上手机后隔几秒自动敲一段字效果还挺唬人的。这类项目的本质就是把ESP32配置成BLE的HID设备角色注意它在协议栈层面和普通BLE外设略有不同但API封装得很好用起来不复杂。蓝牙音频方向就要复杂得多了。原版ESP32支持A2DP高级音频分发配置文件可以做蓝牙音箱的接收端也可以用I2S接口接麦克风做蓝牙发射端。Arduino环境下有ESP32-A2DP这个库播放手机上的音乐给I2S音频解码芯片输出代码量不大。但A2DP的延迟和音质都不算好做产品的话要慎重。如果你对音质有要求建议换一颗专门处理音频的芯片或者用ESP32-S3配合成熟音频方案来做。5.3 蓝牙Mesh与大规模设备组网最后提一下蓝牙Mesh这是BLE的组网扩展协议基于BLE 4.0以上版本的设备都能运行但需要特定的协议栈支持。ESP32官方SDK里提供了蓝牙Mesh的完整实现常见玩法是让多个ESP32节点通过Mesh网络互相转发消息适合智能家居这种需要覆盖多房间、多设备的场景。有人问过“ESP32能不能接入米家Mesh”这里简单说明一下米家Mesh是小米自研的Mesh组网生态ESP32芯片不是官方支持的成员想接入需要自己做网关适配和协议对接技术上有路子但门槛很高不建议零基础玩家尝试。我自己更推荐的做法是用ESP32自己组一张Mesh网络或者接入Home Assistant这类开源平台可玩性和自由度都更大。6. 常见问题与排查技巧6.1 手机扫描不到设备先查这四步扫描不到设备的排查优先级从我踩坑的经验来看是这样的。第一步确认代码里的广播已经开启。很多人写了BLEDevice::init就以为广播会自动开其实要先pServer-getAdvertising()拿到广播对象然后执行start()。如果少了这一步设备是不会出现在手机列表里的。第二步确认开发板没有进入深度睡眠或者异常复位状态。有些板子带电池管理芯片电压低了会自动断电接上USB线的同时还要看下板载指示灯是否正常。第三步确认手机蓝牙权限已经打开。注意iOS和安卓新版系统都会要求App具备定位权限才能扫描BLE设备App权限没给够一样搜不到。第四步确认用的是BLE协议不同协议的设备在App里可能不在同一个列表页面有些App甚至只显示BLE设备。把上面这四点过一次大多数扫描问题都能解决。6.2 为什么连上了却收不到数据连接成功但收不到数据的坑排查起来更隐蔽。Common的情况是特征值的属性没有配置对。如果你用手机写数据没反应确认createCharacteristic时加了PROPERTY_WRITE如果手机收不到设备推送确认加了PROPERTY_NOTIFY而且还要加上BLE2902描述符并且手机App里要执行“订阅”操作。这个三步缺一不可少了任何一环数据都传不到手机。另一个容易忽略的问题是设备端主动发送数据的频率。BLE的传输特点是“小快灵”单包数据一般不超过20字节发送频率太高会触发流控导致消息堆积丢失。我的建议是20到50毫秒至少发一包不要在一个循环里连续狂发。如果你想传大图片、大文件就要做分包和重传机制复杂度会上去不少。6.3 低功耗和轻度睡眠下的BLE注意事项做电池供电项目时BLE的低功耗优势很关键但实际用起来有几个坑。ESP32在轻度睡眠模式下BLE仍然可以保持连接但要手动配置蓝牙唤醒源和定时唤醒。很多初学者一开睡眠发现蓝牙断连原因就是没有配置UART或BLE唤醒事件。Electronics这类经验总结下来合理利用BLE广播休眠、连接间隔拉长比如从15ms调到200ms能把平均功耗降到几百微安级别。不过这些参数调到极致之后交互响应会变慢点一下按钮要等几百毫秒才响应属于可以接受的代价。如果是做对续航有硬性要求的产品我建议再在硬件层面用MOS管做外设电源控制把传感器等外设在非采集期断电效果比单纯调协议参数更明显。6.4 烧录时遇到的几个小坑蓝牙示例编译烧录时最容易出问题的是开发板型号选错导致编译报错或者Flash容量不对。有些板子标称“ESP32 Dev Module”实际Flash是4MB但有些板子只有2MB甚至1MB如果代码体积大烧录时会报overlap之类的错误这个问题的本质是分区表和代码超过了Flash容量要么换8MB版本要么精简代码。另外烧录时同时开着串口监视器在Windows下会占用COM口导致烧录失败。正确的顺序是先关掉串口监视器再点烧录。烧录完成后如果想看日志再重新打开监视器。这个小习惯能帮你少走很多弯路。还有一点有时候代码改了但烧录后行为没变这通常是因为Arduino IDE没有触发重新编译或者Flash里面还留着上一次的程序残留。遇到这种情况试试esptool.py erase_flash擦除整片Flash再重新烧录基本能解决。这在反复改UUID、反复调试广播名时特别管用我实测过很多次都是这么解决的。不写总结说点真心话做ESP32蓝牙这章内容的时候我反复在想到底什么才叫“零基础也能学会”。后来我觉得不是给一堆代码注释而是让人知道每一步为什么要这么做、出了问题往哪个方向排查。BLE这东西初看一堆术语真把一次收发跑通之后整个协议栈在你的心里就会有了清晰的形状。根据我的个人经验学蓝牙通信最好的路径不是啃厚厚的协议文档而是先跑通一个最简单的例程然后用调试工具把它的每个服务、每个特征值、每一条通知都看得明明白白再回头去理解协议概念。那个时刻你就发现原来BLE并不是什么高深莫测的东西只不过是一个“约定好格式的无线快递系统”。后面你可以试着把这一章的透传例程接上一个温度传感器再做一个简单的手机控制界面一个真正的物联网小项目就成型了。等再过一阵子你会发现当初觉得难懂的概念早就变成顺手拈来的基本功了。