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でどのように行われるか理解します。

github.com

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_HandlerIRQハンドラとして登録されます。

|> 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;
    }
...
}

アプリケーションを通じての周辺回路の操作は、このくらいです。