nRF5 SDK:Buttonless DFU実装方法のまとめ

# はじめに
nRF5 SDKはメンテナンスモードに入ってアップデートされることはなくなりましたが、後継SDKであるnRF Connect SDKにはDFUでアップデートすることができないため、すでにリリースした製品のこともあってまだしばらくは生き残りそうです。
もし初めてNordic製品を取り扱うということでしたらnRF Connect SDKを習得することを強くお奨めします。
# DFUファイルを作成するための環境構築
## Pythonのインストール
Microsoft StoreでPythonを検索してインストールします。執筆時点(2024/8/28)での最新のPythonは3.12ですが、ここでは3.10を使用します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3797041/b6b5d484-3c69-2667-86ca-e4f75bf8d49c.png)
## nrfutilのインストール
```
pip install nrfutil
```
でnrfutilをインストールします。執筆時点(2024/8/28)でのnrfutilのバージョンは6.1.2で、こちらもメンテナンスモードに入ったため今後アップデートされることはありません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3797041/c24ded00-3ec1-f3e2-a7da-dbc34537fd73.png)

:::note warn
nrfutilに依存関係のあるパッケージが色々とインストールされますがアップデートしないでください。libusb1やprotobufは最新版ではないものを使うため、依存関係が壊れてnrfutilが動かなくなります。
:::
## システム環境変数の追加
コマンドプロンプトを起動して
```
where nrfutil
```
と打ち込むとパスが表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3797041/7ed619b3-17d9-181a-1e4f-c829ba0c2b53.png)
このパスをコピーしてシステム環境変数のパスに貼り付けます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3797041/921fa5b9-53ee-f620-20ca-825243bb79f4.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3797041/c6ca9cef-08df-521b-f6df-ff9e8e5f0256.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3797041/ab1e69a1-86d4-7dd8-f39e-2fa1de4c4107.png)
## micro eccライブラリの生成(オプション)
:::note
この部分は読み飛ばしても大丈夫です
:::

DFUを実行するためのBootloaderには暗号化が必須となります。nRF5 SDKでは様々な暗号化が用意されていますが、そのうちの一つであるmicro eccという暗号化方式を使用する場合はmicro eccのインストールが必要になります。
nRF52840を使う場合はCC310/CC310 BLというハードウェア暗号化が使えるのでそちらを使うのがよいと思いますし、実際にサンプルプロジェクトもCC310 BLを使うように作られています。nRF52382を使う場合もデフォルトはOberonという暗号化を使うように設定されていますので、どうしてもmicro eccを使いたいんだ！という強い意志があるのでなければ飛ばして全然かまいません。

:::note
それ以外にもMBEDTLSなども用意されているので基本的にmicro eccのインストールは不要です。ただ、ここらへんを私がちゃんと理解していなかったので改めて記載しました。
:::
### micro-eccのダウンロードとインストール
githubからダウンロードしたzipを展開してSDKフォルダ下にある external\micro-ecc へ置きます。

https://github.com/kmackay/micro-ecc

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3797041/8ddbebdc-7cd1-8e30-534c-9e25a83d8243.png)

### makeのインストールとパスの追加
micro eccをビルドするためにWindows用のmakeをインストールします。かなり古いですが、もう更新されていないようです。

https://gnuwin32.sourceforge.net/packages/make.htm

こちらもパスを通します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3797041/012e2c65-3f78-3054-05a8-3b6dac76680a.png)

## GNU Arm Embedded Toolchainのダウンロードとインストール
下記ホームページからダウンロードできますが、ホームページ自体がDeprecatedとなっていますのでいつなくなってもおかしくないです。

https://developer.arm.com/tools-and-software/open-source-software/developer-tools/gnu-toolchain/gnu-rm/downloads

ちなみにDeprecatedのホームページから飛ぶ新しいホームページでは下記の対応するgccをダウンロードすることはできません。
SDK 16.0.0用コンパイラー：gcc-arm-none-eabi-7-2018-q2-update-win32
SDK 17.1.0用コンパイラー：gcc-arm-none-eabi-9-2020-q2-update-win32

:::note warn
それくらい古いということですね……。nRF Connect SDKを習得することをお奨めします。
:::

ここまでインストールできたらnRF52シリーズが使用するnrf52_armgccフォルダでmakeを実行してライブライファイルを生成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3797041/55bdd2e1-465e-a1e0-15a9-7e2f027b1476.png)
# 評価ボードの準備
このプロジェクトではnRF52840DKを使用します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3797041/65ac80e5-3d46-6823-4182-1ff4b2e1931e.png)
もしnRF52840以外の評価ボードを使用する場合、後述するCC310[BL]は使用できないため、その場合はOberonを使用することをお奨めします。Bootloaderの暗号化部分については手を加えていないので意識しなくても大丈夫です。
# プロジェクトの作成
サンプルプロジェクトを作成します。使用するプロジェクトはexampleフォルダ内にあるble_app_templateとsecure_bootloaderの2プロジェクトです。プロジェクトフォルダを置く場所ですが、プロジェクトが相対パスで参照することを考慮してexampleと同じ深さのところにprojectsフォルダを作って置くことにします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3797041/28a780bb-41f1-33d3-f77b-dc5d32ef322d.png)
gitに作成したプロジェクトを置いておきますので作るのがめんどくさい人はこちらからダウンロードしてください。相対パスを使用しているので正しい階層に展開しないと動きません。

https://github.com/yoshihiro0709X/ble_app_template_pca10056

## 秘密鍵の生成
コンソール(cmd.exe)からnrfutilを使って

```
nrfutil keys generate private.key
```
と打ち込んで秘密鍵を生成します。現在のフォルダにprivate.keyというテキストファイルが生成されます。ちなみに中身はこんな感じです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3797041/3099ed48-f4fe-b411-bcd0-c8f03f01ae6d.png)

次にその生成した秘密鍵を使って

```
nrfutil keys display --key pk --format code private.key --out_file public_key.c
```

と打ち込んで公開鍵を生成します。現在のフォルダにpublic_key.cというテキストファイルが生成されます。ちなみに中身はこんな感じです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3797041/d2982eda-a38b-10b2-c1fa-2feff99ba58f.png)

:::note warn
ここで生成したprivate.keyはこの後DFUパッケージを生成するために使用します。また、public_key.cはBootloaderのビルド時に使用します。private.keyは同じものを作ることはできないので、ここで生成したprivate.keyファイルを紛失すると二度とDFUできなくなります。
:::

# Bootloaderのビルド
先ほど生成した公開鍵を使用してBootloaderプロジェクトをビルドします。このBootloaderプロジェクトにはdfu_public_key.cというソースコードが含まれていますが、いわゆるダミーファイルでそのまま使うことはできません。ビルドしてもエラーになるので、先ほど生成したpublic_key.cファイルと差し替えます。
# Application(ble_app_template)のビルド
## プロジェクト参照パスの追加
プロジェクトがデフォルトでは参照していないパスを追加します。
```
../../components/libraries/bootloader
../../components/libraries/bootloader/dfu
```

## コンパイルオプションの追加
以下のコンパイルオプションを追加します。
```
BL_SETTINGS_ACCESS_ONLY
NRF_DFU_SVCI_ENABLED
NRF_DFU_TRANSPORT_BLE=1
```

## ファイルの追加
DFUの機能に関連する以下のファイルを追加します。
- ble_dfu*.c(計3ファイル)
- nrf_dfu_svci.c
これらをnRF_DFUというフォルダを生成して追加します。
::: note
一般的な開発プロジェクトと同じで、ファイルのフォルダ分けはただ分かりやすくしているだけで名前はnRF_DFUでなくても全然問題ないです。
:::

## ソースコードの追加
### インクルードファイル
```main.c
#include "nrf_bootloader_info.h"
#include "ble_dfu.h"
#include "nrfx_power.h"
```

### DFUコード本体
```main.c
/**@brief Callback function for asserts in the SoftDevice.
 *
 * @details This function will be called in case of an assert in the SoftDevice.
 *
 * @warning This handler is an example only and does not fit a final product. You need to analyze
 *          how your product is supposed to react in case of Assert.
 * @warning On assert from the SoftDevice, the system can only recover on reset.
 *
 * @param[in] line_num   Line number of the failing ASSERT call.
 * @param[in] file_name  File name of the failing ASSERT call.
 */
void assert_nrf_callback(uint16_t line_num, const uint8_t * p_file_name)
{
    app_error_handler(DEAD_BEEF, line_num, p_file_name);
}


/* ----- DFU Function Start ----- */
/**@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);
    }
}


/**@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;
    }
}
/* ----- DFU Function End ----- */
```

### main()関数
```main.c
/**@brief Function for application main entry.
 */
int main(void)
{
    bool erase_bonds;

    // Initialize.
    log_init();

    // Initialize the async SVCI interface to bootloader before any interrupts are enabled.
#if !defined(DEBUG)
    err_code = ble_dfu_buttonless_async_svci_init();
    APP_ERROR_CHECK(err_code);
#endif
}
```

### services_init()関数

```main.c
static void services_init(void)
{
    ble_dfu_buttonless_init_t dfus_init = {0};

#if !defined(DEBUG)
    // Initialize DFU(Device Firmware Update).
    dfus_init.evt_handler = ble_dfu_evt_handler;

    err_code = ble_dfu_buttonless_init(&dfus_init);
    APP_ERROR_CHECK(err_code);
#endif
}
```

### コンフィギュレーションファイル

```sdk_config.h
// <q> BLE_DFU_ENABLED  - Enable DFU Service.


#ifndef BLE_DFU_ENABLED
#define BLE_DFU_ENABLED 1
#endif

// <q> NRF_PWR_MGMT_CONFIG_AUTO_SHUTDOWN_RETRY  - Blocked shutdown procedure will be retried every second.


#ifndef NRF_PWR_MGMT_CONFIG_AUTO_SHUTDOWN_RETRY
#define NRF_PWR_MGMT_CONFIG_AUTO_SHUTDOWN_RETRY 1
#endif

// <o> 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 1
#endif
```

# DFUファイルの生成
ここで再びnrfutilの登場になります。コマンドプロンプトから以下のコマンドを入力するとDFU用のzipファイルが生成されます。
```
nrfutil pkg generate --hw-version 52 --application-version 1 --sd-req 0xCA --application Output\Release\Exe\app.hex --key-file private.key dfu.zip
```
BootloaderやSoftdeviceもバージョンアップする場合はもう少しコマンドが長くなります。
```
nrfutil pkg generate --hw-version 52 --application-version 1 --bootloader-version 1 --sd-id 0xCA --sd-req 0xB6,0xCA --application Output\Release\Exe\app.hex --bootloader Output\Release\Exe\bootloader.hex --softdevice ../../../components/softdevice/s140/hex/s140_nrf52_7.0.1_softdevice.hex --key-file private.key dfu_full.zip
```
::: note
application-versionやbootloader-versionは数字を増やすことができます。このサンプルプロジェクトではダウングレードを禁止する設定になっていないのでこの数字に意味はありません。
:::

# ファームウェアの書き込み
## Bootloaderの書き込み
ターゲットプロジェクトをBootloaderに合わせてDownload Bootloaderメニューで書き込みできます。書き込みが終了するとLED1とLED2が点灯します。
## nRF Connect for mobileでの接続
DFUを実行するためにはnRF Connectが必要です。nRF Connect for mobileもしくはnRF Connect for desktopのいずれでもよいですが、nRF Connect for desktopでBluetoothを制御するためには評価ボードが別途必要になります。(以下、両方をnRF Connectと呼びます)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3797041/c222cbd3-42ef-bf50-d30a-8f27f6a70822.png)

https://www.nordicsemi.com/Products/Development-tools/nRF-Connect-for-Desktop

::: note warn
今回は関係ないですが、nRF Connect for desktopは拡張アドバタイズに非対応なのでその点からもお勧めできません。
:::
nRF Connectを動かすと何やら怪しげな名前(笑)のデバイスが表示されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3797041/74e1ecd5-b894-682b-086d-579812f37c9e.png)
これが先ほどBootloaderを書き込んだ評価ボードです。デフォルトのDfuTargという名前でアドバタイズをしています。右端のCONNECTボタンをタップすると接続します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3797041/6086a5d4-2f57-f78d-ee71-fc7c1471b88a.png)
デバイスの詳細が表示され、右上にDFUというボタンが表示されるので、タップするとファイルを選択するダイアログが表示されます。ここで先ほど作ったDFU用のzipファイルを選択するとDFUが実行されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3797041/09741e79-6646-5899-3e8d-0abf7c4d7363.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3797041/2744b2af-cafb-893a-d1ec-76061c98e198.png)
なお、アップロードが完了すると再起動後に自動的に接続されるのですが、ここではBootloaderのアドバタイズに接続しており、アプリケーション部分を書き込んだ場合はBootloaderがアドバタイズしないため自動接続は失敗します。
また、余談ですがPCとスマートフォンでのファイルの受け渡しはBluetoothが楽です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3797041/5aafa5ec-5370-e2a2-f7b2-65ad35330ee1.png)
## めんどくさくないですか……？
Bootloader付きのプロジェクトを書き込むためにはこのようにまずBootloaderを書き込み、そのBootloaderを立ち上げてDFUでアプリケーションを書き込む必要があります。
え、でもBootloaderのIntel HEXとAppのIntel HEXはアドレスがかぶっていないから普通に書き込めばいいのでは……？って一度は誰もが思うはずです。が、残念ながら実際にはそれでは動きません。
なぜ動かないかと言うと、0xFE000番地から始まる設定エリアに何も書き込まれないため、その部分でエラーが発生してアプリケーションが起動しないためです。DFUでアプリケーションを書き込む場合はその設定も自動で書き込んでくれます。
なお、その設定部分の情報を生成するためのコマンドもあります。
```
nrfutil settings generate --family NRF52840 --application Output\Release\Exe\app.hex --application-version 1 --bootloader-version 1 --bl-settings-version 1 --key-file private.key settings.hex
```
BootloaderとApplicationに続いてこれで生成されたファイル(Settings)も書き込むとダウンロードケーブル経由で書き込みができます。
mergehexを使って3つのファイル(とSoftdevice)を全部くっつけてしまうと、1回の書き込みでBootloader付きのアプリケーションを書き込むことができます。
mergehexを2回やっているのは、mergerhexでマージできるのが最大で3ファイルまでなので2回に分けてmergehexしているためです。(全部で4ファイルあります)
```
nrfutil settings generate --family NRF52840 --application Output\Release\Exe\app.hex --application-version 1 --bootloader-version 1 --bl-settings-version 1 --key-file private.key settings.hex
mergehex -m Output\Release\Exe\app.hex Output\Release\Exe\bootloader.hex settings.hex -o master_temp.hex
mergehex -m master_temp.hex ../../\components\softdevice\s140\hex\s140_nrf52_7.0.1_softdevice.hex -o dfu_master.hex
del settings.hex
del master_temp.hex
```
# おまけ
じつは最初にこれを書いていたのですが、DFUやっても動かないことが判明したのでおまけとして残しておきます(笑)
なぜかダウンロードケーブルで書き込んだときは普通に動きますが、全然理由がわかりません。ただ、最初に書いたようにメンテナンスモードに入っているSDKなのでこれ以上追及はしません。
## CC310に差し替え(オプション)
:::note warn
nRF52840DK以外の評価ボードでは使用できません。その場合は読み飛ばしてください。
:::

サンプルプロジェクトのBootloaderはデフォルトでCC310 BLを使う設定になっています。もちろんこれでも全然かまわないのですが、せっかくなので簡易版ではなくちゃんとしたCC310に差し替えてみましょう。
### 追加するファイル
以下のファイルをプロジェクトに追加します。
- libnrf_cc310_0.9.12.a
- cc310_backend_*.c(計13ファイル)
- nrf_crypto_rng.c

:::note
libnrf_cc310_0.9.12.aはC:\Nordic\nRF5_SDK_16.0.0_98a08e2\external\nrf_cc310\lib\cortex-m4\hard-floatに、cc310_backend_*.cはC:\Nordic\nRF5_SDK_16.0.0_98a08e2\components\libraries\crypto\backend\cc310にそれぞれあります。
:::
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3797041/044843a8-e812-1f3b-7f95-3e7b6c3e5ffa.png)
### 削除するファイル(任意)
以下のファイルをプロジェクトから削除します。ただし、残っていても問題はありません。
- libnrf_cc310_bl_0.9.12.a(とそのフォルダ)
- cc310_bl_backend_*.c(計5ファイル)(とそのフォルダ)
- oberon_backend_*.c(計7ファイル)(とそのフォルダ)
- liboberon_3.0.1.a(とそのフォルダ)
### sdk_config.hの変更
以下の設定を0に変更します
```
#ifndef NRF_CRYPTO_BACKEND_CC310_BL_ENABLED
#define NRF_CRYPTO_BACKEND_CC310_BL_ENABLED 1 → 0
#endif
```
以下の設定を1に変更します
```
#ifndef NRF_CRYPTO_BACKEND_CC310_ENABLED
#define NRF_CRYPTO_BACKEND_CC310_ENABLED 0 → 1
#endif
```
以上でCC310 BLからCC310への変更は完了です。