BLE-NRF51822教程3-sdk程式架構剖析
nordicBLE 技術交流群498676838
本講為架構介紹,不會牽涉到太多代碼細節。
51822的官方SDK其實是沒有架構依耐性的。什麼叫架構,比如TI的BLE SDK中就有一個作業系統抽象層(OSAL)他是一個輪訓的調度。你需要按照他的方式去建立任務等等。
而51822的SDK本質上只是提供了各種調用介面,比如開啟初始化協議棧,初始化一些硬體功能模組,開始廣播,發起連結等等。這些介面怎麼用完全取決於自己。不過一般韌體開發都是一些類似的流程各種資源的初始化,51822也不例外。所以sdk中的作為從機的例子main函數都是類似如下的步驟:
以官方的串口BLE 為例:
int main(void)
{
leds_init(); //非必須,只是該例子中用到了
timers_init(); //非必須,只是該例子中用到了
buttons_init(); //非必須,只是該例子中用到了
uart_init(); //非必須,只是該例子中用到了串口
ble_stack_init(); //必須
gap_params_init(); //必須
services_init(); //跟自己建立的服務相關,不同的服務細節不同但大體建立
//過程基本一致,通常在直接使用官方的例子修改一些參數即可
advertising_init(); //廣播資料初始化,必須
conn_params_init(); //是情況而定,如果串連後不需要串連參數的協商,該初始化也 //可不要
sec_params_init(); //安全參數初始化,如果沒用到配對綁定相關這個也可以不初始化
advertising_start(); //開啟廣播,必須
// Enter main loop
for (;;)
{
power_manage(); //進入睡眠
}
}
可以看到其實核心必要的只有這5個函數而已。你可以將其他代碼全都去掉,只要留下這5個函數裝置一樣可以運行,手機也能搜到裝置並與裝置通訊。
這種初始化的方式可以說是與我們一般的單片機開發沒有區別。
那麼初始化之後呢。以前的裸板單片機開發我們就是進入一個while迴圈執行一些周而復始的事,後面為了降低功耗開始在while(1)迴圈中加個睡眠代碼讓沒有工作時晶片處於睡眠狀態,並依靠中斷來喚醒從而處理到來的事物。
而上面的51822的main函數最後也是一個for{}迴圈,power_manage(); 內部代碼其實就是一個睡眠指令。Main函數到這裡就已經沒了,最後其實就是一個迴圈睡眠。這裡看不到任何任務(task),只有睡眠。那麼可想而知,51822的協議棧實現應該是基於”事件喚醒”的,也就是沒事的時候睡眠,有事的時候喚醒工作而後繼續睡眠。那麼那些處理事件的代碼都是在哪裡的?
那協議棧到底是怎麼運作的?
我希望建立一個服務在哪裡添加?
手機發送來的資料在哪裡?
我怎麼發送資料給手機?
下面一一解釋這些問題:
協議棧如何運作?
要明白協議棧怎麼運作,首先就要理解51822的協議棧是基於100%的事件驅動的。就是說協議棧向app發送的任何資料都是基於事件的。
比如裝置收到手機發來的連結請求,或是手機發過來的資料等等。協議棧首先收到這些資料後做一些處理,然後將這些資料(比如連結請求,或是普通資料等)打包成一個結構體,並附上事件ID,比如BLE_GAP_EVT_CONNECTED或BLE_GATTS_EVT_WRITE來分別告訴上層app這個事件結構體代表的事件。
比如BLE_GAP_EVT_CONNECTED代錶鏈接事件,那麼這個事件結構體中包含的資料就是串連參數等資料。
而BLE_GATTS_EVT_WRITE代表寫事件,那麼結構體中的資料就是對端裝置(比如手機)寫給板子的資料。
比如uart的demo中dispatch派發函數
static void ble_evt_dispatch(ble_evt_t *p_ble_evt)
{
ble_conn_params_on_ble_evt(p_ble_evt);
ble_nus_on_ble_evt(&m_nus, p_ble_evt);
on_ble_evt(p_ble_evt);
}
在任何與BLE相關的事件被協議棧上拋上來給app時,ble_evt_dispatch就會被調用。從而將事件拋給各個服務函數或處理模組,這裡是將事件拋給了
串連參數管理處理函數ble_conn_params_on_ble_evt
Uart服務的事件處理函數ble_nus_on_ble_evt (nus為Nordicuart server)
通用的事件處理函數on_ble_evt
不同的事件在事件結構體ble_evt_t中通過id來區別。不同是事件處理函數通常也只是處理自己感情去的事件,我們來看看ble_nus_on_ble_evt事件處理函數的內部
voidble_nus_on_ble_evt(ble_nus_t * p_nus,ble_evt_t * p_ble_evt)
{
if ((p_nus == NULL) || (p_ble_evt == NULL))
{
return;
}
switch (p_ble_evt->header.evt_id)
{
caseBLE_GAP_EVT_CONNECTED:
on_connect(p_nus, p_ble_evt);
break;
caseBLE_GAP_EVT_DISCONNECTED:
on_disconnect(p_nus, p_ble_evt);
break;
caseBLE_GATTS_EVT_WRITE:
on_write(p_nus, p_ble_evt);
break;
default:
// No implementation needed.
break;
}
}
可以看到,uart服務事件處理函數只關心三個事件,連結事件,取消連結事件以及寫事件(對端裝置發資料過來),不同的事件再針對做不同的,這個就由開發人員自己來實現了。比如對於串連事件通常應該記錄下事件結構體中的串連控制代碼,因為後續的BLE操作基本都要基於串連控制代碼(可以看做是兩個裝置通訊的通道ID,實際為鏈路層中的資料接入地址概念)。
PS: 事件是交給dispatch來派發給各個服務以及模組的,對於更底層的事件又是如何交給dispatch函數的過程請參考群公告中的 51822教程-協議棧概述教程。
解決了所謂的事件驅動再來解決:如果希望建立一個服務在哪裡添加?
在main函數的初始化過程中有一個services_init();這個函數的內部就是添加服務,添加特徵值等代碼。
函數內部其實就是註冊了一會回呼函數nus_data_handler(該函數會在手機發資料給板子時將資料從電腦串口列印出來) 然後再執行真正的初始化函數ble_nus_init。
該函數的內部又會調用sd_ble_gatts_service_add這個協議棧的api介面來添加服務。
後面也會調用sd_ble_gatts_characteristic_add這個協議棧的api介面來添加特徵值。
層次關係如下:
也就是說完成一個完整的服務建立函數其實只要sd_ble_gatts_service_add()和sd_ble_gatts_characteristic_add()這兩個核心函數。
通常建立服務並不需要自己去從頭寫過。而是直接賦值官方的這個services_init()函數,然後做一些小改動就可以。比如修改一下uuid, 修改一下讀/寫屬性,多添加一個特徵值等。要修改的其實很少。
下面解決最後兩個問題:手機發送來的資料在哪裡?我怎麼發送資料給手機?
要搞清楚這兩個問題,先來看一下群裡常問的幾個與上面相關的問題:
問:
手機發給51822裝置的資料在哪個函數裡出來的呀,
答:
沒有函數
協議棧會拋上來一個事件結構體
收到的資料在結構體中
問:
藍芽上傳函數,與下發函數都是一樣的嗎?都是服務API函數?
答:
只有上傳函數是伺服器用來將資料傳給用戶端的。
下發資料是藍芽晶片收到資料後,協議棧會拋上來一個有資料的事件結 構體。具體參看範例程式碼中的dispatch派發程式中各個事件處理函數對各 種事件的資料。
問:
sd_ble_gatts_hvx()這個函數是藍芽的發送函數,有知道藍芽的接收函數?
答:
藍芽沒有接收函數,藍芽的資料接收在底層,接收完後會返回事件給上層的 ble_evt_dispatch分發函數,它將事件分發給各個服務或者事件處理函數。服 務或處理函數會捕獲是否存在寫事件caseBLE_GATTS_EVT_WRITE:存在就做 相應的處理。收到的資料都在返回的事件結構體裡
其實看完這三個問題基本上上面的問題其實已經解決差不多了。作為從裝置,BLE的發送資料給手機是有API介面的,就是上面問到的sd_ble_gatts_hvx(),可以通過參數來設定是以通知方式發送還是指示方式發送(通知不需要回複確認,指示需要)。但是手機發過來資料卻是沒有接收函數,為什嗎?因為協議棧是基於事件驅動的!所以收到資料後協議棧會給上層app一個寫事件(指示對端裝置寫資料過來了),而寫過來的資料時在這個事件結構體中。我們只要提取出來就行了。所以沒有接收函數API。
從另一方面也可以解釋為什麼沒有接收資料函數。因為發送資料時”同步的”,是主動調用的,在往想發送資料的時候。但是接收資料時”非同步”,資料可能隨時到來,總不來一直調用一個函數然後原地等待資料到來吧,如果資料不來豈不是什麼事都幹不了了。所以接收是基於事件驅動的。有資料來再轉過去處理。
用個圖來解釋下:
如果還是覺得有點抽象,回到前面看看協議棧運作講解部分。應該更能體會所謂的事件驅動