淺析Nordic nRF5 SDK例程架構

HannibalWang發表於2023-04-07

很多剛接觸Nordic nRF5 SDK的初學者出於對新平臺的不熟悉,會覺得這個SDK很難,本文講淺析nRF5 SDK中例程的架構,讓初學者能夠快速上手SDK。

在開始之前,先推薦閱讀觀看下面這些文章和影片,這些文章和影片都出自Nordic中國區的FAE,強烈推薦。

1、【[nRF5] nRF5 SDK Getting Start-嗶哩嗶哩】 https://b23.tv/FxfJBVW 

2、【Nordic nRF5 SDK和softdevice介紹】https://www.cnblogs.com/iini/p/9095551.html

3、nRF5 SDK軟體架構及softdevice工作原理https://www.cnblogs.com/iini/p/9332463.html

其實在nRF5 SDK軟體架構及softdevice工作原理】這篇文章中已經總結了SDK的軟體架構,這裡我直接引用出來:

“當我們開發Nordic平臺的BLE應用時,主要需要做兩件事:

  1. 第1件事:初始化。為了簡化初始化工作,Nordic SDK所有模組初始化時,只需要將相應API輸入結構體引數清0即可完成初始化工作,也就是說,只要你保證初始化引數為0,藍芽協議棧就可以工作起來,這對很多Nordic初學者來說,大大減輕了開發工作量。
  2. 第2件事:寫藍芽事件回撥處理函式。一般來說,你的應用邏輯都是放在藍芽事件回撥處理函式中,所以寫好回撥處理函式程式碼,你的開發工作就完成了大半了。”

本文將透過介紹 bsp 和 ble_app_uart 這兩個例程來分析nRF5 SDK的例程架構,這兩個例程可以在nRF5_SDK_17.1.0_ddde560\examples\peripheral和nRF5_SDK_17.1.0\examples\ble_peripheral下找到。

 

一、bsp 例程淺析

bsp是不帶協議棧的裸機例程,所以main函式非常簡單,下面具體分析一下這幾個函式的作用。

int main(void)
{
    clock_initialization();

    uint32_t err_code = app_timer_init();
    APP_ERROR_CHECK(err_code);

    APP_ERROR_CHECK(NRF_LOG_INIT(NULL));
    NRF_LOG_DEFAULT_BACKENDS_INIT();

    NRF_LOG_INFO("BSP example started.");
    bsp_configuration();

    while (true)
    {
        NRF_LOG_FLUSH();
        __SEV();
        __WFE();
        __WFE();
        // no implementation needed
    }
}

1、clock_initialization是直接透過配置暫存器來配置低頻時鐘,這裡是使用了外部32.768kHz晶振做為時鐘源。需要注意的是,因為bsp是不帶協議棧的裸機工程,所以如果用到低頻時鐘源的時候,需要對其初始化。

/**@brief Function for initializing low frequency clock.
 */
void clock_initialization()
{
    NRF_CLOCK->LFCLKSRC            = (CLOCK_LFCLKSRC_SRC_Xtal << CLOCK_LFCLKSRC_SRC_Pos);
    NRF_CLOCK->EVENTS_LFCLKSTARTED = 0;
    NRF_CLOCK->TASKS_LFCLKSTART    = 1;

    while (NRF_CLOCK->EVENTS_LFCLKSTARTED == 0)
    {
        // Do nothing.
    }
}

在帶協議棧的例程中,因為協議棧會用到低頻時鐘源。協議棧初始化的時候呼叫了nrf_sdh_enable_request,我們可以從該函式中看到在sd_softdecice_enable寫入低頻時鐘配置對低頻時鐘源進行了初始化,所以在帶協議棧的例程中只要初始化協議棧即可,不用再初始化低頻時鐘源。

ret_code_t nrf_sdh_enable_request(void)
{
    ret_code_t ret_code;

    if (m_nrf_sdh_enabled)
    {
        return NRF_ERROR_INVALID_STATE;
    }

    m_nrf_sdh_continue = true;

    // Notify observers about SoftDevice enable request.
    if (sdh_request_observer_notify(NRF_SDH_EVT_ENABLE_REQUEST) == NRF_ERROR_BUSY)
    {
        // Enable process was stopped.
        return NRF_SUCCESS;
    }

    // Notify observers about starting SoftDevice enable process.
    sdh_state_observer_notify(NRF_SDH_EVT_STATE_ENABLE_PREPARE);
  //配置低頻時鐘的引數
    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
    };

    CRITICAL_REGION_ENTER();
#ifdef ANT_LICENSE_KEY
    ret_code = sd_softdevice_enable(&clock_lf_cfg, app_error_fault_handler, ANT_LICENSE_KEY);
#else
    ret_code = sd_softdevice_enable(&clock_lf_cfg, app_error_fault_handler);
#endif
    m_nrf_sdh_enabled = (ret_code == NRF_SUCCESS);
    CRITICAL_REGION_EXIT();

    if (ret_code != NRF_SUCCESS)
    {
        return ret_code;
    }

    m_nrf_sdh_continue  = false;
    m_nrf_sdh_suspended = false;

    // Enable event interrupt.
    // Interrupt priority has already been set by the stack.
    softdevices_evt_irq_enable();

    // Notify observers about a finished SoftDevice enable process.
    sdh_state_observer_notify(NRF_SDH_EVT_STATE_ENABLED);

    return NRF_SUCCESS;
}

 完成低頻時鐘的配置後,接下來是呼叫 app_timer_init 對定時器進行初始化。這個函式是Nordic的庫中已經封裝好的函式,所以在main直接呼叫即可,nRF52系列有 Timer0-Timer4 一共5個Timer可以用,這裡需要注意的是協議棧開啟後會佔用Timer0。

 這裡推薦閱讀Nordic中國區FAE寫的一篇文章,詳細介紹了app_timer的用法和常見問題:https://www.cnblogs.com/iini/p/9347460.html

 

2、NRF_LOG_INIT 和 NRF_LOG_DEFAULT_BACKENDS_INIT 的作用是Log模組初始化,呼叫這兩個函式之後,就可以在程式碼中呼叫 NRF_LOG_INFO、NRF_LOG_ERROR、NRF_LOG_WARNING、NRF_LOG_DEBUG 這幾個函式來列印LOG。Noridc在SDK的程式碼中透過上述的四個LOG等級寫入了大量的日誌,透過列印不同級別的LOG日誌,可以幫助開發者除錯和快速找到程式碼不合理的設計導致的出錯。

關於如何除錯Debug問題,可以參考Nordic中國區FAE的文章 https://www.cnblogs.com/iini/p/9279618.html

我們也可以在bsp的main函式中看到,LOG模組初始化完成後,呼叫NRF_LOG_INFO列印了“BSP example started.”

 

3、接下來就是bsp的初始化,這裡也是呼叫了SDK中的庫函式bsp_init,在裡面寫入了兩個引數。

/**@brief Function for initializing bsp module.
 */
void bsp_configuration()
{
    uint32_t err_code;

    err_code = bsp_init(BSP_INIT_LEDS | BSP_INIT_BUTTONS, bsp_evt_handler);
    APP_ERROR_CHECK(err_code);
}

bsp_init的函式定義在bsp.h中,檢視其註釋可以知道兩個入參分別是使用外設的型別和回撥函式。在main 中 BSP_INIT_LEDS 和 BSP_INIT_BUTTONS 分別是對LED和BUTTON進行了初始化。

/**@brief       Function for initializing BSP.
 *
 * @details     The function initializes the board support package to allow state indication and
 *              button reaction. Default events are assigned to buttons.
 * @note        Before calling this function, you must initiate the following required modules:
 *              - @ref app_timer for LED support
 *              - @ref app_gpiote for button support
 *
 * @param[in]   type               Type of peripherals used.
 * @param[in]   callback           Function to be called when button press/event is detected.
 *
 * @retval      NRF_SUCCESS               If the BSP module was successfully initialized.
 * @retval      NRF_ERROR_INVALID_STATE   If the application timer module has not been initialized.
 * @retval      NRF_ERROR_NO_MEM          If the maximum number of timers has already been reached.
 * @retval      NRF_ERROR_INVALID_PARAM   If GPIOTE has too many users.
 * @retval      NRF_ERROR_INVALID_STATE   If button or GPIOTE has not been initialized.
 */
uint32_t bsp_init(uint32_t type, bsp_event_callback_t callback);

下面我們來看看bsp的回撥函式bsp_evt_handler,在回撥中使用了按鍵中斷,DK板上的Button1和Button2按下的時候,會產生相對應的事件。

/**@brief Function for handling bsp events.
 */
void bsp_evt_handler(bsp_event_t evt)
{
    uint32_t err_code;
    switch (evt)
    {
        case BSP_EVENT_KEY_0:
            if (actual_state != BSP_INDICATE_FIRST)
                actual_state--;
            else
                actual_state = BSP_INDICATE_LAST;
            break;

        case BSP_EVENT_KEY_1:

            if (actual_state != BSP_INDICATE_LAST)
                actual_state++;
            else
                actual_state = BSP_INDICATE_FIRST;
            break;

        default:
            return; // no implementation needed
    }
    err_code = bsp_indication_set(actual_state);
    NRF_LOG_INFO("%s", (uint32_t)indications_list[actual_state]);
    APP_ERROR_CHECK(err_code);
}

我們可以在bsp_event_t中找到全部bsp事件。

/**@brief BSP events.
*
* @note Events from BSP_EVENT_KEY_0 to BSP_EVENT_KEY_LAST are by default assigned to buttons.
*/

typedef enum
{
    BSP_EVENT_NOTHING = 0,                  /**< Assign this event to an action to prevent the action from generating an event (disable the action). */
    BSP_EVENT_DEFAULT,                      /**< Assign this event to an action to assign the default event to the action. */
    BSP_EVENT_CLEAR_BONDING_DATA,           /**< Persistent bonding data should be erased. */
    BSP_EVENT_CLEAR_ALERT,                  /**< An alert should be cleared. */
    BSP_EVENT_DISCONNECT,                   /**< A link should be disconnected. */
    BSP_EVENT_ADVERTISING_START,            /**< The device should start advertising. */
    BSP_EVENT_ADVERTISING_STOP,             /**< The device should stop advertising. */
    BSP_EVENT_WHITELIST_OFF,                /**< The device should remove its advertising whitelist. */
    BSP_EVENT_BOND,                         /**< The device should bond to the currently connected peer. */
    BSP_EVENT_RESET,                        /**< The device should reset. */
    BSP_EVENT_SLEEP,                        /**< The device should enter sleep mode. */
    BSP_EVENT_WAKEUP,                       /**< The device should wake up from sleep mode. */
    BSP_EVENT_SYSOFF,                       /**< The device should enter system off mode (without wakeup). */
    BSP_EVENT_DFU,                          /**< The device should enter DFU mode. */
    BSP_EVENT_KEY_0,                        /**< Default event of the push action of BSP_BUTTON_0 (only if this button is present). */
    BSP_EVENT_KEY_1,                        /**< Default event of the push action of BSP_BUTTON_1 (only if this button is present). */
    BSP_EVENT_KEY_2,                        /**< Default event of the push action of BSP_BUTTON_2 (only if this button is present). */
    BSP_EVENT_KEY_3,                        /**< Default event of the push action of BSP_BUTTON_3 (only if this button is present). */
    BSP_EVENT_KEY_4,                        /**< Default event of the push action of BSP_BUTTON_4 (only if this button is present). */
    BSP_EVENT_KEY_5,                        /**< Default event of the push action of BSP_BUTTON_5 (only if this button is present). */
    BSP_EVENT_KEY_6,                        /**< Default event of the push action of BSP_BUTTON_6 (only if this button is present). */
    BSP_EVENT_KEY_7,                        /**< Default event of the push action of BSP_BUTTON_7 (only if this button is present). */
    BSP_EVENT_KEY_LAST = BSP_EVENT_KEY_7,
} bsp_event_t;

在 bsp 原始例程中的bsp回撥函式中,按下Button1是倒序顯示bsp_indication_t這個結構體中定義好的LED狀態並在串列埠列印bsp_indication_t中的事件。

/**@brief BSP indication states.
 *
 * @details See @ref examples_bsp_states for a list of how these states are indicated for the PCA10028/PCA10040 board and the PCA10031 dongle.
 */
typedef enum
{
    BSP_INDICATE_FIRST = 0,
    BSP_INDICATE_IDLE  = BSP_INDICATE_FIRST, /**< See \ref BSP_INDICATE_IDLE.*/
    BSP_INDICATE_SCANNING,                   /**< See \ref BSP_INDICATE_SCANNING.*/
    BSP_INDICATE_ADVERTISING,                /**< See \ref BSP_INDICATE_ADVERTISING.*/
    BSP_INDICATE_ADVERTISING_WHITELIST,      /**< See \ref BSP_INDICATE_ADVERTISING_WHITELIST.*/
    BSP_INDICATE_ADVERTISING_SLOW,           /**< See \ref BSP_INDICATE_ADVERTISING_SLOW.*/
    BSP_INDICATE_ADVERTISING_DIRECTED,       /**< See \ref BSP_INDICATE_ADVERTISING_DIRECTED.*/
    BSP_INDICATE_BONDING,                    /**< See \ref BSP_INDICATE_BONDING.*/
    BSP_INDICATE_CONNECTED,                  /**< See \ref BSP_INDICATE_CONNECTED.*/
    BSP_INDICATE_SENT_OK,                    /**< See \ref BSP_INDICATE_SENT_OK.*/
    BSP_INDICATE_SEND_ERROR,                 /**< See \ref BSP_INDICATE_SEND_ERROR.*/
    BSP_INDICATE_RCV_OK,                     /**< See \ref BSP_INDICATE_RCV_OK.*/
    BSP_INDICATE_RCV_ERROR,                  /**< See \ref BSP_INDICATE_RCV_ERROR.*/
    BSP_INDICATE_FATAL_ERROR,                /**< See \ref BSP_INDICATE_FATAL_ERROR.*/
    BSP_INDICATE_ALERT_0,                    /**< See \ref BSP_INDICATE_ALERT_0.*/
    BSP_INDICATE_ALERT_1,                    /**< See \ref BSP_INDICATE_ALERT_1.*/
    BSP_INDICATE_ALERT_2,                    /**< See \ref BSP_INDICATE_ALERT_2.*/
    BSP_INDICATE_ALERT_3,                    /**< See \ref BSP_INDICATE_ALERT_3.*/
    BSP_INDICATE_ALERT_OFF,                  /**< See \ref BSP_INDICATE_ALERT_OFF.*/
    BSP_INDICATE_USER_STATE_OFF,             /**< See \ref BSP_INDICATE_USER_STATE_OFF.*/
    BSP_INDICATE_USER_STATE_0,               /**< See \ref BSP_INDICATE_USER_STATE_0.*/
    BSP_INDICATE_USER_STATE_1,               /**< See \ref BSP_INDICATE_USER_STATE_1.*/
    BSP_INDICATE_USER_STATE_2,               /**< See \ref BSP_INDICATE_USER_STATE_2.*/
    BSP_INDICATE_USER_STATE_3,               /**< See \ref BSP_INDICATE_USER_STATE_3.*/
    BSP_INDICATE_USER_STATE_ON,              /**< See \ref BSP_INDICATE_USER_STATE_ON.*/
    BSP_INDICATE_LAST = BSP_INDICATE_USER_STATE_ON
} bsp_indication_t;

4、最後就是while死迴圈中的函式,NRF_LOG_FLUSH是用來處理快取中的LOG,__SEV() 和 __WFE() 是ARM的指令,用來上報事件和在低功耗下等待事件發生。

    while (true)
    {
        NRF_LOG_FLUSH();
        __SEV();
        __WFE();
        __WFE();
        // no implementation needed
    }

 

5、練習:

原始 bsp 例程中的bsp回撥函式對於初學者而言不太友好,我們可以寫一個簡單的 bsp 回撥函式,在按下DK板上對應的按鍵1-4的時候,在RTT列印相應的Log。

void bsp_evt_handler(bsp_event_t evt)
{
    switch (evt)
    {
        case BSP_EVENT_KEY_0:
            NRF_LOG_INFO("Button 1 is pressed");
            break;

        case BSP_EVENT_KEY_1:
            NRF_LOG_INFO("Button 2 is pressed");
            break;

        case BSP_EVENT_KEY_2:
            NRF_LOG_INFO("Button 3 is pressed");
            break;

        case BSP_EVENT_KEY_3:
            NRF_LOG_INFO("Button 4 is pressed");
            break;

         
        default:
            return;
    }
}

將 bsp 例程中的 bsp_evt_handler 替換為上面的程式碼,編譯並下載到DK中,開啟串列埠工具,按下對應的按鍵就可以看到串列埠列印相應的LOG。

 

 

 小結:熟悉了 bsp 這個例程,我們再去看examples\ble_peripheral下其他的外設例程,就會發現架構是相同的,都是先初始化外設,寫入外設的配置引數,然後再根據相應事件結構體中給出的事件編寫回撥函式,在回撥中處理中斷髮生時要做的事即可。

 

二、ble_app_uart 例程淺析

ble_app_uart 是帶協議棧的一個例程,此例程是一個較為實用的例程,它使用了NUS服務(Nordic UATR Service)可以透過串列埠實現藍芽的上下行收發資料,因此只要改變傳送的外設,就可以改為其他外設,如SPI或者I2C來收發資料。熟悉了 bsp 例程,你就會發現 ble_app_uart 例程是一個放大版的 bsp 例程,下面我們將分析這個例程。

1、首先來看main函式,從這個例程中就更可以看出NRF5 SDK的例程軟體架構,在main中初始化了所有的功能模組,然後在初始化中配置相應模組的回撥,當回撥中的事件觸發時來做中斷處理。

int main(void)
{
    bool erase_bonds;

    // Initialize.
    uart_init();
    log_init();
    timers_init();
    buttons_leds_init(&erase_bonds);
    power_management_init();
    ble_stack_init();
    gap_params_init();
    gatt_init();
    services_init();
    advertising_init();
    conn_params_init();

    // Start execution.
    printf("\r\nUART started.\r\n");
    NRF_LOG_INFO("Debug logging for UART over RTT started.");
    advertising_start();

    // Enter main loop.
    for (;;)
    {
        idle_state_handle();
    }
}

 

2、第一個函式 uart_init 中是定義了串列埠相關的一些引數,如串列埠引腳、流控、波特率等引數,然後把這些引數寫入APP_UART_FIFO_INIT來初始化串列埠。

/**@brief  Function for initializing the UART module.
 */
/**@snippet [UART Initialization] */
static void uart_init(void)
{
    uint32_t                     err_code;
    app_uart_comm_params_t const comm_params =
    {
        .rx_pin_no    = RX_PIN_NUMBER,
        .tx_pin_no    = TX_PIN_NUMBER,
        .rts_pin_no   = RTS_PIN_NUMBER,
        .cts_pin_no   = CTS_PIN_NUMBER,
        .flow_control = APP_UART_FLOW_CONTROL_DISABLED,
        .use_parity   = false,
#if defined (UART_PRESENT)
        .baud_rate    = NRF_UART_BAUDRATE_115200
#else
        .baud_rate    = NRF_UARTE_BAUDRATE_115200
#endif
    };

    APP_UART_FIFO_INIT(&comm_params,
                       UART_RX_BUF_SIZE,
                       UART_TX_BUF_SIZE,
                       uart_event_handle,
                       APP_IRQ_PRIORITY_LOWEST,
                       err_code);
    APP_ERROR_CHECK(err_code);
}

APP_UART_FIFO_INIT的定義如下,要初始化串列埠需要寫入五個引數,串列埠相關引數的結構體、RX和TX的buffer大小、回撥函式、IRQ優先順序。

/**@brief Macro for safe initialization of the UART module in a single user instance when using
 *        a FIFO together with UART.
 *
 * @param[in]   P_COMM_PARAMS   Pointer to a UART communication structure: app_uart_comm_params_t
 * @param[in]   RX_BUF_SIZE     Size of desired RX buffer, must be a power of 2 or ZERO (No FIFO).
 * @param[in]   TX_BUF_SIZE     Size of desired TX buffer, must be a power of 2 or ZERO (No FIFO).
 * @param[in]   EVT_HANDLER     Event handler function to be called when an event occurs in the
 *                              UART module.
 * @param[in]   IRQ_PRIO        IRQ priority, app_irq_priority_t, for the UART module irq handler.
 * @param[out]  ERR_CODE        The return value of the UART initialization function will be
 *                              written to this parameter.
 *
 * @note Since this macro allocates a buffer and registers the module as a GPIOTE user when flow
 *       control is enabled, it must only be called once.
 */
#define APP_UART_FIFO_INIT(P_COMM_PARAMS, RX_BUF_SIZE, TX_BUF_SIZE, EVT_HANDLER, IRQ_PRIO, ERR_CODE) \
    do                                                                                             \
    {                                                                                              \
        app_uart_buffers_t buffers;                                                                \
        static uint8_t     rx_buf[RX_BUF_SIZE];                                                    \
        static uint8_t     tx_buf[TX_BUF_SIZE];                                                    \
                                                                                                   \
        buffers.rx_buf      = rx_buf;                                                              \
        buffers.rx_buf_size = sizeof (rx_buf);                                                     \
        buffers.tx_buf      = tx_buf;                                                              \
        buffers.tx_buf_size = sizeof (tx_buf);                                                     \
        ERR_CODE = app_uart_init(P_COMM_PARAMS, &buffers, EVT_HANDLER, IRQ_PRIO);                  \
    } while (0)

接下來我們來看uart的事件,從這裡我們可以看出,串列埠收到資料後會產生一個事件APP_UART_DATA_READY

typedef enum
{
    APP_UART_DATA_READY,          /**< An event indicating that UART data has been received. The data is available in the FIFO and can be fetched using @ref app_uart_get. */
    APP_UART_FIFO_ERROR,          /**< An error in the FIFO module used by the app_uart module has occured. The FIFO error code is stored in app_uart_evt_t.data.error_code field. */
    APP_UART_COMMUNICATION_ERROR, /**< An communication error has occured during reception. The error is stored in app_uart_evt_t.data.error_communication field. */
    APP_UART_TX_EMPTY,            /**< An event indicating that UART has completed transmission of all available data in the TX FIFO. */
    APP_UART_DATA,                /**< An event indicating that UART data has been received, and data is present in data field. This event is only used when no FIFO is configured. */
} app_uart_evt_type_t;

所以我們可以在回撥函式中使用這個事件,當產生這個事件的時候說明串列埠有資料發過來,可以呼叫app_uart_get來接收串列埠資料,收到資料後再呼叫ble_nus_data_send向藍芽傳送串列埠收到的資料。

/**@brief   Function for handling app_uart events.
 *
 * @details This function will receive a single character from the app_uart module and append it to
 *          a string. The string will be be sent over BLE when the last character received was a
 *          'new line' '\n' (hex 0x0A) or if the string has reached the maximum data length.
 */
/**@snippet [Handling the data received over UART] */
void uart_event_handle(app_uart_evt_t * p_event)
{
    static uint8_t data_array[BLE_NUS_MAX_DATA_LEN];
    static uint8_t index = 0;
    uint32_t       err_code;

    switch (p_event->evt_type)
    {
        case APP_UART_DATA_READY:
            UNUSED_VARIABLE(app_uart_get(&data_array[index]));
            index++;

            if ((data_array[index - 1] == '\n') ||
                (data_array[index - 1] == '\r') ||
                (index >= m_ble_nus_max_data_len))
            {
                if (index > 1)
                {
                    NRF_LOG_DEBUG("Ready to send data over BLE NUS");
                    NRF_LOG_HEXDUMP_DEBUG(data_array, index);

                    do
                    {
                        uint16_t length = (uint16_t)index;
                        err_code = ble_nus_data_send(&m_nus, 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);
                        }
                    } while (err_code == NRF_ERROR_RESOURCES);
                }

                index = 0;
            }
            break;

        case APP_UART_COMMUNICATION_ERROR:
            APP_ERROR_HANDLER(p_event->data.error_communication);
            break;

        case APP_UART_FIFO_ERROR:
            APP_ERROR_HANDLER(p_event->data.error_code);
            break;

        default:
            break;
    }
}

 

3、log_init、timers_init、buttons_leds_init這三個函式的作用分別在 bsp 例程中已經有過詳細介紹,在此不多做贅述。

 

4、power_management_init中呼叫了nrf_pwr_mgmt_init(),其函式定義如下,主要是用於初始化低功耗管理模組,在實際的應用中可以直接使用。

/**@brief   Function for initializing power management.
 *
 * @warning Depending on configuration, this function sets SEVONPEND in System Control Block (SCB).
 *          This operation is unsafe with the SoftDevice from interrupt priority higher than SVC.
 *
 * @retval NRF_SUCCESS
 */
ret_code_t nrf_pwr_mgmt_init(void);

 

5、ble_stack_init 是一個非常重要的函式,其作用是初始化BLE協議棧,對於初學者而言不需要過多去檢視協議棧初始化的具體細節,只要照搬例程中的初始化函式即可,這些需要關注的是回撥函式ble_evt_handler

/**@brief Function for the SoftDevice initialization.
 *
 * @details This function initializes the SoftDevice and the BLE event interrupt.
 */
static void ble_stack_init(void)
{
    ret_code_t err_code;

    err_code = nrf_sdh_enable_request();
    APP_ERROR_CHECK(err_code);

    // Configure the BLE stack using the default settings.
    // Fetch the start address of the application RAM.
    uint32_t ram_start = 0;
    err_code = nrf_sdh_ble_default_cfg_set(APP_BLE_CONN_CFG_TAG, &ram_start);
    APP_ERROR_CHECK(err_code);

    // Enable BLE stack.
    err_code = nrf_sdh_ble_enable(&ram_start);
    APP_ERROR_CHECK(err_code);

    // Register a handler for BLE events.
    NRF_SDH_BLE_OBSERVER(m_ble_observer, APP_BLE_OBSERVER_PRIO, ble_evt_handler, NULL);
}

這裡我們分析一下例程中的ble_evt_handler,首先來看ble_evt_t這個結構體,這是一個非常複雜的結構體,給出了Gap、Gattc、Gatts、L2CAP等會產生的所有事件對應的結構體。

/**@brief Common BLE Event type, wrapping the module specific event reports. */
typedef struct
{
  ble_evt_hdr_t header;           /**< Event header. */
  union
  {
    ble_common_evt_t  common_evt; /**< Common Event, evt_id in BLE_EVT_* series. */
    ble_gap_evt_t     gap_evt;    /**< GAP originated event, evt_id in BLE_GAP_EVT_* series. */
    ble_gattc_evt_t   gattc_evt;  /**< GATT client originated event, evt_id in BLE_GATTC_EVT* series. */
    ble_gatts_evt_t   gatts_evt;  /**< GATT server originated event, evt_id in BLE_GATTS_EVT* series. */
    ble_l2cap_evt_t   l2cap_evt;  /**< L2CAP originated event, evt_id in BLE_L2CAP_EVT* series. */
  } evt;                          /**< Event union. */
} ble_evt_t;

這裡涉及到BLE協議相關的知識,簡單來說BLE協議棧的host層可以從下到上分為L2CAP、ATT、GAP、GATT四層,一般而言只需要應用程式中只需要處理GAP和Gatt層的事件即可。

對於初學者而言,在沒有詳細瞭解藍芽協議的情況下,建議照搬例程中已經寫好的ble_evt_handler函式,只在應用層在ble_evt_handler的事件中根據自己的需求加入一些邏輯處理,例如BLE連線、斷開後串列埠列印資料或是改變某個GPIO的狀態。協議棧產生的GAP和GATT事件在ble_gap.h、ble_gattc.h、ble_gatts.h這三個檔案中,感興趣的朋友可以自己去這兩個檔案中檢視所有事件和其註釋,這裡我們只分析例程中的回撥給出的這些事件的作用。

/**@brief Function for handling BLE events.
 *
 * @param[in]   p_ble_evt   Bluetooth stack event.
 * @param[in]   p_context   Unused.
 */
static void ble_evt_handler(ble_evt_t const * p_ble_evt, void * p_context)
{
    uint32_t err_code;

    switch (p_ble_evt->header.evt_id)
    {
        case BLE_GAP_EVT_CONNECTED:
            NRF_LOG_INFO("Connected");
            err_code = bsp_indication_set(BSP_INDICATE_CONNECTED);
            APP_ERROR_CHECK(err_code);
            m_conn_handle = p_ble_evt->evt.gap_evt.conn_handle;
            err_code = nrf_ble_qwr_conn_handle_assign(&m_qwr, m_conn_handle);
            APP_ERROR_CHECK(err_code);
            break;

        case BLE_GAP_EVT_DISCONNECTED:
            NRF_LOG_INFO("Disconnected");
            // LED indication will be changed when advertising starts.
            m_conn_handle = BLE_CONN_HANDLE_INVALID;
            break;

        case BLE_GAP_EVT_PHY_UPDATE_REQUEST:
        {
            NRF_LOG_DEBUG("PHY update request.");
            ble_gap_phys_t const phys =
            {
                .rx_phys = BLE_GAP_PHY_AUTO,
                .tx_phys = BLE_GAP_PHY_AUTO,
            };
            err_code = sd_ble_gap_phy_update(p_ble_evt->evt.gap_evt.conn_handle, &phys);
            APP_ERROR_CHECK(err_code);
        } break;

        case BLE_GAP_EVT_SEC_PARAMS_REQUEST:
            // Pairing not supported
            err_code = sd_ble_gap_sec_params_reply(m_conn_handle, BLE_GAP_SEC_STATUS_PAIRING_NOT_SUPP, NULL, NULL);
            APP_ERROR_CHECK(err_code);
            break;

        case BLE_GATTS_EVT_SYS_ATTR_MISSING:
            // No system attributes have been stored.
            err_code = sd_ble_gatts_sys_attr_set(m_conn_handle, NULL, 0, 0);
            APP_ERROR_CHECK(err_code);
            break;

        case BLE_GATTC_EVT_TIMEOUT:
            // Disconnect on GATT Client timeout event.
            err_code = sd_ble_gap_disconnect(p_ble_evt->evt.gattc_evt.conn_handle,
                                             BLE_HCI_REMOTE_USER_TERMINATED_CONNECTION);
            APP_ERROR_CHECK(err_code);
            break;

        case BLE_GATTS_EVT_TIMEOUT:
            // Disconnect on GATT Server timeout event.
            err_code = sd_ble_gap_disconnect(p_ble_evt->evt.gatts_evt.conn_handle,
                                             BLE_HCI_REMOTE_USER_TERMINATED_CONNECTION);
            APP_ERROR_CHECK(err_code);
            break;

        default:
            // No implementation needed.
            break;
    }
}

 下面是我直接從SDK中複製了這些事件的註釋,從註釋中可以看出這些事件基本是把BLE連線過程中會發生的事件列了出來。

BLE_GAP_EVT_CONNECTED                      /**< Connected to peer
BLE_GAP_EVT_DISCONNECTED                  /**< Disconnected from peer.
BLE_GAP_EVT_PHY_UPDATE_REQUEST        /**< PHY Update Request.
BLE_GAP_EVT_SEC_PARAMS_REQUEST          /**< Request to provide security parameters.
BLE_GATTS_EVT_SYS_ATTR_MISSING           /**< A persistent system attribute access is pending.        
BLE_GATTC_EVT_TIMEOUT                      /**< Timeout event.    
BLE_GATTS_EVT_TIMEOUT                      /**< Peer failed to respond to an ATT request in time.

(1)、手機或者其他主機裝置後,BLE協議棧會產生BLE_GAP_EVT_CONNECTED這個事件,在這個事件產生後,回撥函式在應用層對其的處理是首先使用LOG函式列印連線資訊。然後呼叫bsp_indication_set改變DK板上LED的狀態,讓LED狀態進入BSP_INDICATE_CONNECTED這個連線狀態,透過檢視 bsp.c 中的相關函式,我們可以知道這個連線狀態是讓DK板上的LED1進入常亮狀態。最後是呼叫nrf_ble_qwr_conn_handle_assign,這個函式的功能是用於將連線控制程式碼m_conn_handle分配給 Queued Writes 模組。 簡單來說,這個m_conn_handle相當於協議棧給連線的對端裝置分配的號碼,nrf_ble_qwr_conn_handle_assign的作用就是把這個號碼與 Queued Writes 模組關聯起來,Queued Writes 模組用於處理對端BLE裝置在GATT上的操作。

        case BLE_GAP_EVT_CONNECTED:
            NRF_LOG_INFO("Connected");
            err_code = bsp_indication_set(BSP_INDICATE_CONNECTED);
            APP_ERROR_CHECK(err_code);
            m_conn_handle = p_ble_evt->evt.gap_evt.conn_handle;
            err_code = nrf_ble_qwr_conn_handle_assign(&m_qwr, m_conn_handle);
            APP_ERROR_CHECK(err_code);
            break;
NRF_BLE_QWR_DEF(m_qwr);                        /**< Context for the Queued Write module.*/

(2)、手機或者其他主機裝置後,BLE協議棧會產生BLE_GAP_EVT_DISCONNECTED這個事件,從例程中可以看到,在這個事件產生後,回撥函式對其的處理是使用LOG函式列印斷開連線的資訊,然後重置m_conn_handle的值。

        case BLE_GAP_EVT_DISCONNECTED:
            NRF_LOG_INFO("Disconnected");
            // LED indication will be changed when advertising starts.
            m_conn_handle = BLE_CONN_HANDLE_INVALID;
            break;

(3)、BLE_GAP_EVT_PHY_UPDATE_REQUEST這個事件主要是針對對端裝置請求更新PHY的速率,當對端裝置請求更新PHY速率後,協議棧會產生此事件,從例程中可以看到,在這個事件產生後對其的處理是配置PHY引數,並呼叫sd_ble_gap_phy_update更新PHY速率。

        case BLE_GAP_EVT_PHY_UPDATE_REQUEST:
        {
            NRF_LOG_DEBUG("PHY update request.");
            ble_gap_phys_t const phys =
            {
                .rx_phys = BLE_GAP_PHY_AUTO,
                .tx_phys = BLE_GAP_PHY_AUTO,
            };
            err_code = sd_ble_gap_phy_update(p_ble_evt->evt.gap_evt.conn_handle, &phys);
            APP_ERROR_CHECK(err_code);
        } break;

(4)、BLE_GAP_EVT_SEC_PARAMS_REQUEST是安全相關引數請求事件,在配對資訊交換階段,BLE_GAP_EVT_SEC_PARAMS_REQUEST事件會由協議棧上報給應用層。在這個事件中,從機會把自己的資訊與主機進行交換,其中就包含了從機的IO能力、配對完成是否繫結等資訊。

        case BLE_GAP_EVT_SEC_PARAMS_REQUEST:
            // Pairing not supported
            err_code = sd_ble_gap_sec_params_reply(m_conn_handle, BLE_GAP_SEC_STATUS_PAIRING_NOT_SUPP, NULL, NULL);
            APP_ERROR_CHECK(err_code);
            break;

 (5)、BLE_GATTS_EVT_SYS_ATTR_MISSING這是事件是當沒有儲存系統屬性時,協議棧上報此事件,回撥函式中的處理是呼叫sd_ble_gatts_sys_attr_set函式來設定系統屬性。

        case BLE_GATTS_EVT_SYS_ATTR_MISSING:
            // No system attributes have been stored.
            err_code = sd_ble_gatts_sys_attr_set(m_conn_handle, NULL, 0, 0);
            APP_ERROR_CHECK(err_code);
            break;

(6)、BLE_GATTC_EVT_TIMEOUT和BLE_GATTS_EVT_TIMEOUT是當主機和從機連線超時協議棧產生的事件,回撥函式中的處理是呼叫sd_ble_gap_disconnect函式來斷開連線。

        case BLE_GATTC_EVT_TIMEOUT:
            // Disconnect on GATT Client timeout event.
            err_code = sd_ble_gap_disconnect(p_ble_evt->evt.gattc_evt.conn_handle,
                                             BLE_HCI_REMOTE_USER_TERMINATED_CONNECTION);
            APP_ERROR_CHECK(err_code);
            break;

        case BLE_GATTS_EVT_TIMEOUT:
            // Disconnect on GATT Server timeout event.
            err_code = sd_ble_gap_disconnect(p_ble_evt->evt.gatts_evt.conn_handle,
                                             BLE_HCI_REMOTE_USER_TERMINATED_CONNECTION);
            APP_ERROR_CHECK(err_code);
            break;

 

6、gap_params_init函式是用來配置gap的相關引數,包括裝置名稱、連線間隔、Slave latency、監督超時時間、GAP連線的安全模式等。一般來說,配置比較多的是裝置名稱、連線間隔、Slave latency這三個引數,其他沿用例程中的配置即可。

/**@brief Function for the GAP initialization.
 *
 * @details This function will set up all the necessary GAP (Generic Access Profile) parameters of
 *          the device. It also sets the permissions and appearance.
 */
static void gap_params_init(void)
{
    uint32_t                err_code;
    ble_gap_conn_params_t   gap_conn_params;
    ble_gap_conn_sec_mode_t sec_mode;

    BLE_GAP_CONN_SEC_MODE_SET_OPEN(&sec_mode);

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

    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、gatt_init用於初始化GATT庫。其中,nrf_ble_gatt_init 函式用於初始化GATT協議棧,gatt_evt_handler 函式用於處理GATT事件。nrf_ble_gatt_att_mtu_periph_set函式用於設定GATT伺服器的MTU大小。

/**@brief Function for initializing the GATT library. */
void gatt_init(void)
{
    ret_code_t err_code;

    err_code = nrf_ble_gatt_init(&m_gatt, gatt_evt_handler);
    APP_ERROR_CHECK(err_code);

    err_code = nrf_ble_gatt_att_mtu_periph_set(&m_gatt, NRF_SDH_BLE_GATT_MAX_MTU_SIZE);
    APP_ERROR_CHECK(err_code);
}

在gatt_init的回撥函式gatt_evt_handler中主要處理當ATT MTU更新之後的事,這裡ATT MTU是L2CAP層的內容,對於初學者而言不需要去深入瞭解,在自己的專案中只需要照搬例程中的程式碼即可。

/**@brief Function for handling events from the GATT library. */
void gatt_evt_handler(nrf_ble_gatt_t * p_gatt, nrf_ble_gatt_evt_t const * p_evt)
{
    if ((m_conn_handle == p_evt->conn_handle) && (p_evt->evt_id == NRF_BLE_GATT_EVT_ATT_MTU_UPDATED))
    {
        m_ble_nus_max_data_len = p_evt->params.att_mtu_effective - OPCODE_LENGTH - HANDLE_LENGTH;
        NRF_LOG_INFO("Data len is set to 0x%X(%d)", m_ble_nus_max_data_len, m_ble_nus_max_data_len);
    }
    NRF_LOG_DEBUG("ATT MTU exchange completed. central 0x%x peripheral 0x%x",
                  p_gatt->att_mtu_desired_central,
                  p_gatt->att_mtu_desired_periph);
}

 

7、services_init也是一個非常重要的函式,從機端所有的服務都是在這裡新增並初始化,Nordic為我們提供了一個庫在SDK根目錄下的components\ble\ble_services這個資料夾中,這裡有常用的一些服務的庫,例如DIS(Devies Information、Service)、DFU、HIDS等,所以想要在從機新增服務只需要在services_init函式中呼叫對應的服務初始化介面,然後編寫對應服務的回撥函式即可,具體可以參考NUS服務的初始化。

需要注意的是在這裡的nrf_ble_qwr_init是用來初始化之前提到的Queued Writes 模組的函式,在初始化服務的時候首先要初始化Queued Writes模組。

/**@snippet [Handling the data received over BLE] */


/**@brief Function for initializing services that will be used by the application.
 */
static void services_init(void)
{
    uint32_t           err_code;
    ble_nus_init_t     nus_init;
    nrf_ble_qwr_init_t qwr_init = {0};

    // Initialize Queued Write Module.
    qwr_init.error_handler = nrf_qwr_error_handler;

    err_code = nrf_ble_qwr_init(&m_qwr, &qwr_init);
    APP_ERROR_CHECK(err_code);

    // Initialize NUS.
    memset(&nus_init, 0, sizeof(nus_init));

    nus_init.data_handler = nus_data_handler;

    err_code = ble_nus_init(&m_nus, &nus_init);
    APP_ERROR_CHECK(err_code);
}

 

 

 這裡ble_app_uart例程只使用了NUS服務,所以我們來看看NUS服務的回撥函式nus_data_handler做了什麼,當產生BLE_NUS_EVT_RX_DATA這個事件時,p_evt->params.rx_data.p_data這個指標收到BLE的資料,回撥函式中呼叫app_uart_put這個函式將收到的資料列印到串列埠。

/**@brief Function for handling the data from the Nordic UART Service.
 *
 * @details This function will process the data received from the Nordic UART BLE Service and send
 *          it to the UART module.
 *
 * @param[in] p_evt       Nordic UART Service event.
 */
/**@snippet [Handling the data received over BLE] */
static void nus_data_handler(ble_nus_evt_t * p_evt)
{

    if (p_evt->type == BLE_NUS_EVT_RX_DATA)
    {
        uint32_t err_code;

        NRF_LOG_DEBUG("Received data from BLE NUS. Writing data on UART.");
        NRF_LOG_HEXDUMP_DEBUG(p_evt->params.rx_data.p_data, p_evt->params.rx_data.length);

        for (uint32_t i = 0; i < p_evt->params.rx_data.length; i++)
        {
            do
            {
                err_code = app_uart_put(p_evt->params.rx_data.p_data[i]);
                if ((err_code != NRF_SUCCESS) && (err_code != NRF_ERROR_BUSY))
                {
                    NRF_LOG_ERROR("Failed receiving NUS message. Error 0x%x. ", err_code);
                    APP_ERROR_CHECK(err_code);
                }
            } while (err_code == NRF_ERROR_BUSY);
        }
        if (p_evt->params.rx_data.p_data[p_evt->params.rx_data.length - 1] == '\r')
        {
            while (app_uart_put('\n') == NRF_ERROR_BUSY);
        }
    }

}

 

8、advertising_init也是一個非常重要的函式,所有的廣播引數都在這裡設定,包括廣播週期、廣播包中的相關引數等,配置的方式透過配置相對應的引數,然後呼叫ble_advertising_init去設定。這些需要注意的是,配置完廣播引數之後,還要呼叫ble_advertising_conn_cfg_tag_set來更改即將用於連線的連線設定標記。

/**@brief Function for initializing the Advertising functionality.
 */
static void advertising_init(void)
{
    uint32_t               err_code;
    ble_advertising_init_t init;

    memset(&init, 0, sizeof(init));

    init.advdata.name_type          = BLE_ADVDATA_FULL_NAME;
    init.advdata.include_appearance = false;
    init.advdata.flags              = BLE_GAP_ADV_FLAGS_LE_ONLY_LIMITED_DISC_MODE;

    init.srdata.uuids_complete.uuid_cnt = sizeof(m_adv_uuids) / sizeof(m_adv_uuids[0]);
    init.srdata.uuids_complete.p_uuids  = m_adv_uuids;

    init.config.ble_adv_fast_enabled  = true;
    init.config.ble_adv_fast_interval = APP_ADV_INTERVAL;
    init.config.ble_adv_fast_timeout  = APP_ADV_DURATION;
    init.evt_handler = on_adv_evt;

    err_code = ble_advertising_init(&m_advertising, &init);
    APP_ERROR_CHECK(err_code);

    ble_advertising_conn_cfg_tag_set(&m_advertising, APP_BLE_CONN_CFG_TAG);
}

廣播相關引數的設定都在ble_advertising_init_t這個結構體中,一般用到比較多的是config、advata和srdata,config中主要是涉及配置廣播週期引數,廣播超時時間等,advata和srdata主要是配置廣播包和掃描響應包。一般而言,建議是把資料放在廣播包中,但廣播包只有31個位元組,如果你的廣播資料比較長,例如你希望在廣播包加入裝置名稱、UUID、地址等資料,如果在廣播包中放不下全部資料,也可以把不重要的資料放在掃描響應包中,廣播包和掃描響應包的長度都是31個位元組。

typedef struct
{
    ble_advdata_t           advdata;       /**< Advertising data: name, appearance, discovery flags, and more. */
    ble_advdata_t           srdata;        /**< Scan response data: Supplement to advertising data. */
    ble_adv_modes_config_t  config;        /**< Select which advertising modes and intervals will be utilized.*/
    ble_adv_evt_handler_t   evt_handler;   /**< Event handler that will be called upon advertising events. */
    ble_adv_error_handler_t error_handler; /**< Error handler that will propogate internal errors to the main applications. */
} ble_advertising_init_t;

廣播初始化中還有一個廣播的回撥函式on_adv_evt,這個回撥主要處理在廣播模式下的事件,例程中只羅列了兩個簡單的事件,BLE_ADV_EVT_FAST是在快速廣播模式下,改變LED燈的狀態,進入BSP_INDICATE_ADVERTISING模式,從bsp.c中可以得知此狀態是讓DK板上的LED1閃爍。BLE_ADV_EVT_IDLE是廣播超時之後產生的事件,廣播超時時間可以在上面結構體的config中找到相應引數去配置,例程中對此的處理是此事件產生後,進入休眠模式。

/**@brief Function for handling advertising events.
 *
 * @details This function will be called for advertising events which are passed to the application.
 *
 * @param[in] ble_adv_evt  Advertising event.
 */
static void on_adv_evt(ble_adv_evt_t ble_adv_evt)
{
    uint32_t err_code;

    switch (ble_adv_evt)
    {
        case BLE_ADV_EVT_FAST:
            err_code = bsp_indication_set(BSP_INDICATE_ADVERTISING);
            APP_ERROR_CHECK(err_code);
            break;
        case BLE_ADV_EVT_IDLE:
            sleep_mode_enter();
            break;
        default:
            break;
    }
}

 

9、conn_params_init這個函式的作用是初始化連線引數,連線引數包括了BLE主機與從機連線過程中協商的一些引數,例如連線間隔、從啟動事件(連線或通知開始)到第一次呼叫sd_ble_gap_conn_param_update的時間、第一次之後每次呼叫sd_ble_gap_conn_param_update之間的時間、在放棄協商之前嘗試的次數等引數。

/**@brief Function for initializing the Connection Parameters module.
 */
static void conn_params_init(void)
{
    uint32_t               err_code;
    ble_conn_params_init_t cp_init;

    memset(&cp_init, 0, sizeof(cp_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);
}

具體的連線引數可以在ble_conn_params_init_t這個結構體中找到。對於初學者而言,如果你對BLE協議不熟悉,對這些引數具體的用途不清楚,這裡建議照搬例程中的程式碼,不去做改動,只需要知道此函式的作用即可。

/**@brief Connection Parameters Module init structure. This contains all options and data needed for
 *        initialization of the connection parameters negotiation module. */
typedef struct
{
    ble_gap_conn_params_t *       p_conn_params;                    //!< Pointer to the connection parameters desired by the application. When calling ble_conn_params_init, if this parameter is set to NULL, the connection parameters will be fetched from host.
    uint32_t                      first_conn_params_update_delay;   //!< Time from initiating event (connect or start of notification) to first time sd_ble_gap_conn_param_update is called (in number of timer ticks).
    uint32_t                      next_conn_params_update_delay;    //!< Time between each call to sd_ble_gap_conn_param_update after the first (in number of timer ticks). Recommended value 30 seconds as per BLUETOOTH SPECIFICATION Version 4.0.
    uint8_t                       max_conn_params_update_count;     //!< Number of attempts before giving up the negotiation.
    uint16_t                      start_on_notify_cccd_handle;      //!< If procedure is to be started when notification is started, set this to the handle of the corresponding CCCD. Set to BLE_GATT_HANDLE_INVALID if procedure is to be started on connect event.
    bool                          disconnect_on_fail;               //!< Set to TRUE if a failed connection parameters update shall cause an automatic disconnection, set to FALSE otherwise.
    ble_conn_params_evt_handler_t evt_handler;                      //!< Event handler to be called for handling events in the Connection Parameters.
    ble_srv_error_handler_t       error_handler;                    //!< Function to be called in case of an error.
} ble_conn_params_init_t;

在ble_conn_params_init_t中有兩個回撥函式,分別是事件回撥和錯誤回撥,在例程中對應的是on_conn_params_evt和conn_params_error_handler,這兩個回撥函式用於處理連線過程中的事件和連線引數錯誤的情況。

可以看到在 on_conn_params_evt 中,只對BLE_CONN_PARAMS_EVT_FAILED這個事件做了處理,檢視註釋可以知道此事件是連線引數協商失敗會產生的,對其的處理是呼叫sd_ble_gap_disconnect斷開連線。

conn_params_error_handler是對連線引數錯誤的回撥,例程中對其處理是直接呼叫 APP_ERROR_HANDLER 來檢查錯誤。

/**@brief Function for handling an event from the Connection Parameters Module.
 *
 * @details This function will be called for all events in the Connection Parameters Module
 *          which are passed to the application.
 *
 * @note All this function does is to disconnect. This could have been done by simply setting
 *       the disconnect_on_fail config parameter, but instead we use the event handler
 *       mechanism to demonstrate its use.
 *
 * @param[in] p_evt  Event received from the Connection Parameters Module.
 */
static void on_conn_params_evt(ble_conn_params_evt_t * p_evt)
{
    uint32_t err_code;

    if (p_evt->evt_type == BLE_CONN_PARAMS_EVT_FAILED)
    {
        err_code = sd_ble_gap_disconnect(m_conn_handle, BLE_HCI_CONN_INTERVAL_UNACCEPTABLE);
        APP_ERROR_CHECK(err_code);
    }
}


/**@brief Function for handling errors from the Connection Parameters module.
 *
 * @param[in] nrf_error  Error code containing information about what went wrong.
 */
static void conn_params_error_handler(uint32_t nrf_error)
{
    APP_ERROR_HANDLER(nrf_error);
}

 

10、完成初始化所有模組的初始化之後,例程中是呼叫了 printf 和 NRF_LOG_INFO 列印了Log,二者的區別是printf直接在串列埠列印,而NRF_LOG_INFO是需要開啟Log列印模組的。advertising_start的作用是開啟廣播,例程中的advertising_start函式是呼叫了ble_advertising_start這個函式來實現開啟廣播的。

/**@brief Function for starting advertising.
 */
static void advertising_start(void)
{
    uint32_t err_code = ble_advertising_start(&m_advertising, BLE_ADV_MODE_FAST);
    APP_ERROR_CHECK(err_code);
}

 

11、最後就是for迴圈中的程式碼,在帶協議棧的例程中,是呼叫idle_state_handle來實現CPU在低功耗狀態等待和上報事件,其效果和 __SEV() 和 __WFE() 類似。但是因為在BLE協議棧開啟後,CPU要優先處理協議棧相關的任務,且協議棧開啟後不能直接操作底層暫存器和指令,所以需要呼叫協議棧提供的介面來實現, 這就是nrf_pwr_mgmt_run的作用 ,它用於處理空閒模式並進入System ON睡眠模式,在BLE狀態下,如果CPU處於空閒狀態,就會進入sd_app_evt_wait()函式,來等待協議棧上報事件。這就是之前 power_management_ini t中要呼叫 nrf_pwr_mgmt_init 對低功耗管理模組初始化的原因,因為要在for迴圈中使用nrf_pwr_mgmt_run。

/**@brief Function for handling the idle state (main loop).
 *
 * @details If there is no pending log operation, then sleep until next the next event occurs.
 */
static void idle_state_handle(void)
{
    if (NRF_LOG_PROCESS() == false)
    {
        nrf_pwr_mgmt_run();
    }
}

 

12、練習

介紹完ble_app_uart這個例程,我們來做一個小練習,修改裝置的廣播名稱、連線間隔、廣播週期。

(1)廣播名稱在例程中是在 gap_params_init 函式中透過sd_ble_gap_device_name_set來設定的,例程中是用了DEVICE_NAME這個宏定義來控制,所以只需要更改這個宏定義即可。

#define DEVICE_NAME                     "Nordic_UART"                               

err_code
= sd_ble_gap_device_name_set(&sec_mode, (const uint8_t *) DEVICE_NAME, strlen(DEVICE_NAME));

(2)、連線間隔同樣是在gap_params_init 函式配置,透過配置ble_gap_conn_params_t中的min_conn_interval和max_conn_interval來實現,這是一個範圍值,之所以是一個範圍值是為了相容不同的主機裝置,因為不同的主機裝置的連線間隔是不同的。ble_app_uart例程中預設的範圍值是20-75ms,我們可以透過修改最小連線間隔和最大連線間隔來修改實際的連線間隔。連線間隔越大,功耗越低,但是相對應的,連線時間和傳送資料的速率會降低。

#define MIN_CONN_INTERVAL            MSEC_TO_UNITS(20, UNIT_1_25_MS)/**< Minimum acceptable connection interval (20 ms), Connection interval uses 1.25 ms units. */
#define MAX_CONN_INTERVAL            MSEC_TO_UNITS(75, UNIT_1_25_MS)/**< Maximum acceptable connection interval (75 ms), Connection interval uses 1.25 ms units. */ 

gap_conn_params.min_conn_interval
= MIN_CONN_INTERVAL; gap_conn_params.max_conn_interval = MAX_CONN_INTERVAL;

(3)、廣播週期則是在advertising_init中透過配置ble_advertising_init_t中的ble_adv_fast_interval或者ble_adv_slow_interval來實現,需要注意的是ble_app_uart中使用的是fast advertising,相對應的還有slow advertising,二者的區別只是廣播資料包的傳送頻率,fast advertising的傳送頻率比slow advertising的傳送頻率更高。

#define APP_ADV_INTERVAL           64  /**< The advertising interval (in units of 0.625 ms. This value corresponds to 40 ms). */

init.config.ble_adv_fast_interval
= APP_ADV_INTERVAL;

 

 小結:從ble_app_uart的例程就更可以看出,Nordic的例程的軟體架構主要就是相應模組的初始化和回撥函式,在開發自己的工程的時候,建議在SDK中找一個和你實際產品類似例程,在其基礎上去做開發,而不是新建一個新的工程從零開始,因為例程中已經完成了主體軟體架構的搭建,可以直接使用例程中BLE部分的相關初始化程式碼和回撥函式,只需要根據實際情況去做微調一些引數即可,剩餘的只是開發者在相應的例程中根據自己產品的實際需求去添磚加瓦即可。

 

相關文章