蓝牙BLE芯片Ti CC2541 Notify最大吞吐量解析

关于CC2541吞吐量的最权威测试来自Ti官方的wiki:http://processors.wiki.ti.com/index.php/CC2540_Data_Throughput

这篇文章里面其实一共也没几句话,然后配了一个图和一个源码包。。但信息量还是蛮大的。。

首先解释一下为数不多的这几句话:

This is example modification of CC2540 SimpleBLEPeripheral application to measure user data throughput. Initial testing shows we can reach 5.9K bytes per second. 

This is using a 10ms connection interval and 20 user data bytes sent in GATT notifications. 4 notifications are sent every 7ms, based on an OSAL timer. When sending the notifications, a check is made to see if a buffer is available. 

In all, 1000 notifications are sent. This is 20K bytes, which are sent over 3.35 seconds. 

首先,这个测试吞吐量的例子是用SimpleBLEPeripheral项目修改得到的,从源码来看确实也没做什么大的改动,主要是调小了连接间隔,并添加了定时器来规律的通过Notify发送数据。

测量结果声称达到了5.9K/s,但按照理论计算,10ms的连接间隔说明1秒内触发100次连接事件,而每个连接事件内触发4次射频发送(Tx),每个Tx携带最大的20字节数据,那么这个理论值应该是8K/s,也就是说实测数值低于理论数值。

再看一下代码实现,主要有这么几个步骤:

  1. 主机端将连接间隔设置到主从设备双方可接收的最小限度,在外设初始化的回调里有如下代码:
GAPRole_SetParameter( GAPROLE_PARAM_UPDATE_ENABLE, sizeof( uint8 ), &enable_update_request );
GAPRole_SetParameter( GAPROLE_MIN_CONN_INTERVAL, sizeof( uint16 ), &desired_min_interval );
GAPRole_SetParameter( GAPROLE_MAX_CONN_INTERVAL, sizeof( uint16 ), &desired_max_interval );
GAPRole_SetParameter( GAPROLE_SLAVE_LATENCY, sizeof( uint16 ), &desired_slave_latency );
GAPRole_SetParameter( GAPROLE_TIMEOUT_MULTIPLIER, sizeof( uint16 ), &desired_conn_timeout );

其中GAPROLE_PARAM_UPDATE_ENABLE是要求外设向主设备发起更新连接参数申请,并提交自己期望的连接参数配置。
之后主设备会根据自己的情况决定采纳该配置或拒绝。若采纳该配置,则主设备会调用GAPCentralRole_UpdateLink()函数来通过GAP协议更新连接参数。

  1. 在全局的消息处理回调函数中SimpleBLEPeripheral_ProcessEvent()中,负责处理所有的任务事件,包括计时器、消息以及各种用户定义事件。
if ( events & SYS_EVENT_MSG )
  {
    uint8 *pMsg;

    if ( (pMsg = osal_msg_receive( simpleBLEPeripheral_TaskID )) != NULL )
    {
      simpleBLEPeripheral_ProcessOSALMsg( (osal_event_hdr_t *)pMsg );

      // Release the OSAL message
      VOID osal_msg_deallocate( pMsg );
    }

    // return unprocessed events
    return (events ^ SYS_EVENT_MSG);
  }

static void simpleBLEPeripheral_ProcessOSALMsg( osal_event_hdr_t *pMsg )
{
  switch ( pMsg->event )
  {
  #if defined( CC2540_MINIDK )
    case KEY_CHANGE:
      simpleBLEPeripheral_HandleKeys( ((keyChange_t *)pMsg)->state, ((keyChange_t *)pMsg)->keys );
      break;
  #endif // #if defined( CC2540_MINIDK )
   .....
  }
}

static void simpleBLEPeripheral_HandleKeys( uint8 shift, uint8 keys )
{
  uint8 SK_Keys = 0;

  (void)shift;  // Intentionally unreferenced parameter

  if ( keys & HAL_KEY_SW_1 )
  {
    SK_Keys |= SK_KEY_LEFT;

  
  osal_start_timerEx( simpleBLEPeripheral_TaskID, SBP_BURST_EVT, SBP_BURST_EVT_PERIOD );
  }
  ...
}

如果接收到SYS_EVENT_MSG消息,则检查该消息中的事件是否是KEY_CHANGE事件,如果是则检查被按下的是否是左键(HAL_KEY_SW_1),如果是则开启数据发送定时器。事件名称为SBP_BURST_EVT,定时器延时为SBP_BURST_EVT_PERIOD=7,也就是说7ms后发起事件。

  1. 接收到SBP_BURST_EVT事件后,连续发送四次sendData,其中每次通过GATT_Notification来发送20个字节,根据函数返回状态来决定索引指针是否向前移动。
if ( events & SBP_BURST_EVT )
  {
    // Restart timer
    if ( SBP_BURST_EVT_PERIOD )
    {
      osal_start_timerEx( simpleBLEPeripheral_TaskID, SBP_BURST_EVT, SBP_BURST_EVT_PERIOD );
    }

     sendData();
     sendData();
     sendData();
     sendData();

    //burstData[0] = !burstData[0];
    return (events ^ SBP_BURST_EVT);
  }  
  1. SBP_BURST_EVT_PERIOD设置为7ms是因为其需要比连接间隔稍小一点保证每次连接事件发生的时候,缓冲区里都有足够的数据可以发送。连续调用四次是因为CC2541的发射缓冲区只能存4个数据包的内容长度(4*20=80字节)。这样的话实际上sendData的频率大于GATT_Notification的频率,也就是说生产者速度大于消费者,而缓冲区又极其有限,必然会经常触发缓冲区满的错误,也就是说GATT_Notification返回错误码,因为GATT_Notification的该错误码是标示数据并没有从上位机通过HCI发送到下位机的发送缓冲区,则上位机的应用层必须控制其重发机制。对这个问题在e2e论坛上Ti开发者解释如下:
7ms is just a OSAL timer which is suppose to be less than the connection interval, so we always keep the output buffer full and thereby maximizing the throughput. since the minimum connection we usually recommend is 10ms you could just as well test 9ms OSAL timer. But if you'd like to test minimum possible connection interval of 7.5ms, you'd like to push as many packets (maximum allowed is currently 4) you can between every connection event, so they might be sent during the actual connection interval.

Again, the OSAL timer is just used to put packets in the output buffer, and the buffer is emptied upon the connection event.

这里说明了为什么定时器设置为7ms,也说明了GATT_Notification只是把数据转移到下位机的发射缓冲当中(output buffer)。另外还隐含了一个信息,buffer is emptied upon the connection event。也就是说每个连接事件都会清空缓冲区,所以因为缓冲区最大容纳4个20字节数据包,那么最多对传4次数据,如果缓冲区不满应该就不会传够4次。并且在发射缓冲区数据的过程中应该是对缓冲区加锁的(互斥资源),应用层在这个时候不能往缓冲区里继续添加数据(GATT_Notification返回错误,这个是我猜的,隐约记得在哪里看到过这个说法)。
另外我觉得这里还忽略了一个问题,一次连接事件并不一定能清空缓冲区,因为还涉及到链路层未收到数据或者收到了以后CRC校验失败的情况下的重传问题,应该是确认传输成功再清空一个数据包的字节,那么如果连续传输不成功,很可能在一个连接事件里并不能完成清空缓冲区,虽然是个很小的缓冲区。另外连接事件的window size和window offset是怎么确定的?主机决定并告知从机的?但从机的时钟准确度决定其采用的windows offset的大小,这个是否是固件程序中可设置的?又如何能让主从设备做到该参数互相知晓?

  1. iOS限制每个连接间隔的最大数据对传次数是4,而CC2541恰好也是4,这就正好是可采用的最大发送次数。Android允许发送次数1-11,大部分手机允许最大稳定到7,但受制于CC2541的缓冲区限制,也只能用到4。另外一点不太明确的问题就是,主从设备的连接参数协商里面并没有包含对传次数的参数,那么双方是怎么确认在一个连接间隔里已经达到了最大的传输次数而避免继续开射频发射和接收来做无效的能量消耗??

最后再分析一下那个FTS的截图来解释一下为什么传输速率没有达到理论的10K/s的速率。


FTS的截图

如图2的位置,side1代表主机,side2代表从机。灰色代表空包,蓝色代表数据开始包,深灰色代表数据延续包。而只有writeLongValue才会产生延续包,所以sendData里面单次调用GATT_Notification都是数据开始包。这就解释了下面图中一个side1中包含空包的M->S后面紧跟的是side2中包含数据开始包的S->M。

如图3的位置,说明这个数据包sniffer没有捕获到,虽然漏掉了但他知道这里应该有一个包,所以留出来了空白。另外竖向的每一行代表一个连接事件的时间轴,但横向是为什么把每个连接事件错开的原因我还没想明白。

然后说下结论,我认为wiki中目前版本的代码,跟跑出来这个sniffer截图的代码根本不一致。代码只发了1000个包一共20K字节,但sniffer在3.5秒的范围内673-2523是1850个包。

如图1的位置,可以看到蓝色的先在选中那个区域的平均值大概是120000Bits/s,这个值怎么来的呢?懒得查BPA的软件参考手册,这里我推理一下。
看其下方的select packets显示的是673-2523,一共1850个packets,显示的消耗时间是3.35s。那么计算可知552packets/s,而552个包要达到120000Bits,那么一个包被统计的字节数应该是120000 / 8 / 552 = 27.17,那么说明这里记录的是LL层数据包的payload = 27字节。而实际GATT层单个包20字节的有效数据,所以有效数据传输速率应该是552包/秒 * 20字/包 = 11KB/秒,而不是声称的5.9K/s。

若每个连接事件发4个包,则连接事件数量为552/4=138,而想达到该数值的连接间隔应该是1000/138 = 7.25ms。这里肯定是4个包而不是3个包,因为如果是3,计算出来的连接间隔是5.4ms,是低于BLE规范要求的最小值7.5ms的。但是从另外从下方的抓包图来看,其每个连接事件只有3次数据包对传,怀疑可能截图的这部分正好连接不是很稳定所以只发了3个包,具体如何还要看cfa文件。

也就是说,我怀疑sniffer截图里的配置,其采用的是6的连接间隔6*1.25=7.5ms(这里又有一个疑问,最小的连接间隔6来计算,应该是7.5ms,为什么上段中算出来的是7.25ms),而不是wiki上所说的10ms。

这样基本就能解释通了,这个配置的理论传输速率应该是1000/7.5 * 4 * 20 = 10.66K/s,而看sniffer的截图也应该是这个速率,所以5.9K/s这个结论不知道是从哪里来的。如果按照wiki文档里说的10ms连接间隔,每次4个包,其速度也应该是1000 / 10 * 4 * 20 = 8K/s。

参考https://e2e.ti.com/support/wireless_connectivity/bluetooth_low_energy/f/538/t/169928
这个帖子后面有大量有价值的回复
另外http://processors.wiki.ti.com/index.php/Category:BluetoothLE 有海量的值得研究的资源。。

最新修改

已经可以确认,Ti在这个帖子里对CC2541的吞吐量测试,是基于1.3的协议栈进行的。
而在1.4协议栈以后增加了OverlappedProcessing,从而显著的增加了吞吐量,但在Ti的wiki上并没有新的throughput测试内容。
并且OverlappedProcessing的wiki页现在也打不开了,在这里放两个能用时候截的图。

OverlappedProcessing_1.png
OverlappedProcessing_2.png

根据以上的内容可以看出来,其实1.4协议栈引入OverlappedProcessing以后,传输速率已经大幅提升。
用截图上的指标来计算最高传输速率:
脉冲的数量有28个,橙色范围的时间是9ms,看起来整个连接事件的时间是18ms左右,那么连接间隔至少要大于连接事件假设为20ms
max throughput = 1000ms / 20ms * 28packets * 20Byte = 28000Byte/s
也就是说理论可以达到28K/S,当然这是只考虑两个2541对传的情况,如果对端设备是手机,那瓶颈很可能在手机蓝牙芯片的协议栈一端。

另外TI论坛帖子里,言之凿凿的说CC2541 BT4.0最大到305kbps,要做到1Mbps建议采用CC2652R或者CC2640R2F能到1.4mbps。

经实测,iPhone7/iPhoneX采用15ms连接间隔(iOS11以上允许15ms)的情况下,每个连接间隔往packet queue里填充的包数超过8个的话,会导致iPhone端蓝牙连接断开。也就是说与iPhone连接传输的最大参考速率大概在 1000/15 * 8 * 20 = 10.6KB/S左右。

而安卓机型之间的差异比较大,华为mate的机型测试可以稳定达到7KB/S以上,OPPO的某些机型只能4KB/S左右,连接间隔设置的过小就会在传输过程中出现各种诡异现象。

为了达到最高速率,需要监听每个连接事件结束的通知,并且在这个时间去重新填满packets queue,见下图的策略和链接地址。
http://e2e.ti.com/support/wireless-connectivity/bluetooth/f/538/p/353327/1244676#1244676

蓝牙包数据填充方案.png

蓝牙包填充策略.png

但这也不能达到最大,因为packets queue=12+Tx buffer=4,一共也只有16个packet的空间,达不到上面wiki demo中示波器截图的28个packets,所以不能只是一次性填满就不管了,而是在连接事件结束的通知里再加一个低于连接间隔的定时器,定时器触发后再用很短的时间间隔周期性的执行一个填充任务。

再放两个链接,第一个是2540最大5.9K那个wiki的地址。
http://processors.wiki.ti.com/index.php/CC2540_Data_Throughput
第二个是2640号称最高可以超过100KB/S的github文档。
https://github.com/ti-simplelink/ble_examples/blob/ble_examples-2.2/docs/throughput_example.md

其他参考资料:
https://blog.csdn.net/Wendell_Gong/article/details/50386849
https://www.cnblogs.com/jeffkuang/category/1009458.html
下面是LightBlue的官网关于BLE速率的知识库文章,总体来说写的很好,但其中对包数上限的言之凿凿不知道是哪里来的信心
https://punchthrough.com/pt-blog-post/maximizing-ble-throughput-on-ios-and-android/
https://punchthrough.com/pt-blog-post/maximizing-ble-throughput-part-2-use-larger-att-mtu/

推荐阅读更多精彩内容

  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 2,892评论 3 6
  • 蓝牙 蓝牙的波段为2400-2483.5MHz(包括防护频带)。这是全球范围内无需取得执照(但定不是无管制的)的工...
    苏永茂阅读 3,339评论 1 11
  • pdf下载地址:Java面试宝典 第一章内容介绍 20 第二章JavaSE基础 21 一、Java面向对象 21 ...
    王震阳阅读 70,120评论 26 501
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 67,679评论 12 114
  • 翻翻手机,在网易云音乐上,偶然看到了一个专访 对于很少涉略日本音乐的我,点开这个专访栏目,实属偶然。原想点开瞄上几...
    时尚铲屎官阅读 140评论 1 0