4、BLE从机开发:广播配置、服务与特征值创建、数据收发

好,咱们进入BLE从机开发的正题。

说实话,从机开发是蓝牙低功耗里最基础、也最核心的部分。你想想看,市面上那些智能手环、温湿度传感器、ibeacon信标,本质上都是BLE从机。它们不停地广播自己的存在,然后等着主机来连接。

我个人习惯把从机开发拆成三块:广播服务与特征值数据收发。这三块搞明白了,你就能自己造一个BLE设备了。

4.1 广播配置——让设备被“看见”

广播是什么?说白了就是你的设备在喊:“我在这儿!我在这儿!快来连我!”

但广播不是瞎喊的,它有一套固定的格式。我刚开始做的时候,以为广播数据随便填就行,结果手机死活搜不到设备。后来才发现,广播包的格式是有严格规定的。

4.1.1 广播包结构

一个标准的广播包,由若干个AD Structure组成。每个AD Structure又分三部分:

  • 长度(1字节):表示后面数据的总长度
  • 类型(1字节):表示这段数据的类型,比如设备名、UUID、厂商自定义数据等
  • 数据(N字节):具体的内容

举个例子,假设我要广播设备名为“MySensor”:

// 广播数据示例
// 0x02 0x01 0x05          -> 长度2,类型0x01(标志),数据0x05(可发现+不支持BR/EDR)
// 0x09 0x09 'M' 'y' 'S' 'e' 'n' 's' 'o' 'r'  -> 长度9,类型0x09(完整设备名),数据"MySensor"

uint8_t adv_data[] = {
    0x02, 0x01, 0x05,
    0x09, 0x09, 'M', 'y', 'S', 'e', 'n', 's', 'o', 'r'
};
我的经验:广播数据总长度不能超过31字节。这是BLE规范定的。我曾经为了塞更多信息进去,把设备名缩写成了“MS”,省出来的字节放厂商自定义数据。嗯,有时候就得做点取舍。

4.1.2 广播间隔与功耗

广播间隔,就是设备每隔多久喊一次。这个参数直接影响功耗和被发现的速度。

广播间隔 发现速度 平均功耗 适用场景
20ms 极快(<100ms) 高(~10mA) 需要快速连接的场景,如蓝牙遥控器
100ms 较快(<500ms) 中等(~2mA) 一般传感器设备
1000ms 较慢(~3s) 低(~0.2mA) 电池供电、不要求实时性的设备

我建议你初期调试时用100ms间隔,这样手机搜得快,调试效率高。等产品定型了,再根据实际功耗需求调整。

4.2 服务与特征值创建——定义设备的能力

广播只是让设备被发现。真正让设备“有用”的,是服务和特征值。

你可以把服务想象成一个文件夹,特征值就是文件夹里的文件。主机通过读写这些“文件”,来获取数据或下发指令。

4.2.1 标准服务 vs 自定义服务

BLE规范定义了很多标准服务,比如:

  • 电池服务(0x180F):用来上报电量
  • 设备信息服务(0x180A):用来读取设备型号、序列号等
  • 心率服务(0x180D):用来传输心率数据

如果你的设备功能比较特殊,比如采集土壤湿度、控制电机转速,那就得用自定义服务。自定义服务的UUID是128位的,比如:

// 自定义服务UUID
#define CUSTOM_SERVICE_UUID        "0000FFF0-0000-1000-8000-00805F9B34FB"
#define CUSTOM_CHAR_TX_UUID        "0000FFF1-0000-1000-8000-00805F9B34FB"
#define CUSTOM_CHAR_RX_UUID        "0000FFF2-0000-1000-8000-00805F9B34FB"
注意:自定义UUID不能随便编。虽然技术上你可以用任何128位值,但建议用在线生成器生成一个随机UUID,避免和别人的服务冲突。我曾经见过两个不同厂家的设备用了同一个自定义UUID,结果手机APP同时连接时数据串了,排查了好久才发现是UUID撞车了。

4.2.2 特征值的属性

每个特征值都有属性,告诉主机你能对它做什么:

  • 读(Read):主机可以读取这个特征值的数据
  • 写(Write):主机可以向这个特征值写入数据
  • 通知(Notify):从机主动向主机推送数据,不需要主机轮询
  • 指示(Indicate):类似通知,但主机收到后会回复确认,更可靠

我个人最常用的是“通知”。为什么呢?因为传感器数据是实时变化的,让主机每隔几秒来读一次,既浪费带宽又费电。不如让从机数据变化时主动推过去。

// 以Nordic nRF5 SDK为例,创建服务和特征值
static void ble_custom_service_init(void)
{
    uint32_t   err_code;
    ble_uuid_t ble_uuid;
    
    // 添加自定义服务
    ble_uuid128_t base_uuid = {CUSTOM_SERVICE_BASE_UUID};
    err_code = sd_ble_uuid_vs_add(&base_uuid, &ble_uuid.type);
    APP_ERROR_CHECK(err_code);
    
    ble_uuid.uuid = CUSTOM_SERVICE_UUID_SHORT;
    
    // 添加特征值(TX特征,用于发送数据)
    ble_gatts_char_md_t char_md;
    memset(&char_md, 0, sizeof(char_md));
    char_md.char_props.notify = 1;  // 支持通知
    
    // ... 后续配置略
}

4.3 数据收发——让数据流动起来

服务和特征值建好了,接下来就是让数据真正跑起来。

4.3.1 从机发送数据(通知)

当传感器采集到新数据,从机通过通知把数据推给主机。代码大概长这样:

// 发送温度数据
void send_temperature_data(int16_t temperature)
{
    uint32_t err_code;
    uint16_t hvx_len = 2;  // 温度值占2字节
    
    // 把温度值打包成字节
    uint8_t data[2];
    data[0] = (temperature >> 8) & 0xFF;
    data[1] = temperature & 0xFF;
    
    // 通过通知发送
    ble_gatts_hvx_params_t hvx_params;
    memset(&hvx_params, 0, sizeof(hvx_params));
    hvx_params.handle = m_custom_service.tx_handles.value_handle;
    hvx_params.type   = BLE_GATT_HVX_NOTIFICATION;
    hvx_params.offset = 0;
    hvx_params.p_len  = &hvx_len;
    hvx_params.p_data = data;
    
    err_code = sd_ble_gatts_hvx(m_conn_handle, &hvx_params);
    if (err_code != NRF_SUCCESS) {
        // 处理错误,比如连接已断开
    }
}
避坑指南:我曾经犯过一个低级错误——在通知发送失败时没有做任何处理。结果设备断连后,数据一直在发送缓冲区里堆积,最后导致内存溢出。记住,发送前一定要检查连接状态,发送失败后要清空缓冲区或者重连。

4.3.2 从机接收数据(写请求)

主机下发指令时,会触发从机的写事件回调。比如主机要设置采样间隔:

// 写事件回调
static void on_write(ble_evt_t const *p_ble_evt)
{
    ble_gatts_evt_write_t const *p_evt_write = &p_ble_evt->evt.gatts_evt.params.write;
    
    // 判断是哪个特征值被写
    if (p_evt_write->handle == m_custom_service.rx_handles.value_handle) {
        // 解析数据
        uint8_t cmd = p_evt_write->data[0];
        uint8_t param = p_evt_write->data[1];
        
        switch (cmd) {
            case 0x01:  // 设置采样间隔
                set_sampling_interval(param * 100);  // param单位是100ms
                break;
            case 0x02:  // 立即采集一次
                trigger_sampling();
                break;
            default:
                // 未知命令,可以回复错误码
                break;
        }
    }
}

这里有个细节要注意:写请求分为“有响应写”和“无响应写”。有响应写更可靠,但速度慢;无响应写速度快,但主机不知道从机是否收到。我一般用有响应写来下发配置命令,用无响应写来传输大量数据。

4.4 实战要点总结

嗯,说了这么多,我总结几个关键点:

  1. 广播数据别超31字节,这是硬性限制。如果需要广播更多信息,可以用扫描响应数据(Scan Response),那也是31字节。
  2. 服务UUID尽量用标准化的,除非你的功能真的很特殊。标准服务兼容性好,手机APP开发也省事。
  3. 通知比轮询高效得多,能省电就省电。但要注意,通知的最大速率是有限的,BLE 4.0大概每秒10-20包,BLE 5.0可以到每秒上百包。
  4. 数据格式要提前定义好。比如温度值用有符号16位整数,单位0.01°C。主机和从机约定好,否则解析出来全是乱码。

我记得有一次帮客户调试一个BLE体温贴,他们死活收不到数据。我一看代码,发现他们把温度值当成了无符号整数,结果零下温度全变成了65535。这种问题,说白了就是协议没定义清楚。

好了,这一章的内容就到这儿。下一章咱们聊聊BLE主机的开发——怎么去扫描、连接、读写数据。到时候你会发现,主机和从机其实是一枚硬币的两面,很多概念是相通的。