わふうの人が書いてます。

iOSアプリケーション開発、BLEのファームウェアとハードウェア試作のフリーランスエンジニア。

nRF51からnRF52へのポーティング作業メモ

nRF51からnRF52へのファーム移植

SenStickというBLEデバイスのファームウェア開発をしています https://github.com/ubi-naist/SenStick 。このデバイスは加速度や照度などの複数のセンサデータを取得し蓄積するものです。

初代はnRF51822(RAM 16kB/ROM256kB)を採用しています。次のデバイスでは、nRF52832(RAM 64kB/ROM 256kB)が採用されます。その次のデバイスへのファームウェア移植で盛大につまづいたところをメモしておこうかなと思います。

ハードの変更

移植で対応するものは、nRF51とnRF52とのハードウェアの違い由来と、使用するソフトデバイスおよびソフトウェア開発キットのバージョンの違い由来の、2種類があります。

今回は、nRF51822(rev3) + S110 + SDK10 を、nRF52(rev1) + S132 + SDK12.1 に移植します。機能は同じであり、またnRF51版とnRF52版とは、基本機能は並行して提供していくので、メンテをやりやすくなるようコードは可能な部分は共有する方針です。

nRF51からnRF52へのハード由来の移植ではまったのは:

  • デジタル・アナログ変換が逐次変換型(Successive approximation ADC, SAADC)。
  • P0.9とP0.10はデフォルトでNFCに割り当てられている。
    です。

nRF52はnRF51とは異なる種類のSAADCが採用されました。そして、nRF51のDACとnRF52のSAADCとでは、ハードウェア抽象層(Hardware Abstraction Layer, HAL)とドライバのソースコードが異なります。

ですからADC周りは、自分でラッパーを作ってドライバの違いを吸収するか、コード自体を分離するかします。SDK側で、ADCの種類が違っても、ドライバでHALをラップしてnRF51とnRF52どっちでも同じコードで動くようにしといてくれたらいいのにって思いますが。

P0.9とP0.10は、デフォルトでNFCに割当てられています。ですから何も設定をしないと、GPIOとして設定していても、GPIOとして機能しません。この2つのピンを通常のGPIOとして使うには、レジスタ設定が必須です。

出てくるはずの信号が全く出てこないで、はまるとけっこう精神的にきついです。データシートには、その旨が書いてあるのですが、よく読まずに回路を組むと、信号が出てこないと頭を抱えることになるので、やっかいです。

C/C++のプリプロセッサシンボルで、CONFIG_NFCT_PINS_AS_GPIOS を定義しておけば、P0.9とP0.10はGPIOとして使えるようになります。この処理は system_nrf52.c にあって、こんなコードです:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* Configure NFCT pins as GPIOs if NFCT is not to be used in your code. If CONFIG_NFCT_PINS_AS_GPIOS is not defined,
two GPIOs (see Product Specification to see which ones) will be reserved for NFC and will not be available as
normal GPIOs. */
#if defined (CONFIG_NFCT_PINS_AS_GPIOS)
if ((NRF_UICR->NFCPINS & UICR_NFCPINS_PROTECT_Msk) == (UICR_NFCPINS_PROTECT_NFC << UICR_NFCPINS_PROTECT_Pos)){
NRF_NVMC->CONFIG = NVMC_CONFIG_WEN_Wen << NVMC_CONFIG_WEN_Pos;
while (NRF_NVMC->READY == NVMC_READY_READY_Busy){}
NRF_UICR->NFCPINS &= ~UICR_NFCPINS_PROTECT_Msk;
while (NRF_NVMC->READY == NVMC_READY_READY_Busy){}
NRF_NVMC->CONFIG = NVMC_CONFIG_WEN_Ren << NVMC_CONFIG_WEN_Pos;
while (NRF_NVMC->READY == NVMC_READY_READY_Busy){}
NVIC_SystemReset();
}
#endif

不揮発の設定レジスタをみて、NFCにピンを割り当てる設定になっていたら、そのフラグをクリアする、というコードです。ほぼおまじないなので、プリプロセッサに CONFIG_NFCT_PINS_AS_GPIOS を定義して忘れましょう。

あとで出てきますが、SDKの設定が sdk_config.h に集約されているなら、この定義もそこに集約してくれればいいものをと思うのですが、system_nrf52.c とそのinlucde先のファイルをみても、どこも sdk_config.h をincludeしていないので、Keil MDKのツールの設定側でC/C++の定義をするほかないという、定義する場所が2つに分裂状態になるのですが、そういうものなので、仕方ないと諦めます。

SDKの変更

S110+SDK10から、S132+SDK12に切り替えます。ソフトデバイスとソフトウェア開発キットの切り替えで気をつけるのは:

  • Keil MDKのSoftware Packsの利用をやめている。SDK配布はZIPファイル。
  • SDKの設定は sdk_config.h に集約している。

まずSDKの配布が、Keil MDKのSoftware Packsの仕組みで提供するものから、以前のようなZIPファイル配布に戻りました。Software Packsは、モジュールそれぞれのバージョンと依存性とを管理してくれていて、あるモジュールを選択したときに、それが依存するモジュールが読み込まれてなければ自動的に読み込み設定をしてくれるなど便利さはあったのですが、よほどツール依存が嫌われたのか、トラブルがあったのかしりませんが、SDK自体をZIPファイルで配布するようになっています。

SDKがSoftware Packsで管理されている場合は、MDK Keilの設定ファイルだけあれば、環境やビルドマシンが変わっても自動的に必要なソースコードをネットからダウンロードしてきます。ですが、その仕組がなくなったので、SDK12ではファームウェアのgitレポジトリにSDKそれ自体を置きました。

正直、SDKは1つのZIPファイルで依存性が閉じているので、SoftwarePacksよりも、gitで一元管理できるこちらのほうが便利だと思います。

SDKを通じて利用する機能の設定は、sdk_config.h に集約されています。

sdk_config.h は、いろんな設定がごっちゃーと全て1つのファイルにまとまっていて、かなり長いです。

USE_APP_CONFIG をC/C++をで定義しておくと、sdk_config.hは、”app_config.h”というファイルをインクルードします。sdk_config.h の定義は ifdef を使って、もしも定義されていなかったら定義する、という作りなので、sdk_config.h をデフォルト設定そのままとして、sdk_config.h は変更せずに、変更部分だけを app_config.h に書くようにすると、とてもスッキリ見通しが良くなります。

ちょっとはまったのが、BLEのサービスを利用する場合、そのソースコードをプロジェクトに追加するだけではだめで、このsdk_config.hで利用設定しないとだめな作りになっています。例えばBattery serviceを組み込むなら、ble_bas.c をプロジェクトに追加して、さらに BLE_BAS_ENABLED を定義します。

ble_bas.cは、先頭で

1
2
#include "sdk_config.h"
#if BLE_BAS_ENABLED

としているので、定義がなければCソースコード全体が無視されます。おそらく、テンプレートプロジェクトには、BLEサービスの全ソースコードをプロジェクトに追加しておいて、どのBLEサービスを組み込むかは、sdk_config.hに一元管理する作りなのかなと思います。ふつー、ソースコードをプロジェクトに追加したらその機能が有効になると思うような気もしますが、SDKの作りがそうなので、そういうものなのでしょう。

RTTでログの出力

デバッグ時のログ出力には、Segger RTT Viewerを使っています。これはファーム書き込みにも使うシリアルワイヤインタフェース経由でデバッグログとデバッグメッセージのやり取りができるSegger独自のプロトコルです。

SDK12ではデフォルトでSDKに組み込まれています。RTTが使えるようになるまで、ちょっとひっかかりました。

使えるようになるまでの手順は、まず、sdk_config.h で設定をします。
設定差分でいうと、こんな感じで、ログを有効にして、バックエンドをRTTに、UARTは今回は使わないので無効化しているので、そっちがバックエンドにならないようにしました。

1
2
3
4
5
6
7
8
9
10
11
// ===
// ログ
#define NRF_LOG_ENABLED 1
#define NRF_LOG_BACKEND_SERIAL_USES_UART 0
#define NRF_LOG_BACKEND_SERIAL_USES_RTT 1
// <0=> Off
// <1=> Error
// <2=> Warning
// <3=> Info
// <4=> Debug
#define NRF_LOG_DEFAULT_LEVEL 4

プロジェクトに追加するソースコードは、ログ関係:

  • nrf_log_backend_serial.c
  • nrf_log_frontend.c

RTT関係:

  • RTT_Syscalls_KEIL.c
  • SEGGER_RTT.c
  • SEGGER_RTT_printf

です。

NRF_LOG_DEFAULT_LEVEL の設定以下のメッセージは出力されません。DEBUGで出力しているメッセージがRTT Viewerに出てこなくて、なぜだろうと思ったら NRF_LOG_DEFAULT_LEVEL が3(Info)になっていた、なんていうつまらないオチをやらかしてました。

あと、RTT Viewer で最初に出てくるダイアログに、チップの種類選択があります。ここがnRF51のままで、nRF52になっていなくて、ログが出てこなくて出なくて困りました。

ログの出力はデフォルトでバッファリング(sdk_config.h をみると NRF_LOG_DEFERREDうんたらとあるあたり)されています。デバッグ用ビルドとリリースビルドで、バッファ処理で処理時間が違い振る舞いが変わると困る、なんてことが少なくなるのかもしれません。ぶっちゃけどっちでもいいのですが。

ログをバッファリングしている場合、 NRF_LOG_FLUSH(); を呼び出して初めてログが出力されます。BLEのイベントループにでも NRF_LOG_FLUSH(); を追加しておけばいいです。最初、これを入れていなくて、たしかにログが出てくるはずなのに何も出てこない、なんていう、よくある失敗をしていました。

pstorageが廃止、fstorageに

nRF5のフラッシュのROMへのデータ保存と読み出しのライブラリは pstorageが長く使われてきましたが、SDK12ではこれは廃止されていてfstorageというライブラリを使うようになっています。

pstorageもfstorageも、nRF5のフラッシュ読み書きのレジスタを使うライブラリでしかないので、pstorageのソースコードを持ってくればSDK12でもpstorageは使えますし、pstorageとfstorageの共存も可能だそうですが、ややこしいので、fstorageで書き換えました。

fstorageはページ単位のフラッシュ消去と書き込みのメソッドを提供します。フラッシュメモリは、書き込める値は0だけで、値が0のビットを1に戻すことはできません。0xffを書き込むには、フラッシュの消去をします。この消去はページ単位でのみできます。

fstorageには読み出しのメソッドがありません。フラッシュなので、構造体から書き込み対象のページ領域先頭アドレスを取得して、普通のメモリとして読み出せばいいです。また書き込みが完了したときに、その書き込みポインタが返されるので、それを覚えておくかします。

読み書きのこの辺りが手間だと思うならば、次のflash data storageを使うのが良いです。ただし、適切なタイミングでのガーベージコレクションを入れておくようにします。

ボンディングを使うときは、SDK10でdevice_managerがありましたが、これがpeer_managerに置換されているので、そちらを使います。peer_managerは内部でfstorageを使いke-valueで値を保存するFlash data storage(fds)ライブラリを使っています。

fdsの内部実装は単純なリストです。値を追加あるいはアップデートするたびにリストが伸びていくので、フラッシュの割当容量末尾までリストが届かないうちに、適当なタイミングで、ガベージコレクションのメソッドを呼び出す必要があります。

peer_managerのコードを

1
% grep -R fds_ .//nRF5_SDK_12/components/ble/peer_manager/*

とかしてfdsを使っている部分を見ても、ガベージコレクションはしていないので、BLE接続が切れたときにでも、自分でそれを呼び出すようにします。peer_manager内部で、fdsの容量使い勝ったかのチェックとエラー通知でも入れてくれればいいのに、きっとボンディングの追加や削除を何度も繰り返すと、変な振る舞いになる嫌なバグ的な症状でそうで、なんかいやんな感じです。

ソフトウェアデバイスの変更

S110(ver8.0)からS132(ver3)への切り替えは、MTU EXCHANGEリクエストの対応と、GATTのwrite with authorizationの構造体フィールド追加への対応の2つです。

S132ではMTU Exchangeが飛んできます。これを処理してリプライを返さないと、エラーになって切断されます。MTUデフォルトでいいので、内部でよろしく処理してくれればいいのでめんどくさいのですが、処理コードを追加します。

1
2
3
4
5
6
7
8
//nRF52, S132v3
#if (NRF_SD_BLE_API_VERSION == 3)
case BLE_GATTS_EVT_EXCHANGE_MTU_REQUEST:
// S132では、ble_gatts.hで、GATT_MTU_SIZE_DEFAULTは、23に定義されている。
err_code = sd_ble_gatts_exchange_mtu_reply(p_ble_evt->evt.gatts_evt.conn_handle, GATT_MTU_SIZE_DEFAULT);
APP_ERROR_CHECK(err_code);
break; // BLE_GATTS_EVT_EXCHANGE_MTU_REQUEST
#endif

GATT_MTU_SIZE_DEFAULT はble_gatts.hで、BLEの仕様にある最小値23に設定されています。今回は通信相手がiPhoneなので、もっと大きい値にしてもいいのですが、S110+SDK10のnRF51版と同じふるまいにしたかったので、MTUは23にしています。

次に、write with authenticationの振る舞いが異なる部分に対応します。write with authenticationは、GATTSへの値の書き込みイベントをファーム側で受け取る場合に用います。

S110のタイミングチャートは、書き込みタイミングを取り出すだけのシンプルなものでした。sd_ble_gatts_rw_authorize_reply()に渡すreplyの構造体は、 reply_params.params.write.gatt_status = BLE_GATT_STATUS_SUCCESS; のように、gatt_statusだけを設定して返していました。

S110 SoftDevice v8.0.0 API, GATTS Write Request with Authorization
http://infocenter.nordicsemi.com/index.jsp?topic=%2Fcom.nordic.infocenter.s110.api.v8.0.0%2Findex.html

このタイミングチャートが、S132ではこうなります。
S132 SoftDevice v3.0.0 API,GATTS Write Request with Authorization
http://infocenter.nordicsemi.com/index.jsp?topic=%2Fcom.nordic.infocenter.s110.api.v8.0.0%2Findex.html

S132では、この書き込みへのリプライ時に、peerの値をそのままGATTSに反映させる従来通りの振る舞いと、peerの値ではなくファーム側から値を指定してGATTSの値を更新する2つのフローがあります。そのため、値へのポインタとその値のバイトサイズを指定するフィールドそしてupdateという1ビット幅のフィールドが追加されています。

updateフィールドは、ファーム側からGATTSの値更新を示すフラグかと思いきや、このフィールドが1でなければ、sd_ble_gatts_rw_authorize_reply()がinvalid parameterのエラーを返してくるので、盲目的にupdateは1を設定ます。なんのこっちゃ。

ドキュメントに、その旨ちゃんと書いてあったので、よく読めということですねorz
http://infocenter.nordicsemi.com/index.jsp?topic=%2Fcom.nordic.infocenter.s110.api.v8.0.0%2Findex.html

uint8_t ble_gatts_authorize_params_t::update
If set, data supplied in p_data will be used to update the attribute value.
Please note that for BLE_GATTS_AUTHORIZE_TYPE_WRITE operations this bit must always be set, as the data to be written needs to be stored and later provided by the application.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
} else if(p_auth_req->type == BLE_GATTS_AUTHORIZE_TYPE_WRITE) {
bool result = true;
if( p_auth_req->request.write.handle == p_context->sensor_setting_char_handle.value_handle){
result = senstickSensorControllerWriteSetting(p_context->device_type, p_auth_req->request.write.data, p_auth_req->request.write.len);
} else if( p_auth_req->request.write.handle == p_context->sensor_logid_char_handle.value_handle) {
senstickSensorControllerWriteLogID(p_context->device_type, p_auth_req->request.write.data, p_auth_req->request.write.len);
} else {
// 一致しないハンドラ
return;
}
// リプライ
// nRf51ではリプライのフィールドには、gatt_statusのみがある。SDK12 S132v3では、gatt_status, update, offset, len, p_dataの5フィールド。
reply_params.type = BLE_GATTS_AUTHORIZE_TYPE_WRITE;
// iOSアプリ側でBLEのアクセスでAUTH_ERRORがあると、一連の処理が破棄されるのか?、みたいな振る舞い。読み出せるべき値が読み出せないとか。なので、ここはスルー。
reply_params.params.write.gatt_status = BLE_GATT_STATUS_SUCCESS;
#ifdef NRF52
// このパラメータが1になっていなければ、invalid parameterでsd_ble_gatts_rw_authorize_reply()が落ちる。
reply_params.params.write.update = 1;
#endif
// reply_params.params.read.gatt_status = result ? BLE_GATT_STATUS_SUCCESS : BLE_GATT_STATUS_ATTERR_WRITE_NOT_PERMITTED;
err_code = sd_ble_gatts_rw_authorize_reply(p_context->connection_handle, &reply_params);
APP_ERROR_CHECK(err_code);

プロジェクトの作り直し

nRF52のテンプレートプロジェクトが .//nRF5_SDK_12/examples/ble_peripheral/ble_app_template/ にあります。今回は、この下にある、nRF52のテストボードpca10040でs132を使う pca10040/s132 なんとかのプロジェクトをコピーして使っています。

Keil MDKを使っているので、それでプロジェクトを開いて、必要なソースコードを登録します。メモリ配置を設定して、C/C++定義をいじっていきます。

C/C++の定義は、エラッタ対応のコードを呼び出すための定義がいくつかあります。その処理は system_nrf52.c にまとまっています。nRF52(rev1)のエラッタをみると、スリープから復帰するときRAMの内容が吹き飛ぶとか、GPIOTEとTIMERを同時に動かすとスタティックに消費電流が増えるとか、起きると困る、電池を使っているときは結構困るエラッタがあります。それらのエラッタへの対応が入っているので、このC/C++定義は必ず入っているか、確認します。

nRF52 rev1のエラッタを見ていると、nRF51のrev3で修正解決されたようなエラッタが再現しているようにみえるので、nRF51 rev2から設計分岐させて、nRF52にエラッタがそのまま引き継がれた状態なのでしょうか。なかのことしらないですけど。

まとめ

そんな感じです。