☰
ESP32蓝牙BLE开发入门:从零基础到跑通透传例程
2026/10/3 5:47:53 网站建设 项目流程

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-IDF

3.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并不是什么高深莫测的东西,只不过是一个“约定好格式的无线快递系统”。

后面你可以试着把这一章的透传例程接上一个温度传感器,再做一个简单的手机控制界面,一个真正的物联网小项目就成型了。等再过一阵子,你会发现当初觉得难懂的概念,早就变成顺手拈来的基本功了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询