1. ESP32 BLE开发环境搭建
1.1 硬件准备要点
做ESP32 BLE开发,首先得准备好硬件设备。我推荐使用ESP32-DevKitC开发板,这是乐鑫官方的开发板,自带USB转串口芯片,调试起来特别方便。板子上那个小小的ESP32-WROOM-32模组,内置了4MB Flash,完全够BLE应用使用。
选购时要注意模组型号后缀,比如ESP32-WROOM-32UE比普通版本多了外部天线接口。如果项目需要更远的通信距离,建议选带UE后缀的版本。我自己测试过,在开阔场地,使用外接天线能比板载天线多出20-30米的通信距离。
1.2 软件工具链配置
开发环境我习惯用VSCode+PlatformIO的组合,比官方的ESP-IDF开发环境更轻量。安装PlatformIO插件后,在platformio.ini配置文件中加上:
ini复制[env:esp32dev]
platform = espressif32
board = esp32dev
framework = espidf
monitor_speed = 115200
这里有个坑要注意:NimBLE协议栈需要ESP-IDF v4.0以上版本。我建议直接用最新稳定版,避免兼容性问题。安装完环境后,记得运行idf.py menuconfig进入配置界面,在"Component config" -> "Bluetooth"里启用NimBLE选项。
1.3 基础工程创建
新建工程时,建议直接从乐鑫官方例程开始。在ESP-IDF目录下,examples/bluetooth/nimble里有十几个现成的案例。我通常以bleprph(外设例程)或blecent(中心设备例程)为基础进行修改。
第一次编译可能会遇到python依赖问题,这时候需要:
bash复制pip install -r $IDF_PATH/requirements.txt
编译完成后,用idf.py -p /dev/ttyUSB0 flash monitor一键完成烧录和串口监控。如果看到"BLE Host Task Started"的日志,说明NimBLE协议栈已经成功运行。
2. NimBLE协议栈架构解析
2.1 协议栈分层结构
NimBLE是Apache开源的轻量级BLE协议栈,相比传统Bluedroid,内存占用少了约40%。它的架构分为三层:
- 控制器层(Controller):处理射频信号和底层协议
- 主机层(Host):实现GATT、GAP等高层协议
- 应用层(App):用户业务逻辑实现
在ESP32上,控制器运行在专门的RISC-V协处理器上,主机层和应用程序共享主CPU资源。这种设计让ESP32在做BLE通信时,主CPU还能同时处理其他任务。
2.2 关键组件交互流程
当设备启动时,初始化顺序很重要:
- 先调用
esp_nimble_hci_init()初始化HCI接口 - 然后
nimble_port_init()创建主机任务 - 接着
ble_svc_gap_init()初始化GAP服务 - 最后才是应用层的
ble_svc_init()
我遇到过因为初始化顺序不对导致设备无法广播的问题。正确的顺序应该是:硬件接口→协议栈→服务→应用。这个流程一旦出错,调试起来会非常头疼。
2.3 内存管理机制
NimBLE使用动态内存分配,但BLE操作对实时性要求高,所以它采用了内存池技术。通过os_mbuf和os_mempool来管理报文缓存。开发时要特别注意:
- 每个连接会消耗约1KB的RAM
- 每个特征值(Characteristic)需要额外的内存
- 广播数据最大31字节,超出部分会被截断
在资源紧张的ESP32上,建议在menuconfig里合理设置CONFIG_BT_NIMBLE_MSYS_*参数,根据实际连接数调整内存池大小。
3. BLE外设开发实战
3.1 设备广播配置
想让设备被手机扫描到,需要配置广播参数:
c复制static void bleprph_advertise(void)
{
struct ble_gap_adv_params adv_params;
struct ble_hs_adv_fields fields;
memset(&fields, 0, sizeof(fields));
fields.flags = BLE_HS_ADV_F_DISC_GEN;
fields.tx_pwr_lvl_is_present = 1;
fields.tx_pwr_lvl = BLE_HS_ADV_TX_PWR_LVL_AUTO;
fields.name = (uint8_t *)device_name;
fields.name_len = strlen(device_name);
fields.name_is_complete = 1;
adv_params.conn_mode = BLE_GAP_CONN_MODE_UND;
adv_params.disc_mode = BLE_GAP_DISC_MODE_GEN;
ble_gap_adv_set_fields(&fields);
ble_gap_adv_start(own_addr_type, NULL, BLE_HS_FOREVER,
&adv_params, NULL, NULL);
}
这里有几个实用技巧:
tx_pwr_lvl设为AUTO让系统自动选择最佳功率- 广播间隔默认100ms,可通过
adv_params.itvl_min/itvl_max调整 - 添加厂商自定义数据用
fields.mfg_data和fields.mfg_data_len
3.2 GATT服务实现
创建自定义服务的标准流程:
c复制// 定义UUID
static const ble_uuid128_t gatt_svc_uuid =
BLE_UUID128_INIT(0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,
0x09,0x0a,0x0b,0x0c,0x0d,0x0e,0x0f,0x10);
// 定义特征值属性
static uint8_t char_value[10];
static struct ble_gatt_chr_def characteristic = {
.uuid = BLE_UUID16_DECLARE(0x2A00),
.access_cb = NULL,
.flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE,
.val_handle = &char_handle,
.min_key_size = 16,
};
// 注册服务
static void register_services(void)
{
struct ble_gatt_svc_def svc = {
.type = BLE_GATT_SVC_TYPE_PRIMARY,
.uuid = &gatt_svc_uuid.u,
.characteristics = &characteristic,
};
ble_gatts_count_cfg(&svc);
ble_gatts_add_svcs(&svc);
}
实际开发中,我总结出几个要点:
- 特征值UUID最好遵循蓝牙标准定义
- 读写回调函数要做数据长度检查
- 通知功能需要显式调用
ble_gatts_chr_updated()
3.3 安全连接实现
安全是BLE开发经常忽视的部分。NimBLE支持多种安全模式:
c复制static int bleprph_gap_event(struct ble_gap_event *event, void *arg)
{
case BLE_GAP_EVENT_ENC_CHANGE:
if (event->enc_change.status == 0) {
// 加密成功
}
return 0;
case BLE_GAP_EVENT_PASSKEY_ACTION:
if (event->passkey.params.action == BLE_SM_IOACT_DISP) {
// 显示配对码
printf("Passkey: %06d\n",
event->passkey.params.numcmp);
}
return 0;
}
在menuconfig中需要配置:
code复制CONFIG_BT_NIMBLE_SM_LEGACY=y
CONFIG_BT_NIMBLE_SM_SC=y
CONFIG_BT_NIMBLE_HS_FLOW_CTRL=y
安全连接要注意:
- 同时启用传统配对和安全连接
- IO能力设置为KEYBOARD_DISPLAY最通用
- 加密后通信会增加约20%的功耗
4. BLE中心设备开发
4.1 扫描与连接流程
中心设备首先要扫描周围外设:
c复制static void scan_start(void)
{
struct ble_gap_disc_params disc_params = {
.passive = 0, // 主动扫描
.itvl = 0x60, // 扫描间隔
.window = 0x30,// 扫描窗口
.filter_duplicates = 1,
};
ble_gap_disc(own_addr_type, BLE_HS_FOREVER,
&disc_params, NULL, NULL);
}
发现设备后发起连接:
c复制static void connect_to_device(const ble_addr_t *addr)
{
struct ble_gap_conn_params conn_params = {
.scan_itvl = 0x10,
.scan_window = 0x10,
.itvl_min = BLE_GAP_INITIAL_CONN_ITVL_MIN,
.itvl_max = BLE_GAP_INITIAL_CONN_ITVL_MAX,
.latency = 0,
.supervision_timeout = BLE_GAP_INITIAL_SUPERVISION_TIMEOUT,
.min_ce_len = BLE_GAP_INITIAL_CONN_MIN_CE_LEN,
.max_ce_len = BLE_GAP_INITIAL_CONN_MAX_CE_LEN,
};
ble_gap_connect(own_addr_type, addr,
BLE_HS_FOREVER, &conn_params, NULL, NULL);
}
连接参数优化建议:
- 连接间隔(itvl)通常设为20-50ms
- 从机延迟(latency)根据应用场景调整
- 监控超时(supervision_timeout)至少是连接间隔的10倍
4.2 服务发现过程
连接成功后需要发现服务:
c复制static int discover_service(uint16_t conn_handle)
{
struct ble_gatt_svc *svc;
int rc;
// 发现所有主服务
rc = ble_gattc_disc_all_svcs(conn_handle,
service_discovered_cb, NULL);
if (rc != 0) {
return rc;
}
// 发现特定UUID服务
ble_uuid16_t svc_uuid = BLE_UUID16_INIT(0x180A);
rc = ble_gattc_disc_svc_by_uuid(conn_handle,
&svc_uuid.u,
service_discovered_cb, NULL);
return rc;
}
服务发现回调示例:
c复制static int service_discovered_cb(uint16_t conn_handle,
const struct ble_gatt_error *error,
const struct ble_gatt_svc *service,
void *arg)
{
if (error->status == 0) {
printf("Service found: start_handle=%d, end_handle=%d\n",
service->start_handle, service->end_handle);
// 发现特征值
ble_gattc_disc_all_chrs(conn_handle,
service->start_handle,
service->end_handle,
chr_discovered_cb, NULL);
}
return 0;
}
4.3 数据读写操作
读取特征值:
c复制static int read_characteristic(uint16_t conn_handle,
uint16_t handle)
{
return ble_gattc_read(conn_handle, handle,
read_cb, NULL);
}
static int read_cb(uint16_t conn_handle,
const struct ble_gatt_error *error,
struct ble_gatt_attr *attr,
void *arg)
{
if (error->status == 0) {
printf("Read value: ");
print_bytes(attr->om->om_data, attr->om->om_len);
}
return 0;
}
写入数据:
c复制static int write_characteristic(uint16_t conn_handle,
uint16_t handle,
const uint8_t *value,
size_t length)
{
struct os_mbuf *om = ble_hs_mbuf_from_flat(value, length);
return ble_gattc_write_no_rsp(conn_handle, handle, om);
}
实际使用中发现:
- 无响应写入(Write Without Response)速度最快
- 带响应写入(Write With Response)更可靠
- 长数据要分片传输,每片不超过20字节
5. 低功耗优化技巧
5.1 电源管理配置
ESP32在BLE模式下有三种功耗模式:
- Modem Sleep:射频关闭时自动进入,电流约5mA
- Light Sleep:CPU暂停,保持连接,电流约0.8mA
- Deep Sleep:完全断电,只能通过定时器或外部唤醒
启用低功耗模式:
c复制esp_pm_config_t pm_config = {
.max_freq_mhz = 80, // 降频运行
.min_freq_mhz = 10,
.light_sleep_enable = true,
};
esp_pm_configure(&pm_config);
实测数据对比:
- 常开模式:~50mA
- Modem Sleep:~15mA
- Light Sleep:~0.9mA
- Deep Sleep:~5μA
5.2 连接参数优化
BLE连接参数对功耗影响巨大:
c复制static void update_conn_params(uint16_t conn_handle)
{
struct ble_gap_upd_params params = {
.itvl_min = 80, // 100ms (1.25ms单位)
.itvl_max = 100, // 125ms
.latency = 4, // 允许跳过4个连接事件
.supervision_timeout = 400, // 4s
};
ble_gap_update_params(conn_handle, ¶ms);
}
优化原则:
- 数据量小→增大间隔和延迟
- 实时性要求高→减小间隔
- 移动场景→增大监控超时
5.3 广播策略调整
低功耗广播配置:
c复制static void configure_adv(void)
{
struct ble_gap_adv_params adv_params = {
.conn_mode = BLE_GAP_CONN_MODE_UND,
.disc_mode = BLE_GAP_DISC_MODE_NON,
.itvl_min = 1600, // 2s
.itvl_max = 2400, // 3s
};
ble_gap_adv_start(own_addr_type, NULL,
BLE_HS_FOREVER, &adv_params, NULL, NULL);
}
实测发现:
- 快速广播(100ms间隔)功耗约12mA
- 慢速广播(2s间隔)功耗约0.5mA
- 定向广播比非定向更耗电
6. 调试与性能分析
6.1 常见问题排查
-
广播不可见
- 检查
ble_gap_adv_start()返回值 - 确认广播数据不超过31字节
- 使用nRF Connect等工具验证
- 检查
-
连接不稳定
- 检查电源是否稳定
- 调整连接参数
- 查看RSSI信号强度
-
GATT操作失败
- 确认特征值属性匹配(读/写/通知)
- 检查handle是否正确
- 查看协议栈日志
6.2 日志分析技巧
启用详细日志:
c复制// 在menuconfig中设置
CONFIG_LOG_DEFAULT_LEVEL_INFO=y
CONFIG_BT_NIMBLE_LOG_LEVEL=3
关键日志信息:
BLE_HS_ADV_*:广播相关事件BLE_GAP_EVENT_*:连接状态变化BLE_GATT_EVENT_*:GATT操作结果
6.3 性能测试方法
测试吞吐量:
c复制static void throughput_test(void)
{
uint64_t start = esp_timer_get_time();
size_t total_bytes = 0;
while (testing) {
send_data_packet();
total_bytes += packet_size;
}
uint64_t duration = (esp_timer_get_time() - start)/1000;
printf("Throughput: %.2f kbps\n",
(total_bytes*8)/(float)duration);
}
优化方向:
- 增大MTU(默认23,可协商到512)
- 使用无确认写入
- 调整连接间隔
7. 项目实战案例
7.1 智能手环开发
典型功能实现:
c复制// 心率监测服务
static const ble_uuid16_t hr_svc_uuid = BLE_UUID16_INIT(0x180D);
static uint8_t hr_measurement = 72;
static int hr_chr_access(uint16_t conn_handle,
uint16_t attr_handle,
struct ble_gatt_access_ctxt *ctxt,
void *arg)
{
if (ctxt->op == BLE_GATT_ACCESS_OP_READ) {
os_mbuf_append(ctxt->om, &hr_measurement, 1);
}
return 0;
}
// 运动数据通知
static void send_motion_data(uint16_t conn_handle)
{
struct os_mbuf *om = ble_hs_mbuf_from_flat(&motion_data,
sizeof(motion_data));
ble_gattc_notify_custom(conn_handle, motion_handle, om);
}
7.2 蓝牙网关实现
多设备管理关键代码:
c复制static struct ble_gap_conn_desc connections[MAX_DEVICES];
static void store_connection_info(uint16_t conn_handle)
{
ble_gap_conn_find(conn_handle, &connections[dev_count]);
dev_count++;
}
static void forward_data_to_wifi(void)
{
for (int i=0; i<dev_count; i++) {
if (connections[i].conn_handle != BLE_HS_CONN_HANDLE_NONE) {
read_device_data(connections[i].conn_handle);
}
}
}
7.3 OTA固件升级
安全升级流程:
- 建立加密连接
- 创建专用GATT服务
- 分片传输固件
- 校验签名
- 重启到bootloader
关键实现:
c复制static int ota_write_cb(uint16_t conn_handle,
uint16_t attr_handle,
struct ble_gatt_access_ctxt *ctxt,
void *arg)
{
static size_t offset = 0;
size_t len = ctxt->om->om_len;
esp_ota_write(ota_handle, ctxt->om->om_data, len);
offset += len;
if (offset >= firmware_size) {
esp_ota_end(ota_handle);
esp_restart();
}
return 0;
}
8. 进阶开发技巧
8.1 多协议共存
BLE与WiFi共存配置:
c复制static void configure_radio(void)
{
esp_wifi_set_ps(WIFI_PS_MIN_MODEM);
esp_coex_preference_set(ESP_COEX_PREFER_BALANCED);
// 在menuconfig中设置
CONFIG_ESP32_WIFI_SW_COEXIST_ENABLE=y
CONFIG_ESP_COEX_SW_COEXIST_ENABLE=y
}
实测影响:
- 同时传输时WiFi吞吐量下降约30%
- BLE延迟可能增加50-100ms
- 建议分时复用射频资源
8.2 长连接保活
心跳机制实现:
c复制static void heartbeat_timer_cb(void *arg)
{
if (ble_conn_count > 0) {
send_heartbeat_packet();
}
// 每30秒一次
esp_timer_start_once(&heartbeat_timer, 30*1000000);
}
连接监控:
c复制static void check_connections(void)
{
for (int i=0; i<MAX_DEVICES; i++) {
if (connections[i].conn_handle != BLE_HS_CONN_HANDLE_NONE) {
if (esp_timer_get_time() - last_activity[i] > TIMEOUT_US) {
ble_gap_terminate(connections[i].conn_handle,
BLE_ERR_REM_USER_CONN_TERM);
}
}
}
}
8.3 大数据传输
分片传输方案:
c复制#define CHUNK_SIZE 512
static void send_large_data(uint16_t conn_handle,
const uint8_t *data,
size_t total_len)
{
size_t remaining = total_len;
size_t offset = 0;
while (remaining > 0) {
size_t chunk_len = MIN(CHUNK_SIZE, remaining);
struct os_mbuf *om = ble_hs_mbuf_from_flat(data+offset,
chunk_len);
ble_gattc_write_long(conn_handle, data_handle,
offset, om, write_cb, NULL);
offset += chunk_len;
remaining -= chunk_len;
}
}
优化建议:
- 先协商更大的MTU
- 使用流控避免缓冲区溢出
- 添加进度反馈机制