2017年1月時点で、nRF52のファームウェア開発は、S132 SDK 12.2.0 を使っています。その開発環境でのブートローダの作り方のメモです。NordicのDFUは、SDKのバージョンごとに変更が多く入ります。

ドキュメント: BLE Secure DFU Bootloader

SDK12でのブートローダの作り

まず、ブートローダをセキュアにするために、バイナリ転送に公開鍵暗号方式が使われます。
これまでのDFUには、書き込もうとしているファームウェアが正規の開発元が提供するものかどうかを確認する機能はありませんでした。ですから、DFUに入る方法さえ調べてしまえば、改造したファームウェアを正規のDFU手順で書き込むことができました。
セキュアになったDFUは、転送されたバイナリが開発元しか知らない秘密鍵で署名されているかどうかを確認します。この機能で、開発元しか、ファームウェア更新ができなくなります。

ブートローダの実装では、DFUを開始するinit packetに、protocol buffersが採用されています。
Protocol buffersは、Googleの社内で使われているデータ構造定義ファイルから、シリアライザ/デシリアライザおよびRPCサーバのコードを生成するツールです。
拡張子( .proto )のファイルに、C言語に似たProtocol buffersの記法で列挙型や構造体のデータ構造を表記すれば、そこからC++を始めとする各種言語用のシリアライザ/デシリアライザのコードを生成します。
Protocol buffersのシリアライザには、プリミティブな値型のバイナリを単に並べて詰めていくだけではなく、連続する0は短い表現にしてデータ長を短くする簡単な圧縮機能もあります。

ブートローダの実装では、DFUに入るかどうかを判定するbooleanの値を返す関数が、weak宣言されています。ですから、ユーザがその関数を宣言すればユーザが宣言した関数がリンクされ、もしも宣言していなければデフォルトの関数としてSDK内部にあるweakなその関数がリンクされます。
SDK10のDFUでは、DFUに入るかどうかの判定関数がSDKのソースの一部に入っていました。そのため、そのDFUに入るかどうかの処理を独自に作りたい部分は、SDKから該当のソースコードを取り出して、直接ソースを変更する手間がかかっていました。

ブートローダのサンプルコード

ブートローダのテンプレートプロジェクトは、 /examples/dfu/bootloader_secure/ にあります。pca10xxxという開発ボードの名前ごとに、プロジェクトファイルがあります。

このブートローダに対応する、ファームウェア側でDFUに入るためのコードサンプルは、Experimental: Buttonless DFU Template Application にあるように、 /examples/ble_peripheral/experimental_ble_app_buttonless_dfu のボタンレスDFUのファームウェアです。これはDFU専用のサービスとキャラクタリスティクスを作り、そこに0x01が書き込まれればDFUに入ります。

DFUに入るフラグの保存方法

SDK12のDFUは、デフォルトの実装では、DFUに入るフラグをフラッシュに書き込み、そして再起動してブートローダがそのフラッシュのフラグを読み出して、DFUに入る動作をします。
フラッシュへの書き込みは、専用のライブラリがSDKに用意されています。 experimental_ble_app_buttonless_dfu のソースコードを追っていけば、その関数などが見つかります。

SDK10では、リセットされても初期化されないユーザが任意に使えるレジスタにDFUモードに入るフラグを書き込む実装を使っていました。フラッシュを使おうとすると、BLEの接続切断の完了を待ってから、フラッシュの書き込み処理を行い、それが完了するのを待って再起動をする処理が必要になり、手間だと思ったのでSDK10と同じレジスタに書き込むだけの簡易処理を今回は使いました。

ブートローダの作成メモ

makeコマンドやPythonが必要になります。開発環境はMacで、その上で仮想化したWindowsを使いKeilを使っています。Keilは単なるビルドとデバッグのIDEとなっています。
Windowsにmakeコマンドを入れたりPythonの環境を作るのは手間だったので、はじめからそれらの環境があるMacで、以下の手順を行っています。

まず、テンプレートプロジェクトをコピーしてきます。プロトコルバッファの定義ファイルからのファイル生成は、既存の生成ファイル(dfu-cc.pb.h, dfu-cc.pb.c)を、そのまま使います。

次に、秘密鍵/公開鍵の生成や、ファームウェアのイメージを作る nrfutil をインストールします。nrfutilのバージョンは以下のように、SDK12に対応するバージョンは1.5.0以降です。2017年1月時点では2.1.0になっています。

  • Version 0.5.2 generates legacy firmware packages compatible with nRF SDK 11.0 and older
  • Versions 1.5.0 and later generate modern firmware packages compatible with nRF SDK 12.0 and newer

nrfutilは、https://github.com/NordicSemiconductor/pc-nrfutil/ にインストール手順がありますが、pythonのパッケージ管理システムを使うと便利です。

pip install nrfutil

次に、秘密鍵/公開鍵を生成します。公開鍵は、dfu_public_key.c にバイナリ配列で定義されます。
デバッグ用プロジェクトでは、デバッグ用の鍵が予め入っているのですが、開発時に今のブートローダがデバッグ版なのかプロダクト版なのか区別するのは面倒なので、最初から鍵を生成して指定しておきます。

鍵の生成手順は、 Working with keysにあります。
秘密鍵を作りそれをpriv.pemというファイルに保存します。その秘密鍵から公開鍵を作りpublic_key.cに、ソースコードとして出力します。このファイルの内容を dfu_public_key.c にコピーします。

# Generate a private key in c:\vault\priv.pem
nrfutil keys generate c:\vault\priv.pem
# Write the public key that corresponds to the generated private key
# to the file public_key.c (in code format)
nrfutil keys display --key pk --format code c:\vault\priv.pem --out_file public_key.c

次に、ファームウェアで使うための、micro-eccというオープンソースの暗号処理ライブラリをインストールします。このライブラリのライセンス条項で、ビルドしたライブラリのファイルは提供されていないので、自分でビルドしなければなりません。
その手順はWorking with keysの次の節に書いてあります。

Mac では、gccコンパイラツールチェインを、リンクにあるARMのサイトからダウンロードしてインストールします。次に、ソースコードをコピーして、ビルドします。
Makefileは、インストールされたgccツールチェインのバージョン入りのフォルダ名を直接指定して、gccを使うようになっています。指定されたバージョンのツールチェインを入れていれば、make一発です。

コマンドで書くと、こんな感じです。

cd <InstallFolder>\external\micro-ecc
git clone https://github.com/kmackay/micro-ecc 
cd nrf52_keil/armgcc
make

できたライブラリ \external\micro-ecc\nrf52_keil\armgcc\micro_ecc_lib_nrf52.lib は、Keilのプロジェクトに、リンク対象として追加しておきます。

最後に、ファームウェアのパッケージを作ります。コマンドにすればこのような感じです:

nrfutil pkg generate dfu.zip –key-file private.pem –hw-version 52 –sd-req 0x00,0x91 –application nrf52832_xxaa.hex –application-version 0x0105

–sd-req には、ソフトウェアデバイスのバージョンを指定します。このソフトウェアデバイスのバージョンは、Windowsで、nRFgo Studioでソフトウェアデバイスを書き込んだとき、ソフトウェアデバイスのバージョンの下辺りに表示される2バイトの値を指定します。
–hw-versionは、51または52を指定します。あとは、見た目のとおりです。

この設定を間違えていると、nRF Toolboxからファームウェアの書き込みを開始したと思ったら、すぐに書き込みが終了して書き込めません。その時の原因は、いくつも設定項目があるので、どこに原因があるのかわかりにくいです。その場合は、ブートローダをデバッグ版のものを入れておけば、デバッグメッセージが出力されます。デバッグメッセージの出力先は、シリアルもしくはSegger RTTをsdk_config.hで指定します。

テンプレートからのファイル変更

main.c の変更箇所のメモです。BSP_BOARD_LED_0 にLEDを接続したピンを定義しています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
50) /**@brief Function for initialization of LEDs.
51) */
52) static void leds_init(void)
53) {
54) bsp_board_leds_init();
55) bsp_board_led_on(BSP_BOARD_LED_0);
56) }
61) static void buttons_init(void)
62) {
63) // dfu/nrf_dfu.hで BSP_BUTTON_3は、BOOTLOADER_BUTTONに定義されている。
64) nrf_gpio_cfg_sense_input(BOOTLOADER_BUTTON,
65) NRF_GPIO_PIN_PULLUP,
66) NRF_GPIO_PIN_SENSE_LOW);
67) }

nrf_dfu_enter_check(void)は、DFUに入るかどうかを判定する関数です。デフォルトでは、ピンが押されている(値が0)、もしくはフラッシュにDFUモードのフラグが立っている場合に、DFUに入ります。めんどくさかったので、SDK10で使っていたレジスタを使う実装にしています。デフォルトの関数はweakなので、実際に使われる nrf_dfu_enter_check() は、ここで宣言した関数です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
34) #define BOOTLOADER_DFU_START 0xB1
70) __WEAK bool nrf_dfu_enter_check(void)
071) {
072) if (nrf_gpio_pin_read(BOOTLOADER_BUTTON) == 0)
073) {
074) return true;
075) }
076)
077) if (s_dfu_settings.enter_buttonless_dfu == 1)
078) {
079) s_dfu_settings.enter_buttonless_dfu = 0;
080) APP_ERROR_CHECK(nrf_dfu_settings_write(NULL));
081) return true;
082) }
083) return false;
084) }
085) */
086) bool nrf_dfu_enter_check(void)
087) {
088) return (NRF_POWER->GPREGRET == BOOTLOADER_DFU_START);
089) }

Comments

2017-01-16