六腹町会

作って、計算して、読んだものの記録です

nRF51822のセントラル機能試験結果報告(前半)

本記事の内容について、間違いや不明点があればご指摘願います。
疑問点があればいつでもお問い合わせください。(掲示板やメールはたまにしか確認していないので、ツイッターなら確実に連絡がつきます。)
また、本記事の内容について一切の権利を放棄しますので、自由に改変、拡散していただいても構いません。

【何をしたいか】

1つのBLEモジュールnRF51822をセントラルとして、もう2つのnRF51822をペリフェラルとして使用します。
サンプルプログラムにはこれを実行するためのble_app_multilink_centralとble_app_multilink_peripheralというプロジェクトがあります。(ネットで名前を検索すれば、サンプルプログラムが見つかるはずです。)
前者がセントラル用で、後者がペリフェラル用です。

ただし、このセントラルは2つのペリフェラルをBLE接続に成功した順番に番号付けして区別します。
これは2つのペリフェラルがBLE接続のタイミングにより毎回異なる番号を持つことになるので、少し不便なことです。

f:id:rokuharatown:20160123191453j:plain

例えば、2体のロボットにBLEをペリフェラルとして搭載して、リモコンに搭載したセントラルBLEで操作するとします。
リモコンのスイッチAでロボットAを動かし、スイッチBでロボットBを動かします。
ですがセントラルnRF51822 が接続した順番に番号付けすると、どちらの番号がロボットAとBのnRF51822なのか識別できません。
接続した順番でペリフェラルの識別番号が変わってしまうので、起動するごとに番号が変わります。するとスイッチAでロボットBが動いたりしてしまうわけです。

そこで次の方法でペリフェラルを区別させてみました。
ます2つのペリフェラルnRF51822にそれぞれ異なるデバイス名をつけます。デバイス名はアドバタイジングパケットに含まれるデータの1つです。
セントラルnRF51822はデバイス名と番号を対応付けることで、2つのペリフェラルを常時区別できるようになります。

f:id:rokuharatown:20160123191520j:plain

【環境】

今回の開発環境は以下の通りです。全体的に古い環境を使用しています。
SDK Ver6.1
ペリフェラルのソトデバイスS110 V7.1
セントラルのソフトデバイスS120 V1.0

以下では、ソフトデバイスの書き込みやペリフェラルの設定はできるものとみなして、セントラルの設定方法を中心に解説を行います。

サンプルプロジェクトはnRF51822開発ボードで動かすことを前提に解説し、修正したプロジェクトはmbed HRM1017で動かすことを前提に解説します。

【サンプルプロジェクトの分析】

まずは、app_multilink_centralが何をしているか分析してみます。
https://github.com/linklayer/BLEKey/tree/master/nordic/nrf51822/Board/nrf6310/s120/ble_app_multilink_central


セントラル機能のサンプルプロジェクトにはもう1つble_app_hrsがあります。ここでは詳しく説明しませんが(私もまだ十分理解できていないのです。)そちらも並行して参照すると理解がより深まると思います。

①UUIDの登録

関数client_handling_initにおいてBLE接続の対象とするペリフェラルのUUIDを設定しています。
セントラルはこのUUIDと一致しないペリフェラルとはBLE接続しません。

ble_uuid128_t base_uuid = MULTILINK_PERIPHERAL_BASE_UUID;	
uuid.uuid = MULTILINK_PERIPHERAL_SERVICE_UUID;	
②スキャン開始

BLE接続は、セントラルがペリフェラルの発信するアドバタイジングパケットをスキャンすることで確立します。
ですからセントラルのプログラムは、どこかでスキャン開始を指示しないといけません。
スキャン開始関数scan_startを実行することでセントラルは、ペリフェラルの送信するアドバタイジングパケットのスキャンを開始します。

サンプルプログラムでは、main関数の起動後に初期設定をした直後、scan_startを実行しています。

int main(void)
{	
    // Initialization of various modules.
    app_trace_init();
    leds_init();
    buttons_init();
    ble_stack_init();
    client_handling_init();
    device_manager_init();

    // Start scanning for devices.
    scan_start();

私が作成したプログラムでは、main関数でスキャン開始するのではなく、スイッチを押した時にスリープモードから復帰してスキャンを開始するという動作にしました。

関数scan_startの中身は、Flashへのアクセスができるかを確認した後に、スキャン開始許可しています。
もう1つのサンプルble_app_hrsではホワイトリストの初期化もしていましたが、今回は使用しないので省略します。
つまりサンプルble_app_multilink_centralの関数scan_startをそのまま使います。

③アドバタイジングパケットの検知

main.cの関数on_ble_evtには以下の2つの分岐が確認できます。
BLE_GAP_EVT_ADV_REPORT
BLE_GAP_EVT_TIMEOUT

また、もう1つのサンプルble_app_hrs_cにはパラメータ更新の分岐もあるので必要な場合は追加するといいと思います。
BLE_GAP_EVT_CONN_PARAM_UPDATE_REQUEST

今回はBLE_GAP_EVT_ADV_REPORTに注目します。
これはペリフェラルが送信するアドバタイジングパケットを検知した時に発生するイベントです。
このイベントにより、構造体変数adv_dataにアドバタイジングパケットが格納されます。
アドバタイジングパケットの構造については、BLEの参考書や他の人が書いているブログなどをご参照ください。

この関数において具体的に何をしているかを以下に列挙します。

(1)
アドバタイジングパケットのデータ格納

adv_data.p_data = p_ble_evt->evt.gap_evt.params.adv_report.data;

(2)
アドバタイジングパケットのデータ長格納

adv_data.data_len = p_ble_evt->evt.gap_evt.params.adv_report.dlen;

(3)
アドバタイジングパケットのデータadv_dataにtype:BLE_GAP_AD_TYPE_COMPLETE_LOCAL_NAMEのデータが有るか無いかを検索
→有る場合はBLE_GAP_AD_TYPE_COMPLETE_LOCAL_NAMEのデータをtype_dataに格納

err_code = adv_report_parse(BLE_GAP_AD_TYPE_COMPLETE_LOCAL_NAME,	&adv_data, &type_data);

→無い場合はアドバタイジングパケットのデータadv_dataにtype:BLE_GAP_AD_TYPE_SHORT_LOCAL_NAMEのデータが有るか無いかを検索
→有る場合はBLE_GAP_AD_TYPE_SHORT_LOCAL_NAMEのデータをtype_dataに格納

if (err_code != NRF_SUCCESS)				
{ // Compare short local name in case complete name does not match.				
                err_code = adv_report_parse(BLE_GAP_AD_TYPE_SHORT_LOCAL_NAME,  &adv_data, &type_data);	
}			

(4)
なお、adv_report_parseでは以下を行っている。

アドバタイジングパケットのデータ部を抜き出して取得する。

p_data = p_advdata->p_data;

index(データの何バイト目かを示す変数)がアドバタイジングパケットのデータ長を上回るまで検索する

while (index < p_advdata->data_len)

アドバタイジングパケットのデータ部は複数に区切られており、各部の最初の1byte目が1区切りのデータ長を、2 byte目が1区切りのデータtypeを示しているのでそれを取得。

uint8_t field_length = p_data[index]; 
uint8_t field_type   = p_data[index+1];	

アドバタイジングパケットに目当てのtypeと一致するtypeがある場合、p_typedataにデータを格納し、p_typedataにデータ長を格納する。

p_typedata->p_data   = &p_data[index+2];					            
p_typedata->data_len = field_length-1;		

一致するtypeがない場合、次の区切りまでindexを移動する。

index += field_length+1;


(5)
アドバタイジングパケットから取得したデータtype_dataがターゲットデバイス名と一致しているかを比較

if ((err_code == NRF_SUCCESS) &&   (0 == memcmp(TARGET_DEV_NAME,type_data.p_data,type_data.data_len)))	

→一致した場合、スキャンを終了後、peer address、スキャンパラメータ、接続パラメータを指定してBLE接続を確立させる。
peer addressの説明は以下をご覧ください。
http://developer.nordicsemi.com/nRF51_SDK/nRF51_SDK_v7.x.x/doc/7.0.1/s120/html/a00069.html

err_code = sd_ble_gap_scan_stop();	

err_code = sd_ble_gap_connect(&p_ble_evt->evt.gap_evt.params.adv_report. peer_addr, &m_scan_param, &m_connection_param);

もう1つのサンプルble_app_hrs_cはもっと複雑な分岐をして、UUIDの確認などを行っていますが、基本的にしていることは同じです。
このイベントでは、検出したアドバタイジングパケットから必要なデータを抽出し、接続を確立しています。
複数ペリフェラルと接続してそれらを識別する場合は、ここで一工夫が必要になるのですが、それは後述いたします。

④BLE接続・BLE切断

BLE接続やBLE切断時に呼び出されるイベントがあります。
main.cの関数device_manager_event_handlerです。

BLE接続時にはDM_EVT_CONNECTIONへ分岐し、BLE切断時にはDM_EVT_DISCONNECTIONへ分岐します。

BLE接続時には以下のことをしています。
新規クライアント作成関数を実行し、

err_code = client_handling_create(p_handle, p_event->event_param.p_gap_param->conn_handle);

接続ペリフェラルカウントを加算して、設定した最大ペリフェラル接続数を超えていなければ、③で停止させていたスキャンを再開します。

m_peer_count++;
if (m_peer_count < MAX_PEER_COUNT)
{
   scan_start();
}


このとき、p_handle->connection_idにBLE接続したペリフェラルの番号が格納されます。


BLE切断時には以下のことをしています。

p_handleを破棄し、

err_code = client_handling_destroy(p_handle);

既に接続カウントが最大ペリフェラル接続数に達しているならば、停止しているスキャンを再開させて、接続ペリフェラルカウントを減算しています。

if (m_peer_count == MAX_PEER_COUNT)	
{	
scan_start();	
}	
m_peer_count--;	
⑤Characteristicの確認

BLE接続確立時にはclient_handling.cの関数db_discovery_evt_handlerも呼び出されます。

この関数では、アドバタイジングパケットから取得したcharacteristic数であるchar_countの上限まで網羅的に検索を行います。

for (i = 0; i < p_evt->params.discovered_db.char_count; i++)

アドバタイジングパケットから取得したcharacteristicの中から、こちらで設定したNotifyサービスのCHARACTER UUIDと一致するものを探します。
ここではMULTILINK_PERIPHERAL_CHAR_UUIDがそれです。
自作したペリフェラルの場合は、この設定を書き換えます。

if ((p_characteristic->characteristic.uuid.uuid == MULTILINK_PERIPHERAL_CHAR_UUID) && (p_characteristic->characteristic.uuid.type == m_base_uuid_type))

対象のcharacteristicが発見された場合、そのchracteristic番号を変数p_clientに保存してから、

p_client->char_index = i;

Notifyを有効にしています。

notif_enable(p_client);
⑥Notify

BLE接続確立後、ペリフェラルからNotifyがあると、client_handling.cの関数client_handling_ble_evt_handlerが呼び出されます。

この関数では、まずNotifyを送信したペリフェラルの情報を取得します。ここで④で取得したp_handle->connection_idと同じ番号が得られます。
このindex番号で、複数接続しているペリフェラルを区別しています。

uint32_t index = client_find(p_ble_evt->evt.gattc_evt.conn_handle);

その後、BLE_GATTC_EVT_HVXに分岐してclient_handling.cの関数on_evt_hvxを実行します。

case BLE_GATTC_EVT_HVX:	
 on_evt_hvx(p_ble_evt, p_client, index);	
 break;	

on_evt_hvxでは以下のことが行われています。

クライアント状態の確認

if ((p_client != NULL) && (p_client->state == STATE_RUNNING))

→問題がなければNotifyの判別
受信したハンドル p_ble_evt->evt.gattc_evt.params.hvx.handleが登録したハンドルと一致しており、かつデータ長さが1byteかどうかを確認します。

if((p_ble_evt->evt.gattc_evt.params.hvx.handle == p_client->srv_db.services[0].charateristics[p_client->char_index].characteristic.handle_value) && (p_ble_evt->evt.gattc_evt.params.hvx.len == 1))

ここではNotifyが1byteであることを前提にしていますが、私の作成したプログラムでは1パケット20byteにしているので、この条件は不要もしくは値を変更します。

→Notifyの判別OKの場合
BLE接続番号indexに対応した開発ボードのLEDを制御します。
Notifyデータが0なら点灯し、それ以外なら消灯します。
1番目にBLE接続したペリフェラルからのNotifyならばLED0が点灯や消灯し、
2番目にBLE接続したペリフェラルからのNotifyならばLED1が点灯や消灯します。

if (p_ble_evt->evt.gattc_evt.params.hvx.data[0] == 0)
{
    nrf_gpio_pin_clear(index + LED_PIN_NO_OFFSET);
}
else
{
    nrf_gpio_pin_set(index + LED_PIN_NO_OFFSET);
}


以上がサンプルプログラムの内容です。
セントラルとペリフェラルをBLE接続して、ペリフェラル側のスイッチを押すとNotifyが送信され、セントラル側のLEDのどれかが点いたり、消えたりするという動作をするわけです。

長くなって申し訳ありませんが、いよいよ次はこのサンプルプログラムを改造します。