手把手教你用ESP32 NimBLE API实现一个蓝牙心率监测从机(GATT Server保姆级教程)
手把手教你用ESP32 NimBLE API实现蓝牙心率监测从机开发全流程
去年夏天,我在为一个健康科技初创公司开发可穿戴设备原型时,第一次接触到ESP32的NimBLE协议栈。当时我们需要在两周内完成一个能实时传输心率数据的演示设备,而传统蓝牙协议栈的开发复杂度让我们差点错过截止日期。直到发现NimBLE这个轻量级解决方案——它不仅将代码量减少了40%,还显著降低了内存占用。本文将分享如何从零开始,用ESP32和NimBLE构建一个专业级的心率监测从机设备。
1. 开发环境准备与基础配置
在开始编写心率监测应用之前,我们需要搭建好ESP32的开发环境并进行必要的配置。ESP-IDF(Espressif IoT Development Framework)是开发ESP32应用的官方框架,最新版本已经内置了对NimBLE协议栈的支持。
首先通过以下命令创建一个新项目:
cp -r $IDF_PATH/examples/bluetooth/nimble/blehr my_blehr_project cd my_blehr_project接着配置工具链和编译选项。运行idf.py menuconfig进入配置界面,在"Component config > Bluetooth"下选择:
- Bluetooth controller mode:BLE Only
- Bluetooth Host:NimBLE
- NimBLE ACL Buffer Count:建议设置为12(默认值可能不足)
提示:如果开发板上有外部SPIRAM,记得在"Component config > ESP32-specific"中启用"Support for external, SPI-connected RAM"
基础代码框架应该包含以下核心组件:
#include "esp_nimble_hci.h" #include "nimble/nimble_port.h" #include "nimble/nimble_port_freertos.h" #include "host/ble_hs.h" void ble_host_task(void *param) { nimble_port_run(); nimble_port_freertos_deinit(); } void app_main() { // 初始化NVS存储 nvs_flash_init(); // 初始化蓝牙控制器和主机栈 esp_nimble_hci_and_controller_init(); nimble_port_init(); // 配置主机栈参数 ble_hs_cfg.sync_cb = blehr_on_sync; ble_hs_cfg.reset_cb = blehr_on_reset; // 启动BLE任务 nimble_port_freertos_init(ble_host_task); }2. 心率服务(GATT)设计与实现
蓝牙心率监测设备的核心是符合蓝牙SIG规范的Heart Rate Service(HRS)。根据官方规范,一个完整的心率服务应包含:
必需特性:
- Heart Rate Measurement (0x2A37):通知型特性,用于实时传输心率数据
- Body Sensor Location (0x2A38):可选项,指示传感器佩戴位置
可选特性:
- Heart Rate Control Point (0x2A39):用于重置能量消耗计数
我们先定义服务UUID和特性UUID:
// 蓝牙SIG定义的标准UUID #define GATT_SVR_SVC_ALERT_UUID 0x180D // 心率服务 #define GATT_SVR_CHR_HR_MEAS_UUID 0x2A37 // 心率测量特性 #define GATT_SVR_CHR_SENSOR_LOC_UUID 0x2A38 // 传感器位置特性 // 自定义的128位UUID格式(示例) static const ble_uuid128_t gatt_svr_svc_sec_uuid = BLE_UUID128_INIT(0xfb, 0x34, 0x9b, 0x5f, 0x80, 0x00, 0x00, 0x80, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00);接下来是实现服务的核心代码。我们使用ble_gatts_add_svcs函数注册服务:
static const struct ble_gatt_svc_def gatt_svr_svcs[] = { { .type = BLE_GATT_SVC_TYPE_PRIMARY, .uuid = BLE_UUID16_DECLARE(GATT_SVR_SVC_ALERT_UUID), .characteristics = (struct ble_gatt_chr_def[]) { { .uuid = BLE_UUID16_DECLARE(GATT_SVR_CHR_HR_MEAS_UUID), .access_cb = gatt_svr_chr_access, .flags = BLE_GATT_CHR_F_NOTIFY, .val_handle = &hr_meas_handle }, { .uuid = BLE_UUID16_DECLARE(GATT_SVR_CHR_SENSOR_LOC_UUID), .access_cb = gatt_svr_chr_access, .flags = BLE_GATT_CHR_F_READ, }, { 0, /* No more characteristics */ } } }, { 0, /* No more services */ } }; int gatt_svr_init(void) { int rc = ble_gatts_count_cfg(gatt_svr_svcs); if (rc != 0) return rc; rc = ble_gatts_add_svcs(gatt_svr_svcs); return rc; }3. 关键回调函数实现与数据处理
NimBLE协议栈通过回调机制与应用程序交互。对于心率监测设备,最重要的三个回调函数是:
- 同步回调 (sync_cb):当主机与控制器完成同步时触发
- GATT注册回调 (gatts_register_cb):服务注册完成后调用
- 特征值访问回调 (access_cb):处理客户端读写请求
以下是sync_cb的典型实现:
static void blehr_on_sync(void) { int rc; // 确认设备地址 ble_addr_t addr; rc = ble_hs_id_copy_addr(BLE_ADDR_PUBLIC, addr.val, NULL); assert(rc == 0); // 打印设备地址 ESP_LOGI(TAG, "Device Address: %02x:%02x:%02x:%02x:%02x:%02x", addr.val[5], addr.val[4], addr.val[3], addr.val[2], addr_val[1], addr_val[0]); // 开始广播 rc = blehr_advertise(); assert(rc == 0); }特征值访问回调需要处理多种操作类型:
static int gatt_svr_chr_access(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg) { switch (ctxt->op) { case BLE_GATT_ACCESS_OP_READ_CHR: if (ble_uuid_cmp(ctxt->chr->uuid, BLE_UUID16_DECLARE(GATT_SVR_CHR_SENSOR_LOC_UUID)) == 0) { // 返回传感器位置(示例为手腕) uint8_t location = 3; // 3表示手腕 os_mbuf_append(ctxt->om, &location, 1); return 0; } break; case BLE_GATT_ACCESS_OP_WRITE_CHR: // 处理写入请求 break; case BLE_GATT_ACCESS_OP_READ_DSC: // 处理描述符读取 break; } return BLE_ATT_ERR_UNLIKELY; }4. 心率数据模拟与通知机制
在实际产品中,心率数据来自PPG传感器。在开发阶段,我们可以模拟数据来测试通知机制:
static void blehr_tx_hrate(struct ble_npl_event *ev) { static uint8_t hrm[2]; static bool increasing = true; static uint16_t hr = 60; // 模拟心率变化 if (increasing) { hr++; if (hr == 100) increasing = false; } else { hr--; if (hr == 60) increasing = true; } // 构建心率数据包 hrm[0] = 0x06; // 8位心率值,传感器接触状态 hrm[1] = hr; // 实际心率值 // 发送通知 int rc = ble_gattc_notify_custom(conn_handle, hr_meas_handle, hrm, sizeof hrm); if (rc != 0) { ESP_LOGE(TAG, "Notify failed: %d", rc); } // 设置下一次定时器 ble_npl_time_t delay = BLEHR_MEASUREMENT_PERIOD_MS * 1000; ble_npl_callout_reset(&hr_timer, delay); }通知机制需要客户端先启用。当手机APP订阅通知时,我们会收到BLE_GAP_EVENT_SUBSCRIBE事件:
static int blehr_gap_event(struct ble_gap_event *event, void *arg) { switch (event->type) { case BLE_GAP_EVENT_CONNECT: ESP_LOGI(TAG, "Connection established"); conn_handle = event->connect.conn_handle; break; case BLE_GAP_EVENT_DISCONNECT: ESP_LOGI(TAG, "Disconnected"); conn_handle = BLE_HS_CONN_HANDLE_NONE; blehr_advertise(); break; case BLE_GAP_EVENT_SUBSCRIBE: ESP_LOGI(TAG, "Subscribe event: attr_handle=%d subscribed=%d", event->subscribe.attr_handle, event->subscribe.cur_notify); break; } return 0; }5. 设备广播与连接管理
为了让手机能够发现并连接我们的设备,需要配置适当的广播参数:
static int blehr_advertise(void) { struct ble_gap_adv_params adv_params; struct ble_hs_adv_fields fields; int rc; // 设置广播数据 memset(&fields, 0, sizeof fields); fields.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP; fields.tx_pwr_lvl_is_present = 1; fields.tx_pwr_lvl = BLE_HS_ADV_TX_PWR_LVL_AUTO; fields.name = (uint8_t *)ble_svc_gap_device_name(); fields.name_len = strlen(ble_svc_gap_device_name()); fields.name_is_complete = 1; rc = ble_gap_adv_set_fields(&fields); if (rc != 0) return rc; // 配置广播参数 memset(&adv_params, 0, sizeof adv_params); adv_params.conn_mode = BLE_GAP_CONN_MODE_UND; adv_params.disc_mode = BLE_GAP_DISC_MODE_GEN; // 开始广播 rc = ble_gap_adv_start(BLE_OWN_ADDR_PUBLIC, NULL, BLE_HS_FOREVER, &adv_params, blehr_gap_event, NULL); return rc; }6. 使用nRF Connect进行功能验证
完成代码编写后,我们可以使用nRF Connect等蓝牙调试工具验证设备功能:
设备发现:
- 确保设备名称和广播数据正确显示
- 检查RSSI信号强度是否正常
服务发现:
- 确认心率服务(0x180D)及其特性完整显示
- 验证传感器位置特性是否可读
通知测试:
- 启用心率测量特性的通知
- 观察是否定期收到心率数据包
- 检查数据格式是否符合预期
常见问题排查表:
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 设备不可见 | 广播未启动 | 检查blehr_advertise()返回值 |
| 服务缺失 | 服务注册失败 | 验证ble_gatts_add_svcs调用 |
| 通知不工作 | 客户端未订阅 | 检查BLE_GAP_EVENT_SUBSCRIBE事件 |
| 数据格式错误 | 特征值定义不符 | 对照蓝牙SIG规范检查数据包 |
在项目最后阶段,我发现一个容易忽略的细节:当设备进入深度睡眠时,需要特别处理BLE连接状态。有次为了优化功耗添加了睡眠模式,结果导致已建立的连接异常断开。后来通过添加连接状态检查解决了这个问题:
void enter_light_sleep() { if (conn_handle == BLE_HS_CONN_HANDLE_NONE) { // 无活跃连接时进入睡眠 esp_sleep_enable_timer_wakeup(SLEEP_DURATION_US); esp_light_sleep_start(); } else { // 有连接时延迟睡眠 ble_npl_callout_reset(&sleep_timer, SLEEP_DELAY_MS * 1000); } }