Nordic nRF52 OTA (BLE DFU) part III - buttonless dfu

上次說要談談buttonless dfu的。

Buttonless DFU主要的想法很簡單,只是取代基本DFU要人去按住Button 4不放,再Reset的機制。因為Reset時會先執行Secure Bootloader, 而Secure Bootloader會先檢查是否Button 4有被按住,若被按住就執行DFU的功能,否則就去執行我們燒進去的firmware。Secure Bootloader除了檢查Button 4的狀態外,再檢查retention register GPREGRET,看看是否內容值為(BOOTLOADER_DFU_START = 0xB1) ,若為此值就執行DFU。透過這個做法,在要做DFU時,不用人去按Button 4, 只要將GPREGRET寫入適當的值,再Reset就可以了。

在SDK v13之後,加入了DFU Buttonless特徵值,前面談到的寫入GPREGRET的動作由這個特徵值來做外,手機APP也可以透過這個特徵的存在與否來判斷設備是否支持Buttonless DFU。等於是訂定了一個標準,讓nRF Connect, nRF Toolbox或自己寫的APP可以很容易的來支持Buttonless DFU。

了解原理後,要為自己的專案加入這個功能才是比較有挑戰的。但官方也提供了example code, 所以並不難!example code存在SDK以下目錄:

\examples\ble_peripheral\ble_app_buttonless_dfu

所以打開它,把程式看一看,大約880行的main(),其實也不多!
不要被嚇到,開個玩笑而已。
以下會告訴大家主要的重點,並不需要全部都看啦!

1. 在SES中,打開example code, 看一下project explorer, 比對一下自己的專案裡是否存在如下圖的這四個.c檔。如果沒有,就自己加進去。加進去之後build一下,看是不是可以build成功,沒成功的話,看一下原因是什麼,可能要加一下include path或是常數定義之類的。
在SES裡,include path的設定方式為,先選擇專案,再按options圖示,或右鍵選options。就會出現如下圖的視窗。注意要先選Common (箭號1),然後左邊選項選Preprocessor (箭號2), 再按下 (箭號3) 所指的小按鈕。
同樣地,常數定義的設定位置,也如下圖所示,在Preprocessor Definitions (箭號4) 這個地方。
2. 很有可能沒有加進去的常數定義:Undefined identifiers in ble_dfu_unbonded.c,當出現類似這樣的錯誤訊息時,就表示以下常數沒有定義到:NRF_DFU_SVCI_ENABLED, NRF_DFU_TRANSPORT_BLE=1
 3. 將main.c的line 129 - 282複製起來,貼到自己專案的main.c的前面,約略相當的地方。這幾行程式提供給我們在發起DFU之前可以做一些檢查系統狀態的能力,讓你可以依系統狀態來決定是否要真的進行DFU,或者在做DFU之前可以先做一些動作,例如儲存資料到FLASH。

/**@brief Handler for shutdown preparation.

 *

 * @details During shutdown procedures, this function will be called at a 1 second interval

 *          untill the function returns true. When the function returns true, it means that the

 *          app is ready to reset to DFU mode.

 *

 * @param[in]   event   Power manager event.

 *

 * @retval  True if shutdown is allowed by this power manager handler, otherwise false.

 */

static bool app_shutdown_handler(nrf_pwr_mgmt_evt_t event)

{

    switch (event)

    {

        case NRF_PWR_MGMT_EVT_PREPARE_DFU:

            NRF_LOG_INFO("Power management wants to reset to DFU mode.");

            // YOUR_JOB: Get ready to reset into DFU mode

            //

            // If you aren't finished with any ongoing tasks, return "false" to

            // signal to the system that reset is impossible at this stage.

            //

            // Here is an example using a variable to delay resetting the device.

            //

            // if (!m_ready_for_reset)

            // {

            //      return false;

            // }

            // else

            //{

            //

            //    // Device ready to enter

            //    uint32_t err_code;

            //    err_code = sd_softdevice_disable();

            //    APP_ERROR_CHECK(err_code);

            //    err_code = app_timer_stop_all();

            //    APP_ERROR_CHECK(err_code);

            //}

            break;


        default:

            // YOUR_JOB: Implement any of the other events available from the power management module:

            //      -NRF_PWR_MGMT_EVT_PREPARE_SYSOFF

            //      -NRF_PWR_MGMT_EVT_PREPARE_WAKEUP

            //      -NRF_PWR_MGMT_EVT_PREPARE_RESET

            return true;

    }


    NRF_LOG_INFO("Power management allowed to reset to DFU mode.");

    return true;

}


//lint -esym(528, m_app_shutdown_handler)

/**@brief Register application shutdown handler with priority 0.

 */

NRF_PWR_MGMT_HANDLER_REGISTER(app_shutdown_handler, 0);



static void buttonless_dfu_sdh_state_observer(nrf_sdh_state_evt_t state, void * p_context)

{

    if (state == NRF_SDH_EVT_STATE_DISABLED)

    {

        // Softdevice was disabled before going into reset. Inform bootloader to skip CRC on next boot.

        nrf_power_gpregret2_set(BOOTLOADER_DFU_SKIP_CRC);


        //Go to system off.

        nrf_pwr_mgmt_shutdown(NRF_PWR_MGMT_SHUTDOWN_GOTO_SYSOFF);

    }

}


/* nrf_sdh state observer. */

NRF_SDH_STATE_OBSERVER(m_buttonless_dfu_state_obs, 0) =

{

    .handler = buttonless_dfu_sdh_state_observer,

};



static void advertising_config_get(ble_adv_modes_config_t * p_config)

{

    memset(p_config, 0, sizeof(ble_adv_modes_config_t));


    p_config->ble_adv_fast_enabled  = true;

    p_config->ble_adv_fast_interval = APP_ADV_INTERVAL;

    p_config->ble_adv_fast_timeout  = APP_ADV_DURATION;

}



static void disconnect(uint16_t conn_handle, void * p_context)

{

    UNUSED_PARAMETER(p_context);


    ret_code_t err_code = sd_ble_gap_disconnect(conn_handle, BLE_HCI_REMOTE_USER_TERMINATED_CONNECTION);

    if (err_code != NRF_SUCCESS)

    {

        NRF_LOG_WARNING("Failed to disconnect connection. Connection handle: %d Error: %d", conn_handle, err_code);

    }

    else

    {

        NRF_LOG_DEBUG("Disconnected connection handle %d", conn_handle);

    }

}




// YOUR_JOB: Update this code if you want to do anything given a DFU event (optional).

/**@brief Function for handling dfu events from the Buttonless Secure DFU service

 *

 * @param[in]   event   Event from the Buttonless Secure DFU service.

 */

static void ble_dfu_evt_handler(ble_dfu_buttonless_evt_type_t event)

{

    switch (event)

    {

        case BLE_DFU_EVT_BOOTLOADER_ENTER_PREPARE:

        {

            NRF_LOG_INFO("Device is preparing to enter bootloader mode.");


            // Prevent device from advertising on disconnect.

            ble_adv_modes_config_t config;

            advertising_config_get(&config);

            config.ble_adv_on_disconnect_disabled = true;

            ble_advertising_modes_config_set(&m_advertising, &config);


            // Disconnect all other bonded devices that currently are connected.

            // This is required to receive a service changed indication

            // on bootup after a successful (or aborted) Device Firmware Update.

            uint32_t conn_count = ble_conn_state_for_each_connected(disconnect, NULL);

            NRF_LOG_INFO("Disconnected %d links.", conn_count);

            break;

        }


        case BLE_DFU_EVT_BOOTLOADER_ENTER:

            // YOUR_JOB: Write app-specific unwritten data to FLASH, control finalization of this

            //           by delaying reset by reporting false in app_shutdown_handler

            NRF_LOG_INFO("Device will enter bootloader mode.");

            break;


        case BLE_DFU_EVT_BOOTLOADER_ENTER_FAILED:

            NRF_LOG_ERROR("Request to enter bootloader mode failed asynchroneously.");

            // YOUR_JOB: Take corrective measures to resolve the issue

            //           like calling APP_ERROR_CHECK to reset the device.

            break;


        case BLE_DFU_EVT_RESPONSE_SEND_ERROR:

            NRF_LOG_ERROR("Request to send a response to client failed.");

            // YOUR_JOB: Take corrective measures to resolve the issue

            //           like calling APP_ERROR_CHECK to reset the device.

            APP_ERROR_CHECK(false);

            break;


        default:

            NRF_LOG_ERROR("Unknown event from ble_dfu_buttonless.");

            break;

    }

}


4. 還有在services_init(void)加入以下一行:
dfu_buttonless_init();
當然在前面就要先定義這個函數:
static void dfu_buttonless_init(void)
{
    uint32_t                  err_code;
    ble_dfu_buttonless_init_t dfus_init = {0};

    // Initialize the async SVCI interface to bootloader before any interrupts are enabled.
    err_code = ble_dfu_buttonless_async_svci_init();
    APP_ERROR_CHECK(err_code);

    dfus_init.evt_handler = ble_dfu_evt_handler;

    err_code = ble_dfu_buttonless_init(&dfus_init);
    APP_ERROR_CHECK(err_code);

}

5. 以上這個函數其實會新增一個服務和特徵值,所以和所有加入服務的程式一樣,記憶體的設定必須調整。首先在sdk_config.h檔案裡找以下字串,把值加1,例如我原本是1,所以現在的值變成2。接著可以進入除錯模式,Debug -> Go (F5), 然後觀察錯誤訊息,就會告訴你記憶體的大小要改為多才才對,如下圖,照著建議值修改,再試著執行一次,應該就會成功了。
//  NRF_SDH_BLE_VS_UUID_COUNT - The number of vendor-specific UUIDs. 
#ifndef NRF_SDH_BLE_VS_UUID_COUNT
#define NRF_SDH_BLE_VS_UUID_COUNT 2
#endif
---------------------------------------------
接著要試看看是不是真的成功整合到我們自己的專案了:

1. 先打開之前改好的Secure Bootloader, 燒錄到開發板去
2. 照之前的說明,把自己的專案打包成.zip檔
3. 用nRF Connect 找到DfuTarg裝置,執行DFU,把打包檔OTA進去
4. 再用nRF Connect找一次裝置,會發現,現在多了一個服務,如下圖。同時也可以看到這個裝置多了一個DFU的圖示。按下這個圖示,果然也可以進行DFU的功能了,而且不用去按裝置的Button 4
使用手機版的nRF Connect, 也會發現,現在可以直接啓動DFU,並不用先按裝置的按鈕。連設備的圖示也會出現DFU的樣子,如下圖。按Connect連線上去之後, 可能會出現要求配對的請求(視你的專案設定而異),我兩種情況都試過,不管有沒有配對,都可以DFU成功。這有點讓我喜出望外!因為文件https://devzone.nordicsemi.com/nordic/short-range-guides/b/software-development-kit/posts/getting-started-with-nordics-secure-dfu-bootloader的說明,當有配對時可能無法DFU成功,必須做Bond forwarding的工作才行。
但看來目前已經可以正常運作了,我就不往下玩了。等日後若碰到問題再來看了!








留言

這個網誌中的熱門文章

D-BUS學習筆記

Cisco Switch學習筆記: EtherChannel

Cisco Switch學習筆記: interface的封包錯誤統計