Scheduling Application Tasks while Running BLE Stack

Introduction

The core of all embedded application is the main loop, which can be implemented in different ways according to developer preferences. However, if Bluetooth stack is running in the background, it puts restrictions on the implementation because occasionally, Bluetooth tasks have to be served and Bluetooth events have to be handled.

Note: Most Bluetooth tasks are time critical and executed in high-priority interrupt handlers. However, some tasks can't be executed in interrupt context (e.g., writing to flash), and some tasks need response from the application. As a result, the application should be implemented so that these tasks are occasionally served. The timing accuracy of these tasks is in the order of seconds.

The developer can choose between 3 options regarding the main loop implementation:

Bluetooth Main Loop Extended with Application Tasks

In most Bluetooth applications it is recommended to apply one of the sample main loops described in UG136: Silicon Labs Bluetooth® C Application Developer's Guide section 5. Bluetooth Stack Event Handling. You can then extend these loops with your application specific tasks (e.g., reading sensors). Here, three typical scenarios regarding the scheduling of the application task are considered:

  1. Application tasks need to run continuously
  2. Application tasks need to run at every T ms
  3. Application tasks need to run when an interrupt occurs

You may also combine these solutions if you have multiple tasks with different scheduling requirements.

Application Tasks Need to Run Continuously

If an application tasks need to run continuously, e.g., to read sensors or control peripherals, the main loop has to run continuously and cannot be blocked by the stack. This can be achieved with the non-blocking gecko_peek_event() function, which returns the oldest pending stack event if there is any event in the event queue or NULL if the event queue is empty (no event to be processed). In the following example, the application task is called continuously after handling all pending stack events.

Note: You have to pop events from the event queue with gecko_peek_event() even if you do not implement a handler for them in the switch-case statement to empty the event queue.

void appMain(gecko_configuration_t *pconfig)
{
  /* Initialize stack */
  gecko_init(pconfig);

  while (1) {
    /* Event pointer */
    struct gecko_cmd_packet* evt;

    /* Check for stack events. Serve all pending events. */
    while (evt = gecko_peek_event()) {

      switch (BGLIB_MSG_ID(evt->header)) {

        case gecko_evt_system_boot_id:
          /* Serve boot event, e.g. start advertising here */
          break;

        /* ...Further event handlers here... */

        default:
          break;
      }
    }

    /* Run application specific task*/
    applicationTask();
  }
}

In this use case the scheduling looks as follows:

Scenario 1

Keep in mind that timing accuracy requirement of the host stack is seconds. If the application tasks run for too long, (many many seconds), the stack may suffer latency which can lead to different issues (e.g., bonding timeout, GATT timeout, loss of events, and so on).

Application Tasks Need to Run at Every T ms

Often, it is sufficient to invoke the application with a much lower frequency to reduce energy consumption. In this case the microcontroller can be sent into sleep mode, while no task is running (neither stack task nor application task). Putting the device into sleep mode can be achieved by using the blocking gecko_wait_event() function. This function checks for new events and sends the device into sleep mode while it is not needed. It returns when at least one event is in the event queue of the stack.

If an application task needs to be called regularly, the soft timer of the Bluetooth stack can be used to wake up the device and trigger the application task. After starting the timer with gecko_cmd_hardware_set_soft_timer(), an _evt_hardware_soft_timer_ event will be occasionally triggered, which can start your application task. For the usage of gecko_cmd_hardware_set_soft_timer() see Bluetooth Software API Reference Manual.

Note that you can use multiple soft timers concurrently to schedule different application tasks with different intervals. The maximum number of concurrent soft timers can be set in the gecko_configuration structure. By default, you can use 4 soft timers at a time.

#define APP_TASK_INTERVAL 10 //ms

void appMain(gecko_configuration_t *pconfig)
{
  /* Initialize stack */
  gecko_init(pconfig);

  while (1) {
    /* Event pointer */
    struct gecko_cmd_packet* evt;

    /* Wait for next stack event. */
    evt = gecko_wait_event();

    switch (BGLIB_MSG_ID(evt->header)) {

      case gecko_evt_system_boot_id:
        /* Serve boot event, e.g. start advertising here */
        /* Start soft timer */
        gecko_cmd_hardware_set_soft_timer(32768*APP_TASK_INTERVAL/1000,0,0);
        break;

      /* ...Further event handlers here... */

      case gecko_evt_hardware_soft_timer_id:
        /* Run application specific task */
        applicationTask();
        break;

      default:
        break;
    }
  }
}

In this example, the soft timer is configured to generate an event every 10 ms (which is the minimum interval you can set for a soft timer). If precise timing is not as important, a lazy_soft_timer can also be used, which allows timing with some slack and ensures lower energy consumption. See Bluetooth Software API Reference Manual for reference.

In this use case, the scheduling looks like this:

Scenario 3

Application Tasks Need to Run when an Interrupt Occurs

Application tasks are often interrupt driven, meaning that they will be executed upon interrupt reception. In this case, the interrupt handler function can be used to serve the application. However, if the application needs to do more than basic operations, the interrupt handler should only be used for signaling the need for task execution. The task itself should be executed in the main loop. Note also that stack functions can't be called from the interrupt context.

The signaling between the interrupt handler and the main loop can be implemented in different ways, such as by using global variables. However, the user is encouraged to call gecko_external_signal() function in the interrupt handler, which will add a new _system_external_signal_ event to the Bluetooth event queue. The application task can then be executed in the main loop upon receiving _system_external_signal_ event.

To differentiate interrupt sources, gecko_external_signal() passes a 32-bit bitfield. Set different bits in different interrupt handlers, so you can check in the main loop which interrupt was triggered.

/* external signal flags */
#define EXT_SIGNAL_TIMER_EXPIRED_FLAG  0x01

/* Interrupt handler */
void TIMER0_IRQHandler(void)
{
  /* Send external signal to Bluetooth stack */
  gecko_external_signal(EXT_SIGNAL_TIMER_EXPIRED_FLAG);

  /* Clear flag for TIMER0 overflow interrupt */
  TIMER_IntClear(TIMER0, TIMER_IF_OF);
}

void appMain(gecko_configuration_t *pconfig)
{
  /* Initialize peripheral and enable interrupts */
  timer_init();

  /* Initialize stack */
  gecko_init(pconfig);

  while (1) {
    /* Event pointer */
    struct gecko_cmd_packet* evt;

    /* Wait for next stack event. */
    evt = gecko_wait_event();

    switch (BGLIB_MSG_ID(evt->header)) {

      case gecko_evt_system_boot_id:
        /* Serve boot event, e.g. start advertising here */
        break;

      /* ...Further event handlers here... */

      case gecko_evt_system_external_signal_id:
        if (evt->data.evt_system_external_signal.extsignals & EXT_SIGNAL_TIMER_EXPIRED_FLAG) {
          /* Run application specific task */
          applicationTask();
        }
        break;

      default:
        break;
    }
  }
}

In this example, an external interrupt occurs when TIMER0 expires. This interrupt will generate a _system_external_signal_ event, passing the flag that identifies the interrupt source. This event is then handled in the main loop as a stack event. This way, the stack and the application can work in parallel with an easy to understand code structure.

Scenario 4

Custom Main Loop Extended with Bluetooth Event Handler

If you have already implemented your application with a custom main loop (e.g., using a message queue) and want to add Bluetooth functionality, you probably don't want to rewrite the whole main loop to fit one of the previously discussed schema. Instead, you want to extend it to handle Bluetooth events.

In this case, use the _stack_schedule_callback_ function, which will be called

Both use cases can be handled by calling gecko_peek_event(). Because the callback function is called from interrupt context, gecko_peek_event() cannot be directly called from the callback function. Instead, the callback function needs to signal to the main application that the stack needs an update. Then, gecko_peek_event() is called from the main application, as shown in the following example:

msg_q my_msg_q;
enum msg_types {
  APP_TASK1_RUN,
  APP_TASK2_RUN,
  BLUETOOTH_UPDATE
}

msgq_init(msg_q*);
msgq_send(msg_q*, int msg_type, int* msg_data);
msgq_recv(msg_q*, int* msg_type, int* msg_data);

void bluetoothUpdate()
{
  /* Signal the need for stack update */
  msgq_send(&my_msg_q, BLUETOOTH_UPDATE, NULL);
}

void bluetoothHandleEvents()
{
  struct gecko_cmd_packet* bluetooth_evt;

  /* Check for stack events. Serve all pending events. */
  while (bluetooth_evt = gecko_peek_event()) {
    switch (BGLIB_MSG_ID(bluetooth_evt->header)) {
      case gecko_evt_system_boot_id:
        /* Serve boot event, e.g. start advertising here */
        break;

      /* ...Further event handlers here... */

      default:
        break;
    }
  }

  /* If further update is needed, signal it */
  if (gecko_can_sleep_ms() == 0) {
    msgq_send(&my_msg_q, BLUETOOTH_UPDATE, NULL);
  }
}

void appMain(gecko_configuration_t *pconfig)
{
  int  msg_type;
  int* msg_data;

  msgq_init(&my_msg_q);

  pconfig->stack_schedule_callback = bluetoothUpdate;

  /* Initialize stack */
  gecko_init(pconfig);

  while (1) {
    msgq_recv(&my_msg_q, &msg_type, msg_data);

    switch (msg_type) {

      case APP_TASK1_RUN:
        /* Run application specific task */
        applicationTask1();
        break;

      case APP_TASK2_RUN:
        /* Run application specific task */
        applicationTask2();
        break;

      case BLUETOOTH_UPDATE:
        /* Serve Bluetooth stack */
        bluetoothHandleEvents();
        break;

      default:
        break;
    }
  }
}

Running Bluetooth and Application Tasks in Parallel Using RTOS

Bluetooth tasks and application tasks can also be run in parallel using Micrium RTOS. In this case, a dedicated OS task handles Bluetooth events, while application can run in other OS tasks independently from the stack. This approach can be useful in more complicated applications, but it needs more resources. The implementation of Bluetooth applications over Micrium RTOS is discussed in detail in the following document: AN1114: Integrating Silicon Labs Bluetooth ® Applications with the Micrium RTOS.