Nordic nRF52を使った省電力でステートマシンを実装する方法

こんにちは。Akerunエンジニアの @ishturk です。

Akerun Advent Calendar7日目の記事です。昨日に引き続き、よろしくお願いします。

IoT製品の避けて通れない道「電池持ち・省電力」。今日のネタは、ここを強化する一つのメソッドです。

弊社でもいろんなところで使っているBLE SoCにNordic Semiconductor社の nRF52があります。 このChipで省電力で動作するステートマシンを実装する方法を紹介します。

動作確認環境

nRF5 SDK v13 + softdevice s132 v2

SDKに含まれているexamples/ble_peripheral/ble_app_uartのサンプルコードをベースにしてみます。

元のソースコード

/* Copyright (c) 2014 Nordic Semiconductor. All Rights Reserved.
 *
 * The information contained herein is property of Nordic Semiconductor ASA.
 * Terms and conditions of usage are described in detail in NORDIC
 * SEMICONDUCTOR STANDARD SOFTWARE LICENSE AGREEMENT.
 *
 * Licensees are granted free, non-transferable use of the information. NO
 * WARRANTY of ANY KIND is provided. This heading must NOT be removed from
 * the file.
 *
 */

/** @file
 *
 * @defgroup ble_sdk_uart_over_ble_main main.c
 * @{
 * @ingroup  ble_sdk_app_nus_eval
 * @brief    UART over BLE application main file.
 *
 * This file contains the source code for a sample application that uses the Nordic UART service.
 * This application uses the @ref srvlib_conn_params module.
 */
 
 === 省略 ===

/**@brief Function for placing the application in low power state while waiting for events.
 */
static void power_manage(void)
{
    uint32_t err_code = sd_app_evt_wait();
    APP_ERROR_CHECK(err_code);
}

=== 省略 ===

/**@brief Application main function.
 */
int main(void)
{
    uint32_t err_code;
    bool erase_bonds;

    // Initialize.
    APP_TIMER_INIT(APP_TIMER_PRESCALER, APP_TIMER_OP_QUEUE_SIZE, false);
    uart_init();

    buttons_leds_init(&erase_bonds);
    ble_stack_init();
    gap_params_init();
    services_init();
    advertising_init();
    conn_params_init();

    printf("\r\nUART Start!\r\n");
    err_code = ble_advertising_start(BLE_ADV_MODE_FAST);
    APP_ERROR_CHECK(err_code);

    // Enter main loop.
    for (;;)
    {
       power_manage();
    }
}


/**
 * @}
 */

シーケンス

下記の動作になるコードを書いてみます。 nRF52のSDKでは sd_app_evt_wait()呼び出しでsleepし、その間は省電力モードになります。 割り込み(GPIOやタイマーなどのイベント)でmainがwakeupして、sd_app_evt_wait()の後から復帰します。

f:id:photosynth-inc:20171206193722p:plain

ステートマシンのコード

割り込みのコンテキストでは、イベントを配送するだけで、処理はなるべくしません。 割り込み処理後、mainがコンテキストにスイッチし、polling()が呼ばれます。 polling()では、イベントキューが空になるまでステートマシンにイベントを配送し、空になったら再びsleepします。

/**
 * @brief イベント型
 */
typedef struct Event_s {
    Signal s;
    EventDataT p;
} Event;

/**
 * @brief MainStateMachine にイベントを通知する
 * @param e [in] 通知するイベント
 */
void send_event(const Event *e);

/**
 * @brief mainループで呼び出す
 */
bool polling(void);

/**
 * @brief 状態のラベル
 */
typedef enum State_e {
    STATE_A,
    STATE_B,
} State;

/**
 * @brief ステート関数の型
 */
typedef uint8_t (*StateFunc)(const Event *e);

static State main_state_machine;    ///< ステート変数<
// StateMachine のイベントキュー
#define SM_EVENT_QUEUE_LENGTH (10)    ///< イベントキューサイズ
static Event event_queue[EVENT_QUEUE_LENGTH];    ///< イベントキューの実体。リングバッファとして利用。
static uint8_t event_queue_head = 0;    ///< リングバッファの先頭
static uint8_t event_queue_empty_head = 0; ///< リングバッファの未使用領域の先頭

// ステートに対応する関数の管理テーブル
static StateFunc state_func_table[] = {StateFunc_A, StateFunc_B};

static uint8_t dispatch(const Event *e) {
    while (state_func_table[main_state_machine](e)) {
                // イベントが消化されなければCommonに配送
        StateFunc_Common(e);
        break;
    }
}

void send_event(const Event *e) {
    if (((event_queue_empty_head + 1) == event_queue_head)
        || ((event_queue_empty_head == (EVENT_QUEUE_LENGTH-1)) && (event_queue_head == 0))) {
        //    queue full
        ERROR_PRINT("Event queue is full!! FatalError!!!\n");
        return RETURN_QUEUE_FULL;
    }
    event_queue[event_queue_empty_head] = *e;
    event_queue_empty_head = (event_queue_empty_head >= (EVENT_QUEUE_LENGTH -1))
        ? 0 : event_queue_empty_head + 1;
    return;
}

bool polling() {
    if (event_queue_empty_head == event_queue_head) {
        // empty
        return false;
    }
    dispatch(&event_queue[event_queue_head]);
    event_queue_head = (event_queue_head >= (EVENT_QUEUE_LENGTH -1))
        ? 0 : event_queue_head + 1;
    return true;
}

ステートのコード

// ステート関数
/**
 * @brief Aステート
 */
uint8_t StateFunc_A(const Event *e) {
    switch(e->s) {
        case EVENT_SIG_HOGE:
            // 何かの処理
            break;
        default:
            // イベントを消化しない場合はCommonに配送するようにtrueを返す
            return true;
    }
    return false;
}

/**
 * @brief Bステート
 */
uint8_t StateFunc_B(const Event *e) {
    switch(e->s) {
        case EVENT_SIG_HOGERA:
            // 何かの処理
            break;
        default:
            // イベントを消化しない場合はCommonに配送するようにtrueを返す
            return true;
    }
    return false;
}

/**
 * @brief 共通ステート
 */
uint8_t StateFunc_Common(const Event *e) {
{
    switch(e->s) {
        case EVENT_SIG_HOGE:
            // 何かの処理
            break;
        case EVENT_SIG_HOGERA:
            // 何かの処理
            break;
        default:
            break;
    }
    return false;
}

イベントハンドラのコード

処理はせず、ステートマシンにイベントを通知します。

void timer_event_handler(void* param) {
    Event e = {EVENT_SIG_HOGE, param};
    send_event(&e);
}

void ble_event_handler(void* param) {
    Event e = {EVENT_SIG_HOGERA, param};
    send_event(&e);
}

ステートマシンの実装方法は過去にこちらの記事でも紹介しています。

qiita.com

main.c のコード

前置きが長くなりましたが。。。 ここに1行追加します。

diff --git a/main.c b/main.c
--- a/main.c
+++ b/main.c
@@ -538,6 +538,7 @@ int main(void)
     // Enter main loop.
     for (;;)
     {
+        while(polling());
          power_manage();
     }
 }

だめな例

以下の例は、sleepしないので、省電力になりません。

  • ビジーループ
diff --git a/main.c b/main.c
--- a/main.c
+++ b/main.c
@@ -538,6 +538,7 @@ int main(void)
     // Enter main loop.
     for (;;)
     {
+        while(polling());
-         power_manage();
     }
 }
  • waitしてるだけのビジーループ
diff --git a/main.c b/main.c
--- a/main.c
+++ b/main.c
@@ -538,6 +538,7 @@ int main(void)
     // Enter main loop.
     for (;;)
     {
+        while(polling()) {
+              nrf_delay_ms(1);
+         }
-        power_manage();
     }
 }

終わりに

いかがでしょうか。今回は必要なときだけwakeupしてステートマシンをドライブする方法を具体的に書いてみました。 nRF52を使った省電力のテクニックは他にもいろいろあるので、またまとめてみたいと思います。

告知

弊社ではエンジニアを募集しています!

www.wantedly.com