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'
};
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"
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 实战要点总结
嗯,说了这么多,我总结几个关键点:
- 广播数据别超31字节,这是硬性限制。如果需要广播更多信息,可以用扫描响应数据(Scan Response),那也是31字节。
- 服务UUID尽量用标准化的,除非你的功能真的很特殊。标准服务兼容性好,手机APP开发也省事。
- 通知比轮询高效得多,能省电就省电。但要注意,通知的最大速率是有限的,BLE 4.0大概每秒10-20包,BLE 5.0可以到每秒上百包。
- 数据格式要提前定义好。比如温度值用有符号16位整数,单位0.01°C。主机和从机约定好,否则解析出来全是乱码。
我记得有一次帮客户调试一个BLE体温贴,他们死活收不到数据。我一看代码,发现他们把温度值当成了无符号整数,结果零下温度全变成了65535。这种问题,说白了就是协议没定义清楚。
好了,这一章的内容就到这儿。下一章咱们聊聊BLE主机的开发——怎么去扫描、连接、读写数据。到时候你会发现,主机和从机其实是一枚硬币的两面,很多概念是相通的。