4. SDP与GATT:经典蓝牙的“名片”与BLE的“数据通道”

各位同学,今天我们聊点实在的。SDP和GATT,这两个协议在车载蓝牙开发里,几乎是天天打交道的角色。

我个人习惯把SDP比作“名片交换”,而GATT则是“数据管道”。一个负责告诉别人“我能干什么”,另一个负责“咱们怎么干活”。

嗯,咱们先看SDP。

4.1 SDP:经典蓝牙的服务发现机制

在经典蓝牙(BR/EDR)的世界里,设备连接后第一件事是什么?不是传数据,而是互相问一句:“兄弟,你会啥?”

这就是SDP干的事。它全称Service Discovery Protocol,说白了就是一本服务黄页。

SDP的核心逻辑

  • 服务记录:每个服务都有一个唯一的UUID。比如HFP(免提)是0x111E,A2DP(音频流)是0x110B。
  • 属性列表:服务名称、协议栈信息、版本号等,都挂在服务记录下面。
  • 查询机制:客户端发一个SDP_ServiceSearchRequest,服务端回一个SDP_ServiceSearchResponse。

我在项目中遇到过一个问题:车机连接手机后,明明支持A2DP,但就是不出声。查了半天,发现是手机端的SDP记录里,A2DP的版本号写错了。你想想看,一个字节的差异,整个音频流就建立不起来。

避坑指南:我曾经调试过一个TWS耳机,它同时支持经典蓝牙和BLE。结果经典蓝牙的SDP里,把HFP和A2DP的服务记录顺序搞反了。车机先读到HFP,以为这是个纯通话设备,直接跳过了A2DP。后来我把服务记录按优先级排好序,问题就解决了。

SDP的典型流程

  1. 设备A发起SDP连接(L2CAP信道,PSM=0x0001)。
  2. 设备A发送服务搜索请求,指定UUID(比如0x110B)。
  3. 设备B返回匹配的服务记录句柄。
  4. 设备A根据句柄,请求服务属性(比如协议描述列表)。
  5. 设备B返回属性值,包含RFCOMM通道号、版本号等。

这里有个细节:SDP本身不负责数据传输,它只负责“指路”。真正的音频数据走的是A2DP的媒体通道,而控制指令走的是AVRCP。SDP只是告诉你“A2DP在哪个RFCOMM通道上等着呢”。

个人经验:我建议你在车载蓝牙协议栈里,把SDP的缓存时间设长一点。因为车机每次上电都会重新扫描设备,如果每次都重新查询SDP,会浪费几百毫秒。缓存起来,下次直接读缓存,用户体验会好很多。

4.2 GATT:BLE世界的属性操作

到了BLE(低功耗蓝牙)时代,事情变得不一样了。BLE没有SDP,它用的是GATT(Generic Attribute Profile)。

GATT的核心思想是:一切皆属性。每个属性都有一个UUID,一个值,以及一组权限(读、写、通知、指示)。

GATT的层次结构

  • Profile:最高层,定义了一个应用场景(比如心率监测、电池服务)。
  • Service:一组相关功能的集合。比如电池服务(0x180F)包含电池电量这个特征。
  • Characteristic:一个数据点。比如电池电量百分比,就是一个Characteristic。
  • Descriptor:对Characteristic的描述。比如客户端特征配置描述符(CCCD),用来开启通知。

说白了,GATT就是一个树形结构。你从根节点(Profile)往下找,就能找到你想要的数据。

GATT的三种操作模式

操作 方向 典型场景
Read Client → Server 读取设备名称、序列号
Write Client → Server 设置音量、切换EQ模式
Notify Server → Client 实时推送电池电量、播放状态

我在项目中遇到过一个问题:车机通过BLE读取手机的电量,但每次Read都返回错误。后来发现,手机端的电池服务把Read权限关了,只允许Notify。你想想看,如果不知道这个细节,你可能会怀疑是协议栈的bug。

注意:Notify和Indicate的区别。Notify不需要确认,速度快但可能丢包;Indicate需要确认,可靠但慢。在车载场景下,如果传输的是音量控制指令,我建议用Indicate;如果是实时频谱数据,用Notify更合适。

GATT的MTU协商

默认情况下,BLE的MTU是23字节(其中3字节是协议头,实际有效载荷只有20字节)。但你可以通过MTU协商,把MTU提升到512字节甚至更大。

我记得有一次,客户抱怨车机接收音频元数据(歌名、歌手、专辑封面)太慢。我一看代码,发现MTU还是默认的23字节。一首歌名“Hotel California (Live at the Forum, 1977)”就占了30多个字节,得拆成两次传输。后来我把MTU协商到247字节,一次就传完了。

避坑指南:我曾经调试过一个第三方BLE模块,它声称支持MTU协商,但实际上只支持到128字节。我这边协商到512,它那边直接断连。后来我加了一个降级逻辑:如果协商失败,就回退到默认MTU。嗯,这里要注意,不是所有设备都支持大MTU。

GATT的并发操作

在车载场景下,经常需要同时处理多个GATT操作。比如一边读取设备信息,一边接收电量通知。这时候就要注意操作队列的管理。

我个人习惯的做法是:

  • 建立一个GATT操作队列,按优先级排序。
  • 每个操作设置超时时间(比如5秒)。
  • 如果某个操作超时,直接丢弃,继续下一个。

为什么?因为BLE的链路层是半双工的,同一时间只能有一个操作在进行。如果不加队列,多个操作同时发起,会导致协议栈混乱。

4.3 SDP vs GATT:什么时候用哪个?

很多新手会问:SDP和GATT到底有什么区别?

我简单总结一下:

  • SDP:用于经典蓝牙,只负责服务发现,不负责数据传输。查询一次就够了,后续通信走其他协议。
  • GATT:用于BLE,既负责服务发现,也负责数据传输。整个通信过程都在GATT框架下完成。

在车载音响里,经典蓝牙主要用于音频流(A2DP)和通话(HFP),而BLE则用于遥控(AVRCP over BLE)、OTA升级、设备配置等。

嗯,这里要注意:有些车机同时支持经典蓝牙和BLE。这时候,SDP和GATT会共存。经典蓝牙用SDP发现A2DP和HFP服务,BLE用GATT发现遥控和配置服务。两者互不干扰。

个人经验:我建议你在设计车载蓝牙协议栈时,把SDP和GATT的发现结果合并到一个统一的设备信息结构体里。这样上层应用不用关心底层是经典蓝牙还是BLE,直接读取设备能力即可。我当年就是这么做的,后来换协议栈供应商时,上层代码一行都没改。

好了,这一章的内容就到这里。SDP和GATT是蓝牙协议栈的基石,理解透了,后面的章节会轻松很多。