如何開發BLE數據透傳應用程序?什麼是BLE
service和characteristic?如何開發自己的service和characteristic?如何區分ATT和GATT?有沒有什麼工具可以對BLE設備進行壓力測試?如何提高BLE設備的數據上傳速度?本文將對以上問題進行解答。
在很多應用場合,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開發參考代碼。
1. 開發準備
1) Nordic nRF52或者nRF51開發板1塊。請參考「
Nordic nRF51/nRF52開發流程說明」,購買相應開發板(DK)。
2) 開發環境搭建。簡述如下(詳細說明請參考「
Nordic nRF51/nRF52開發環境搭建」):
- 安裝Keil5 MDK
- 安裝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/32925 (nRF51最高SDK版本只能到12.3.0,後續SDK就不再支持nRF51了)
- 安裝ARM CMSIS4.5.0,下載鏈接:https://github.com/ARM-software/CMSIS/releases/download/v4.5.0/ARM.CMSIS.4.5.0.pack。
- 安裝Keil5 Device Family Pack,下載鏈接:https://www.nordicsemi.com/eng/nordic/download_resource/58865/28/26535159/87790。
- 安裝nRF5 Command Line Tools,下載鏈接(Windows版):https://www.nordicsemi.com/eng/nordic/download_resource/58850/47/60411125/53210。
- 安裝安卓版或者iOS版nRF connect。iOS版nRF connect請到蘋果app store下載,搜索「nRF」即可以找到。安卓版nRF connect可以到Nordic Github官網上下載,下載鏈接為:https://github.com/NordicSemiconductor/Android-nRF-Connect/releases
- 安裝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官方開發板的話,芯片型號和開發板編號對應關係如下:
這裡我會以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_packs
後續將以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,三者選其一來執行擦除操作。
- 藍牙協議棧下載(整個開發週期只需下載一次)。在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函數體,你會發現它是分三步來做的:
- 添加服務的UUID。如果是藍牙標準服務,這步可以省略。由於NUS不是藍牙聯盟定義的,所以需要調用sd_ble_uuid_vs_add以增加一個供應商自定義的UUID。
- 添加服務本身。直接調用sd_ble_gatts_service_add就可以完成。
- 添加服務下面的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模塊來編寫你的手機端軟件。