认识BLE 5协议栈 —— 通用属性规范层

通用属性规范GATT(Generic Attribute Profile)将ATT层定义的属性打包成不同的属性实体,包括服务项、特征项和描述符,这些属性实体组合在一起组成规范,即GATT规范。GATT规范是服务项的集合,服务项是特征项的集合,特征项携带了属性参数和数据,描述符协助特征项描述特征值的形式和功能。

GATT层按照命令的传输方向将设备分成GATT客户端和GATT服务端。客户端发起命令,服务端发出数据。GATT规范定义了客户端设备发现服务端设备的服务项的方法,建立连接以后,客户端设备可以通过发现方法检索服务端设备的GATT服务项和特征项,进而发送命了或数据。

服务端向客户端发送数据以通知和指示的形式发送,客户端收到指示信息需要返回确认信息。

服务端可以向客户端发送通知和指示,客户端按需返回响应。

1. 属性

1.1 GATT角色

客户端:设备发起命令、请求并接受响应、通知和指示。

服务端:设备接收命令、 请求并发出响应、通知和指示。

设备可以同时属于客户端和服务端。

GATT角色与执行过程相关,它不与设备绑定。设备在执行一个过程时,根据发起命令或接收命令而决定它是服务端还是客户端,该过程结束后就释放GATT角色。

GATT角色不与链路层的主机和从机角色绑定。一个链路层的主机,通常担任GATT客户端角色,也可以担任GATT服务端角色。

1.2 属性PDU

属性实体PDU如下:

字段 Attribute Handle Attribute Type Attribute Value Attribute Permissions
长度 2 octets 2 or 16 octets variable implementation specific

一个属性包含四个字段:属性句柄、属性类型、属性值和属性权限。

属性句柄用于指定具体的属性。属性句柄有效范围为0x0000-0xFFFF,属性句柄按步进1的增序排列,但有时可能会出现空缺。

属性类型为2字节、4字节或16字节的UUID。 如果是4字节UUID,在封装成属性PDU时根据蓝牙基础UUID转换成16字节标准UUID。

属性值字段包含了属性的具体数据。

属性权限决定了属性是否可读或可写。

1.3 属性协议PDU

两个设备属性层之间根据属性协议传输数据,属性协议包括几种类型:命令、请求、响应、通知、指示和确认。

属性协议PDU如下:

字段 Opcode Attribute Parameters Authentication Signature
长度 1 octet variable 12 octets

操作码Opcode决定了该PDU的操作过程类型。另外,操作码中包含一个认证标志位。

属性参数中包含了命令或请求的参数,或响应的数据。

最后字段的认证签名为可选字段,仅用于带签名的写操作,当操作码的认证标志位为1,则需要认证签名字段,否则不需要改字段。

1.4 属性缓存

客户端与服务端建立连接后,执行发现过程,以获取服务端所携带的全部属性。属性缓存功能用于保存服务端设备的属性句柄,使下一次重新连接时无需执行发现过程。

一般情况下,服务端设备的属性不会改变,但是执行固件升级则可以改变设备的属性。

如果改变设备的属性,将从Service Changed characteristic发出一个指示PDU,告知客户端设备服务端设备的属性发生了改变。该指示PDU中包含了发生改变的属性句柄范围,客户端设备收到该指示,重新执行发现过程,获取更新后的服务端设备的属性句柄。

如果设备的属性确定不能发生改变,则无需增加Service Changed characteristic属性。

如果两端设备完成绑定,则属性缓存信息一直有效,直到收到了Service Changed characteristic发出的指示。如果在服务端的属性在断开后发生了改变,则服务端在下次重连时候发送指示给客户端设备重新缓存属性句柄。

1.5 属性分组

GATT定义了三种属性分组:主要服务、次要服务和特征项。

一个属性分组包括声明和定义。

主要服务和次要服务可以使用“按组类型读取”请求获得,特征项不可以。

1.6 属性结构

蓝牙协议中包含了许多种GATT规范,每个规范适配一种用户案例,比如FindMe规范适配查找物件的场景,心率传感器规范适配心率测量场景。

每个规范均中均有若干服务项和特征项,服务项和特征项都属于属性实体,它们携带了通信中传输的数据。

服务项分为主要服务和次要服务,主要服务可以引用(Include)另一个主要服务或次要服务,客户端设备可以通过“主要服务发现过程”获取主要服务信息。

特征项包括一个声明、配置、数据和描述符。描述符用于描述特征项的数据如何被访问和展示。

规范、服务项和特征项之间有明确的包含关系,一个GATT规范中可以包括多个服务项,一个服务项中可以包括多个特征项。

GATT的规范结构框图如下:

GATT_Profile_Structure

2. 属性类型

属性的类型由UUID表示,协议栈预留了一些16-bit的UUID来表示常用的属性类型。

2.1 服务项

服务项必须包含一个服务项声明,可选地包含多个其他服务项和特征项。所包含的其他服务项和特征项均是该服务项的一部分。

服务项的声明格式如下:

GATT_Service_Declaration

服务项可以是主要服务项(UUID=0x2800)或次要服务项(UUID=0x2801)。

主要服务项可以独立使用,次要服务项一定要被其他服务项包含引用。

协议栈文档中对次要服务项的使用场景解释有限,在绝大多数情况下均可以不使用次要服务项,仅使用主要服务。

2.2 包含

包含现了一个引用机制,比如需要扩展一个现有的服务项,可以在新的服务项中引用该服务项。

假如服务项中包含了其他服务项,则需要加入包含的声明(UUID=0x2802)。

协议栈文档中对包含的使用场景解释有限,在绝大多数情况下均可以不使用包含功能。

2.2 特征项

特征项是GATT数据的载体。

特征项包括:特征项的声明(UUID=0x2803),特征值的声明,以及若干描述符。特征值也是一个属性,它的句柄和UUID在特征项的声明中给出。

特征项始于该特征项的声明,结束语下一个特征项的声明。

特征项的声明数据格式如下:

GATT_Characteristic_Format

其中属性值字段包括了特征值功能特性(Characteristic Properties),特征值的属性句柄和特征值的UUID。

特征值功能特性如下表所示:

特征值功能 描述
Broadcast 0x01 允许广播该特征值
Read 0x02 允许读该特征值
Write Without Response 0x04 允许写该特征值,不需要Response
Write 0x08 允许写该特征值,需要Response
Notify 0x10 允许该特征值发送通知
Indicate 0x20 允许该特征值发送指示
Authenticated Signed Writes 0x40 允许带认证签名的写该特征值
Extended Properties 0x80 扩展特性

其中,Broadcase(0x01)、Notify(0x10)和Indicate(0x20)要求该特征值具有服务端特征项配置描述符(CCCD)。

特征项的声明中属性字段的特征值UUID跟特征值的声明中的UUID一致。

特征值的声明中包含了特征值所携带的数据内容,其格式如下:

GATT_Characteristic_Value_Format

2.3 描述符

描述符也是一种属性,它是特征项的一部分,用以提供特征值的额外信息。协议栈定义了6种不同的描述符,如下:

属性类型 UUID 描述
«Characteristic Extended Properties» 0x2900 特征项的扩展描述符
«Characteristic User Description» 0x2901 特征项的用户描述符
«Client Characteristic Configuration» 0x2902 客户端特征项配置描述符
«Server Characteristic Configuration» 0x2903 服务端特征项配置描述符
«Characteristic Format» 0x2904 特征项数据格式描述符
«Characteristic Aggregate Format» 0x2905 聚合特征项数据格式描述符

0x2900 扩展性描述符,用于Reliable Write和Writable Auxiliaries这两类写属性。

0x2901 用户描述符,用于给出该特征值的文字描述。

0x2902 客户端特征项配置描述符,简称为CCCD,客户端设备通过一个标志参数,设置该特征值能否发送通知和指示。如果该标志参数为0x0001,表示该特征值允许发送通知;如果该标志参数为0x0002,表示该特征值允许发送指示。如果该标志参数为0x0000,表示该特征值不能发送通知和指示。

每个特征项最多能包含一个CCCD,对于具有Broadcast、Notify和Indicate功能的特征项,必须拥有一个CCCD。在两个建立了绑定的设备中,断开连接不会丢失CCCD信息。

0x2903 服务端特征项配置描述符,服务端设备通过一个标志参数,设置该特征值是否在广播中发出。如果该标志参数为0x0001,则广播消息中应该包含该特征值;如果该标志参数为0x0000,则广播消息中不包含该特征值。

0x2904 特征值格式描述符,用于提供特征值的数据格式。可选的数据类型包括:Boolean、1/4字节、1/2字节、1字节、2字节、3字节、4字节、8字节、16字节、带符号整数、无符号整数、浮点数、字符串、结构体等。还可以指定数据的指数、单位、名字空间、描述信息等。

0x2905 聚合特征项格式描述符,专用于聚合特征项。所谓聚合特征值,是指多个特征值共同组合成一个数值,每个特征值仅是该聚合数值的一部分。

3. GATT功能

GATT规范实现了以下功能:

  1. Server Configuration
  2. Primary Service Discovery
  3. Relationship Discovery
  4. Characteristic Discovery
  5. Characteristic Descriptor Discovery
  6. Reading a Characteristic Value
  7. Writing a Characteristic Value
  8. Notification of a Characteristic Value
  9. Indication of a Characteristic Value
  10. Reading a Characteristic Descriptor
  11. Writing a Characteristic Descriptor

这些功能利用了“深入BLE协议栈 —— 属性协议”中的属性协议PDU一节中的多种读写属性PDU。

下面具体分析。

3.1 服务端配置

该功能呢包含一个子功能:交换两端设备的ATT_MTU。

客户端设备发送Exchange MTU Request,其中包含了该设备的ATT_MTU,服务端设备返回Exchange MTU Response,其中包含了该设备的ATT_MTU,取二者的较小值作为协商的ATT_MTU值。

3.2 发现主要服务项

该功能包含两个子功能:发现全部主要服务项,按UUID发现主要服务项。

发现全部主要服务项

该功能向服务端设备发送Read By Group Type Request,起始句柄为0x0001,结束句柄为0xFFFF,属性类型为0x2800(主要属性的UUID),查找全部符合条件的首要服务项。

服务端返回Read By Group Type Response,响应中包含多个属性信息组成的列表,单个属性信息包含三个参数:元素长度、属性组首尾句柄、属性的UUID。

因为该列表长度不能超过一个属性层的PDU长度,所以需要多次执行请求和响应,直到服务端设备返回“未找到属性项”或到达结束句柄,才能获取全部的主要服务项,如下图所示:

GATT_Discover_All_Primary_Services

观察上图,客户端第一次发起请求,查找主要服务项,首末句柄分别是0x0001和0xFFFF。

服务端返回响应中包含三个元素,每个元素代表一个首要服务项。每个元素的长度为0x06。第一个首要服务项的属性句柄为0x0001,类型为UUID1,末尾句柄为0x000F。第二个服务项的属性句柄为0x0010,类型为UUID2,末尾句柄为0x0017。第三个服务项的属性句柄为0x0100,类型为UUID3,末尾句柄为0x01FF。

客户端发起第二次请求,起始句柄设为0x2000。

服务端返回响应中仍然包含三个元素,每个元素的长度为0x06。三个元素分别表示三个首要服务项,其UUID分别为UUID4、UUID5和UUID6,UUID6的末尾句柄为0x04FF。

客户端接着发起第三次请求,起始句柄设为0x0500。

服务端返回错误,错误原因是未找到属性。客户端根据该错误原因,判断已经获取服务端设备的全部主要服务项。

按UUID发现主要服务项

该功能向服务端设备发送Read By Group Type Request,起始句柄为0x0001,结束句柄为0xFFFF,属性类型为0x2800(主要属性的UUID),指定的UUID为xxxx,查找全部符合条件的主要服务项。

具体的操作步骤与“发现全部主要服务”一致。

通常具有指定UUID的服务项仅有一个。

3.3 发现关系

该功能呢包含一个子功能:查找包含的服务项。

该功能向服务端设备发送Read By Type Request,起始句柄为0x0001,结束句柄为0xFFFF,属性类型为0x2802(包含的声明UUID),查找全部符合条件的被包含服务项。

服务端返回响应,包含了满足条件的服务项的句柄和属性值。

3.4 发现特征项

该功能包含两个子功能:发现服务项下的全部特征项,按UUID发现特征项。

发现服务项下的全部特征项

该功能向服务端设备发送Read By Type Request,设置已知的服务项首末句柄,属性类型为0x2803(特征项声明的UUID),查找全部符合条件的特征项。

服务端返回Read By Type Response,响应中包含多个“属性句柄 – 值”元素组成的列表。单个元素包含三个参数:元素长度、特征项声明的句柄和特征值参数。特征值参数包括特征值的功能特性、特征值句柄和UUID。

因为该列表长度不能超过一个属性层的PDU长度,所以需要多次执行请求和响应,直到服务端设备返回“未找到属性项”或到达结束句柄,才能获取全部的特征项,如下图所示:

GATT_Discovery_Characteristic

观察上图,客户端第一次发起请求,查找特征项,首末句柄分别是0x0200和0x0214。

服务端返回响应中包含两个元素,每个元素代表一个特征项。每个元素的长度为0x07。第一个特征项的声明句柄为0x0203,特征值的功能特性为0x02,即具有Read功能,特征值的句柄为0x0204,特征值的UUID为UUID1。第二个特征项的声明句柄为0x0210,特征值的功能特性为0x02,即具有Read功能,特征值的句柄为0x0212,特征值的UUID为UUID2。

由于每个元素的长度为7,表明该两个特征值的UUID均是2字节UUID,如果是16字节UUID,则每个元素的长度应该为0x15。

按UUID发现特征项

该功能根据已知的特征项UUID和首末句柄范围,查找满足条件的 特征项。

具体与“发现服务项下的全部特征项”完全一致。

3.5 发现描述符

该功能包含一个子功能:发现全部描述符。

该功能向服务端设备发送Find Information Request,设置已知的特征项首末句柄,查找全部符合条件的描述符。

服务端返回Find Information Response,响应中包含多个“属性句柄 – 值”元素组成的列表。单个元素包含三个参数:UUID格式、特征值的句柄和描述符的UUID。如果UUID格式参数等于1,表示描述符的UUID为2字节UUID,如果等于2,表示描述符的UUID为16字节UUID。

因为该列表长度不能超过一个属性层的PDU长度,所以需要多次执行请求和响应,直到服务端设备返回“未找到属性项”或到达结束句柄,才能获取全部的描述符,如下图所示:

GATT_Discovery_Descriptor

观察上图,服务端的响应数据第一个参数0x01表示UUID1和UUID2均为2字节UUID,第二个参数0x0205表示该描述符上级的特征值的句柄。

3.6 读特征值

该功能包含四个子功能:读特征值,按UUID读特征值,读长包特征值,读多个特征值。

读特征值

客户端已知特征值句柄,向服务端发送Read Request读取该句柄的特征值。

服务端返回指定句柄的特征值。该特征值长度应小于等于(ATT_MTU-1),如果大于该限制,则仅返回前(ATT_MTU-1)个数据。

下图为一次读取过程:

GATT_Read_Request

按UUID读特征值

客户端已知特征值的UUID,不知道其句柄,向服务端发送Read By Type Request读取该特征值。

具体操作与“按UUID发现特征项”一致。

读长包特征值

客户端已知特征值的句柄,但是特征值的长度大于(ATT_MTU-1),向服务端发送Read Request以读取前(ATT_MTU-1)个字节,然后发送Read Blob Request并设置合适的偏移量,以读取随后的(ATT_MTU-1)个字节,重复执行Read Blob Request直到服务端的Read Blob Response内容小于(ATT_MTU-1),表明该特征值完全被读取。

具体步骤如下:

GATT_Read_Blob_Request

读多个特征值

客户端已知多个特征值的句柄,向服务端发送Read Multiple Request,参数为多个特征值句柄。

服务端返回Read Multiple Response,包含了多个指定的特征值数据。

3.7 写特征值

该功能包含五个子功能:写命令,带签名的写命令,写请求,写长包请求,可靠的写请求。

写命令

客户端已知特征值句柄,向服务端发送Write Command,写入指定数据。

数据长度不能超过(ATT_MTU-3)字节,如果超过,仅写入前(ATT_MTU-3)个字节。

该命令无需服务端返回响应。

带签名的写命令

客户端已知特征值句柄,且链接没有经过认证,向服务端发送Write Command,并设置签名认证标志位,实现带签名的写命令。

数据长度不能超过(ATT_MTU-3-12)字节,其中12表示认证签名的长度,如果超过,仅写入前(ATT_MTU-3-12)个字节。

该命令无需服务端返回响应。

写请求

客户端已知特征值句柄,向服务端发送Write Request,写入指定数据。

数据长度不能超过(ATT_MTU-3)字节,如果超过,仅写入前(ATT_MTU-3)个字节。

该命令需要服务端返回响应Write Response。

写长包请求

客户端已知特征值句柄,但待写入数据长度过长,向服务端发送Prepare Write Request,设置适当的偏移量,将数据发送至服务端缓存起来,数据发送完毕后,项服务端发送Execute Write Request执行写请求。

待写数据总长度不受限制,但是分步发送数据每次数据长度不得超过(ATT_MTU-3)。

两种请求均需要对应的服务端响应。

一个写长包请求流程如下:

GATT_Long_Write_Request

可靠的写请求

客户端已知特征值句柄,希望一次性写入多字节的数据,或者要求数据的每个字节都必须被安全写入服务端设备,向服务端发送Prepare Write Request,偏移量永远等于0,一次性只发送一个数据,带多字节数据缓存完毕,再发送Execute Write Request执行写请求。

具体的操作与“写长包请求”完全一致。

3.8 通知

该功能包含一个子功能:通知。

服务端执行Handle Value Notification,参数为特征值句柄和通知数据,向客户端推送通知。

执行通知前,该特征值需要已经使能通知,并且将通知数据写入该特征值。

该命令无需客户端返回响应。

3.9 指示

该功能包含一个子功能:指示。

服务端执行Handle Value Indication,参数为特征值句柄和指示数据,向客户端推送指示。

执行指示前,该特征值需要已经使能指示,并且将指示数据写入该特征值。

该命令需要客户端返回响应Handle Value Confirmation。

3.10 读写描述符

该功能包含四个子功能:读描述符,读长包描述符,写描述符,写长包描述符。

读描述符与读特征值一致。

读长包描述符与读长包特征值一致。

写描述符与写请求一致。

写长包描述符与写长包请求一致。

4. 与L2CAP层互操作

GATT使用的L2CAP固定信道传输属性数据。

GATT的PDU长度限制ATT_MTU默认大小为23,它与L2CAP层的MTU值保持一致。

(完)