1. 程式人生 > >提高ble資料傳送速率

提高ble資料傳送速率

參考 :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個),從串列埠透傳資料也可以看出速率有很明顯的提升。