❤️ 心率传感器 (HRM)

基于 BLE HRM Profile 的心率监测设备,实时上报心率和 RR 间期数据。

📐 系统架构

flowchart TB subgraph Sensor["心率传感器"] HR[心率检测模块] --> MCU[BLE MCU] MCU --> ANT[天线] end subgraph Phone["手机 App"] ANT2[天线] --> MCU2[手机 BLE] MCU2 --> APP[健身 App] end ANT -.->|BLE 广播/连接 | ANT2 style Sensor fill:#e3f2fd,stroke:#007bff style Phone fill:#d4edda,stroke:#28a745

📋 什么是 Profile?

Profile(配置文件) 是蓝牙联盟 (SIG) 定义的标准应用规范,确保不同厂商的设备可以互操作。

为什么需要 Profile?

  • 统一数据格式: 所有 HRM 设备使用相同的数据结构
  • 即插即用: App 无需适配每个设备
  • 互操作性: Nordic 芯片可连接 iOS/Android
  • 降低开发成本: 无需自定义协议

HRM Profile 组成

  • 服务 UUID: 0x180D (心率服务)
  • 特征值 UUID: 0x2A37 (心率测量)
  • 数据格式: 标志位 + 心率值 + 可选字段
  • 操作流程: 发现→启用通知→接收数据

📋 HRM Profile 结构

HRM Profile 定义了一个服务和多个特征值,用于标准化心率数据的传输。

服务与特征值关系

flowchart TB subgraph HRM["心率服务 0x180D"] HRM_Char[心率测量特征值 0x2A37] HRM_CCCD[CCCD 0x2902] BODY[身体位置特征值 0x2A38] CTRL[控制点特征值 0x2A39] end HRM_Char --> HRM_CCCD subgraph Data["数据格式"] Flags[标志位 1B] BPM[心率值 1B] Energy[能量消耗 2B] RR[RR 间期 2B*N] end style HRM fill:#e3f2fd,stroke:#007bff style Data fill:#fff3cd,stroke:#ffc107

特征值详细说明

特征值 UUID 权限 用途 是否必需
心率测量 0x2A37 Notify 实时推送心率和 RR 间期 ✅ 必需
身体位置 0x2A38 Read 设备佩戴位置 (手腕/胸部等) ⭕ 可选
控制点 0x2A39 Write 重置能量消耗累计值 ⭕ 可选
CCCD 0x2902 Read/Write 启用/禁用通知 ✅ 必需

特征值属性说明

❤️ 心率测量 (0x2A37)

  • 操作: Notify (设备主动推送)
  • 数据: 标志位 + 心率值 + 可选字段
  • CCCD: 0x2902 (启用通知)
  • 触发: 每次心跳或定时上报

👤 身体位置 (0x2A38)

  • 操作: Read (手机读取)
  • 值: 0=其他,1=胸部,2=手腕
  • 用途: 优化算法精度
  • 示例: 0x01 = 胸部心率带

🎮 控制点 (0x2A39)

  • 操作: Write (手机写入)
  • 值: 0x01 = 重置能量累计
  • 用途: 清零卡路里计数
  • 响应: 无需确认

📊 心率测量数据格式

// 心率测量特征值 (0x2A37) 数据格式
// 启用 Notify 后,传感器主动推送数据

typedef struct {
    uint8_t  flags;           // 标志位
    uint8_t  heart_rate;      // 心率值 (BPM)
    uint16_t energy_expended; // 能量消耗 (kJ) - 可选
    uint16_t rr_interval[];   // RR 间期 (1/1024 秒) - 可选
} heart_rate_measurement_t;

// 标志位定义
#define HRM_FLAG_HR_16BIT       0x01  // 0: 8bit, 1: 16bit
#define HRM_FLAG_CONTACT_DET    0x02  // 接触检测
#define HRM_FLAG_CONTACT_STAT   0x04  // 接触状态
#define HRM_FLAG_ENERGY_EXP     0x08  // 能量消耗存在
#define HRM_FLAG_RR_INTERVAL    0x10  // RR 间期存在

// 示例数据:正常心率 72 BPM,有 RR 间期
uint8_t hrm_data[] = {
    0x16,       // Flags: RR 间期存在
    0x48,       // Heart Rate: 72 BPM
    0x00, 0x04, // RR Interval 1: 1024 * 0.0039 = 4ms (正常)
    0x00, 0x04  // RR Interval 2: 4ms
};

📈 完整工作流程

sequenceDiagram participant HRM as 心率传感器 participant Phone as 手机 App Note over HRM,Phone: 1. 广播阶段 HRM->>Phone: ADV_IND (HRM Service 0x180D) Note over HRM,Phone: 2. 连接阶段 Phone->>HRM: CONNECT_IND HRM-->>Phone: 连接完成 Note over HRM,Phone: 3. 服务发现 Phone->>HRM: 发现服务 0x180D Phone->>HRM: 发现特征值 0x2A37 Note over HRM,Phone: 4. 启用通知 Phone->>HRM: Write CCCD (0x0001) HRM-->>Phone: Write Response Note over HRM,Phone: 5. 数据推送 loop 每秒 HRM->>Phone: Handle Value Notification (心率 72) end Note over HRM,Phone: 6. 断开连接 Phone->>HRM: 断开连接

🔬 实际心率值传输详解

以用户心率为 75 BPM 为例,详细展示每一层协议传输的具体数值。

步骤 1: 传感器检测心率

// 光电传感器 (PPG) 检测心跳
// 检测到一次心跳,计算心率为 75 BPM

uint8_t heart_rate_bpm = 75;  // 实际心率值

// 同时测量 RR 间期 (两次心跳间隔)
// 假设间隔为 800ms = 819 个单位 (1 单位 = 1/1024 秒)
uint16_t rr_interval = 819;

步骤 2: 构建 HRM 数据

// 构建心率测量数据包 (5 bytes)
uint8_t hrm_payload[] = {
    0x10,       // [0] Flags: 
                //   bit0=0 (8-bit HR), 
                //   bit4=1 (RR interval present)
    0x4B,       // [1] Heart Rate: 75 BPM (0x4B = 75)
    0x33, 0x03  // [2-3] RR Interval: 0x0333 = 819 (819/1024 = 0.8 秒)
};

// 标志位详解:
// 0x10 = 0001 0000
// bit4=1 → 有 RR 间期
// bit0=0 → 8 位心率值 (0-255 BPM 足够)

步骤 3: ATT 层封装

// ATT Handle Value Notification (Opcode = 0x1B)
// 特征值句柄 = 0x0015 (假设)

uint8_t att_pdu[] = {
    0x1B,           // Opcode: Handle Value Notification
    0x15, 0x00,     // Handle: 0x0015 (心率特征值句柄)
    // 下面是 GATT 数据
    0x10,           // Flags
    0x4B,           // Heart Rate: 75
    0x33, 0x03      // RR Interval: 819
};
// 总长度:7 bytes

步骤 4: L2CAP 层封装

// L2CAP B-Frame (面向连接的信道)
// CID = 0x0004 (动态分配的 ATT 信道)

uint8_t l2cap_packet[] = {
    0x07, 0x00,     // L2CAP Length: 7 bytes (ATT PDU 长度)
    0x04, 0x00,     // CID: 0x0004 (ATT 信道)
    // 下面是 ATT PDU
    0x1B, 0x15, 0x00, 0x10, 0x4B, 0x33, 0x03
};
// 总长度:11 bytes

步骤 5: HCI ACL 数据包

// HCI ACL Data Packet (Host → Controller)
// 连接句柄 = 0x0001

uint8_t hci_acl_packet[] = {
    // HCI ACL Header
    0x01, 0x00,     // Handle: 0x0001 (连接句柄)
    0x0B, 0x00,     // Data Total Length: 11 bytes
    
    // ACL Data
    0x07, 0x00,     // L2CAP Length
    0x04, 0x00,     // CID
    0x1B, 0x15, 0x00, 0x10, 0x4B, 0x33, 0x03
};
// 总长度:15 bytes

步骤 6: LL 层传输

// Link Layer Data PDU
// 分为 2 个 LL 数据包发送 (因为单个 LL PDU 最大 27 bytes)

// LL PDU #1 (包含大部分数据)
// LL PDU #2 (如果有剩余)

// 实际空中传输使用 2M PHY 或 1M PHY
// 每个数据包包含:
// - LL Header (2 bytes)
// - Payload (数据)
// - MIC (加密校验,4 bytes)
// - CRC (3 bytes)

步骤 7: 手机接收并解析

// 手机收到数据后逐层解析

// 1. 解析 L2CAP → 提取 ATT PDU
val attPdu = l2capPayload  // [0x1B, 0x15, 0x00, 0x10, 0x4B, 0x33, 0x03]

// 2. 解析 ATT → 提取 GATT 数据
val opcode = attPdu[0]     // 0x1B = Notification
val handle = attPdu[1] or (attPdu[2] shl 8)  // 0x0015
val gattData = attPdu.drop(3)  // [0x10, 0x4B, 0x33, 0x03]

// 3. 解析 HRM 数据
val flags = gattData[0]    // 0x10
val hrFormat = flags and 0x01  // 0 = 8-bit
val hasRR = (flags and 0x10) != 0  // true = 有 RR 间期

val heartRate = gattData[1].toInt() and 0xFF  // 0x4B = 75 BPM

if (hasRR) {
    val rrInterval = ((gattData[2].toInt() and 0xFF) or 
                     ((gattData[3].toInt() and 0xFF) shl 8))
    // 0x0333 = 819 → 819/1024 = 0.8 秒
    val rrSeconds = rrInterval / 1024.0
}

// 显示结果:心率 75 BPM, RR 间期 0.8 秒

完整数据流图

flowchart LR subgraph Sensor["传感器"] HR[心率 75 BPM] --> PPG[PPG 检测] end subgraph Device["设备固件"] PPG --> HRM[HRM 数据 0x10,0x4B,0x33,0x03] HRM --> ATT[ATT PDU 0x1B,0x15,0x00...] ATT --> L2CAP[L2CAP 0x07,0x00,0x04,0x00...] L2CAP --> HCI[HCI ACL Packet] HCI --> LL[LL Data PDU] LL --> RF[RF 射频] end subgraph Phone["手机"] RF2[RF 接收] --> HCI2[HCI ACL] HCI2 --> L2CAP2[L2CAP 解析] L2CAP2 --> ATT2[ATT 解析] ATT2 --> GATT2[GATT 通知回调] GATT2 --> APP[App 显示:75 BPM] end RF -.->|BLE 空中传输 | RF2 style Sensor fill:#d4edda,stroke:#28a745 style Device fill:#e3f2fd,stroke:#007bff style Phone fill:#fff3cd,stroke:#ffc107

实际抓包数据示例

# Wireshark/BT 抓包显示

Frame 1234: 27 bytes on wire
Bluetooth HCI ACL Packet
    Handle: 0x0001
    Data Length: 11 bytes
L2CAP
    Length: 7
    CID: 0x0004 (ATT)
ATT
    Opcode: Handle Value Notification (0x1B)
    Handle: 0x0015
    Value: 104b3303
Heart Rate Measurement
    Flags: 0x10
        ..00 0000 = Heart Rate Value Format: uint8 (0)
        .001 .... = Sensor Status: RR Interval present (1)
    Heart Rate: 75 (0x4B)
    RR-Interval: 0.800 sec (819)

💻 固件代码示例

#include "nrf_sdm.h"
#include "nrf_sdh.h"
#include "nrf_sdh_ble.h"
#include "ble_hrs.h"

// 心率服务句柄
BLE_HRS_DEF(m_hrs);

// 心率测量数据
static uint8_t heart_rate = 72;

// 初始化心率服务
void hrs_init(void) {
    ble_hrs_init_t hrs_init_struct = {0};
    
    // 配置心率测量特征值
    hrs_init_struct.evt_handler = hrs_evt_handler;
    hrs_init_struct.supported_body_locations = BLE_HRS_BODY_LOCATION_CHEST;
    
    // 添加 RR 间期支持
    hrs_init_struct.qr_code_supported = false;
    
    // 初始化服务
    ble_hrs_init(&m_hrs, &hrs_init_struct);
}

// 心率服务事件处理
void hrs_evt_handler(ble_hrs_t * p_hrs, ble_hrs_evt_t * p_evt) {
    switch (p_evt->evt_type) {
        case BLE_HRS_EVT_CCCD_ENABLED:
            // 客户端启用了通知
            start_heart_rate_measurement();
            break;
        case BLE_HRS_EVT_CCCD_DISABLED:
            // 客户端禁用了通知
            stop_heart_rate_measurement();
            break;
        default:
            break;
    }
}

// 测量并上报心率
void update_heart_rate(uint8_t bpm) {
    uint8_t hrm_data[BLE_HRS_MAX_DATA_LEN];
    uint16_t hrm_len = 0;
    
    // 构建心率数据
    hrm_data[hrm_len++] = 0x00;  // Flags: 8bit HR, no RR
    hrm_data[hrm_len++] = bpm;   // Heart rate value
    
    // 发送通知
    ble_hrs_heart_rate_measurement_send(&m_hrs, hrm_data, hrm_len);
}

// 带 RR 间期的心率上报
void update_heart_rate_with_rr(uint8_t bpm, uint16_t *rr_intervals, uint8_t count) {
    uint8_t hrm_data[BLE_HRS_MAX_DATA_LEN];
    uint16_t hrm_len = 0;
    
    // Flags: 8bit HR + RR intervals present
    hrm_data[hrm_len++] = 0x10;
    hrm_data[hrm_len++] = bpm;
    
    // 添加 RR 间期
    for (int i = 0; i < count; i++) {
        hrm_data[hrm_len++] = rr_intervals[i] & 0xFF;
        hrm_data[hrm_len++] = (rr_intervals[i] >> 8) & 0xFF;
    }
    
    ble_hrs_heart_rate_measurement_send(&m_hrs, hrm_data, hrm_len);
}

📱 手机 App 代码示例

// Android - 连接心率传感器
class HeartRateMonitor(private val context: Context) {
    
    private var bluetoothGatt: BluetoothGatt? = null
    private var heartRateCharacteristic: BluetoothGattCharacteristic? = null
    
    // HRM Service 和 Characteristic UUID
    companion object {
        val HRM_SERVICE_UUID = UUID.fromString("0000180d-0000-1000-8000-00805f9b34fb")
        val HRM_MEASUREMENT_UUID = UUID.fromString("00002a37-0000-1000-8000-00805f9b34fb")
        val CCCD_UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
    }
    
    // 连接设备
    fun connect(address: String) {
        val device = bluetoothAdapter.getRemoteDevice(address)
        bluetoothGatt = device.connectGatt(context, false, gattCallback)
    }
    
    private val gattCallback = object : BluetoothGattCallback() {
        override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
            if (status == BluetoothGatt.GATT_SUCCESS) {
                // 发现 HRM 服务
                val hmsService = gatt.getService(HRM_SERVICE_UUID)
                heartRateCharacteristic = hmsService?.getCharacteristic(HRM_MEASUREMENT_UUID)
                
                // 启用通知
                enableHeartRateNotification()
            }
        }
        
        override fun onCharacteristicChanged(
            gatt: BluetoothGatt,
            characteristic: BluetoothGattCharacteristic
        ) {
            if (characteristic.uuid == HRM_MEASUREMENT_UUID) {
                parseHeartRate(characteristic.value)
            }
        }
    }
    
    // 启用通知
    private fun enableHeartRateNotification() {
        bluetoothGatt?.setCharacteristicNotification(heartRateCharacteristic, true)
        
        val cccd = heartRateCharacteristic?.getDescriptor(CCCD_UUID)
        cccd?.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
        bluetoothGatt?.writeDescriptor(cccd)
    }
    
    // 解析心率数据
    private fun parseHeartRate(data: ByteArray) {
        val flags = data[0].toInt()
        val hrFormat = flags and 0x01
        
        val heartRate = if (hrFormat == 0) {
            data[1].toInt() and 0xFF  // 8-bit
        } else {
            ((data[1].toInt() and 0xFF) or ((data[2].toInt() and 0xFF) shl 8))  // 16-bit
        }
        
        Log.d("HRM", "心率:$heartRate BPM")
        
        // 检查是否有 RR 间期
        if (flags and 0x10 != 0) {
            parseRRIntervals(data, hrFormat)
        }
    }
}

🔍 广播数据配置

// 广播数据:让手机快速识别 HRM 设备
// 在广播中包含 HRM Service UUID

uint8_t adv_data[] = {
    // Flags
    0x02, 0x01, 0x06,
    
    // 16-bit Service UUIDs (HRM Service)
    0x03, 0x03, 0x0D, 0x18,  // 0x180D = Heart Rate
    
    // 设备名称
    0x0A, 0x09, 'H', 'R', 'M', '-', 'D', 'e', 'v', 'i', 'c', 'e'
};

// 扫描响应数据
uint8_t scan_rsp[] = {
    // 完整设备名称
    0x0A, 0x09, 'H', 'R', 'M', '-', 'D', 'e', 'v', 'i', 'c', 'e',
    
    // 外观 (Appearance): 心率带
    0x03, 0x19, 0x03, 0x04  // 0x0403 = Heart Rate Sensor
};

⚡ 功耗优化技巧

优化项 配置 效果
连接间隔 100-500ms 降低通信频率
从机延迟 0-4 允许跳过事件
广播间隔 500-1000ms 降低待机电流
传感器采样 按需唤醒 仅在连接时测量
RR 间期上报 批量发送 减少数据包数量

🛠️ 调试要点

常见问题

  • 通知未启用:检查 CCCD 写入
  • 数据解析错误:确认标志位
  • 连接不稳定:调整连接参数
  • 功耗过高:优化广播间隔

推荐工具

  • nRF Connect: 查看服务/特征值
  • Heart Rate Simulator: 模拟 HRM 设备
  • Wireshark: 空口抓包分析
  • Segger RTT: 固件日志输出