【轉】手把手教你開發BLE數據透傳應用程序


Nordic開發環境如何快速搭建?如何理解Nordic的BLE透傳服務?如何開發自己的數據透傳應用?如何提高BLE數據傳輸速率?手機和BLE設備之間通信有沒有什么工具可以進行壓力測試?哪里可以找到手機端BLE app參考程序?本文將對以上問題進行解答。

在很多應用場合,BLE只是作為一個數據透傳模塊,即將設備端數據上傳給手機,同時接收手機端下發的數據。本文將和大家一起,一步一步演示如何開發一個BLE透傳應用程序。按照本文的說明,大家可以很快就實現一個BLE透傳應用,BLE透傳應用已經是BLE應用中比較復雜的一種,一旦大家掌握了BLE透傳應用,其他BLE應用開發就更不在話下了。本文還將手把手教大家如何提高BLE數據傳輸速度(在藍牙4.2模式下我們實測速度達到了85kB/s(理論值90kB/s);在藍牙5.0模式下我們實測速度達到了150kB/s(理論值170kB/s))。最后,我們將告訴大家如何使用安卓版nRF Connect來對你的BLE設備進行壓力測試,以測試設備的穩定性和可靠性。文章的最后還會告訴大家如何找到安卓和iOS手機BLE app開發參考代碼。

這是一篇純實踐的文章,如果你對低功耗藍牙的一些基本概念還不是很懂的話,那么建議你先看一下這篇文章:低功耗藍牙ATT_GATT_Profile_Service_Char規格解讀,有了BLE這些基本概念后,再去看下面的內容,就容易多了。

文中涉及的所有代碼都可以在百度網盤找到,地址如下所示:

下載“ble_app_uart_hs_SDK16_0_0.rar”,解壓縮到SDK16.0.0如下目錄下:nRF5SDK160098a08e2\examples\ble_peripheral,即可成功編譯和運行。

注意:雖然本文代碼是基於SDK16.0.0開發的,但所有新增代碼都可以直接拷貝到SDK15.0.0/SDK15.2.0/SDK15.3.0的ble_app_uart例子,並可以成功編譯和運行,他們是完全兼容的。 

1. 開發准備

1)      Nordic nRF52或者nRF51開發板1塊。請參考“Nordic nRF51/nRF52開發流程說明”,購買相應開發板(DK)。

2)      開發環境搭建。簡述如下(詳細說明請參考“Nordic nRF51/nRF52開發環境搭建”):

注:如果你使用的是Linux系統/Mac系統,或者你使用的不是Keil5-MDK,請參考“Nordic nRF51/nRF52開發環境搭建”來搭建你的開發環境。

2. 運行Nordic ble_app_uart應用程序

Nordic SDK已經提供了一個直接就可以編譯和運行的數據透傳應用程序:ble_app_uart,Nordic將BLE透傳服務稱為Nordic UART Service(NUS),所以在Nordic SDK中,NUS就是BLE透傳服務。請按照如下步驟運行SDK自帶的ble_app_uart程序:

  • 確認自己的芯片型號或者開發板。如果采用Nordic官方開發板的話,芯片型號和開發板編號對應關系如下:
    • nRF52832和nRF52810對應開發板編號為PCA10040。雖然52832和52810共用同一塊開發板,但是他們在SDK中的項目編號是不一樣的,52832對應PCA10040目錄,52810對應PCA10040e目錄,由於52810和52832 PIN to PIN兼容,軟件也是完全兼容的,因此SDK很多項目只有PCA10040的目錄,而沒有PCA10040e目錄,此時需要你自己來建立PCA10040e對應的目錄和工程,具體說明可參考:https://infocenter.nordicsemi.com/topic/sdk_nrf5_v16.0.0/nrf52810_user_guide.html?cp=7_1_5_0
    • nRF52840和nRF52811對應開發板編號為PCA10056。與832/810相似,52840對應的工程目錄為PCA10056,而52811對應的工程目錄為PCA10056e。如何建立一個PCA10056e的項目工程可以參考:https://infocenter.nordicsemi.com/topic/sdk_nrf5_v16.0.0/nrf52811_user_guide.html?cp=7_1_5_1
    • nRF52840 dongle編號為PCA10059。PCA10059是一個可以直接插入電腦的小dongle,它也是使用52840芯片,但本身不帶JLink接口芯片。
    • nRF52833對應開發板編號為PCA10100
    • nRF51系列對應開發板編號為PCA10028

這里我會以nRF52832開發板PCA10040為例來闡述整個開發過程,其他開發板與之類似,大家可以舉一反三來開始自己的開發之旅。

  • 將開發板與PC機通過USB線相連,同時打開開發板電源(將左下角的撥位開關打到“ON”位置)。

 

  • 打開SDK中的ble_app_uart程序。如果是52832開發板,請打開:nRF5_SDK_15.0.0_a53641a\examples\ble_peripheral\ble_app_uart\pca10040\s132\arm5_no_packs;如果是51822開發板,請打開:nRF5_SDK_12.3.0_d7731ad\examples\ble_peripheral\ble_app_uart\pca10028\s130\arm5_no_packs

 后續將以52832開發板為例來闡述,51822等其他芯片與之類似就不再闡述了。 

注:Nordic SDK例程目錄結構為:SDK版本/ examples /藍牙角色/例子名稱/開發板型號/協議棧型號/工具鏈類型/具體工程,比如下面例子:

  

Nordic每一個例子都支持5種工具鏈:Keil5/Keil4/IAR/GCC/SES,如下所示:

   

  • 編譯程序。如果你已經按照之前的說明配置好了開發環境,那么這里編譯是不會報任何錯的。(如果你遇到了編譯錯誤,請重新按照前面說明去搭建你的開發環境,不要懷疑SDK例子代碼有問題哦)
  • 下載程序。程序下載包括2步:一先下載softdevice,二再下載應用。Softdevice是Nordic藍牙協議棧的名稱,整個開發過程中只需下載一次。應用就是我們這里的ble_app_uart程序。如果你的開發板已經下載了其他代碼,那么最好先把開發板全擦一次,然后再下載softdevice和應用。
    • 芯片全擦(可選)。你可以使用nRF connect桌面版或者nrfjprog,二選其一來執行擦除操作。
      • 打開桌面版nRF Connect,選擇啟動“Programmer”應用,由於驅動之前已經安裝好了,設備可以立即識別成功,執行“Erase all”操作,以擦除芯片原始內容

 

 

      • 使用nrfjprog執行全擦操作

 

    • 藍牙協議棧下載(整個開發周期只需下載一次)。在Keil ‘select target’下拉列表中,默認選擇的是Keil工程對應的Target,即‘nrf52832_xxaa’。我們還可以選擇另一個target ‘flash_s132_nrf52_7.0.1_softdevice’,即softdevice對應的target,然后點擊“下載download”(不需要編譯哦!),此時會把softdevice下載到開發板中。

  

注:你也可以通過桌面版nRF connect的programmer或者nrfjprog來下載softdevice,下載的時候,找到相應的softdevice hex文件,然后直接下載進去即可。 

    • 應用下載。重新選擇Target:‘nrf52832_xxaa’,點擊“下載Download”,此時會把ble_app_uart應用程序下載到開發板中。此時開發板的LED1閃爍,表示程序運行正常。
  • 連接手機。打開手機藍牙和手機版nRF connect。在nRF connect中,你將看到一個廣播設備:Nordic_UART,這個就是開發板的廣播名字。點擊“CONNECT”,手機將與設備建立連接,並開始服務發現過程,連接成功后,LED1熄滅,LED2點亮,兩個關鍵界面如下所示:

 

  

上圖的Nordic UART Service(NUS)就是我們的數據透傳服務, NUS具體包括兩個characteristic:TX和RX,由於NUS是由設備提供的,所以TX表示設備發送數據給手機,RX表示設備接收手機發過來的數據。

  • 測試NUS服務。ble_app_uart使用串口與上位機交互,選擇一款串口助手軟件,比如Putty,打開該串口軟件,並做如下設置:
Baud rate: 115.200kbps
8 data bits
1 stop bit
No parity
HW flow control: None

復位開發板,你會發現串口助手會打印如下信息:

  

重新將開發板連上手機,然后點擊右上角的“Enable CCCDs”以使能notification,如下所示: 

 

設備接收數據: 點擊RX characteristic旁邊的向上箭頭,通過手機藍牙往設備發送:12345678,如下所示:

  

此時設備通過串口打印出剛才接收到的數據,如下所示:

  

設備發送數據:在串口助手中輸入“abcdefgh”並輸入“\n”(注:在Putty中,先按“CTRL”再按“J”就會發出“\n”換行符)作為結束符,設備將把串口收到的數據通過藍牙發送給手機,手機的TX characteristic將顯示上述字符串,如下所示: 

 

 注:如果你的串口助手發不出“\n”換行符,那么你需要最少輸入MTU-3個字符,設備才會把收到的全部字符通過藍牙發出去。

通過上面的測試,大家可以發現Nordic SDK已經把藍牙數據透傳服務做好了,大家可以直接拿過來使用,下面將對其工作原理進行闡述,最后在Nordic藍牙透傳例子ble_app_uart上進行二次開發,以增加一些其他有用的功能。

3. 設備端固件代碼一覽

現在我們一起來看一下ble_app_uart的源代碼,看看它是怎么工作起來的。首先我們來看main函數:

  

如上所述,ble_stack_init用於初始化配置和使能藍牙協議棧,其代碼如下所示:

 

其中,nrf_sdh_enable_request需要選擇藍牙協議棧的低頻時鍾(由於藍牙協議棧的高頻時鍾必須為外部32M晶振,所以高頻時鍾無需配置;而低頻時鍾可以選擇為內部32K RC或者外部32K晶振,所以低頻時鍾需要人工配置),因此如下宏需要根據實際情況進行調整:

復制代碼
nrf_clock_lf_cfg_t const clock_lf_cfg =
{
        .source       = NRF_SDH_CLOCK_LF_SRC,
        .rc_ctiv      = NRF_SDH_CLOCK_LF_RC_CTIV,
        .rc_temp_ctiv = NRF_SDH_CLOCK_LF_RC_TEMP_CTIV,
        .accuracy     = NRF_SDH_CLOCK_LF_ACCURACY
};
復制代碼

通過sdk_config.h文件可以看到,默認是選擇外部32K晶振作為低頻時鍾的,如果你想選擇內部32K RC作為低頻時鍾,那么需要做如下修改

NRF_SDH_CLOCK_LF_SRC = 0
NRF_SDH_CLOCK_LF_RC_CTIV = 16    //每4s啟動一次校准
NRF_SDH_CLOCK_LF_RC_TEMP_CTIV = 2
NRF_SDH_CLOCK_LF_ACCURACY = 1  //500ppm

nrf_sdh_ble_default_cfg_set用來配置softdevice協議棧,如下宏是經常需要修改的:

復制代碼
NRF_SDH_BLE_PERIPHERAL_LINK_COUNT  //作為從模式的連接同時能有幾個
NRF_SDH_BLE_CENTRAL_LINK_COUNT  //作為主模式的連接同時能有幾個
NRF_SDH_BLE_TOTAL_LINK_COUNT  //一共同時可以支持多少個連接,NRF_SDH_BLE_TOTAL_LINK_COUNT= NRF_SDH_BLE_PERIPHERAL_LINK_COUNT+ NRF_SDH_BLE_CENTRAL_LINK_COUNT
NRF_SDH_BLE_GATT_MAX_MTU_SIZE //MTU為多大
NRF_SDH_BLE_GAP_DATA_LENGTH //鏈路層數據包的最大長度,它的值要大於NRF_SDH_BLE_GATT_MAX_MTU_SIZE
NRF_SDH_BLE_GAP_EVENT_LENGTH /*一次連接間隔中分配給某個connection的物理層時間,這個時間必須大於等於NRF_SDH_BLE_GAP_DATA_LENGTH最長的包占用的時間,否則協議棧初始化會報錯。如果這個時間是NRF_SDH_BLE_GAP_DATA_LENGTH的兩倍甚至更多,那么在一個連接間隔中就可以連發兩個甚至多個notify或者write command命令 */
NRF_SDH_BLE_VS_UUID_COUNT  //用戶自定義的base UUID有幾個
NRF_SDH_BLE_SERVICE_CHANGED  //要不要包含service change characteristic
NRF_SDH_BLE_GATTS_ATTR_TAB_SIZE  /*Attribute table總共占多少藍牙協議棧RAM空間,這個宏的取值是需要用戶不斷去試錯的,每當你添加了或者刪除了BLE service/characteristic,那么attribute table占用的RAM的空間就會變。注意:NRF_SDH_BLE_GATTS_ATTR_TAB_SIZE是藍牙協議棧占用的總RAM空間的一部分,所以當NRF_SDH_BLE_GATTS_ATTR_TAB_SIZE變大時,softdevice占用的總RAM空間也會變大。Softdevice占用的總RAM空間是在編譯的時候靜態設置的,如下所示,本應用程序分配了0x20000000~0x20002AD8 RAM空間給協議棧(所有的nRF5芯片的RAM物理起始地址都是0x20000000),即softdevice總共占用了0x2AD8的RAM空間。而0x20002AD8往上直到RAM最高物理地址就是留給應用程序使用的,本程序分配了0xD528 RAM空間給應用程序。0x2AD8 + 0xD528 = 0x10000 = 64kB,正好是nRF52832的RAM總空間大小。*/
復制代碼

 

 

nrf_sdh_ble_enable真正使能BLE功能,它的參數ram_start既是一個輸入參數又是一個輸出參數,作為輸入參數,系統自動會把上面的RAM起始地址(0x20002AD8)傳入,同時nrf_sdh_ble_enable會把softdevice當前配置情況下,它實際需要占用的RAM空間通過ram_start返回,如果這個返回值不等於輸入值,那么用戶需要把上圖的IRAM1起始地址修改成它的返回值。這里面又分兩種情況:如果返回值大於輸入值,那么必須調整IRAM1起始地址,否則協議棧初始化失敗;如果返回值小於輸入值,這種情況也推薦去調整IRAM1起始地址,當然如果你不調整IRAM1起始地址也沒有關系,因為這種情況表示協議棧實際需要的RAM的空間小於你分配給它的RAM空間,所以協議棧初始化不會出問題,這種情況只是會浪費一些RAM空間而已。請注意,任何協議棧配置宏的修改,包括NRF_SDH_BLE_GATTS_ATTR_TAB_SIZE,都有可能導致協議棧占用的RAM空間發生變化,進而需要去調整IRAM1起始地址。一種常見的場景是,用戶添加了某個service,從而需要先增大NRF_SDH_BLE_GATTS_ATTR_TAB_SIZE這個宏的值,然后通過nrf_sdh_ble_enable得到ram_start實際起始地址,最后把ram_start的值賦給IRAM1起始地址。

NRF_SDH_BLE_OBSERVER用來為本地文件(此處為main.c文件)注冊一個BLE回調函數(此處為ble_evt_handler),NRF_SDH_BLE_OBSERVER這個宏執行成功后,所有的BLE事件都會被ble_evt_handler捕獲。進入ble_evt_handler,你會發現BLE有上百個回調事件,你不需要每個都處理,你只需要處理你關心的事件即可,比如連接成功事件BLE_GAP_EVT_CONNECTED或者連接斷開事件BLE_GAP_EVT_DISCONNECTED。NRF_SDH_BLE_OBSERVER有一個很大的好處:某個文件如果需要捕獲BLE事件,那么它只需在本文件中某處(可以在函數內也可以在函數外)調用NRF_SDH_BLE_OBSERVER這個宏去注冊一個回調函數即可,而不再需要在其它文件中去注冊這個回調函數,將模塊的耦合性降到最低,符合模塊化編程思想。

gap_params_init用來修改廣播名字和連接間隔的。gatt_init用來修改底層數據包長度的。advertising_init用來修改廣播包內容,廣播間隔以及廣播超時時間。conn_params_init用來請求更新連接間隔的。

下面我們來重點講一下services_init,services_init用來添加服務和characteristic,“低功耗藍牙ATT_GATT_Profile_Service_Char規格解讀”講了那么多的概念和理論,現在我們就來看看services_init是如何做到跟理論一致的。services_init通過ble_nus_init添加了一個藍牙數據透傳服務:NUS,那ble_nus_init是怎么將NUS服務添加成功的呢?查看ble_nus_init函數體,你會發現它是分三步來做的:

  1. 添加base UUID。如果是藍牙標准UUID,這步可以省略。由於NUS不是藍牙聯盟定義的,所以需要調用sd_ble_uuid_vs_add以添加一個供應商自定義的UUID。
  2. 添加服務本身。直接調用sd_ble_gatts_service_add就可以完成。
  3. 添加服務下面的characteristics。服務的characteristic現在可以通過characteristic_add直接添加完成(characteristic_add最終是通過調用sd_ble_gatts_characteristic_add實現自己目的的)。以NUS的TX characteristic添加為例,其對應代碼為:              
 characteristic_add(p_nus->service_handle, &add_char_params, &p_nus->tx_handles)

其中,p_nus->service_handle表示該characteristic屬於那個service,p_nus->tx_handles是輸出參數,由協議棧返回,以后訪問該characteristic都是通過這些句柄來完成,p_nus->tx_handles是一個結構體,它包含如下成員變量:

復制代碼

typedef struct
{
uint16_t value_handle; /**< Handle to the characteristic value. */
uint16_t user_desc_handle; /**< Handle to the User Description descriptor, or @ref BLE_GATT_HANDLE_INVALID if not present. */
uint16_t cccd_handle; /**< Handle to the Client Characteristic Configuration Descriptor, or @ref BLE_GATT_HANDLE_INVALID if not present. */
uint16_t sccd_handle; /**< Handle to the Server Characteristic Configuration Descriptor, or @ref BLE_GATT_HANDLE_INVALID if not present. */
} ble_gatts_char_handles_t;

復制代碼

add_char_params (類型為ble_add_char_params_t)是對characteristic的參數進行賦值,如“低功耗藍牙ATT_GATT_Profile_Service_Char規格解讀”所述,characteristic包含多個attribute,每個attribute都有自己的value/handle/uuid/permission,所以ble_add_char_params_t這個結構體設計的比較復雜。這里需要大家明白的是,characteristic核心attribute是value attribute,所以當我們講characteristic的時候,其實隱含是在說value attribute。換句話說,我們定義characteristic的參數,其實是在定義value的參數,比如我們定義characteristic的訪問權限,其實就是指value的訪問權限。add_char_params參數解讀如下:

 

TX characteristic只需賦值上述參數即可,但對某些其他characteristic,它需要賦值的參數會更多,這里再羅列一下ble_add_char_params_t這個結構體其他一些關鍵參數:

 

  • is_defered_read/ is_defered_write,即authorize read/write,即在read/write characteristic value之前,先進入用戶回調函數,由用戶回調函數決定這個read/write操作是否允許,以及value最終為多少。
  • is_value_user,默認情況下所有attribute/characteristic是存放在協議棧專用RAM中(即上述的IRAM1起始地址以下地方),這種做法的好處是所有attribute由協議棧自動管理,用戶無需操心。但有時候,用戶想自己控制某個characteristic,那么這個時候就可以把is_value_user設為1,將其放在應用程序RAM空間。is_value_user和is_defered_read配合使用,可以達到一些意想不到效果。

這里需要特別提醒大家的是,雖然Nordic API結構體參數設計得很復雜,但是大部分成員變量都直接采用0作為它的默認值,換句話說,大家只需對自己感興趣的變量進行賦值即可,其他的變量可以直接采用默認值,因此大家經常會看到如下操作場合,即先用memset將該結構體變量初始化為0,讓所有成員變量都采用默認值,然后再對某些需要修改的成員變量進行二次賦值。大家一定不要忘了將結構體變量清零這一步操作!

  

ble_nus_init同時注冊了nus_data_handler回調函數,當設備收到手機發過來的數據時,就會觸發nus_data_handler,用戶可以在nus_data_handler中對接收到的數據進行處理,本例程中nus_data_handler直接將ble收到的數據通過uart口轉發出去。如果用戶需要發送數據給手機,在連接成功和notify使能的情況下,直接調用ble_nus_data_send即可,而ble_nus_data_send又是通過調用協議棧API:sd_ble_gatts_hvx來實現數據發送功能的。那么什么時候需要發送數據給手機?本例程的做法是,當串口有數據過來並滿足如下條件時調用ble_nus_data_send:

if ((data_array[index - 1] == '\n') || (index >= (m_ble_nus_max_data_len)))

即遇到換行符或者字符數達到MTU,設備才會把串口收到的數據發給手機,這個測試的時候請注意一下。

main函數最后將調用API讓協議棧跑起來,如果你的設備是一個從設備(peripheral),那么請調用ble_advertising_start,ble_advertising_start將開啟可連接的廣播,從而讓你的設備連接成功之后成為從設備。如果你的設備是一個主設備(central),那么請調用sd_ble_gap_scan_start,sd_ble_gap_scan_start將開啟設備的掃描功能,從而讓你的設備連接成功之后變為主設備。 

最后我們來看main循環,它只有一個函數: idle_state_handle,idle_state_handle先把需要打印的日志打印完,然后讓系統進入idle狀態(Nordic SoC spec稱其為System ON狀態),一旦有協議棧事件或者中斷事件發生,系統將喚醒,以處理相關事件回調函數,然后再執行一遍idle_state_handle。注意:idle狀態下,藍牙連接或者廣播可以正常進行而不受影響,藍牙連接或者廣播都是周期性的,在一個周期中,藍牙連接或者廣播只持續很短一段時間(幾百微妙到幾毫秒,與數據包長度有關),CPU或者系統只會在這段時間內工作,其余時間系統都是處於idle狀態的,比如廣播間隔200ms,CPU或者系統不是持續工作200ms,他們其實只工作幾百微妙,大部分時間(199ms多)系統都是處於idle狀態的,這就是為什么廣播或者連接狀態系統平均電流很低的原因。如下為廣播間隔200ms時,系統的實時電流波形:

 

如下為200ms連接間隔時對應的實時電流波形:

  

4. 定制你的BLE數據透傳應用程序

我們現在在ble_app_uart基礎上加入一些定制功能。很多BLE應用場合,都需要快速地把大量數據從設備上傳給手機,我們現在模擬這種應用場景,看一下如何修改原始的ble_app_uart以達到我們的目的。

這里假設有一個timer,這個timer每7ms發送一次數據,以模擬傳感器等每7ms生成一次數據情況,我們現在來看看如何讓這些數據快速發出去並且不發生丟包。這個Timer的ID設為m_timer_speed,它的回調函數為:throughput_timer_handler。原始數據假設放在數組m_data_array,數組的長度正好等於MTU長度的倍數。也就是說,每隔7ms,進入一次throughput_timer_handler,在這個handler中需要把m_data_array中的數據全部正確發出去,這個就是我們的需求。假設m_data_array數據長度為420字節(m_data_array的長度依情況而定,這里只是一個假設),那么此時就需要每7ms發送420字節數據,相當於420B/7ms = 60kB/s。

藍牙傳輸速度跟得上嗎?怎么做可以滿足這個需求呢?在講解參考代碼之前,我們先看一些反例或者誤區,這樣或許更能幫助大家去理解相關工作機理。

4.1 一些使用上的誤區

大家首先想到的做法就是在throughput_timer_handler直接調用 ble_nus_data_send,如下

static void throughput_timer_handler(void * p_context)
{
     err_code = ble_nus_data_send(&m_nus, m_data_array, &length, m_conn_handle);
}

這種做法首先存在如下兩個大問題:

  • 沒有檢測返回值err_code。實際上,在這種情況下調用ble_nus_data_send,ble_nus_data_send經常返回NRF_ERROR_RESOURCES,只要err_code不是NRF_SUCCESS,就意味着該數據沒有成功放入射頻FIFO中,從而出現所謂“丟包”現象。不是包在空中丟了,而是包沒有正確放入射頻FIFO中,這個就是丟包的原因。
  • length變量沒有進行最大值限制。ble_nus_data_send不能發送任意長度的數據包,它只能發送小於等於MTU長度的數據包。

如果我們不對返回值進行檢測,同時把length設為MTU,然后每調用一次ble_nus_data_send,執行一次m_len_sent += length,即代碼如下所示(代碼片段1):

復制代碼
static void throughput_timer_handler(void * p_context)
{
    ret_code_t err_code;
    uint16_t length;
    m_cnt_7ms++;
   //sending code lines
   length = m_ble_nus_max_data_len;
   //new data
   m_data_array[0]++;
   m_data_array[length-1]++;
   err_code = ble_nus_data_send(&m_nus, m_data_array, &length, m_conn_handle);
   m_len_sent += length;
   //calculating data throughput
   NRF_LOG_INFO("time: %d *7ms == bytes send: %d Bytes == avg speed: %d B/s",m_cnt_7ms,m_len_sent,(m_len_sent * 1000)/(m_cnt_7ms*7));
}
復制代碼

我們可以看到如下日志:

  

由上圖可知,數據上傳吞吐率達到了34.8kB/s,看起來不錯,其實這個吞吐率是假的,因為中間丟了很多包,但計算吞吐率的時候把丟的包也算進去了。如下圖手機端nRF Connect日志所示,0x6E之后應該為0x6F,但實際發送的數據包編號為0x83,丟包非常嚴重。

  

為了防止所謂的“丟包”(前面也提過,這里的丟包不是數據包在空中丟掉了,而是數據包沒有安全送到協議棧的buffer中,從而導致丟包),我們加上如下if語句(代碼片段2),只有ble_nus_data_send返回正確時,才認為數據包正確發送,然后才能算入到最后的數據吞吐率中。

復制代碼
 err_code = ble_nus_data_send(&m_nus, m_data_array, &length, m_conn_handle);
 if (err_code == NRF_SUCCESS)
 {
m_len_sent += length; //new data m_data_array[0]++; m_data_array[length-1]++; }
復制代碼

m_data_array只有在返回成功時才自增(自增表示新數據有效),所以新數據生成速度會比較慢,從而導致數據吞吐率下降。通過查看nRF connect日志,你會發現此時不會發生丟包了,但吞吐率直接降到了4kB/s左右。

之前我們也講過,為了提高吞吐率,我們可以在一個連接間隔中發多個包。如何可以做到一個連接間隔中發送多個包?除了協議棧需要做一些額外的配置,代碼層面可以這么做(代碼片段3):

復制代碼
    do
    {                    
        err_code = ble_nus_data_send(&m_nus, m_data_array, &length, m_conn_handle);
        if ( (err_code != NRF_ERROR_INVALID_STATE) && (err_code != NRF_ERROR_RESOURCES) &&
                 (err_code != NRF_ERROR_NOT_FOUND) )
        {
                APP_ERROR_CHECK(err_code);
        }
        if (err_code == NRF_SUCCESS)
        {
            m_len_sent += length;     
            m_data_array[0]++;
            m_data_array[length-1]++;    
        }
    } while (err_code == NRF_SUCCESS);
復制代碼

測試下來,你會發現上面兩份代碼(代碼片段2和代碼片段3)測試結果是一樣的。那么問題到底出在哪?下面進行解讀。 

4.2 參考代碼解讀

 欲達到最大的數據吞吐率,必須保證藍牙帶寬盡可能被利用,換句話說,必須保證藍牙包盡可能占滿整個時間軸,如下所示:

  

反之,如果連接間隔很長,而且一個間隔只發一個包,那么哪怕這個包包長為251字節,吞吐率也不可能高,如下所示:

  

粗略來說,

藍牙數據傳輸吞吐率 = 一個連接間隔傳輸的數據 / 連接間隔 = 一個數據包長度 * 包數 / 連接間隔

比如連接間隔設為60ms,一個間隔只傳一個244字節的數據包,此時吞吐率為:244B/60ms = 4.1 kB/s,這就是4.1節數據傳輸速率上不去的根本原因。

實際情況比上面公式復雜得多,但是我們做理論分析時可以以這個公式作為參考。從上面公式可知,欲提高數據吞吐率,可以從數據包長度,每個連接間隔發送的總包數,以及連接間隔等三個方面入手,這里需要提醒大家的是,不是包長越長越好,也不是間隔越短越好,必須將三者統一起來一起考慮。如果發送短包時能保證一個間隔發送很多短包,那么此時短包有可能比長包速率還要快;如果連接間隔大時能保證一個間隔發送很多的數據包,那么此時大的連接間隔速率有可能比小的連接間隔還要快。針對不同的手機,不同的藍牙設備,大家一定要統籌好這三個參數,讓他們整體達到一個最優值,從而得到你的應用場景下的最高吞吐率。下面看看我們的參考代碼如何調整這三個參數的。 

一個數據包的長度取決於下面兩個參數:

#define NRF_SDH_BLE_GAP_DATA_LENGTH 251
#define NRF_SDH_BLE_GATT_MAX_MTU_SIZE 247

NRF_SDH_BLE_GAP_DATA_LENGTH ≥ (NRF_SDH_BLE_GATT_MAX_MTU_SIZE+4) ,NRF_SDH_BLE_GAP_DATA_LENGTH最大值為251,這里MTU設為247,也就相當於把數據包應用數據最大長度設為247-3=244字節

連接間隔由下面兩個參數建議:

#define MIN_CONN_INTERVAL               MSEC_TO_UNITS(30, UNIT_1_25_MS)
#define MAX_CONN_INTERVAL               MSEC_TO_UNITS(30, UNIT_1_25_MS)

上面把最小連接間隔和最大連接間隔都設為30ms,請注意最終連接間隔設為多少只能由藍牙主設備決定,藍牙從設備只有建議權。由於我們的設備是一個從設備,所以上面就是對主機提出建議:希望主機把連接間隔設為30ms,主機可以接受也可以拒絕這個請求。

一個連接間隔可以發多個包由下面參數決定:

#define NRF_SDH_BLE_GAP_EVENT_LENGTH 24

NRF_SDH_BLE_GAP_EVENT_LENGTH的單位是1.25ms,所以這里的NRF_SDH_BLE_GAP_EVENT_LENGTH是30ms,如果連接間隔也為30ms,那就意味着整個連接間隔都可以用來發送藍牙數據包,大家知道一個251字節的藍牙數據包和它的ACK包總共在空中大概持續2.5ms時間,這樣我們可以大概估算30ms連接間隔中理論上可以發的包數:NRF_SDH_BLE_GAP_EVENT_LENGTH / 2.5ms = 12,雖然NRF_SDH_BLE_GAP_EVENT_LENGTH等於連接間隔,但不表示整個連接間隔都可以用來發送藍牙數據包,因為每個連接間隔還需要預留一些時間給協議棧調度,射頻初始化,以及應用程序執行,這個時間假設為3ms,那么上面的配置情況下,30ms連接間隔理論上可以發送的最大包數為:(30-3)/2.5 = 10,即理論上藍牙數據傳輸最高速度可以達到:(244*10)/30ms = 81kB/s。

回到上一節問題,為什么代碼片段3和代碼片段2測試結果差不多呢?因為代碼片段3沒有對協議棧進行額外配置,所以傳輸速率起不來。現在我們把上面的配置加上去,看一下代碼片段3傳輸速率能到多少?

  

數據吞吐率只有32kB/s,跟理論值81kB/s還是差了不少,這個又是什么原因呢?

為了分析問題,我們把打印方式改成如下方式:

復制代碼
    if (m_cnt_7ms == 143)
    {
        NRF_LOG_INFO("==**Speed: %d B/s**==", m_len_sent);
        m_cnt_7ms = 0;
        m_len_sent = 0;
        m_data_array[0] = 0;
        m_data_array[length-1] = 0;
    }    
    NRF_LOG_INFO("PacketNo.: %d == Time: %d *7ms", m_data_array[0], m_cnt_7ms);    
復制代碼

相應打印日志如下所示:

  

可以看出,我們的設備每49ms才發了4個包或者5個包,這就是為什么它的吞吐率只有30kB/s左右的原因。49ms發4個包或者5個包,到底是協議棧配置問題,還是手機端限制問題,抑或是應用程序邏輯問題?從上面的日志我們可以推出,協議棧射頻FIFO為4(這個是日志給我們的推論,我們也暫且這么認為),而不是我們預想的10,換句話說,30ms連接間隔最多發4個包,即244*4/30ms = 32kB/s,正好跟我們實測值差不多。

如果我們把連接間隔改為14ms,那么吞吐率是不是可以達到:244*4/14ms = 69.7kB/s

  

可以看到,基本上達到預期了。大家可以嘗試把連接間隔進一步減少,看一下吞吐率會不會進一步提高?答案是否,具體原因大家可以自己去想一下。

但是60kB/s跟我們的理論速度80kB/s相比,還是相差有一點大,有沒有辦法能不能進一步提高吞吐率?我們現在把連接間隔重新改為30ms,然后在nus_data_handler中加入如下代碼:

復制代碼
        else if (p_evt->type == BLE_NUS_EVT_TX_RDY)
        {        
            ret_code_t err_code;
            uint16_t length;                
            //sending code lines
            length = m_ble_nus_max_data_len;    
            do
            {                    
                err_code = ble_nus_data_send(&m_nus, m_data_array, &length, m_conn_handle);
                if ( (err_code != NRF_ERROR_INVALID_STATE) && (err_code != NRF_ERROR_RESOURCES) &&
                         (err_code != NRF_ERROR_NOT_FOUND) )
                {
                        APP_ERROR_CHECK(err_code);
                }
                if (err_code == NRF_SUCCESS)
                {
                    m_len_sent += length;     
                    m_data_array[0]++;
                    m_data_array[length-1]++;    
                }
            } while (err_code == NRF_SUCCESS);
        }
復制代碼

ble_nus_data_send 對應的ATT PDU是notification,notification是沒有response的,但它還是有ACK的,ACK表示對方已經正確收到該包。在Nordic SDK中,BLE_NUS_EVT_TX_RDY事件即表示notification的ACK包,也就是說,每收到BLE_NUS_EVT_TX_RDY這個事件,表示一個或者多個notification包已經被對方正確接收。假設協議棧射頻FIFO為10並且已滿,此時協議棧將自動將這10個包發出去,每發出去1個包或者多個包(這里用n替代),產生一次BLE_NUS_EVT_TX_RDY回調事件,此時FIFO中還有10-n個包待發送,換句話說,射頻FIFO又可以重新入隊n個新的數據包進來。通過這種方式,我們可以讓一個連接間隔所有時間都用來發數據包,直至下一個連接間隔。

下面為新代碼對應的日志,由日志可見,設備在30ms的連接間隔中發出了10個藍牙包,說明之前的協議棧配置沒有問題,手機也支持10個藍牙包,是我們的應用邏輯出問題了,沒有讓協議棧跑到極限。 

 

如下所示,這種情況下藍牙數據傳輸速率可以達到80kB/s左右,基本上快接近藍牙4.2的理論吞吐率了。

  

4.3 藍牙5.0高速率模式

藍牙5.0提出了一個高速率模式,即引入了一個新的調制解調符號率:2Msps,即1秒鍾可以傳2M個bit,一個bit傳輸只需1/2M=0.5us。藍牙4.x低功耗模式只支持1Mbps,也就是說傳輸1bit需要1us時間。采用2M高速率模式,傳輸同樣長度的藍牙數據包,所花費的時間只有1M模式的一半,換句話說,同樣的連接間隔中,2M模式可以發出的數據包基本上是1M模式的兩倍,根據吞吐率計算公式:

藍牙數據傳輸吞吐率 = 一個數據包長度 * 包數 / 連接間隔

包數翻倍,在其他條件不變的情況下,相當於數據傳輸速率翻倍。

只有手機和藍牙設備同時支持2M模式,並且有一方主動要求更新物理層為2M時,藍牙通信才會采用2M,上述的速率翻倍才可能實現。

Nordic所有的nRF52設備都支持高速率2M模式,而手機是否支持2M模式,可以通過手機版nRF Connect的 “Device information” 菜單查看,比如我的手機顯示如下信息,說明它是支持2M模式的。

  

硬件已經滿足了,那軟件需要如何修改呢?2M或者1M模式,這個是物理層的概念,對應用來說是完全透明的,也就是說,不管采用2M還是1M模式,應用代碼都是一模一樣,不需做任何修改。ble_app_uart已經支持2M模式了,相關代碼如下所示:

復制代碼
        case BLE_GAP_EVT_PHY_UPDATE_REQUEST:
        {
            NRF_LOG_DEBUG("PHY update request.");
            ble_gap_phys_t const phys =
            {
                .rx_phys = BLE_GAP_PHY_AUTO,
                .tx_phys = BLE_GAP_PHY_AUTO,
            };
            err_code = sd_ble_gap_phy_update(p_ble_evt->evt.gap_evt.conn_handle, &phys);
            APP_ERROR_CHECK(err_code);
        } break;
復制代碼

phy參數有4種:

  • BLE_GAP_PHY_1MBPS,強制選擇1M模式
  • BLE_GAP_PHY_2MBPS,強制選擇2M模式
  • BLE_GAP_PHY_CODED,強制選擇低速率的長距離模式
  • BLE_GAP_PHY_AUTO,協議棧自動選擇合適的phy層,一般會選擇最快最合適的那個phy

請注意:BLE_GAP_EVT_PHY_UPDATE_REQUEST是響應主機端的phy update請求的,如果主機端不發phy update請求,那么phy仍然維持默認的1M模式。碰到這種情況,設備端需要主動發起phy update請求,怎么做呢?只需在連接成功事件中,調用sd_ble_gap_phy_update這個函數即可,這樣哪怕手機端不發起phy update請求,設備端也會主動請求phy update。

4.2節的參考代碼我都是把phy強制設為BLE_GAP_PHY_1MBPS,現在我們把它重新設為BLE_GAP_PHY_AUTO,然后手機端主動發起2M phy update請求,如下所示:

  

此時藍牙數據吞吐率如下所示:146kB/s,快接近1M模式下的吞吐率的兩倍,符合我們的預期。

  

4.4 更接近實際應用場景的參考代碼

 上面的代碼只是為了測試藍牙數據通信最高速率能到多少,但用戶的實際應用場景與此差別很大,主要差別就是數據生成方式。上面的代碼有效數據生成方式非常簡單:直接對原始的數組元素進行自增,實際應用場景數據都是成塊生成的,我們這里假設每7ms生成420B字節數據,換句話說,藍牙數據傳輸速率必須達到60kB/s,才能把數據全部正確發出去,而不出現所謂丟包或者不同步現象。

在代碼中,每生成一個420B數據,我們將其拆成2包數據放到隊列(queue)中,然后在7ms timer中斷中以及BLE_NUS_EVT_TX_RDY回調中,我們會去查詢queue是否為空,如果不空就把隊列中的數據通過藍牙發出去,核心代碼如下所示:

復制代碼
    while (!nrf_queue_is_empty(&m_buf_queue) && !retry)
    {
        err_code = nrf_queue_pop(&m_buf_queue, &m_buf);
        APP_ERROR_CHECK(err_code);        
        length = m_buf.length;                    
        err_code = ble_nus_data_send(&m_nus, m_buf.p_data, &length, m_conn_handle);
        //NRF_LOG_INFO("Data: %d", m_buf.p_data[0]);
        if ( (err_code != NRF_ERROR_INVALID_STATE) && (err_code != NRF_ERROR_RESOURCES) &&
                 (err_code != NRF_ERROR_NOT_FOUND) )
        {
                APP_ERROR_CHECK(err_code);
        }
        if (err_code == NRF_SUCCESS)
        {
            m_len_sent += length;
            retry = false;
        }
        else
        {
            retry = true;
            break;
        }
    }            
復制代碼

所有相關代碼我們都是通過APP_QUEUE這個宏來控制的,大家可以自己去看一下相關代碼,這里就不再解讀了。

經過測試我們發現(代碼需要打開APP_QUEUE這個宏),當手機亮屏的時候(此時手機需要協調WiFi和藍牙活動),丟包現象時有發生;當手機息屏的時候,基本上沒有丟包現象發生,日志如下所示:

  

這是一個非常接近實際應用場景的代碼,大家可以直接參考它來完成自己的開發。這里要注意一點,由於環境的復雜性以及手機需要調度多種射頻活動,“丟包”不可避免,大家需要做的是,當“丟包”發生時,即上文的queue發生溢出時,該如何處理?一般而言,都是舍棄相關數據以讓程序可以正常往下跑。

上述所有代碼都已上傳到百度網盤,網盤地址見文章最開始的地方。

5. 如何測試BLE設備的穩定性

在開發藍牙設備固件的時候,不可避免需要用手機對其進行測試,尤其需要對其進行穩定性測試。一般而言,固件開發和手機app開發是相互獨立的,很多時候我們會碰到固件開發差不多了但手機app還沒有開發好,這種情況下怎么測試固件和手機交互的功能和穩定性?答案是nRF connect手機版。nRF connect很多功能都簡單明了,一看就會,大家可以用它們來做功能性測試。這里我們講一下nRF connect的宏錄制功能,大家可以用宏錄制功能來測試BLE通信的穩定性。強調一下,宏錄制功能目前只有安卓版nRF Connect支持,iOS版nRF Connect還不支持這個特性。

5.1 手機端宏錄制方式

相信到現在大家對BLE數據上傳機理和實踐有個大概的了解,那如何測試BLE數據下行性能,即怎么測試數據從手機傳到設備的穩定性和可靠性?我們是不是必須開發一款手機app來進行相關測試呢?其實不用的,大家可以直接使用Nordic的nRF connect手機版來與你的設備進行交互,從而完成基本功能驗證,同時可以使用nRF connect宏錄制功能來對設備進行穩定性測試和壓力測試。下面我們來講講宏錄制是怎么工作的。

所謂宏錄制,就是把你對nRF connect的操作錄制下來,然后通過宏播放實現自動化操作。由於nRF connect是一個容器,並支持JavaScript和HTML語法,宏其實就是一個XML腳本,nRF connect定義了自己的一套XML標簽操作,遵守這套XML標簽操作,就可以對nRF connect進行自動化操作。nRF connect支持的所有XML語法都在手機安裝目錄\Nordic Semiconductor中的示例中體現,只要示例中出現過的標簽就支持,相反示例中沒有的標簽就不支持。下面具體講一下宏錄制的操作過程。

當nRF connect連接設備成功后,你會發現右下角有一個紅點,那個就是宏錄制菜單。

  

點擊下面的紅點,我們開始宏錄制操作

 

然后我們按照普通操作來操作nRF connect,這些操作最終對應的BLE指令會被錄制下來,以便后續重復播放。我們先把“1234”發送給設備,如下:

  

發送完上述指令后,我們加一個300ms的延時,如下:

  

然后我們點擊完成按鈕,保存該宏,可以看出這個宏包括兩條操作:發送“1234”到設備,然后睡眠300ms。

 

將宏命名為“test”並保存:

 

到此宏已經錄制成功了,現在我們開始展示宏的神奇功能。如下,選擇循環播放模式,然后點擊“開始”按鈕開始循環播放該錄制宏。

  

大家可以看到,nRF connect先執行“Write 0x31323334 to RX characteristic”,然后睡眠300ms,然后又執行“Write 0x31323334 to RX characteristic”,如此循環往復。打開串口助手,你會發現設備已經收到了手機發過來的一連串“1234”,如下。

 

我們把剛才的test宏導出為XML,看一看它到底長什么樣:

復制代碼
<macro name="test" icon="PLAY">
   <assert-service description="Ensure Nordic UART Service" uuid="6e400001-b5a3-f393-e0a9-e50e24dcca9e">
      <assert-characteristic description="Ensure RX Characteristic" uuid="6e400002-b5a3-f393-e0a9-e50e24dcca9e">
         <property name="WRITE" requirement="MANDATORY"/>
      </assert-characteristic>
   </assert-service>
   <write description="Write 0x31323334 to RX Characteristic" characteristic-uuid="6e400002-b5a3-f393-e0a9-e50e24dcca9e" service-uuid="6e400001-b5a3-f393-e0a9-e50e24dcca9e" value="31323334" type="WRITE_REQUEST"/>
   <sleep description="Sleep 300 ms" timeout="300"/>
</macro>
復制代碼

大家可以看到,宏就是一些XML標記,大家也可以在此基礎上,去修改該XML文件,以實現更復雜的自動化測試,然后通過nRF connect把最新的XML文件裝載進來,就可以自動播放了。

如果你還想了解宏更多的用法信息,請參考:https://github.com/NordicSemiconductor/Android-nRF-Connect/blob/master/documentation/Macros/README.md 

5.2 電腦端XML方式

前面的宏錄制方式,功能還是比較單一,如果要實現更復雜的自動化測試,可以通過在PC端執行XML腳本方式來實現(這個XML格式跟上面的XML是有差別的)。通過安卓調試工具ADB,我們可以直接通過PC來操作nRF connect,這樣就可以讓nRF connect按照XML腳本意圖去執行相關自動化操作。nRF connect支持的所有XML語法都在手機安裝目錄中(手機內部存儲/ Nordic Semiconductor目錄)的示例中體現,只要示例中出現過的標簽就支持,相反示例中沒有的標簽就不支持。

欲了解更多信息請參考:https://github.com/NordicSemiconductor/Android-nRF-Connect/blob/master/documentation/Automated%20tests/README.md 

6. 開發手機端app代碼

Nordic提供很多手機端開源app供大家參考,用得最多的就是nRF Toolbox和nRF Blinky(注:nRF connect代碼不開源),在nRF Toolbox和nRF Blinky中都有相關的BLE操作庫,尤其是nRF Toolbox包含了很多BLE庫,比如BLE管理,DFU,數據透傳,藍牙Mesh等等,大家可以參考他們來開發自己的手機端app。

nRF Toolbox軟件界面如下所示:

 

UART就是前文說的NUS服務,除了nRF connect,其實大家也可以通過nRF Toolbox UART模塊來完成第2章所述的操作。nRF Toolbox另一個用的比較多的功能就是DFU,如果你需要通過手機BLE來實現設備固件的空中升級(OTA),那么可以參考nRF Toolbox DFU模塊來編寫你的手機端app軟件。

【轉】https://www.cnblogs.com/iini/p/9095622.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM