KBA_BT_0909: Scheduling application tasks while running BLE stack
10/278/2016 | 09:08 AM
Introduction
The core of all embedded application is the main loop. This can be implemented in many ways according to the taste of the developer. However, if Bluetooth stack is running in the background, it puts some restriction on the implementation, since Bluetooth tasks have to be served and Bluetooth events have to be handled from time to time.
Note: Most Bluetooth tasks are time critical and they are executed in high priority interrupt handlers. However, some tasks cannot be executed in interrupt context (e.g. writing to flash), and some tasks need response from the application, hence the application should be implemented so, that these tasks are served from time to time. The timing accuracy of these tasks are in the order of seconds.
The developer can choose between 3 options regarding the main loop implementation:
use the recommended Bluetooth event handler main loop and extend it with application tasks or
use a custom main loop to handle application tasks and extend it with Bluetooth event handling or
use RTOS to run Bluetooth task and application tasks parallel.
I. 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 (like e.g. reading sensors). Here we consider 3 typical scenarios regarding the scheduling of the application task:
Application task need to run continuously
Application task need to run at every T ms
Application task need to run when an interrupt occurs
You may also combine these solutions if you have multiple tasks with different scheduling requirements.
1. Application task need to run continuously
If an application task need to be running 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 in order 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 case the scheduling looks like this:
Bear in mind, that timing accuracy requirement of host stack is seconds. If the application task runs for too long time (many many seconds), the stack may suffer latency which can lead to different issues (e.g. bonding timeout, GATT timeout, loss of events, etc.)
2. Application task 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 e.g. 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 only when at least one event is in the event queue of the stack.
If an application task need 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_timerevent will be triggered from time to time, which can start your application task. For the usage of gecko_cmd_hardware_set_soft_timer() see Bluetooth Software API Reference Manual.
Note: 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 10ms (which is the minimum interval you can set for a soft timer). If precise timing is not so important 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 case the scheduling looks like this:
3. Application task 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 some very 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 cannot be called from interrupt context!)
The signaling between the interrupt handler and the main loop can be implemented in different ways, e.g. 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 32bit 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 fully parallel with an easy to understand code structure.
II. Own main loop extended with Bluetooth event handler
Let's say you have already implemented your application with your own preferred main loop (e.g. using a message queue) and you want to add Bluetooth functionality. You probably do not want to rewrite your whole main loop to fit one of the previously discussed schemes. Instead you want to extend it to handle Bluetooth events.
In this case you can use the stack_schedule_callback function. This callback function will be called
when the stack has to run a task that cannot be run from interrupt context or
when there is a new Bluetooth event available
Both cases can be handled by calling gecko_peek_event(). Since 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() shall be called from the main application, e.g. as 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;
}
}
}
III. Running Bluetooth and application tasks parallel using RTOS
Bluetooth tasks and application tasks can also be run completely parallel using Micrium RTOS. In this case there is a dedicated OS task to handle 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 resource than a simple main loop. The implementation of Bluetooth applications over Micrium RTOS is discussed in details in the following document: AN1114: Integrating Silicon Labs Bluetooth ® Applications with the Micrium RTOS.
KBA_BT_0909: Scheduling application tasks while running BLE stack
Introduction
The core of all embedded application is the main loop. This can be implemented in many ways according to the taste of the developer. However, if Bluetooth stack is running in the background, it puts some restriction on the implementation, since Bluetooth tasks have to be served and Bluetooth events have to be handled from time to time.
The developer can choose between 3 options regarding the main loop implementation:
I. 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 (like e.g. reading sensors). Here we consider 3 typical scenarios regarding the scheduling of the application task:
You may also combine these solutions if you have multiple tasks with different scheduling requirements.
1. Application task need to run continuously
If an application task need to be running 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.
In this case the scheduling looks like this:
2. Application task 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 e.g. 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 only when at least one event is in the event queue of the stack.
If an application task need 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_timerevent will be triggered from time to time, which can start your application task. For the usage of gecko_cmd_hardware_set_soft_timer() see Bluetooth Software API Reference Manual.
In this example the soft timer is configured to generate an event every 10ms (which is the minimum interval you can set for a soft timer). If precise timing is not so important 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 case the scheduling looks like this:
3. Application task 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 some very 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 cannot be called from interrupt context!)
The signaling between the interrupt handler and the main loop can be implemented in different ways, e.g. 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 32bit bitfield. Set different bits in different interrupt handlers, so you can check in the main loop, which interrupt was triggered.
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 fully parallel with an easy to understand code structure.
II. Own main loop extended with Bluetooth event handler
Let's say you have already implemented your application with your own preferred main loop (e.g. using a message queue) and you want to add Bluetooth functionality. You probably do not want to rewrite your whole main loop to fit one of the previously discussed schemes. Instead you want to extend it to handle Bluetooth events.
In this case you can use the stack_schedule_callback function. This callback function will be called
Both cases can be handled by calling gecko_peek_event(). Since 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() shall be called from the main application, e.g. as in the following example:
III. Running Bluetooth and application tasks parallel using RTOS
Bluetooth tasks and application tasks can also be run completely parallel using Micrium RTOS. In this case there is a dedicated OS task to handle 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 resource than a simple main loop. The implementation of Bluetooth applications over Micrium RTOS is discussed in details in the following document: AN1114: Integrating Silicon Labs Bluetooth ® Applications with the Micrium RTOS.