nRF5 SDK for Meshで学ぶBluetooth mesh⑤~light switch demoペリフェラル制御ソース解析~
はじめに
Bluetooth Meshを理解するために、nRF5 SDK for Meshでサンプルを動かしながら、動作を解析していきます。
tomo-wait-for-it-yuki.hatenablog.com
前回の続きです。今回は、ソースコードをより詳細に解析していきます。 解析対象は、nRF5 SDK for Meshのlight switch demoです。 今回は、Bluetooth Mesh本体の前にペリフェラルの制御について、nRF5 SDKでどのように行われるか理解します。
client
まずは、clientのソースコードから見ていきます。
|> nRF5-SDK-for-Mesh/examples/light_switch/client
ソースコードは、main.c
のみで、400行弱です。
main()
main関数は非常にシンプルで、initialize()
とstart()
した後、sd_app_evt_wait()
を呼び出して低電力モードでイベント待ちします。
int main(void) { initialize(); start(); for (;;) { (void)sd_app_evt_wait(); } }
initialize()
BLE/Meshを初期化する前に、ログやペリフェラルの初期化を行っています。
static void initialize(void) { __LOG_INIT(LOG_SRC_APP | LOG_SRC_ACCESS | LOG_SRC_BEARER, LOG_LEVEL_INFO, LOG_CALLBACK_DEFAULT); __LOG(LOG_SRC_APP, LOG_LEVEL_INFO, "----- BLE Mesh Light Switch Client Demo -----\n"); ERROR_CHECK(app_timer_init()); hal_leds_init(); ERROR_CHECK(hal_buttons_init(button_event_handler)); ble_stack_init(); mesh_init(); }
__INIT_LOG && __LOG
|> nRF5-SDK-for-Mesh/mesh/core/include/log.h
でマクロが定義されています。
/** * Initializes the logging framework. * @param[in] msk Log mask * @param[in] level Log level * @param[in] callback Log callback */ #define __LOG_INIT(msk, level, callback) log_init(msk, level, callback)
アプリケーションと、Access層、Bearer層のログを、INFOレベルで有効化しています。
__LOG_INIT(LOG_SRC_APP | LOG_SRC_ACCESS | LOG_SRC_BEARER, LOG_LEVEL_INFO, LOG_CALLBACK_DEFAULT);
下記の通り、どの層のログを有効化するか、を選択するマクロ定義があります。
#define LOG_SRC_BEARER (1 << 0) /**< Receive logs from the bearer layer. */ ... #define LOG_SRC_ACCESS (1 << 13) /**< Receive logs from the access layer. */ #define LOG_SRC_APP (1 << 14) /**< Receive logs from the application. */ ...
LOG_CALLBACK_DEFAULT
は、次のように定義されるようです。nRF52840がターゲットなので、log_callback_rtt
が実体になります。stdoutの実装は、ユニットテストやホストターゲットでビルドする場合に使うようです。
|> nRF5-SDK-for-Mesh/mesh/core/api/nrf_mesh_config_core.h
/** The default callback function to use. */ #ifndef LOG_CALLBACK_DEFAULT #if defined(NRF51) || defined(NRF52_SERIES) #define LOG_CALLBACK_DEFAULT log_callback_rtt #else #define LOG_CALLBACK_DEFAULT log_callback_stdout #endif #endif
SEGGERのRTTに出力します。基本は、J-Linkを接続してログを見る構成のようです。
|> nRF5-SDK-for-Mesh/mesh/core/src/log.c
void log_callback_rtt(uint32_t dbg_level, const char * p_filename, uint16_t line, uint32_t timestamp, const char * format, va_list arguments) { SEGGER_RTT_printf(0, "<t: %10u>, %s, %4d, ",timestamp, p_filename, line); SEGGER_RTT_vprintf(0, format, &arguments); }
log_init()
でコールバックを登録して、log_printf() -> log_vprintf()
の中で、登録したコールバック関数 (この場合は、log_callback_rtt()
)を呼び出す作りになっています。
__LOG
は、マクロを展開するとlog_printf()
になります。
|> nRF5-SDK-for-Mesh/mesh/core/src/log.c
void log_init(uint32_t mask, uint32_t level, log_callback_t callback) { g_log_dbg_msk = mask; g_log_dbg_lvl = level; m_log_callback = callback; } void log_printf(uint32_t dbg_level, const char * p_filename, uint16_t line, uint32_t timestamp, const char * format, ...) { va_list arguments; /*lint -save -esym(530,arguments) Symbol arguments not initialized. */ va_start(arguments, format); log_vprintf(dbg_level, p_filename, line, timestamp, format, arguments); va_end(arguments); /*lint -restore */ } void log_vprintf(uint32_t dbg_level, const char * p_filename, uint16_t line, uint32_t timestamp, const char * format, va_list arguments) { if (m_log_callback != NULL) { m_log_callback(dbg_level, p_filename, line, timestamp, format, arguments); } }
app_timer_init()
アプリケーション用タイマを初期化します。SoftDeviceを使用する場合、RTC0はSoftDeviceが占有するため、アプリケーションからは利用できません。 割り込みを設定して、RTC1を初期化/起動します。
|> nRF5-SDK-for-Mesh/external/app_timer/app_timer_mesh.c
ret_code_t app_timer_init(void) { // Stop RTC to prevent any running timers from expiring (in case of reinitialization) rtc1_stop(); // Initialize operation queue m_op_queue.first = 0; m_op_queue.last = 0; m_op_queue.size = APP_TIMER_CONFIG_OP_QUEUE_SIZE+1; mp_timer_id_head = NULL; m_ticks_elapsed_q_read_ind = 0; m_ticks_elapsed_q_write_ind = 0; #if APP_TIMER_WITH_PROFILER m_max_user_op_queue_utilization = 0; #endif NVIC_ClearPendingIRQ(SWI_IRQn); NVIC_SetPriority(SWI_IRQn, SWI_IRQ_PRI); NVIC_EnableIRQ(SWI_IRQn); rtc1_init(APP_TIMER_CONFIG_RTC_FREQUENCY); m_ticks_latest = rtc1_counter_get(); rtc1_start(); m_is_timer_init = true; return NRF_SUCCESS; }
LED制御
LED制御では、点灯するLEDのマスク設定と、インターバルと点滅回数を指定しての点滅が可能です。
初期化
ボード搭載のLEDを制御するために、GPIOピンの初期化を行います。その後、点滅のためのタイマを作成します。
|> nRF5-SDK-for-Mesh/examples/common/src/simple_hal.c
void hal_leds_init(void) { for (uint32_t i = LED_START; i <= LED_STOP; ++i) { NRF_GPIO->PIN_CNF[i] = LED_PIN_CONFIG; NRF_GPIO->OUTSET = 1UL << i; } APP_ERROR_CHECK(app_timer_create(&m_blink_timer, APP_TIMER_MODE_REPEATED, led_timeout_handler)); }
LED_START
といったマクロは、nRF5 SDKでボードごとに定義されています。
|> nRF5_SDK_15.2.0/components/boards/pca10056.h
#define LED_START LED_1
app_timer_create()
では、timer_node_t
構造体のインスタンスを初期化します。
インスタンス指定と、タイマのモード、コールバック関数の登録を行います。
点滅開始
static変数として管理している点滅回数を設定し、タイマを開始します。
void hal_led_blink_ms(uint32_t led_mask, uint32_t delay_ms, uint32_t blink_count) { if (blink_count == 0 || delay_ms < HAL_LED_BLINK_PERIOD_MIN_MS) { return; } m_blink_mask = led_mask; m_blink_count = blink_count * 2 - 1; m_prev_state = NRF_GPIO->OUT; if (app_timer_start(m_blink_timer, APP_TIMER_TICKS(delay_ms), NULL) == NRF_SUCCESS) { /* Start by "clearing" the mask, i.e., turn the LEDs on -- in case a user calls the * function twice. */ NRF_GPIO->OUT &= ~m_blink_mask; } }
タイマがタイムアウトする度に、次のハンドラが実行されます。指定された回数、点滅すると、タイマを停止します。
static void led_timeout_handler(void * p_context) { APP_ERROR_CHECK_BOOL(m_blink_count > 0); NRF_GPIO->OUT ^= m_blink_mask; m_blink_count--; if (m_blink_count == 0) { (void) app_timer_stop(m_blink_timer); NRF_GPIO->OUT = m_prev_state; } }
ボタン
初期化
ボタンの初期化も基本はLEDと同じです。しかし、GPIOTE割り込みの設定を行います。
|> nRF5-SDK-for-Mesh/examples/common/src/simple_hal.c
uint32_t hal_buttons_init(hal_button_handler_cb_t cb) { #if !BUTTON_BOARD return NRF_ERROR_NOT_SUPPORTED; #else if (cb == NULL) { return NRF_ERROR_NULL; } m_button_handler_cb = cb; for (uint32_t i = 0; i < BUTTONS_NUMBER ; ++i) { NRF_GPIO->PIN_CNF[m_buttons_list[i]] = BUTTON_PIN_CONFIG; } NRF_GPIOTE->INTENSET = GPIOTE_INTENSET_PORT_Msk; NRF_GPIOTE->EVENTS_PORT = 0; NVIC_SetPriority(GPIOTE_IRQn, GPIOTE_IRQ_LEVEL); NVIC_EnableIRQ(GPIOTE_IRQn); return NRF_SUCCESS; #endif }
ボタン押下割り込み
初期化で登録したコールバックは、GPIOTEの割り込みハンドラ内で呼ばれます。 nRF5 SDK for Meshでは、最低限のHAL実装しかなされていないため、GPIOTE割り込みはボタン専用になっています。
void GPIOTE_IRQHandler(void) { NRF_GPIOTE->EVENTS_PORT = 0; for (uint8_t i = 0; i < BUTTONS_NUMBER; ++i) { /* Check that the event was generated by a button press, and reject if it's too soon (debounce). * NOTE: There is a bug with this at the wrap-around for the RTC1 where the button could be * pressed before HAL_BUTTON_PRESS_FREQUENCY has passed a single time. It doesn't matter practically. */ if ((~NRF_GPIO->IN & (1 << (m_buttons_list[i]))) && TIMER_DIFF(m_last_button_press, NRF_RTC1->COUNTER) > HAL_BUTTON_PRESS_FREQUENCY) { m_last_button_press = NRF_RTC1->COUNTER; m_button_handler_cb(i); // ★ここ } } }
GPIOTE_IRQHandler
ですが、どうやらこのシンボルがIRQハンドラとして登録される仕組みになっているようです。
明示的にこのシンボルを実装しない場合は、Default_Handler
がIRQハンドラとして登録されます。
|> nRF5_SDK_15.2.9/modules/nrfx/mdk/gcc_startup_nrf52840.S
... .section .isr_vector .align 2 .globl __isr_vector __isr_vector: .long __StackTop /* Top of Stack */ .long Reset_Handler .long NMI_Handler .long HardFault_Handler .long MemoryManagement_Handler ... /* External Interrupts */ .long POWER_CLOCK_IRQHandler .long RADIO_IRQHandler .long UARTE0_UART0_IRQHandler .long SPIM0_SPIS0_TWIM0_TWIS0_SPI0_TWI0_IRQHandler .long SPIM1_SPIS1_TWIM1_TWIS1_SPI1_TWI1_IRQHandler .long NFCT_IRQHandler .long GPIOTE_IRQHandler .long SAADC_IRQHandler .long TIMER0_IRQHandler .long TIMER1_IRQHandler .long TIMER2_IRQHandler ...
登録したコールバックは、下記のハンドラでした。長いので省略しますが、押されたボタンに応じて、GenericOnOffの処理を実施ます。
|> nRF5-SDK-for-Mesh/examples/light_switch/client/src/main.c
static void button_event_handler(uint32_t button_number) { __LOG(LOG_SRC_APP, LOG_LEVEL_INFO, "Button %u pressed\n", button_number); ... switch (button_number) { case 0: case 1: /* Demonstrate acknowledged transaction, using 1st client model instance */ /* In this examples, users will not be blocked if the model is busy */ (void)access_model_reliable_cancel(m_clients[0].model_handle); status = generic_onoff_client_set(&m_clients[0], &set_params, &transition_params); hal_led_pin_set(BSP_LED_0, set_params.on_off); break; case 2: case 3: /* Demonstrate un-acknowledged transaction, using 2nd client model instance */ status = generic_onoff_client_set_unack(&m_clients[1], &set_params, &transition_params, APP_UNACK_MSG_REPEAT_COUNT); hal_led_pin_set(BSP_LED_1, set_params.on_off); break; } ... } アプリケーションを通じての周辺回路の操作は、このくらいです。