1. 蓝牙协议栈核心概念解析
1.1 MTU:数据传输效率的关键参数
在低功耗蓝牙(BLE)通信中,MTU(Maximum Transmission Unit)直接决定了单次数据传输的效率。这个参数在协议栈中具体表现为ATT_MTU,即属性协议层的最大传输单元。默认情况下,BLE协议栈会使用23字节的标准MTU值,其中3字节用于协议头,实际有效载荷为20字节。
在实际项目中,我们通常会根据应用场景调整MTU大小。例如在需要传输大量数据的医疗设备(如连续血糖监测仪)中,我们会将MTU设置为更大的值(如247字节)。通过nRF SDK配置时,需要注意以下要点:
c复制#define NRF_SDH_BLE_GATT_MAX_MTU_SIZE 247
static void ble_stack_init(void)
{
ret_code_t err;
ble_cfg_t ble_cfg;
// 配置GATT MTU大小
memset(&ble_cfg, 0, sizeof(ble_cfg));
ble_cfg.conn_cfg.conn_cfg_tag = APP_BLE_CONN_CFG_TAG;
ble_cfg.gatt_cfg.att_mtu = NRF_SDH_BLE_GATT_MAX_MTU_SIZE;
err = sd_ble_cfg_set(BLE_CONN_CFG_GATT, &ble_cfg, ram_start);
APP_ERROR_CHECK(err);
}
注意:MTU协商是在连接建立阶段完成的,两端设备会协商出一个双方都支持的MTU值。如果一端请求247字节而另一端只支持23字节,最终会使用较小的值。
1.2 UUID:蓝牙服务的身份证
UUID在BLE生态中扮演着关键的角色,它就像是每个服务和特征的身份证号码。蓝牙技术联盟(SIG)定义了大量标准UUID(16位短格式),例如:
- 0x180A:设备信息服务
- 0x180F:电池服务
- 0x2A19:电池电量特征
当我们需要开发自定义功能时,就需要使用128位UUID。在nRF5 SDK中配置自定义UUID时,必须预先声明所需的数量:
c复制#define NRF_SDH_BLE_VS_UUID_COUNT 2
// 自定义UUID示例
static ble_uuid128_t m_custom_uuid = {
.uuid128 = {0x01,0x23,0x45,0x67,0x89,0xAB,0xCD,0xEF,
0x01,0x23,0x45,0x67,0x89,0xAB,0xCD,0xEF}
};
void custom_uuid_init(void)
{
ret_code_t err;
ble_uuid_t uuid;
// 添加自定义UUID基址
err = sd_ble_uuid_vs_add(&m_custom_uuid, &uuid.type);
APP_ERROR_CHECK(err);
uuid.uuid = 0x1234; // 16位短UUID
}
常见错误是低估了自定义UUID的数量需求。假设你的设备需要提供3个自定义服务,每个服务有3个特征,那么至少需要配置NRF_SDH_BLE_VS_UUID_COUNT为6(服务+特征),否则初始化时会返回NRF_ERROR_NO_MEM。
1.3 GATTS:服务端的实现机制
GATT Server(GATTS)是BLE设备提供服务的关键组件。它维护着一个属性表(Attribute Table),这个表实际上是一个数据结构数组,包含了所有服务、特征和描述符的定义。在nRF5 SDK中,我们需要特别关注两个参数:
c复制#define NRF_SDH_BLE_GATTS_ATTR_TAB_SIZE 2048
#define NRF_SDH_BLE_SERVICE_CHANGED 1
属性表大小的计算需要预估所有服务和特征占用的空间。一个简单的估算方法是:
- 每个服务:约12字节
- 每个特征:约20字节
- 每个描述符:约10字节
假设我们有3个服务,每个服务有5个特征,其中2个特征有描述符,那么总大小约为:
3×12 + 3×5×20 + 3×2×10 = 36 + 300 + 60 = 396字节
实际开发中建议至少预留2-3倍的余量,因为协议栈内部也会占用部分空间。
服务变更特性(Service Changed)对于支持动态服务的设备非常重要。当设备运行时添加或删除服务时,这个特性会通知客户端需要重新发现服务。但在资源受限的设备上,如果确定服务不会动态变化,可以禁用此特性以节省资源。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 服务与任务的本质区别
2.1 从系统架构角度理解
用工具箱的比喻确实很形象,但作为开发者我们需要更深入的理解。服务(Service)本质上是数据的静态描述,它定义了:
- 有哪些数据可供访问(特征)
- 这些数据的类型(UUID)
- 如何访问这些数据(权限属性)
而任务(Task)是动态的执行单元,它负责:
- 响应外部事件(如连接、断开、写入请求)
- 处理数据(如传感器采样、数据处理)
- 管理状态机(如连接状态、设备模式)
在FreeRTOS环境下的典型实现结构:
c复制// 服务定义(静态)
static ble_cus_t m_custom_service; // 自定义服务结构体
// 任务函数(动态)
static void ble_task(void *arg)
{
for (;;) {
BaseType_t err;
uint32_t evt;
// 等待蓝牙事件
err = xQueueReceive(m_ble_queue, &evt, portMAX_D
