nRF5 SDK Scheduler Tutorial

2019-02-08: Updated tutorial to cover nRF5 SDK version 15.2.

Introduction

Scope

The following topics will be included in this tutorial:

Necessary prior knowledge

It is expected that you have basic knowledge of how to build and download your application to your kit. Refer to Getting Started in the SDK documentation if this is unfamiliar territory. It is also expected that you are familiar of the concept of interrupts and event handlers. Please refer to the Pin Change Interrupt Example if you need to refresh this topic.

The section on using the scheduler with the application timer require some familiarity with the application timer. Refer to the Application Timer tutorial if required.

Necessary equipment and software

Hardware:

Software:

  • Segger Embedded Studio (SES) or any other supported toolchain, used to build and program the example.
  • RTT Viewer or a UART terminal program, used to observe the log output.

Other files:

Within the tutorial project folder there are sub-folders for different boards:

The Scheduler

The scheduler is used for transferring execution from interrupt handler mode to thread/main mode. This ensures that all interrupt handlers are short, and that all interrupts are processed as quickly as possible. This can be very beneficial for some applications.

Conceptually the scheduler works by using a event queue. Events are scheduled by being put in the queue using app_sched_event_put(). This will typically be done from a event handler running in a interrupt handler mode. By calling app_sched_execute(), events will be executed and popped from the queue one by one until the queue is empty. This will typically be done in the main loop. There is no concept of priorities, so events are always executed in the order they were put in the queue.

The Schedule handling library documentation has a good overview of how the scheduler works, and you should spend a minute to compare the sequence diagrams for applications using the Scheduler and applications not using the scheduler. The Scheduler API is simple and you are recommended to use a few minutes to review it.

Some of the libraries in the SDK, such as the Application Timer library and SoftDevice Handler library, include support for the Scheduler. Most other SDK libraries or your own interrupt routines does not, and you will have to do a bit more work. In this tutorial, you will initialize the Scheduler and use it both directly with GPIOTE and with the application timer.

Visualizing the function of the scheduler

In this tutorial you will not change the functionality of the example, which is to control toggling of LED 1 using button 1 and 2. You will only change how the event handlers are executed, and we will use logging to indicate whether the event handlers are executing in interrupt handler mode (without scheduler) or thread mode (with scheduler). Both timer_handler() and button_handler() in the example project contain a section of code that writes a log line indicating this every time they are run. This code has no practical use in a real application, but it is useful here in the tutorial as a visual indication that the scheduler is doing its job.

Preparations

To get started, download and extract the SDK and clone the example project under <SDK>/examples/peripheral/:

git clone https://github.com/NordicPlayground/nrf5-scheduler-tutorial.git

Erase the flash of the DK using nRF Connect Programmer or nrfjprog in order to ensure that no SoftDevice is installed. Open the project that suits your target device and prefered toolchain. It should compile without any errors or warnings. Download the application to the target device and start a RTT client or UART terminal to observe the log output. The easiest is to debug the application from Segger Embedded Studio, as that will let you edit, build, program, debug and obser the log output in the same application window.

Now that the application is running you can press button 1 to make LED 1 start toggling. Press button 2 to stop toggling.

You should see log output similar to this (depending on how you push the buttons):

<info> app: Scheduler tutorial example started.
<info> app: Start toggling LED 1.
<info> app: Button handler is executing in interrupt handler mode.
<info> app: Timeout handler is executing in interrupt handler mode.
<info> app: Timeout handler is executing in interrupt handler mode.
<info> app: Stop toggling LED 1.
<info> app: Button handler is executing in interrupt handler mode.

Required files, includes and configuration

This section is only here as a reference, as the tutorial project and most SDK example projects contain the required files, includes and sdk_config.h sections. It can be used as a reference if you get issues with something missing when building your application. If not, please move on to the next section.

Add the following files to your project (the path is relative to project file):

  • ..\..\..\..\..\components\libraries\scheduler\app_scheduler.c
  • ..\..\..\..\..\components\libraries\util\app_util_platform.c

Add the following directory to the include paths:

  • ..\..\..\..\..\components\libraries\scheduler

Then include the required header file by adding the following lines below the existing include statements:

#include "app_scheduler.h"

Make sure that your projects sdk_config.h includes the configuration block for the scheduler:

//==========================================================
// <e> APP_SCHEDULER_ENABLED - app_scheduler - Events scheduler
//==========================================================
#ifndef APP_SCHEDULER_ENABLED
#define APP_SCHEDULER_ENABLED 1
#endif
// <q> APP_SCHEDULER_WITH_PAUSE  - Enabling pause feature
 

#ifndef APP_SCHEDULER_WITH_PAUSE
#define APP_SCHEDULER_WITH_PAUSE 0
#endif

// <q> APP_SCHEDULER_WITH_PROFILER  - Enabling scheduler profiling
 

#ifndef APP_SCHEDULER_WITH_PROFILER
#define APP_SCHEDULER_WITH_PROFILER 0
#endif

// </e>

Initialization

The scheduler is initialized using the APP_SCHED_INIT() macro, which takes two parameters:

  • EVENT_SIZE: the maximum size of events to be passed through the scheduler.
  • QUEUE_SIZE: the maximum number of entries in the scheduler queue.

Add the following defined close to the top of the file:

// Scheduler settings
#define SCHED_MAX_EVENT_DATA_SIZE   sizeof(nrf_drv_gpiote_pin_t)
#define SCHED_QUEUE_SIZE            10

Then write the following line above your main loop in order to initialize the Scheduler using the above parameters:

APP_SCHED_INIT(SCHED_MAX_EVENT_DATA_SIZE, SCHED_QUEUE_SIZE);

The last thing you have to do is to add a call to app_sched_execute() in your main loop. The app_sched_execute() function will pull events and call its handler in thread mode. It will execute all events scheduled since the last time it was called. Then the CPU would normally go to sleep again, as is the case in this tutorial with the call to __WFI() (or sd_app_evt_wait() if a SoftDevice was used). Add the following line to your main loop:

        app_sched_execute();

Using the scheduler with any interrupts or events

The scheduler is supported by some components in the SDK, including the application timer and the SoftDevice. It can also easily be used with your own interrupt routines or event handlers. The GPIOTE driver does not use the scheduler out of the box, so any GPIOTE event handlers will run in an interrupt handler mode.

In order to use the scheduler, you will have to create a new button scheduler event handler that will run in thread mode (called from app_sched_execute()). Let's start by creating that handler, the button_scheduler_event_handler(). Add the following function to your file somewhere after button_handler() and before gpiote_event_handler():

/**@brief Button handler function to be called by the scheduler.
 */
void button_scheduler_event_handler(void *p_event_data, uint16_t event_size)
{
    // In this case, p_event_data is a pointer to a nrf_drv_gpiote_pin_t that represents
    // the pin number of the button pressed. The size is constant, so it is ignored.
    button_handler(*((nrf_drv_gpiote_pin_t*)p_event_data));
}

In the existing gpiote_event_handler(), which runs in interrupt handler mode, you will do as little as possible and just schedule an event by putting it in to the schedulers event queue using app_sched_event_put(). In order to achieve this, modify gpiote_event_handler() by removing this line:

    button_handler(pin);

...replacing it with this:

    app_sched_event_put(&pin, sizeof(pin), button_scheduler_event_handler);

With changes you have made, the gpiote_event_handler() still runs in interrupt handler mode, but it is short (thus takes very little time) and only schedules the button_scheduler_event_handler() by putting an event into the schedulers' event queue. The main work is done by the button_scheduler_event_handler() which is called by app_sched_execute() from the main loop.

Build and download the application to your target. When you now press button 1 or 2 you will see the following log line (among others), indicating that button_handler() is now executing in thread mode:

<info> app: Button handler is executing in thread/main mode.

Using the scheduler with the Application Timer

This section demonstrates how to use the scheduler with the application timer so that the timeout handlers are run in thread mode.

The scheduler event queue must be made to hold the maximum size of the data types that can be used as event data. Therefore, we will update it so that it is large enough to hold app timer events. You should remove the following line that you previously added:

#define SCHED_MAX_EVENT_DATA_SIZE   sizeof(nrf_drv_gpiote_pin_t)

...and replace it with this line:

#define SCHED_MAX_EVENT_DATA_SIZE   MAX(sizeof(nrf_drv_gpiote_pin_t), APP_TIMER_SCHED_EVENT_DATA_SIZE)

Then configure the app timer to use the scheduler by finding the line setting APP_TIMER_CONFIG_USE_SCHEDULER in the projects sdk_config.h and modify it to this:

#define APP_TIMER_CONFIG_USE_SCHEDULER 1

Again, build and download the application to your target. When you now press button 1 to start the LED toggling you will see the following log line (among others), indicating that the timer_handler() now executes in thread mode:

<info> app: Scheduler tutorial example started.
<info> app: Start toggling LED 1.
<info> app: Button handler is executing in thread/main mode.
<info> app: Timeout handler is executing in thread/main mode.
<info> app: Timeout handler is executing in thread/main mode.
<info> app: Stop toggling LED 1.
<info> app: Button handler is executing in thread/main mode.

Using the scheduler with the SoftDevice Handler library

The SoftDevice Handler library can be configured to use the scheduler so that the applications SoftDevice event handler is executing in thread mode. This can be enabled by simply setting NRF_SDH_DISPATCH_MODEL to 1 (NRF_SDH_DISPATCH_MODEL_POLLING) in the projects sdk_config.h after following the earlier instructions in this tutorial:

#ifndef NRF_SDH_DISPATCH_MODEL
#define NRF_SDH_DISPATCH_MODEL 1
#endif

(Note that this is not part of the sdk_config.h file used for this tutorial, as it does not use the SoftDevice).

Further Reading

Parents
  • @Tom - typically interrupt flag is set and Interrupt Service Routine is called when previous ISR is returned. (but arm has more priority levels so it also can happened that later coming IRQ has higher priority and interrupts ongoing ISR). Basically if your ISR are fast you shall be ok. If ISR is too long it either slows down system reaction on other ISR which might be quicker. Or it can skip same IRQ - if you imagine that ISR would clear interrupt pending flag at the end and IRQ would be set again before flag clearing then ISR would be run only once...

Comment
  • @Tom - typically interrupt flag is set and Interrupt Service Routine is called when previous ISR is returned. (but arm has more priority levels so it also can happened that later coming IRQ has higher priority and interrupts ongoing ISR). Basically if your ISR are fast you shall be ok. If ISR is too long it either slows down system reaction on other ISR which might be quicker. Or it can skip same IRQ - if you imagine that ISR would clear interrupt pending flag at the end and IRQ would be set again before flag clearing then ISR would be run only once...

Children
No Data