BLE-NRF51822教程4-串列埠BLE解析
本講逐行程式碼解析官方串列埠BLE例程demo
PS: 基於SDK5.1
主要分一下幾個部分:
1 :Main函式的整體註釋
2 :函式單獨解析。
3 :接收串列埠資料併發送給對端裝置
4 :接收手機資料並通過串列埠列印
Ps :第一和第二部分我在教程工程初始化流程中已經詳細說明這裡直接複製過來,做了一些修改以及添加了關於新增服務和新增特徵值的講解,如果之前看過可以直接看下 2函式單獨解析中的 服務初始化後面新增的內容即可
一:main函式整體註釋:
int main(void)
{
//初始化LED指示燈,用來指示廣播和連線狀態
leds_init();
//初始化軟體定時器模組
timers_init();
//設定按鍵作為 DETECT signal 用來喚醒system off模式,具體參看資料手冊power 章節
buttons_init();
//主要設定uart的引腳,波特率。接收,傳送中斷等。並開啟uart模組中斷
uart_init();
//協議棧初試化,設定時鐘,demo裡面設定為外部時鐘。並且註冊事件派發函式
ble_stack_init();
//GAP一些引數的設定,設定裝置名,設定PPCP(外圍裝置首選連結引數)。(手機連上某個藍芽裝置後可以從Generic Access Service中看到設定的這些引數)
gap_params_init();
//服務初始化。新增uart的串列埠服務。主要提供兩個特徵值來供手機和板子以及電腦的通訊
services_init();
//設定廣播資料以及掃描響應資料
advertising_init();
//連結引數設定。主要設定什麼時候發起更新連結引數請求以及間隔和最大嘗試次數。
conn_params_init();
//安全引數初始化。
sec_params_init();
simple_uart_putstring(START_STRING);
//設定廣播型別,白名單,間隔,超時等特性。並開始廣播。
advertising_start();
for (;;)
{
//電源管理,呼叫arm0的指令__WFE();進入睡眠
power_manage();
}
}
二:函式單獨解析:
1 leds_init
static void leds_init(void)
{
nrf_gpio_cfg_output(ADVERTISING_LED_PIN_NO);
nrf_gpio_cfg_output(CONNECTED_LED_PIN_NO);
}
設定的PIN_CONFIG暫存器使能兩個引腳的作為輸出功能。用來當做指示燈指示廣播和連結的狀態。
2 timers_init
static void timers_init(void)
{
// Initialize timer module
APP_TIMER_INIT(APP_TIMER_PRESCALER, APP_TIMER_MAX_TIMERS, APP_TIMER_OP_QUEUE_SIZE, false);
}
初始化軟體定時器模組,該定時器模組並不是使用timer0-2來實現定時功能。而是使用51822中的RTC1 來軟體模擬出定時器模組。RTC1使用32.768K時鐘經過分頻後是時鐘來作為時鐘源。所以該函式內部實現就是設定RTC1相關的暫存器和做一些初始化。其原理和timer 定時/計數器模組類似。具體細節參考晶片資料手冊。
APP_TIMER_PRESCALER:設定分頻係數。(以32.768K來分頻)
APP_TIMER_MAX_TIMERS:設定可以建立的最大定時器個數
APP_TIMER_OP_QUEUE_SIZE:定時器操作佇列,因為是用RTC模擬的軟體定時器,因此內部 是維護了一個軟體定時器的操作佇列
False:不使用排程,排程模組沒有細看。51822關於排程的很多都是傳False不使用調 度。
3buttons_init
static void buttons_init(void)
{
nrf_gpio_cfg_sense_input(WAKEUP_BUTTON_PIN,
BUTTON_PULL,
NRF_GPIO_PIN_SENSE_LOW);
}
這裡的按鍵設定比較簡單,主要通過PIN_CNF暫存器來設定一個IO口來作為來作為sensing mechanism機制的引腳。這裡是設定了WAKEUP_BUTTON_PIN這個引腳來作為這個功能,設定成低電平時觸發這個機制。而這個機制類似一個wakeup機制,當其被觸發時會產生一個DETECT signal而這個訊號會將cpu從system off模式中喚醒。
4 uart_init
static void uart_init(void)
{
simple_uart_config(RTS_PIN_NUMBER, TX_PIN_NUMBER, CTS_PIN_NUMBER, RX_PIN_NUMBER, HWFC);
NRF_UART0->INTENSET = UART_INTENSET_RXDRDY_Enabled<<uart_intenset_rxdrdy_pos; </uart_intenset_rxdrdy_pos;<>
NVIC_SetPriority(UART0_IRQn, APP_IRQ_PRIORITY_LOW);
NVIC_EnableIRQ(UART0_IRQn);
/**@snippet [UART Initialization] */
}
初始化uart設定輸入輸出引腳,是否關閉流控。一般使用官方例子的時候都要先將流控關掉,HWFC為False。然後開啟uart的接收中斷,開啟uart模組的中斷功能,以及設定優先順序。 波特率在simple_uart_config中設定,該函式設定完引腳後使能uart,開啟uart的接收和傳送功能。
5 ble_stack_init
static void ble_stack_init(void)
{
// Initialize SoftDevice.
SOFTDEVICE_HANDLER_INIT(NRF_CLOCK_LFCLKSRC_XTAL_20_PPM, false);
// Subscribe for BLE events.
uint32_t err_code = softdevice_ble_evt_handler_set(ble_evt_dispatch);
APP_ERROR_CHECK(err_code);
}
設定LFCLK(32.768K)的時鐘源(協議棧需要使用),這裡設定為外部晶振。False為不使用排程。softdevice_ble_evt_handler_set(ble_evt_dispatch);註冊事件派發程式,基礎1-協議棧概述說明過,當BLE收到廣播,連結請求,對端裝置資料等後底層處理完會上拋給上冊app一個事件,這個事件的上拋過程是協議棧觸發SWI中斷,在中斷內部將事件放入佇列,然後呼叫app中的SWI中斷。App中的SWI中斷會get佇列中的事件,並最終會呼叫註冊的ble_evt_dispatch函式,這個函式再將事件發給各個服務以及模組的事件處理函式來處理各個服務及模組自己感興趣的事件。相關原理基礎1-協議棧概述視訊教程中有說明。
6gap_params_init
設定必要的裝置的GAP引數。
static void gap_params_init(void)
{
uint32_t err_code;
ble_gap_conn_params_tgap_conn_params;
ble_gap_conn_sec_mode_tsec_mode;
//設定裝置名的寫許可權為普通模式,則手機掃描到裝置連線上後可以在第一個服務Geneic Access Service(有的只顯示UUID為1800)中改寫Device name.(有的app可能本身未實現改寫功能)
BLE_GAP_CONN_SEC_MODE_SET_OPEN(&sec_mode);
//設定裝置名,該裝置名就是在手機app掃描藍芽裝置時顯示的名字。
err_code = sd_ble_gap_device_name_set(&sec_mode,(const uint8_t *) DEVICE_NAME,strlen(DEVICE_NAME));
APP_ERROR_CHECK(err_code);
memset(&gap_conn_params, 0, sizeof(gap_conn_params));
//設定外圍裝置連線首選引數。同device name一樣,手機連上某個藍芽裝置後可以從Generic Access Service中看到設定的這些引數。這個引數主要是讓中央裝置在首次連線外設時可以讀取他們以及時調整連線引數。或者當中央裝置以後重連該外設,並且之前保留了這些引數那麼就免去了連線後可能需要的修改連線引數的麻煩。
//當然,外圍裝置也可以之後通過sd_ble_gap_ppcp_get來獲取之前設定的引數然後通過連線引數跟新請求函式向中央裝置請求更改連線引數。
gap_conn_params.min_conn_interval = MIN_CONN_INTERVAL;
gap_conn_params.max_conn_interval = MAX_CONN_INTERVAL;
gap_conn_params.slave_latency = SLAVE_LATENCY;
gap_conn_params.conn_sup_timeout = CONN_SUP_TIMEOUT;
err_code = sd_ble_gap_ppcp_set(&gap_conn_params);
APP_ERROR_CHECK(err_code);
}
7 services_init
static void services_init(void)
{
uint32_t err_code;
ble_nus_init_tnus_init;
memset(&nus_init, 0, sizeof(nus_init));
//註冊資料處理函式,這裡處理的資料是收到手機發來的資料
// nus_data_handler就是將板子收到的資料通過串列埠列印到電腦上
//實現了手機->開發板->電腦方向的資料流傳輸。
nus_init.data_handler = nus_data_handler;
err_code =ble_nus_init(&m_nus, &nus_init);
APP_ERROR_CHECK(err_code);
}
7.1 ble_nus_init該函式中實現新增服務以及新增特徵值
uint32_t ble_nus_init(ble_nus_t * p_nus, constble_nus_init_t * p_nus_init)
{
uint32_t err_code;
ble_uuid_tble_uuid;
//設定基準uuid
ble_uuid128_t nus_base_uuid = {0x9E, 0xCA, 0xDC, 0x24, 0x0E, 0xE5, 0xA9, 0xE0,0x93, 0xF3, 0xA3, 0xB5, 0x00, 0x00, 0x40, 0x6E};
if ((p_nus == NULL) || (p_nus_init == NULL))
{
return NRF_ERROR_NULL;
}
// 初始化連線控制代碼,因為現在並未與手機連線所以先賦值無效。
//賦值資料處理函式,就是上面剛提到的列印收到的手機資料
//設定notify是否使能的標誌量,該標誌量在手機連上板子並且使能了具 //有notfify的特徵值時(這裡是rx特徵值後面會講到),該標誌會被設 // 置。這個標誌量僅僅只是一個類似flag的作用,甚至可能並未被
// 用到。
p_nus->conn_handle = BLE_CONN_HANDLE_INVALID;
p_nus->data_handler = p_nus_init->data_handler;
p_nus->is_notification_enabled = false;
// 因為是自己定義的uuid,所以需要呼叫該函式來賦值p_nus->uuid_type
//該函式會將這個nus_base_uuid放到協議棧內部的表中
err_code = sd_ble_uuid_vs_add(&nus_base_uuid, &p_nus->uuid_type);
if (err_code != NRF_SUCCESS)
{
returnerr_code;
}
//設定服務uuid以及uuid_type(就是上面呼叫的函式或得的)
ble_uuid.type = p_nus->uuid_type;
ble_uuid.uuid = BLE_UUID_NUS_SERVICE;
// 到這裡就新增服務到協議棧內部表中了
err_code = sd_ble_gatts_service_add(BLE_GATTS_SRVC_TYPE_PRIMARY,
&ble_uuid,
&p_nus->service_handle);
if (err_code != NRF_SUCCESS)
{
returnerr_code;
}
// 一個服務通常有幾個特徵值
//這裡在上面註冊的服務中添加了兩個特徵值。
err_code = rx_char_add(p_nus, p_nus_init);
if (err_code != NRF_SUCCESS)
{
returnerr_code;
}
// Add TX Characteristic.
err_code = tx_char_add(p_nus, p_nus_init);
if (err_code != NRF_SUCCESS)
{
returnerr_code;
}
return NRF_SUCCESS;
}
7.1.1 rx_char_add
這個特徵用來將板子從串列埠收到的資料通過該特徵值使用notify方式傳送給手機
程式碼太長截圖註釋:
PS:後面標記寫的有點問題。是設定讀寫不需要加密或MITM(其實就是設定安全模式和等級)
7.1.2tx_char_add
這個新增的特徵值用來接收手機發送給板子的資料。
和Rx 特徵值的設定基本一致,只是將notify 功能的設定去掉了改成了設定成可寫。其他的程式碼基本是一樣的。這裡就補貼程式碼了。
8 advertising_init
廣播引數的初始化
static void advertising_init(void)
{
uint32_t err_code;
ble_advdata_tadvdata;
ble_advdata_tscanrsp;
//該標誌主要設定廣播型別為有限可發現模式,並且設定不支援經典藍芽
//相比於一般可發現模式的廣播,有限可發現模式的廣播平率更快,但是隻能最多維持 //30s
uint8_t flags = BLE_GAP_ADV_FLAGS_LE_ONLY_LIMITED_DISC_MODE;
//設定需要廣播的uuid,就是上面主測的服務uuid
ble_uuid_tadv_uuids[] = {{BLE_UUID_NUS_SERVICE, m_nus.uuid_type}};
//這裡設定廣播的名字為全名,設定標誌,就是上面提到的。
//appearance為”外觀”,他就是一個整形值,代表裝置是一個手環,手機什麼的。
memset(&advdata, 0, sizeof(advdata));
advdata.name_type = BLE_ADVDATA_FULL_NAME;
advdata.include_appearance = false;
advdata.flags.size = sizeof(flags);
advdata.flags.p_data = &flags;
//這裡設定的是掃描響應資料。該資料在裝置收到掃描請求的時候才會發出去。
//有時候需要廣播的資料可能太多,廣播包中放不下,那麼就可以放在掃描響應
//資料中,這樣對端裝置便可以通過掃描請求來或得剩下的資料。
memset(&scanrsp, 0, sizeof(scanrsp));
scanrsp.uuids_complete.uuid_cnt = sizeof(adv_uuids) / sizeof(adv_uuids[0]);
scanrsp.uuids_complete.p_uuids =adv_uuids;
err_code = ble_advdata_set(&advdata, &scanrsp);
APP_ERROR_CHECK(err_code);
}
9 conn_params_init
設定連線引數
static void conn_params_init(void)
{
uint32_t err_code;
ble_conn_params_init_tcp_init;
memset(&cp_init, 0, sizeof(cp_init));
//這裡連線引數設定為NULL的原因是前面的gap_params_init函式中已經設定了連線 //引數並呼叫了sd_ble_gap_ppcp_set將引數設定到了協議棧中。所以這裡既是不設定,
//下面的ble_conn_params_init會自動判斷是否為空,為空就呼叫提取函式,從協議棧
//中提取之前註冊的引數。
cp_init.p_conn_params = NULL;
//下面主要是設定一些連線引數更新的事件,以及更新週期和最大最大嘗試更新次數。
//部分引數不好描述,視訊中會說明。
cp_init.first_conn_params_update_delay = FIRST_CONN_PARAMS_UPDATE_DELAY;
cp_init.next_conn_params_update_delay = NEXT_CONN_PARAMS_UPDATE_DELAY;
cp_init.max_conn_params_update_count = MAX_CONN_PARAMS_UPDATE_COUNT;
cp_init.start_on_notify_cccd_handle = BLE_GATT_HANDLE_INVALID;
cp_init.disconnect_on_fail = false;
cp_init.evt_handler = on_conn_params_evt;
cp_init.error_handler = conn_params_error_handler;
err_code =ble_conn_params_init(&cp_init);
APP_ERROR_CHECK(err_code);
}
10 sec_params_init
安全引數的初始化。主要設定
超時時間:比如配對過程中某一步的確認超過這個時間還未收到那麼便是超時。APP會收到SD上拋的狀態事件,狀態為超時
Bond: 是否繫結。如果需要繫結,配對過程會有第三步的祕鑰分發,然後app將祕鑰儲存在falsh這樣下次就可以避免了下次重複配對的過程。
MITM: 是否需要中間人保護。
Io_caps:本裝置的I/O能力。比如有顯示屏,有鍵盤。
:當使能了MITM 並且兩端裝置一個有鍵盤,一個有顯示屏時,配對過程中就會顯示一個配對碼,對端裝置通過鍵盤再輸入。
如果沒有MITM保護配對過程中的資訊是很容易被監聽到的。但是如果有了MITM因為這個配對碼資訊是一端顯示一端輸入,並不會通過鏈路傳輸。因為除了兩端裝置不會有第三個裝置知道。因此後續的鏈路加密就很難被破解。
OOB:與MITM類似,只是配對碼不是通過鍵盤輸入而是通過兩端裝置別的通訊通道傳輸,比如NFC,當然前提是該通訊鏈路是安全的。不如也沒必要繞個彎而不直接用BLE來傳輸了。
後面就是設定加密祕鑰的最大和最小值。加密祕鑰的大小在7-16位元組之間
配對的過程相對比較複雜,這裡不做理論解釋。後期需要的話會單獨做一片配對的詳細教程,群檔案中有我上傳了一個作為從機的配對歷程也是基於uart,當主機在使能有第一個特徵值的notify時便會觸發配對,配對碼是通過串列埠列印的。使用的隨機產生的。當然也可以設定為靜態的。
void sec_params_init(void)
{
m_sec_params.timeout = SEC_PARAM_TIMEOUT;
m_sec_params.bond = SEC_PARAM_BOND;
m_sec_params.mitm = SEC_PARAM_MITM;
m_sec_params.io_caps = SEC_PARAM_IO_CAPABILITIES;
m_sec_params.oob = SEC_PARAM_OOB;
m_sec_params.min_key_size = SEC_PARAM_MIN_KEY_SIZE;
m_sec_params.max_key_size = SEC_PARAM_MAX_KEY_SIZE;
}
11 advertising_start
static void advertising_start(void)
{
uint32_t err_code;
ble_gap_adv_params_t adv_params;
memset(&adv_params, 0, sizeof(adv_params));
//設定廣播型別為通用廣播.
廣播型別有四種:
通用廣播:用途最廣的廣播方式。可以被掃描到,以及可以被連線
定向廣播:用來快速建立和目標裝置建立連線。報文中包含自己以及目標地址。
不可連線廣播:只廣播資料,不可以被掃描以及連線。
可發現廣播;可以被掃描(回覆掃描響應資料),不可以被連線。
adv_params.type = BLE_GAP_ADV_TYPE_ADV_IND;
//如果廣播方式為定向廣播,這裡添目標裝置的地址
adv_params.p_peer_addr = NULL;
//設定過濾規則。
//可設定為是否過濾掉非白名單中的掃描請以及非白名單中的連線請求或者兩者都過濾。
adv_params.fp = BLE_GAP_ADV_FP_ANY;
//設定廣播間隔和廣播超時,超時時間到期如果裝置還未連線那麼app會收到協議棧上
//拋的廣播超時時間。App可以做自己想做的處理,比如讓裝置進入睡眠。
adv_params.interval = APP_ADV_INTERVAL;
adv_params.timeout = APP_ADV_TIMEOUT_IN_SECONDS;
//開啟廣播
err_code = sd_ble_gap_adv_start(&adv_params);
APP_ERROR_CHECK(err_code);
nrf_gpio_pin_set(ADVERTISING_LED_PIN_NO);
}
三接收串列埠資料併發送給對端裝置
上面介紹的整個初始化完成後,裝置便進入睡眠模式,每當廣播間隔到期會發送一次廣播。直到有裝置發來連線請求,當裝置連線上手機後邊繼續處於睡眠狀態等待”事件”的發生
先來分析電腦à開發板à手機方向的資料流
在main 函式的串列埠初始化程式uart_init的最後打開了串列埠的接收中斷。
那麼這個方向的資料流的起點就是在串列埠中斷中收到電腦上發來的資料為起點
Uart中斷函式在main函式上方
void UART0_IRQHandler(void)
{
static uint8_t data_array[BLE_NUS_MAX_DATA_LEN];
static uint8_t index = 0;
uint32_t err_code;
uint8_t temp;
//取得電腦串列埠發過來的資料
data_array[index] = simple_uart_get();
index++;
//判斷串列埠傳送給來的資料是否達到20的位元組,或者是不是傳送了字母’q’。如果滿足
//呼叫傳送函式將收到的串列埠資料傳送給手機。否則不傳送等待知道滿足條件。
// (這裡通常新手說手機收不到資料的原因,因為沒輸入達到20個位元組)
if ((data_array[index - 1] == 'q') || (index >= (BLE_NUS_MAX_DATA_LEN - 1)))
{
err_code = ble_nus_send_string(&m_nus, data_array, index + 1);
if (err_code != NRF_ERROR_INVALID_STATE)
{
APP_ERROR_CHECK(err_code);
}
//傳送了資料後清零陣列下標。以繼續快取後續的串列埠資料。
index = 0;
}
}
再來看看傳送資料給手機的函式ble_nus_send_string
uint32_t ble_nus_send_string(ble_nus_t * p_nus, uint8_t * string, uint16_t length)
{
ble_gatts_hvx_params_t hvx_params;
if (p_nus == NULL)
{
return NRF_ERROR_NULL;
}
//這裡是檢測引數是否正確。是否是已經連線上了手機 (只有連線後,conn_handle才會
// 被賦值為有效值),檢查手機是否使能了開發板的通知,因為開發板作為服務端向手機
//傳送資料時通過通知或指示兩種方式,這兩種方式都需要手機先使能開發板。
if((p_nus->conn_handle==BLE_CONN_HANDLE_INVALID)||(!p_nus->is_notification_enabled))
{
return NRF_ERROR_INVALID_STATE;
}
//一次傳送的長度不能超過限定值20
if (length > BLE_NUS_MAX_DATA_LEN)
{
return NRF_ERROR_INVALID_PARAM;
}
memset(&hvx_params, 0, sizeof(hvx_params));
//以為是通過Rx這個引數來發送資料給手機的,所以控制代碼要填rx的控制代碼
//這個控制代碼是在上面的服務初始化函式中的新增特徵值函式呼叫完畢後或得的(最後一 // 個引數為返回的控制代碼)
//然後就是賦值要傳送的資料,並且設定為notify方式
hvx_params.handle = p_nus->rx_handles.value_handle;
hvx_params.p_data = string;
hvx_params.p_len = &length;
hvx_params.type = BLE_GATT_HVX_NOTIFICATION;
//傳送函式
return sd_ble_gatts_hvx(p_nus->conn_handle, &hvx_params);
}
看到這裡應該對電腦-》板子-》手機的資料流有一個認識。在討論另一個方向的資料傳輸過程。我們先來看一個關於連線的問題。
我們呼叫sd_ble_gatts_hvx(p_nus->conn_handle, &hvx_params);傳送資料給手機的時候,第二個引數是上面賦值的,那第一個引數這個連線控制代碼是怎麼回事?在哪裡設定過他?
連線控制代碼你可以看做是通道標誌一樣(實際資料接入地址),每兩個連線的裝置都會具有這個連線控制代碼。他們後續的通訊都是通過這個連線控制代碼來進行(可以理解是通道標誌,兩個裝置的通訊標誌必須一樣,這代表他們是在同樣的通道上通訊才能正確進行通訊)
上面我們說過,板子整個初始化流程走完後就是睡眠和廣播等待手機連線。那麼這個conn_handle就一定是手機發來連線,板子中協議棧處理完後上拋給app的連線事件中賦值的。從而記錄下後續板子和手機通訊的”通道”。 在 程式框架剖析 那一講中介紹過,協議棧拋上來的事件結構體最終是由dispatch這個派發程式發給再發給各個服務的事件處理函式和模組的事件處理函式的。
static void ble_evt_dispatch(ble_evt_t * p_ble_evt)
{
//將事件交給連線管理模組的事件處理函式
ble_conn_params_on_ble_evt(p_ble_evt);
//將事件交給uart服務的事件處理函式
ble_nus_on_ble_evt(&m_nus, p_ble_evt);
//處理一些一般的事件
on_ble_evt(p_ble_evt);
}
再進入 uart服務的事件處理函式中看下發生連線時是如何記下 後續通訊所用的連線控制代碼的
這裡只擷取部分相關程式碼
說完了連線控制代碼下面來說最後一個問題
手機-》板子—》電腦方向的資料處理過程。
四:接收手機資料並通過串列埠列印:
其實看完了上面關於連線控制代碼的記錄。再來理解怎麼收到手機的資料就容易了。
因為我們說過,手機發送資料過來也是一個事件!
既然都是事件,那麼傳遞流程一定是一樣的,只是在最後的處理上不同的事件不同的處理。
那麼第一步一定是協議棧處理完收到的資料打包一個 “寫事件” 然後上拋給app。其實就是上拋給dispatch。然後在由它繼續分發事件
再進入函式內部:
再進入on_write函式內部一看究竟。
這裡最終是呼叫了一個回撥函式來數理最終的資料,那麼這個回撥函式是什麼時候註冊的。在 第二部分 函式單獨解析的 services_init講解中說明過。
再來看看註冊的這個 nus_data_handler 到底幹了什麼
到這裡手機->板子->電腦方向的資料流也理清了
整體的流程就是 手機發送資料給板子後,板子中低層的協議棧將收到的資料打包成一個寫事件結構體,然後上拋給app,最終由app種的diapatch再分發給各個服務或模組的事件處理函式,而uart的事件處理函式收到寫事件後判斷是不是要列印到電腦上的”普通資料”,如果是就呼叫server_init中註冊的回撥函式。該回調函式最終將資料列印到電腦上