nRF52832 — 提高藍牙BLE的數據傳輸速率

文章出處:可不可以不取名(轉載文章,如有不妥,通知後我會立即刪除)

講解2點:

       爲什麼 nordic的4.0協議棧中ble只能發送20字節的應用負載數據。

       大量數據發送時如何提高發送速率

1:爲何上層應用負載每次最多20字節

首先了解 4.0中鏈路層的包格式如下:

       PDU即協議數據單元,即鏈路層的負載數據。應用層用戶發送的數據就是在這裏面,但是並不全是用戶數據。

Ble有分廣播態和連接態。 所以上面的這個鏈路層幀可能是廣播數據也可能是連接後的數據。 所以就有兩種情況,一種爲 廣播通道中的PDU,另一種爲數據通道中的PDU。我們主要討論的是連接態下的數據通道中的數據幀,這裏廣播通道下簡單介紹下。

廣播態下,廣播幀中的PDU如下圖所示,包含2字節的頭,其後的payload即爲廣播數據,比如通常我們設置的 設備名,廠商自定義數據等都在這裏面,廣播數據肯定包含設備的地址,所以payload中的前6字節爲設備地址


再看下 連接態下 數據通道中 鏈路層幀中的PDU組成,與廣播通道幀中的PDU類似,也是有2字節頭,隨後爲payload即鏈路層的真實負載數據。

MIC爲4字節,只有在鏈路加密的情況下才會存在,爲 消息完整性校驗,防止消息被篡改。

PS:加密鏈路中的空包不會存在MIC


協議都是分層的,ble也一樣,那麼鏈路層的負載數據payload即爲上層協議的數據幀,鏈路層的上一層協議爲L2CAP,而L2CAP的幀格式如下如所示前4字節分別爲長度和信道值。

PS:如果上圖Header中的LLID爲3,則其後的負載爲鏈路層控制報文而不是L2CAP層幀,這裏不介紹。


同樣,L2CAP層的負載數據information payload爲上層協議的數據幀,對於傳輸用戶數據而言,設備作爲主機時用write寫數據到從機,設備作爲從機是用notify或indication 發送數據給主機,這時候l2CAP層的負載中包含的就是 上層ATT的協議幀。

這裏討論的是用戶發送數據爲什麼是限制爲最大20字節,所以瞭解下ATT協議中的write,notify,indication的命令格式就可以了。


如上圖所示,包含1字節opcode用來指示 write,notify,indication。2字節handle爲句柄用來標識是操作哪個特性值的。 之後就是真正用戶發送的數據了。

所以最終限制能一次發送多少數據就是這個 ATT_MTU 爲多少了。

規範中默認這個MTU最小爲23字節,這個值其實是可以通過命令來協商的,而nordic的4.0協議棧中默認只支持默認值即23,所以也就限制了最終上層一次發送的數據限制在 20字節。

nrf52832使用的最新的s132協議棧中已經開始支持MTU的協商了,這樣就可以一次傳輸更多數據了。

綜上,鏈路層的PDU中的數據如下圖所示:


PS:回顧最開始的鏈路層 幀結構可以看到 PDU中允許的長度爲2-39,即最少有2字節頭,有效負載數據最多37字節。

但是從ATT協議往鏈路層看,ATT 最多20字節用戶數據,加上3字節頭,加上L2CAP的4字節頭,也就27字節,爲什麼會有差額10字節?

原因在於 PDU因爲分情況有廣播通道的PDU,和數據通道的PDU,PDU除了2字節頭,有效負載爲37字節,在廣播數據中PDU需要包含6字節的廣播地址,其他廣播數據也就只有31字節了。但是數據通道中並不需要,但是爲了簡單起見,也就限制了數據通道中有效負載數據最多31字節。 另一方面 如果鏈路加密了,數據通道中的PDU,最後會包含4字節的MIC,那麼加密的有效負載數據就變成27字節了,這裏又爲了方便起見,也就讓即使不加密的鏈路發送的有效負載數據也爲27。這就是差額的原因。

第二個問題:既然每次發送數據最多才20字節,如果發送較多數據時如何提高發送速率?

以 ble_app_uart例子來說明,該例子中設備作爲從機,已經實現了一個以notify方式向手機發送數據的函數。這裏就直接利用這個發送函數。


一些簡單的應用中通常可能很久才發送一次數據,數據的發送量也沒有達到20字節,這種情況下 直接調用該函數發送數據就可以了。

另一種情況,發送的數據比較多,但是對發送的速率並沒有要求。這種情況最簡單的可以直接用一個循環發送就可以了

While(沒發送完){

       ble_nus_string_send(數據);

       delay_ms(n);

}

通常發送的數據越多delay_ms延遲的時間要越久一點,這個要自己試驗。通常只能用在一些少量數據比如一兩百字節。

更規範的做法應該利用協議棧中的 發送完成事件 BLE_EVT_TX_COMPLETE,這個事件是在底層發送數據完成後由協議棧發上拋給應用層的。

那麼就可以利用這個事件,首先發送20字節,當底層發送完成後上層收到這個 發送完成事件後再發送後續數據。

這裏做一個簡單的實現

點擊(此處)摺疊或打開

  1. 定義了一個關於發送的結構體

  2. typedef struct blk_send_msg_tag{
  3.     uint32_t start;    //發送的起始偏移
  4.     uint32_t max_len;    //待發送數據的總長度
  5.     uint8_t    *pdata;
  6. }blk_send_msg;

  7. //定義一個全局變量
  8. blk_send_msg g_send_msg;

  9. //發送數據時就調用這個函數,傳入buff以及長度
  10. uint32_t ble_send_data(uint8_t *pdata, uint32_t len){
  11.     if ( NULL == pdata || len <=){
  12.         return NRF_ERROR_INVALID_PARAM;
  13.     }
  14.     
  15.     uint8_t temp_len;
  16.     uint32_t err_code;
  17.     g_send_msg.start = 0;
  18.     g_send_msg.max_len = len;
  19.     g_send_msg.pdata = pdata;
  20.     
  21.     temp_len = len>20?20:len;
  22.     err_code = ble_nus_string_send(&m_nus, pdata, temp_len);
  23.     if ( NRF_SUCCESS == err_code ){
  24.         g_send_msg.start += temp_len;    //發送成功才更新起始偏移
  25.     }
  26.     return err_code;
  27. }


  28. //這個函數完成後續數據的發送,將其放在收到 BLE_EVT_TX_COMPLETE事件處理中

  29. uint32_t ble_send_more_data(){
  30.     uint32_t err_code;
  31.     uint32_t dif_value;
  32.     //計算還有多少數據沒發送
  33.     dif_value = g_send_msg.max_len-g_send_msg.start;
  34.     if ( 0 == dif_value || NULL == g_send_msg.pdata ){
  35.         return NRF_SUCCESS;    //後續數據全發送完了
  36.     }
  37.     
  38.     uint8_t temp_len;
  39.     temp_len = dif_value>20?20:dif_value;
  40.     err_code = ble_nus_string_send(&m_nus,                              
  41.                                 g_send_msg.pdata+g_send_msg.start, temp_len);
  42.     if ( NRF_SUCCESS == err_code ){
  43.         g_send_msg.start += temp_len;
  44.     }
  45.     return err_code;
  46. }

修改一下Main函數,定義一個全局的buff用來放數據並初始化,將for循環中的power_manager去掉改成通過一個按鍵來啓動發送 buff中的數據。

燒寫程序,手機連接上後使能 特性值的notify功能,然後按鍵便會受到設備發給手機的100字節數據

 

啓動發送後只會發送前20字節,當這20字節發送完成後會收到BLE_EVT_TX_COMPLETE事件,在該事件處理中添加剩餘數據的發送

直接在on_ble_evt事件處理函數中添加一下這個事件的處理


上面的實現只是針對 對發送速率沒要求的情況,這裏抓包看一下實際的交互過程。

部分截圖如下


因爲每個連接事件到來時都會切換到另一個通道(頻率)上進行數據傳輸,而在這個連接事件持續時間中的數據交互都是在同一個通道上。
即每個連接事件到來時都會切換通道,但是一個連接事件內部的通信都始終在那個通道上

所以由通道號可以區分出來這裏基本上是兩個連接事件纔會發送一次數據,這樣效率就很低,因爲實際的底層基帶發送是很快的1Mbit/s, 也就是1us發送1bit。理論上簡單算一下,這裏就直接以鏈路層最長包來算,1+4+39+3 也就只有47字節,


47*8也就是發送一包的實際時間不足1ms,算上基帶啓動發送以及協議棧的一些處理也應該是幾ms的事,那麼一個連接間隔除了最前面的幾毫秒發送了一下數據,之後這次連接間隔就關了。等之後的連接間隔到來纔會繼續發送後續數據。那麼發送效率就很低。

如果提高每個連接間隔中發送的數據包的數量,那麼就可以提高發送速率。

前面的方法是調用每次發送函數後等待 完成事件,實際上,這個協議棧的底層應該有一個自己的發送buff,能存放一定數據,我們調用發送數據後協議棧會將數據放到這個buff中,最終再發送這個buff中的數據。

如果能在下個連接事件到來前竟可能的將多的數據放入這個協議棧中的buff裏,那麼他下次連接間隔發送的數據就變多了。

Sdk其實提供了這種方法,只不過比較隱晦。

我們利用的發送函數ble_nus_string_send,實際是調用了sd_ble_gatts_hvx 這個協議棧api函數,這個函數有一個返回值NRF_ERROR_BUSY 表示忙,正在處理。

這應該是表示開始發送了。

那麼就可以直接重複調用這個ble_nus_string_send 函數直到其返回NRF_ERROR_BUSY 錯誤,表示已經開始發送了,不能再處理你提交的數據。

另外,協議棧中的buff肯定是有限的,如果我們調用這個發送函數的時候,即將到來下一個連接事件,那麼buff肯定填不滿,最終出現的錯誤是NRF_ERROR_BUSY,表示已經開始發送了,你不能再填了。

但是如果調用的時候恰好離下一次連接事件到來還比較久,那麼就會出現將協議棧中的buff填滿了,從而出現BLE_ERROR_NO_TX_BUFFERS 這個錯誤。

這裏只是介紹這兩種錯誤,實際實現中可以不需要去判斷是不是這些錯誤,因爲發送是分包一點一點發送的,我們可以直接就判斷 ble_nus_string_send函數調用是不是返回NRF_SUCCESS,如果是才 更新 發送偏移,並且繼續循環調用該函數以填更多數據到協議棧buff中,如果返回值不正確,那麼直接跳出,不更新發送偏移就可以了,而並不用去區分是BUSY錯誤還是NO BUFF錯誤。

點擊(此處)摺疊或打開


  1. 如下所示代碼,實現一個新的發送子功能函數

  2. uint32_t send_data(void){
  3.     
  4.     uint8_t temp_len;
  5.     uint32_t dif_value;
  6.     uint32_t err_code = NRF_SUCCESS;
  7.     uint8_t *pdata = g_send_msg.pdata;
  8.     uint32_t start = g_send_msg.start;
  9.     uint32_t max_len = g_send_msg.max_len;
  10.     
  11. //循環發送,只要返回值正確就反覆調用發送函數
  12.     do{
  13.         dif_value = max_len - start;
  14.         temp_len = dif_value>20?20:dif_value;

  15.         err_code = ble_nus_string_send(&m_nus, pdata+start, temp_len);
  16.         if ( NRF_SUCCESS == err_code ){
  17.         //只有返回值正確才更新偏移,
  18.         //不需要考慮是BUSY錯誤還是NO BUFF錯誤
  19.             start += temp_len;    
  20.         }
  21.     //調用函數成功並且還有數據那就繼續調用    
  22.     }while ( (NRF_SUCCESS == err_code) && (max_len-start)>);
  23.     
  24.     g_send_msg.start = start;
  25.     return err_code;
  26. }


  27. 修改之前的實現的ble_send_data和ble_send_more_data函數,他們都直接調用上面的子函數

  28. uint32_t ble_send_data(uint8_t *pdata, uint32_t len){
  29.     if ( NULL == pdata || len <=){
  30.         return NRF_ERROR_INVALID_PARAM;
  31.     }
  32.     
  33.     uint32_t err_code = NRF_SUCCESS;
  34.     g_send_msg.start = 0;
  35.     g_send_msg.max_len = len;
  36.     g_send_msg.pdata = pdata;

  37.     err_code = send_data();
  38.     
  39.     //返回值應該在外面處理,返回值如果是SUCCESS,
  40.     //或者NRF_ERROR_BUSY或者BLE_ERROR_NO_TX_BUFFERS都應該認爲正確
  41.     //因爲這兩種錯誤雖然發生了,但是我們並沒有去更新start偏移,所以以後
  42.     //的發送還是會正確進行。
  43.     //其他情況上層應該根據情況處理
  44.     return err_code;    
  45. }


  46. uint32_t ble_send_more_data(){
  47.     uint32_t err_code;
  48.     uint32_t dif_value;

  49.     dif_value = g_send_msg.max_len-g_send_msg.start;
  50.     if ( 0 == dif_value || NULL == g_send_msg.pdata ){
  51.         return NRF_SUCCESS;    //後續數據全發送完了直接返回
  52.     }
  53.     
  54.     err_code = send_data();
  55.     
  56.     return err_code;
  57. }


最後再修改一下main函數,發送500個字節


燒寫程序後運行代碼,我們再次抓一下空中包看看是否每個連接間隔中發送了多個數據包

由通道號可以看到現在一個連接事件中發送了多個包(最多6個)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章