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()
の後から復帰します。
ステートマシンのコード
割り込みのコンテキストでは、イベントを配送するだけで、処理はなるべくしません。
割り込み処理後、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); }
ステートマシンの実装方法は過去にこちらの記事でも紹介しています。
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を使った省電力のテクニックは他にもいろいろあるので、またまとめてみたいと思います。
告知
弊社ではエンジニアを募集しています!