提高ble資料發送速率__nordic

來源:互聯網
上載者:User

參考 :http://blog.chinaunix.net/uid-28852942-id-5753308.html

講解2點:

       1、為什麼 nordic的4.0協議棧中ble只能發送20位元組的應用負載資料。

       2、大量資料發送時如何提高發送速率

BLE的傳輸速率分析

       根據藍芽BLE協議, 物理層physical layer的傳輸速率是1Mbps,相當於每秒125K位元組。事實上,其只是基準傳輸速率,協議規定BLE不能連續不斷地傳輸資料包,否則就不能稱為低功耗藍芽了。連續傳輸自然會帶來高功耗。所以,藍芽的最高傳輸速率並不由物理層的工作頻率決定的。

在實際的操作過程中,如果主機連線不斷地發送資料包,要麼丟包嚴重要麼串連出現異常而斷開。

在BLE裡面,傳輸速度受其串連參數所影響。串連參數定義如下:

1)串連間隔。藍芽基帶是跳頻工作的,主機和從機會商定多長時間進行跳頻串連,串連上才能進行資料轉送。這個串連和廣播狀態和串連狀態的串連不是一樣的意思。主機在從機廣播時進行串連是應用程式層的主動軟體行為。而跳頻過程中的串連是藍芽基帶協議的規定,完全由硬體控制,對應用程式層透明。明顯,如果這個串連間隔時間越短,那麼傳輸的速度就增大。串連上傳完資料後,藍芽基帶即進入休眠狀態,保證低功耗。其是1.25毫秒一個單位。

2)串連延遲。其是為了低功耗考慮,允許從機在跳頻過程中不理會主機的跳頻指令,繼續睡眠一段時間。而主機不能因為從機睡眠而認為其中斷連線了。其是1.25毫秒一個單位。明顯,這個數值越小,傳輸速度也高。

藍芽BLE協議規定串連參數最小是5,即7.25毫秒;而Android手機規定串連參數最小是8,即10毫秒。iOS規定是16,即20毫秒。

串連參數完全由主機決定,但從機可以發出更新參數申請,主機可以接受也可以拒絕。android手機一部接受,而ios比較嚴格,拒絕的機率比較高。 一般情境,串連參數設定16,即20毫秒,一般的傳輸速率是50* 20 = 1000位元組/每秒。如果每個串連事件傳輸更多的包,可以獲得更高的傳輸速率。



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。這就是差額的原因。





2:既然每次發送資料最多才20位元組,如果發送較多資料時如何提高發送速率。

對發送速率沒要求的情況下簡單的處理方式:
一些簡單的應用中通常很久才發送一次資料,資料的發送量也沒有達到20位元組,這種情況下直接調用該函數發送資料就可以了。
uint32_t ble_nus_string_send(ble_nus_t * p_nus, uint8_t * p_string, uint16_t length);
另一種情況,發送的資料比較多,但是對發送的速率並沒有要求。這種情況最簡單的可以直接用一個迴圈發送就可以了
While(沒發送完){
ble_nus_string_send(資料);
delay_ms(n);
}
通常發送的資料越多delay_ms延遲的時間要越久一點,這個要自己實驗。通常只能用在一些少量資料比如一兩百位元組。

更規範的做法應該利用協議棧中的發送完成事件 BLE_EVT_TX_COMPLETE,這個事件是在底層發送資料完成後由協議棧發上拋給應用程式層的。那麼就可以利用這個事件,首先發送20位元組,當底層發送完成後上層收到這個發送完成事件後再發送後續資料。
這裡做一個簡單的實現:
typedef struct blk_send_msg_tag
{
uint32_t start;    //發送的起始位移
uint32_t max_len;    //待發送資料的總長度
uint8_t  *pdata;
}blk_send_msg;
//定義一個全域變數
blk_send_msg g_send_msg;

//發送資料時就調用這個函數,傳入buff以及長度
uint32_t ble_send_data(uint8_t *pdata, uint32_t len)
{
if (NULL == pdata || len <= 0){ return NRF_ERROR_INVALID_PARAM;}

uint8_t temp_len;
uint32_t err_code;
g_send_msg.start = 0;
g_send_msg.max_len = len;
g_send_msg.pdata = pdata;

temp_len = len>20 ? 20 : len;
err_code = ble_nus_string_send(&m_nus, pdata, temp_len);
if (NRF_SUCCESS == err_code){
g_send_msg.start += temp_len;    //發送成功才更新起始位移
}
return err_code;
}
//這個函數完成後續資料的發送,將其放在收到 BLE_EVT_TX_COMPLETE事件處理中
uint32_t ble_send_more_data()
{
uint32_t err_code;
uint32_t dif_value;
//計算還有多少資料沒發送
dif_value = g_send_msg.max_len - g_send_msg.start;
if (0 == dif_value || NULL == g_send_msg.pdata){ return NRF_SUCCESS; }//後續資料全發送完了


uint8_t temp_len;
temp_len = dif_value>20 ? 20 : dif_value;
err_code = ble_nus_string_send(&m_nus,g_send_msg.pdata + g_send_msg.start, temp_len);
if (NRF_SUCCESS == err_code){
g_send_msg.start += temp_len;
}
return err_code;
}

uint8_t g_data[500];//發給手機的100位元組資料
int main(void)
{
for (uint16_t i = 0; i < 500; i++)
{
if(i>=250)
g_data[i] = i-250;
else
g_data[i] = i;
}
...
...
for (;;)
{
power_manage();
}
}

void USER_Main_Handle(void * p_context)
{
if(send == 1)
{
send = 0;
ble_send_data(g_data, 100);
}
}
啟動發送後只會發送前20位元組,當這20位元組發送完成後會收到BLE_EVT_TX_COMPLETE事件,在該事件處理中添加剩餘資料的發送
直接在on_ble_evt事件處理函數中添加一下這個事件的處理
static void on_ble_evt(ble_evt_t * p_ble_evt)
{
case BLE_EVT_TX_COMPLETE:
ble_send_more_data();
break;
}


因為每個串連事件到來時都會切換到另一個通道(頻率)上進行資料轉送,而在這個串連事件期間中的資料互動都是在同一個通道上。即每個串連事件到來時都會切換通道,但是一個串連事件內部的通訊都始終在那個通道上,所以由通道號可以區分出來這裡基本上是兩個串連事件才會發送一次資料,這樣效率就很低,因為實際的底層基帶發送是很快的1Mbit/s, 也就是1us發送1bit。理論上簡單算一下,這裡就直接以鏈路層最長包來算,1+4+39+3 也就只有47位元組,

47*8 = 376bit 也就是發送一包的實際時間不足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錯誤。


代碼:下面兩個函數直接替換上面即可

uint32_t send_data(void)

{
uint8_t  temp_len;
uint32_t dif_value;
uint32_t err_code = NRF_SUCCESS;
uint8_t  *pdata   = g_send_msg.pdata;
uint32_t start    = g_send_msg.start;
uint32_t max_len  = g_send_msg.max_len;
//迴圈發送,只要傳回值正確就反覆調用發送函數
do{
dif_value = max_len - start;
temp_len = dif_value>20 ? 20 : dif_value;
err_code = ble_nus_string_send(&m_nus, pdata + start, temp_len);
if (NRF_SUCCESS == err_code)
{
//只有傳回值正確才更新位移,
//不需要考慮是BUSY錯誤還是NO BUFF錯誤
start += temp_len;
}
//調用函數成功並且還有資料那就繼續調用    
} while ((NRF_SUCCESS == err_code) && (max_len - start)>0);
g_send_msg.start = start;
return err_code;
}

uint32_t ble_send_data(uint8_t *pdata, uint32_t len)
{
if (NULL == pdata || len <= 0){
return NRF_ERROR_INVALID_PARAM;
}
uint32_t err_code = NRF_SUCCESS;
g_send_msg.start = 0;
g_send_msg.max_len = len;
g_send_msg.pdata = pdata;
err_code = send_data();
//傳回值應該在外面處理,傳回值如果是SUCCESS,
//或者NRF_ERROR_BUSY或者BLE_ERROR_NO_TX_BUFFERS都應該認為正確
//因為這兩種錯誤雖然發生了,但是我們並沒有去更新start位移,所以以後
//的發送還是會正確進行。
//其他情況上層應該根據情況處理
return err_code;
}

uint32_t ble_send_more_data()
{
uint32_t err_code;
uint32_t dif_value;
dif_value = g_send_msg.max_len - g_send_msg.start;
if (0 == dif_value || NULL == g_send_msg.pdata)
{
return NRF_SUCCESS;    //後續資料全發送完了直接返回
}
err_code = send_data();
return err_code;
}

void USER_Main_Handle(void * p_context)
{
if(send == 1)
{
send = 0;
ble_send_data(g_data, 500);
}
}

我們再次抓一下空中包看看是否每個串連間隔中發送了多個資料包由通道號可以看到現在一個串連事件中發送了多個包(最多6個),從串口透傳資料也可以看出速率有很明顯的提升。



聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.