藍牙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/

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,837評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,196評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,688評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,654評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,456評論 6 406
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,955評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,044評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,195評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,725評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,608評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,802評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,318評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,048評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,422評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,673評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,424評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,762評論 2 372

推薦閱讀更多精彩內容

  • 國家電網公司企業標準(Q/GDW)- 面向對象的用電信息數據交換協議 - 報批稿:20170802 前言: 排版 ...
    庭說閱讀 11,045評論 6 13
  • 藍牙 藍牙的波段為2400-2483.5MHz(包括防護頻帶)。這是全球范圍內無需取得執照(但定不是無管制的)的工...
    蘇永茂閱讀 6,251評論 0 11
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,781評論 18 139
  • 翻翻手機,在網易云音樂上,偶然看到了一個專訪 對于很少涉略日本音樂的我,點開這個專訪欄目,實屬偶然。原想點開瞄上幾...
    時尚鏟屎官閱讀 909評論 1 0
  • 我雖然不知道別人是怎么樣的。 但是我想世界上沒有比我更糟糕的人了。
    入夢安眠閱讀 110評論 0 0