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。
從另一方面也可以解釋為什麼沒有接收數據函數。因為發送數據時”同步的”,是主動調用的,在往想發送數據的時候。但是接收數據時”異步的”,數據可能隨時到來,總不來一直調用一個函數然後原地等待數據到來吧,如果數據不來豈不是什麼事都干不了了。所以接收是基於事件驅動的。有數據來再轉過去處理。
用個圖來解釋下:
如果還是覺得有點抽象,回到前面看看協議棧運作講解部分。應該更能體會所謂的事件驅動