0%

noone的ble协议学习

  • 引入-如何发送一个数据包

    假设有两个设备A和B,A想把自己当前的电量 83% (十六进制下0x53)发送给B。

    那么肯定是越简单越好,比如调用一个简单的API:send(0x53) 。这正是 BLE 协议栈的设计。但其并不是简单的在物理层直接发送 0x53 就可以解决的:

    这样会有很多问题,比如,没有考虑用哪个射频信道来进行传输

    再不改变原API send(0x53) 的情况下,只能对协议栈分层并引入 LL 层,即 Link Layer 链路层。即 send(0x53) 会调用 send_LL(0x53,2402M) 来指定发送的信道频率

    另一个问题则是设备B怎样知道这个数据包是给自己的。为此 BLE 引入了 access address 来指明接收者身份。其中地址 0x8E89BED6 用于表示要发给周边所有设备,即广播

    如果要进行一对一通信,则必须生成一个独特随机的 access address 来标识 AB 两者之间的连接

    • 广播

      在广播的情况下,记A为 Advertiser ,B为 scanner/observer。这种状态下A的LL层API为 send_LL(0x53,2402M,0x8E89BED6)

      而由于设备B可以同时接收到很多设备的广播,A发送的数据包还需要包含其自身的 device address (0xE1022AAB753B) 来确认该广播包来自A,那么API则是 send_LL(0x53,2402M,0x8E89BED6,0xE1022AAB753B)

      LL层还需要检查数据完整性,所以引入 crc24 ,假设结果为 0xB2C78E

      同时为了调制解调电路工作更高效,每个数据包最前面会加上一个字节的 preamble 前导帧,其一般为 0x550xAA

      那么整个包即为(注意是小端模式):

      而这个包还有一些问题:

      • 没有对数据包分类组织,设备B难以找到发送过来的数据 0x53 ,所以要在 access address 后面加入两个字段: LL headerlenth ,分别表示数据包的 LL 类型和payload的长度
      • 设备B开启射频窗口来接收空中数据包的时间未知。如图,case1 时A的数据包在空中传输而B的接收窗口关闭,case2 时B接收窗口打开但A没有发送数据包。这两种情况下通信都失败。只有 case3 的情况下能成功通信。也就是说 LL 层必须定义通信时序
      • 设备如何解析 0x53 这个数据,如何确定其表示的是电量而不是其他的东西。这是 GAP 层(通用访问规范)的工作内容。其引入了 LTV(Length-Type-Value) 结构来定义数据, 例如 020105 ,其中长度为 $2$ ,类型为 $1$ (强制字段,表示广播flag,广播包必须包含该字段),值为 $5$。而由于广播包最大 $31$ 个字节,其能定义的数据类型很有限,例如这里的电量就没有被 GAP 定义。如果仍要发送电量数据就必须使用供应商自定义数据类型 0xFF ,即 04FF5900530x0059(小端) 是供应商ID,其是自定义数据中的强制字段,0x53 就是数据,双方约定其表示电量。

        最终广播包如图:

        而广播具有很多缺点:

      • 无法进行一对一的双向通信

      • 不支持组包和拆包所以无法传输大数据
      • 通信不可靠,效率低。广播信道不能太多否则会导致扫描端效率低,所以 BLE 只使用 37(2402MHz)/38(2426MHz)/39(2480MHz) 三个信道来进行广播和扫描,因此广播不支持调频。由于其一对多导致不支持ACK
      • 由于扫描端不知道设备端何时、哪个频道广播,所以只能拉长扫描窗口时间并扫描三个通道
    • 连接

      让设备A与B连接包含以下几方面:

      • 设备A和设备B对接下来要使用的物理信道达成一致
      • 设备A和设备B双方建立一个共同的时间锚点,也就是说,把双方的时间原点变成同一个点
      • 设备A和设备B两者时钟同步成功,即双方都知道对方什么时候发送数据包什么时候接收数据包

        在AB连接成功后,A称为 Central ,B称为 Peripheral 。A将周期性以 CI(connection interval)连接间隔 为间隔向B发送数据包,同时B周期性以 CI 为间隔打开射频接收窗口以接收设备A的数据包。同时在接收到A数据包 $150$ us 后B切换到发送状态将自己的数据发送给A,同时A切换到接收状态。

        其数据包打包发送情况如下:(注意小端)

      • 开发者调用 send(0x53)

      • GATT 层定义数据类型和分组,这里定义 0x0013 表示电量这一数据类型,那么其将数据打包为 130053
      • ATT 层用来选择具体的通信命令,比如读、写、notify,这里选择 notify 0x1B ,数据包打包为 1B130053
      • L2CAP 层用来指定 CI,但其不体现在数据包中。同时指定逻辑通道编号 0004 (表示ATT命令),并且把ATT数据长度 0x0004 加到包头,数据打包为 040004001B130053
      • LL 层需要指定哪个物理信道进行传输,其不体现在数据包中,然后给此连接分配一个 access address:0x50655DAB ,然后加上 LLheader:0x1E 表示此包为数据包而非控制包,payload length 为整个 L2CAP 字段长度,在最后加上 CRC24 字段,在开头加上前导帧:AA AB5D6550 1E 08 0400 0400 1B 1300 53 D550F6