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

 如何開發BLE數據透傳應用程序?什麼是BLE service和characteristic?如何開發本身的service和characteristic?如何區分ATT和GATT?有沒有什麼工具能夠對BLE設備進行壓力測試?如何提升BLE設備的數據上傳速度?本文將對以上問題進行解答。html

在不少應用場合,BLE只是做爲一個數據透傳模塊,即將設備端數據上傳給手機,同時接收手機端下發的數據。本文將和你們一塊兒,一步一步演示如何開發一個BLE透傳應用程序。按照本文的說明,你們能夠很快就實現一個BLE透傳應用,BLE透傳應用已是BLE應用中比較複雜的一種,一旦你們掌握了BLE透傳應用,其餘BLE應用開發就更不在話下了。本文還會以BLE透傳爲例子,來解釋BLE service和characteristic等概念,以幫助你們理解如何定義和開發本身的BLE service和characteristic等,從而完全理解BLE協議棧中的ATT和GATT的運行原理。而後,本文還將手把手教你們如何提升BLE數據傳輸速度(藍牙4.2的理論吞吐率大概爲100kB/s,而咱們實際達到了80kB/s,已經很是接近理論值)。最後,咱們將告訴你們如何使用安卓版nRF Connect來對你的BLE設備進行壓力測試,以測試設備的穩定性和可靠性。固然,文章的最後也會告訴你們如何找到安卓和iOS手機app開發參考代碼。git

 

1. 開發準備

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

2)     開發環境搭建。簡述以下(詳細說明請參考「Nordic nRF51/nRF52開發環境搭建」):編程

  1. 安裝Keil5 MDK
  2. 安裝SDK。若是你使用的是nRF52開發板,請安裝nRF5 SDK15.0.0,下載連接:https://www.nordicsemi.com/eng/nordic/download_resource/59012/70/52858981/116085。若是你手上是nRF51開發板,請下載nRF5 SDK12.3.0:https://www.nordicsemi.com/eng/nordic/download_resource/54280/56/38442131/32925nRF51最高SDK版本只能到12.3.0,後續SDK就再也不支持nRF51
  3. 安裝ARM CMSIS4.5.0,下載連接:https://github.com/ARM-software/CMSIS/releases/download/v4.5.0/ARM.CMSIS.4.5.0.pack
  4. 安裝Keil5 Device Family Pack,下載連接:https://www.nordicsemi.com/eng/nordic/download_resource/58865/28/26535159/87790
  5. 安裝nRF5 Command Line Tools,下載連接(Windows版):https://www.nordicsemi.com/eng/nordic/download_resource/58850/47/60411125/53210
  6. 安裝安卓版或者iOS版nRF connect。iOS版nRF connect請到蘋果app store下載,搜索「nRF」便可以找到。安卓版nRF connect能夠到Nordic Github官網上下載,下載連接爲:https://github.com/NordicSemiconductor/Android-nRF-Connect/releases
  7. 安裝PC版nRF connect或者nRFgo studio,兩個選其一便可。PC版nRF connect下載連接(Windows版):https://www.nordicsemi.com/eng/nordic/download_resource/58847/15/21277021/108233

注:若是你使用的是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程序:瀏覽器

1)     確認本身的芯片型號或者開發板。若是採用Nordic官方開發板的話,芯片型號和開發板編號對應關係以下:安全

  • nRF51系列對應開發板編號爲PCA10028
  • nRF52832和nRF52810對應開發板編號爲PCA10040。雖然52832和52810共用同一塊開發板,可是他們在SDK中的項目編號是不同的,52832對應PCA10040目錄,52810對應PCA10040e目錄,因爲52810和52832 PIN to PIN兼容,軟件也是徹底兼容的,所以SDK不少項目只有PCA10040的目錄,而沒有PCA10040e目錄,此時須要你本身來創建PCA10040e對應的目錄和工程,具體說明可參考:http://infocenter.nordicsemi.com/index.jsp?topic=%2Fcom.nordic.infocenter.sdk5.v15.0.0%2Fnrf52810_user_guide.html&cp=4_0_0_5_0
  • nRF52840對應開發板編號爲PCA10056
  • nRF52840 dongle編號爲PCA10059

這裏我會以nRF52832開發板PCA10040爲例來闡述整個開發過程,其餘開發板與之相似,你們本身能夠觸類旁通來開始本身的開發之旅。服務器

2)     將開發板與PC機經過USB線相連,同時打開開發板電源(將左下角的撥位開關打到「ON」位置),打開桌面版nRF Connect,選擇啓動「Programmer」應用,因爲驅動以前已經安裝好了,設備能夠當即識別成功。執行「full erase」操做,以擦除芯片原始內容。 架構

 

 

3)     打開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_packsapp

後續將以52832開發板爲例來闡述,51822與之相似就再也不闡述了。

 

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

 

 

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

 

 

4)     編譯程序。若是你已經按照以前的說明配置好了開發環境,那麼這裏編譯是不會報任何錯的。(若是你遇到了編譯錯誤,請從新按照前面說明去搭建你的開發環境,不要懷疑SDK例子代碼有問題哦)

5)     下載程序。程序下載包括2步:一先下載softdevice,二再下載應用。Softdevice是Nordic藍牙協議棧的名稱,整個開發過程當中只需下載一次。應用就是咱們這裏的ble_app_uart程序。若是你的開發板已經下載了其餘代碼,那麼最好先把開發板全擦一次,而後再下載softdevice和應用。

  • 芯片全擦(可選)。你可使用nRFgo studio,或者nRF connect桌面版,或者nrfjprog,三者選其一來執行擦除操做。
    • 使用nRFgo studio執行全擦操做

 

  • 使用nRF connect桌面版執行全擦操做

  

 

  • 使用nrfjprog執行全擦操做

 

 

  • 藍牙協議棧下載(整個開發週期只需下載一次)。在Keil ‘select target’下拉列表中,默認選擇的是Keil工程對應的Target,即‘nrf52832_xxaa’。咱們還能夠選擇另外一個target ‘flash_s132_nrf52_6.0.0_softdevice’,即softdevice對應的target,而後點擊「下載download」(不須要編譯哦!),此時會把softdevice下載到開發板中。

 

 

  • 應用下載。從新選擇Target:‘nrf52832_xxaa’,點擊「下載Download」,此時會把ble_app_uart應用程序下載到開發板中。此時開發板的LED1閃爍,表示程序運行正常。

6)     鏈接手機。打開手機藍牙和手機版nRF connect。在nRF connect中,你將看到一個廣播設備:Nordic_UART,這個就是開發板的廣播名字。點擊「CONNECT」,手機將與設備創建鏈接,並開始服務發現過程,鏈接成功後,LED1熄滅,LED2點亮,最後將獲得以下界面。

 

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

7)     測試NUS服務。ble_app_uart使用串口與上位機交互,選擇一款串口助手軟件,好比Putty,打開該串口軟件,並作以下設置:

  • Baud rate: 115.200
  • 8 data bits
  • 1 stop bit
  • No parity
  • HW flow control: None

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

 

 

按照第6)步,從新將開發板連上手機,而後點擊右上角的「Enable CCCDs」以使能notification,以下所示:

 

 

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

 

     此時設備經過串口打印出剛纔接收到的數據,以下所示:

 

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

 

 

                注:若是你的串口助手發不出「\n」換行符,那麼你須要最少輸入MTU-3個字符,設備纔會把收到的所有字符經過藍牙發出去

 

經過上面的測試,你們能夠發現Nordic SDK已經把藍牙數據透傳服務作好了,你們能夠直接拿過來使用,下面將對其工做原理進行闡述,最後在Nordic藍牙透傳例子ble_app_uart上進行二次開發,以增長一些其餘有用功能。若是你們以爲Nordic ble_app_uart已經能夠知足本身的需求,並且也不想花時間去研究裏面的原理,那麼章節3/4/5/6/7.1能夠略過不看。

3. BLE client/server(C/S) 架構

        BLE採用了client/server (C/S)架構來進行數據交互,C/S架構是一種很是常見的架構,在咱們身邊隨處可見,好比咱們常常用到的瀏覽器和服務器也是一種C/S架構,這其中瀏覽器是客戶端client,服務器是服務端server,server好比淘寶服務器,提供商品信息,廣告,社交等服務,而瀏覽器,好比微軟的IE,就能夠用來請求這些服務,並使用server提供的服務。BLE與此相似,通常而言設備提供服務,所以設備是server,手機使用設備提供的服務,所以手機是client。好比藍牙體溫計,它能夠提供「體溫」數據服務,所以是一個server,而手機則能夠請求「體溫」數據以顯示在手機上,所以手機是一個client。

        服務是以數據爲載體的,因此說server提供服務其實就是提供各類有價值的數據。

 

上圖所示的Request和Response其實就是咱們常常說的ATT命令(ATT PDU),也就是說Client和Server之間經過ATT PDU進行交互。另外,一個數據「37」,有多是說體溫「37度」,也有多是說心率「37次」或者溼度「37%」,所以Server須要將數據進行包裝和分類,在BLE中,數據是經過characteristic進行包裝的,並且多個characteristic組成一個service,service是一個獨立的服務單元,或者說service是一個基本的BLE應用。所以咱們能夠把上圖細化爲:

 

若是某個service是一個藍牙聯盟定義的標準服務,也能夠稱其爲profile,好比HID/心率計/體溫計/血糖儀等,都是標準藍牙服務,所以都有相應的profile規格書。

 

4. BLE service, characteristic以及CCCD

如文章「深刻淺出低功耗藍牙(BLE)協議棧」所講,BLE協議棧架構以下所示:

 

        如上圖所示,用戶開發應用程序或者說service的時候,調用的都是GATT API,而GATT又調用了ATT API,前面也講過,BLE數據最終都是經過ATT PDU來傳輸的,那麼爲何還須要GATT層?直接操做ATT層不也能夠達到一樣的目的嗎?

        前面也提過,Server是經過characteristic來表示數據的,雖然一條數據最有價值的部分是它的值(value),可是僅有value是不夠,好比27,究竟是表示27°溫度仍是27%溼度;若是表示的是溫度,那麼它的單位是攝氏度仍是華氏度。同時每一個value還有相應的讀寫屬性以及權限屬性,所以一個characteristic包含三種條目:characteristic聲明,characteristic的值以及characteristic的描述符(能夠有多個描述符),以下所示:

 

        因爲一個service能夠包含多個characteristic,characteristic declaration就是每一個characteristic的分界符,解析時一旦遇到characteristic declaration,就能夠認爲接下來又是一個新的characteristic了,同時characteristic declaration還將包含value的讀寫屬性等。Characteristic value就是數據的值了,這個比較好理解就再也不說了。Characteristic descriptor就是數據的額外信息,好比溫度的單位是什麼,數據是用小數表示仍是百分比表示等之類的數據描述信息。CCCD是一種特殊的characteristic descriptor,通常而言,都是client來訪問server的characteristic,咱們把這種操做稱爲讀或者寫。另外,server能夠直接把本身的characteristic的值告訴client,咱們稱其爲notify或者indicate,跟read操做相比,只有須要傳輸數據的時候或者說只有當數據有效時,server纔開始notify或者indicate數據到client,所以這種操做方式能夠大大節省server的功耗。有時候client不想監聽characteristic notify或者indicate過來的數據,那麼就可使用CCCD來關閉characteristic的notify或者indicate功能;若是client又須要監聽characteristic的notify或者indicate,那麼它能夠從新使能CCCD來打開相關操做。總結一下,當characteristic具備notify或者indicate操做功能時,那麼必須爲其添加相應CCCD,以方便client來使能或者禁止notify或者indicate功能。

        無論是characteristic declaration,characteristic value仍是characteristic descriptor,實現的時候,咱們都是用attribute來表達的,也就是說,他們每個都是一個attribute,attribute能夠用下圖來表示:

 

  • Attribute handle,Attribute句柄,16-bit長度。Client要訪問Server的Attribute,都是經過這個句柄來訪問的,也就是說ATT PDU通常都包含handle的值。用戶在軟件代碼添加characteristic的時候,系統會自動按順序地爲相關attribute生成句柄。
  • Attribute type,Attribute類型,2字節或者16字節長。在BLE中咱們使用UUID來定義數據的類型,UUID是128 bit的,因此咱們有足夠的UUID來表達萬事萬物。其中有一個UUID很是特殊,它被藍牙聯盟採用爲官方UUID,這個UUID以下所示:

 

 因爲這個UUID衆所周知,藍牙聯盟將本身定義的attribute或者數據只用16bit UUID來表示,好比0x1234,其實它也是128bit,完整表示爲:

 

Attribute type通常是由service和characteristic規格來定義,站在藍牙協議棧角度來看,ATT層定義了一個通訊的基本框架,數據的基本結構,以及通訊的指令,而GATT層就是前文所述的service和characteristic,GATT層用來賦予每一個數據一個具體的內涵,讓數據變得有結構和意義。換句話說,沒有GATT層,低功耗藍牙也能夠通訊起來,但會產生兼容性問題以及通訊的低效率。

  • Attribute value,就是數據真正的值,0到512字節長。
  • Attribute permissions,Attribute的權限屬性,權限屬性不會直接在空中包中體現,而是隱含在ATT命令的操做結果中。假設一個attribute read屬性設爲open(即讀操做不須要任何權限),那麼client去讀這個attribute時server將直接返回attribute的值;若是這個attribute read屬性設爲authentication(即須要配對才能訪問),若是client沒有與server配對而直接去訪問這個attribute,那麼server會返回一個錯誤碼:告訴client你的權限不夠,此時client會對server發起配對請求,以知足這個attribute的讀屬性要求。目前主要有以下四種權限屬性:
    • Open,直接能夠讀或者寫
    • No Access,禁止讀或者寫
    •  Authentication,須要配對才能讀或者寫,因爲配對有多種類型,所以authentication又衍生多種子類型,好比帶不帶MITM,有沒有LESC
    • Authorization,跟open同樣,不過server返回attribute的值以前須要應用先受權,也就是說應用能夠在回調函數裏面去修改讀或者寫的原始值。
    • Signed,簽名後才能讀或者寫,這個用得比較少。

         你們還記不記得設備與手機nRF connect鏈接成功後呈現的界面,我這裏再貼一下:

 

         能夠看到手機呈現的就是上文講的service和characteristic,nRF Connect爲了讓整個界面變得更美觀,將訪問屬性,UUID,handle都分列來表示了,以至於不少初學者會把理論和現實兩者對應不起來。Nordic以前推出過一款Master Control Panel(MCP),MCP如今已經不推薦使用了,不過MCP有一個好處,它對service和characteristic的組織方式更接近底層實現方式,對你們理解service和characteristic是很是有幫助的。仍是這個設備,我用MCP跟它鏈接並進行服務發現,你會發現它呈現的界面以下所示:

 

這個圖就跟上面講的理論知識能夠一一對應起來了,NUS包含2個characteristic:RX和TX,每個條目都是一個attribute,NUS服務自己就是一個attribute,而RX characteristic自己又包含2條attribute:一條是declaration attribute,一條是value自己attribute。因爲TX支持notify,因此它包含3條attribute,另一條attribute是CCCD。每一個attribute都有一個handle和UUID,handle用來訪問該attribute,UUID用來指明該attribute的類型。能夠說,server提供數據,而數據是由attribute來表達,全部attribute組成一個attribute table,設備支持的服務不一樣,attribute table就不一樣。這裏說明一下,當你在Nordic已有例程基礎上再去添加新的服務或者刪除已有的服務,記得必定要去修改ATTR_TAB_SIZE那個宏,不然協議棧初始化會有問題。

       

5. 經常使用ATT命令

        Client和Server之間是經過ATT PDU來通訊的,ATT PDU主要包括4類:讀,寫,notify和indicate。若是一個命令須要response,那麼會在相應命令後面加上request;若是一個命令只須要ACK而不須要response,那麼它的後面就不會帶request。這裏要特別強調一點,BLE全部命令都是「必達」的,也就是說每一個命令發出去以後,會立馬等ACK信息,若是收到了ACK包,發起方認爲命令完成;不然發起方會一直重傳該命令直到超時致使BLE鏈接斷開。換句話說,只要你的BLE沒有斷開,那麼你以前發送的數據包,無論它是用什麼ATT PDU來發送的,它確定被對方收到了。我估計不少人對此會產生疑問,由於他們常常碰到丟包的狀況,其實你們常常碰到的「丟包」,不是空中把包丟了或者包在空中被幹擾了,而是你們發送的代碼寫得有問題,致使你要發送的包沒有被安全送達到協議棧射頻FIFO中,因此之後你們碰到丟包狀況,請先檢查你的代碼,保證你的數據包正確完整安全地送達到協議棧射頻FIFO中,只要數據包放到了協議棧射頻FIFO中,藍牙協議棧就能保證該數據包「必達」對方。既然每一個ATT命令都必達對方,那麼還須要request作什麼?若是一個命令帶有request後綴,那麼發起方就能夠收到命令的response包,這個response包在應用層是有回調事件的,而前述的ACK包在應用層是沒有回調事件的。因此採用request/response方式,應用層能夠按順序地發送一些數據包,這個在不少應用場合是很是有用的。相反,若是你對應用層數據包的順序沒有要求,那麼就能夠不使用request/response形式。另外Request/response有一個反作用:大大下降通訊的吞吐率,由於request/response必須在不一樣的鏈接間隔中出現,也就是說,你在間隔1中發送了一個request命令,那麼response包必須在間隔2或者稍後間隔中回覆,而不能在間隔1中回覆,這就致使兩個鏈接間隔最多隻能發一個數據包,而不帶request後綴的ATT命令就沒有這個問題,在同一個鏈接間隔中,你能夠同時發多個數據包,這樣將大大提升數據的吞吐率。你們能夠參考下圖來理解request和非request命令的區別:

 

經常使用的帶request的命令:全部read命令,write request,indication等,而經常使用的不帶request的命令有write command,notification等,完整的ATT命令列表以下所示:

 

6. 設備端固件代碼一覽

如今咱們一塊兒來看一下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_TOTAL_LINK_COUNT  //一共同時能夠支持多少個鏈接

NRF_SDH_BLE_PERIPHERAL_LINK_COUNT  //做爲從模式的鏈接同時能有幾個

NRF_SDH_BLE_CENTRAL_LINK_COUNT  //做爲主模式的鏈接同時能有幾個

NRF_SDH_BLE_GATT_MAX_MTU_SIZE //MTU size爲多大

NRF_SDH_BLE_VS_UUID_COUNT  //用戶自定義的base UUID有幾個

NRF_SDH_BLE_GATTS_ATTR_TAB_SIZE  //Attribute table總共佔多少協議棧RAM空間

NRF_SDH_BLE_SERVICE_CHANGED  //要不要包含service change characteristic

nrf_sdh_ble_enable真正使能BLE功能,它的參數ram_start既是一個輸入參數又是一個輸出參數,做爲輸入參數,系統自動會把以下的RAM起始地址傳入:

 

同時nrf_sdh_ble_enable會把當前softdevice配置狀況下,它實際須要佔用的RAM空間經過ram_start返回,若是這個返回值不等於輸入值,那麼用戶須要把上圖的IRAM1起始地址修改爲它的返回值。其中NRF_SDH_BLE_GATTS_ATTR_TAB_SIZE這個宏的取值是須要用戶不斷去試錯的,所以每當你添加了或者刪除了BLE service,都須要去調整NRF_SDH_BLE_GATTS_ATTR_TAB_SIZE這個宏的值,而後去查看nrf_sdh_ble_enable的返回值,看看這個參數的取值是否合理

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,前面講了那麼多的概念和理論,如今咱們就來看看services_init是如何作到跟理論一致的。services_init經過ble_nus_init添加了一個藍牙數據透傳服務:NUS,那ble_nus_init是怎麼將NUS服務添加成功的呢?查看ble_nus_init函數體,你會發現它是分三步來作的:

  1. 添加服務的UUID。若是是藍牙標準服務,這步能夠省略。因爲NUS不是藍牙聯盟定義的,因此須要調用sd_ble_uuid_vs_add以增長一個供應商自定義的UUID。
  2. 添加服務自己。直接調用sd_ble_gatts_service_add就能夠完成。
  3. 添加服務下面的characteristics。server的characteristic通常都是經過sd_ble_gatts_characteristic_add來添加的。以NUS的RX characteristic爲例,能夠看到:
sd_ble_gatts_characteristic_add(p_nus->service_handle,  &char_md,  &attr_char_value, &p_nus->rx_handles);

其中,p_nus->service_handle表示該characteristic屬於那個service,p_nus->rx_handles是輸出值,由協議棧返回,之後訪問該characteristic都是經過這個句柄來完成,attr_char_value這個是characteristic的value,char_md這個是characteristic的元數據(metadata),前面第4章也講過,一個數據除了有value這個characteristic以外,它還包含其餘attribute,而這些attribute所有都用char_md來表示,好比這個characteristic value能支持的ATT命令類型,CCCD信息,descriptor信息等,這裏要特別指出的是,只有當支持notify或者indicate時,才須要提供cccd_md信息,其餘ATT命令不須要cccd_md信息,因此RX characteristic的char_md以下所示,它同時支持write和write request兩種寫命令,因爲它不支持notify或者indicate,因此cccd_md爲NULL。

 

attr_char_value是一個attribute,因此它包含attribute metadata,以下:

 

attr_char_value具體包含的value信息由如下成員表示:

 

因爲這裏把characteristic value放在了協議棧RAM中,因此協議棧會自動爲這個value建立一個buffer。若是你想把characteristic value放在用戶RAM中,即vloc = BLE_GATTS_VLOC_USER,那麼這裏你還須要把一個全局數組變量賦給attr_char_value. p_value。

TX characteristic與之相似,就再也不額外解讀了。

這裏須要特別提醒你們的是,雖然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)))

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狀態),其他時間系統都是處於idle狀態的,從而大大節省系統功耗。

 

7. 定製你的BLE數據透傳應用程序

7.1 BLE數據上傳吞吐率

如何快速的把大量數據上傳給手機?這是一個很常見的應用場合,如今咱們嘗試去修改一下Nordic的原生例程,以實現最高的數據吞吐率。下面咱們經過幾種不一樣的方法來看看每種方法下它的吞吐率能到多少。

方法1:(經過宏METHOD1來開關)

藍牙spec規定,藍牙鏈接間隔最小隻能爲7.5m,爲了達到最高的吞吐率,咱們建立一個timer,讓其每7ms發一次數據,看一看此時吞吐率能達到多少。7ms中斷服務函數代碼以下所示:

static void throughput_timer_handler(void * p_context)

{

    UNUSED_PARAMETER(p_context);

    ret_code_t err_code;

    uint16_t length;

    m_cnt_7ms++;

    length = m_ble_nus_max_data_len;

    if (m_conn_handle != BLE_CONN_HANDLE_INVALID)

    {

        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);

//      }             

        m_len_sent += length;           

        m_data_array[0]++;

        m_data_array[length-1]++;         

     }

     NRF_LOG_INFO("time: %d *7ms == bytes send: %d Bytes == avg speed: %d B/s",m_cnt_7ms,m_len_sent,m_len_sent/(m_cnt_7ms*7));   

}

 

這種作法會致使ble_nus_data_send報「NRF_ERROR_RESOURCES」錯誤,這個錯誤表示協議棧無資源應付這麼快的調用速度。爲此咱們對ble_nus_data_send返回的錯誤值一律不進行處理,看看會發生什麼?咱們發現程序能夠正常運行,RTT viewer打印的日誌以下所示:

 

由上圖可知,數據上傳吞吐率達到了34.8kB/s,其實這個吞吐率是假的,由於中間丟了不少包,但計算吞吐率的時候把丟的包也算進去了。以下圖所示,0x6E以後應該爲0x6F,但實際發送的數據包編號爲0x83,丟包很是嚴重。

 

   爲了防止所謂的「丟包」(前面也提過,這裏的丟包不是數據包在空中丟掉了,而是數據包沒有安全送到協議棧的buffer中,從而致使丟包),咱們加上以下if語句,只有ble_nus_data_send返回正確時,才認爲數據包正確發送,而後才能算入到throughput中:

  if (err_code == NRF_SUCCESS)

  {

             m_len_sent += length;  

             m_data_array[0]++;

             m_data_array[length-1]++;                                                      

    }

 

經過查看nRF connect日誌,你會發現此時不會發生丟包了,但吞吐率直接降到了1.6kB/s左右。

方法1+:(經過宏METHOD1_PLUS來開關)

咱們對方法一稍做調整,首先咱們持續往發送buffer寫數據,直到返回值不是NRF_SUCCESS

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); 

而後咱們把鏈接間隔設爲儘量小,以期提升吞吐率,以下:

#ifdef CONN_INTERVAL_OPTIMIZE

#define MIN_CONN_INTERVAL               MSEC_TO_UNITS(8, UNIT_1_25_MS)   

#define MAX_CONN_INTERVAL               MSEC_TO_UNITS(12, UNIT_1_25_MS)

#endif

 

這種方法吞吐率能達到10kB/s,但離咱們的目標仍是很遠。

最後咱們把connection event length extension和data length extension都打開(咱們將在方法2+中詳細闡述這2個有效提升吞吐率的利器),即定義以下宏:

 

能夠看到吞吐率將達到70kB/s,這個吞吐率仍是不錯的。但仔細查看nRF connect日誌,你會發現這種模式下仍是有小几率事件會致使「丟包」發生,並且整個發送邏輯也不是很優化,爲此咱們想到了METHOD2.

方法2:(經過宏METHOD2來開關)

ble_nus_data_send每次成功發送數據包,都會產生一個BLE_NUS_EVT_TX_RDY事件,收到這個事件後,再去調用ble_nus_data_send,丟包的狀況就不會再發生了,核心代碼以下所示:

 

if (p_evt->type == BLE_NUS_EVT_TX_RDY)

{

#ifdef METHOD2

    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]++;

     }

     NRF_LOG_INFO("time: %d *10ms == bytes send: %d Bytes == avg speed: %d B/s",m_cnt_10ms,m_len_sent,m_len_sent * 100/m_cnt_10ms);

#endif

你們能夠本身去查看一下nRF  Connect的數據log,這種方式是沒有丟包的,可是打開RTT viewer,你會發現他的吞吐率低得可憐,只有1kB/s。

 

方法2+:(經過宏METHOD2_PLUS來開關)

與方法1+相似,咱們在方法2基礎上,持續往發送buffer送數據直到返回值不爲0,以下:

#ifdef METHOD2_PLUS

//queue multiple tx array

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);

NRF_LOG_INFO("time: %d *10ms == bytes send: %d Bytes == avg speed: %d B/s",m_cnt_10ms,m_len_sent,m_len_sent * 100/m_cnt_10ms);

#endif

 

若是須要支持長的MTU的話,那麼須要修改gap event length。gap event length是指一個鏈接間隔中最多能給某一個設備數據交互的時間長度,若是gap event length設爲1ms,而MTU設爲247的話,那麼協議棧會報配置錯誤。若是gap event length遠大於一個數據長包的長度,那麼在一個鏈接間隔中就能夠傳送多個長包。有的人爲了省事,就把gap event length設的很大,好比大於或者等於鏈接間隔,這個配置自己是沒什麼問題的,可是若是一個設備要跟多個設備相連,那麼這種配置就會使得設備鏈接數有限或者其餘設備帶寬有限,好比不能同時連20個設備,好比其餘設備傳輸速度很慢。因爲咱們如今是一對一的鏈接,偷點懶,我們把gap event length修改爲一個合適的值,以使其儘量佔滿整個鏈接間隔,以下將gap event length修改成30ms

#define NRF_SDH_BLE_GAP_EVENT_LENGTH 24

 

注意:爲了兼容多鏈接以及保證其餘設備的帶寬,咱們通常建議gap event length就使用SDK默認配置:6,這個默認配置已經能夠發送一個241字節的MTU長包了,但只能發送一個,爲了在一個鏈接間隔中發送多個長包,能夠在不修改gap event length的狀況下,經過使能connection event length的作法,以達到一樣的目的,以下面代碼所示:

#ifdef EVT_LEN_EXT_ON

    ble_opt_t  opt;

    memset(&opt, 0x00, sizeof(opt));

    opt.common_opt.conn_evt_ext.enable = true;

    err_code = sd_ble_opt_set(BLE_COMMON_OPT_CONN_EVT_EXT, &opt);

    APP_ERROR_CHECK(err_code);

#endif

 

而後咱們再將鏈接間隔設爲一個合適的值,以保證上述connection event能夠佔據整個鏈接間隔。注意:不是鏈接間隔越短越好,而是整個鏈接間隔中空閒時間越短越好,也就是說,哪怕鏈接間隔比較長,若是能保證connection event/connection interval最大,那麼就有可能達到最大的吞吐率。

#ifdef CONN_INTERVAL_OPTIMIZE

#define MIN_CONN_INTERVAL               MSEC_TO_UNITS(8, UNIT_1_25_MS)   

#define MAX_CONN_INTERVAL               MSEC_TO_UNITS(12, UNIT_1_25_MS)

#endif

 

我如今使用的是華爲P9手機,它將把MTU設爲241,在DLE不開的狀況下(此時鏈路層每一個數據包的長度仍是隻有27個字節!),咱們能夠看到throughput能夠達到10kB以上,以下:

 

 

而後咱們再打開DLE功能,此時鏈路層每一個數據包的長度將變成251字節,以下:

#ifdef DLE_ON

        case BLE_GAP_EVT_DATA_LENGTH_UPDATE_REQUEST:

        {

            NRF_LOG_DEBUG("DLE update request.");

            ble_gap_data_length_params_t dle_param;

            memset(&dle_param, 0, sizeof(ble_gap_data_length_params_t));   //0 means auto select DLE                                                                    

            err_code = sd_ble_gap_data_length_update(p_ble_evt->evt.gap_evt.conn_handle, &dle_param, NULL);

            APP_ERROR_CHECK(err_code);

        } break;

#endif

 

此時咱們能夠看到throughput能夠達到77kB/s,離藍牙4.2的理論throughput已經很接近了。這裏特別須要指出的是,當DLE使能狀況下,connection interval不是越小吞吐率越高,我這裏使用的connection interval大概爲10ms,若是你們把這個connection interval提升到30ms,有可能吞吐率更高,這裏就再也不演示了。

 

 

 

 

 

上述代碼工程已經上傳到百度雲盤中,有須要的同窗能夠到以下連接下載:

下載「tutorial_ble_app_uart_SDK15_0_0.rar」,而後解壓縮到SDK15.0.0以下目錄下:nRF5_SDK_15.0.0_a53641a\examples\ble_peripheral,便可成功編譯運行。

 

7.2使用安卓版nRF connect測試BLE設備的穩定性和可靠性

先說明一下,如下內容只能經過安卓版nRF Connect來實現,iOS版nRF Connect不支持以下特性。

手機端宏錄製方式

相信到如今你們對BLE數據上傳機理和實踐有個大概的瞭解,那如何測試BLE數據下行性能,即怎麼測試數據從手機傳到設備的穩定性和可靠性?咱們是否是必須開發一款手機app來進行相關測試嗎?答案是否認的,感謝Nordic給咱們帶來了nRF connect,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

 

電腦端XML方式

前面的宏錄製方式,功能仍是比較單一,若是要實現更復雜的自動化測試,能夠經過在PC端執行XML腳本方式來實現。經過安卓調試工具ADB,咱們能夠直接經過PC來操做nRF connect,而nRF connect又能識別XML腳本,這樣就可讓nRF connect按照XML腳本意圖去執行相關自動化操做。nRF connect支持的全部XML語法都在手機安裝目錄中(手機內部存儲/ Nordic Semiconductor目錄)的示例中體現,只要示例中出現過的標籤就支持,相反示例中沒有的標籤就不支持。

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

 

8. 開發手機端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模塊來編寫你的手機端軟件。

相關文章
相關標籤/搜索