Zephyr* Scheduling Basics with the Intel® Quark™ microcontroller D2000

ID 673028
Updated 5/1/2017
Version Latest
Public

author-image

By

Overview

In this article, you’ll learn about:

  • The fundamentals of scheduling software execution with the Zephyr* Real-time Operating System (RTOS) and the Intel® Quark™ microcontroller D2000.
  • Zephyr software mechanisms called tasks and fibers, which are essential components of all Zephyr applications.
  • Initialization and use of tasks and fibers in your applications.
  • Common problems when getting started with the Zephyr* RTOS and how to avoid them.

The Intel® Quark™ microcontroller D2000 and the Zephyr* RTOS

With the Intel® Quark™ microcontroller D2000, Intel is staking its place at the edge of the Internet of Things (IoT). The Intel® Quark™ microcontroller D2000 was designed from the ground up for IoT applications where low power is important. Small battery-powered sensor devices, gathering data in homes, businesses, factories, and farm fields require ultra-low power consumption electronics. With sleep currents in the single digit microAmps, a sensor device (powered by an Intel® Quark™ microcontroller D2000) transmitting data over a Bluetooth® low energy radio could run for a couple years on a pair of lithium-ion batteries.

The core of the Intel® Quark™ microcontroller D2000 is a Pentium® processor. Low power but still powerful enough for IoT, it’s fully compatible with the x86 instruction set and capable of executing code written for its desktop counterparts. Benefiting from decades of Pentium® processor architecture refinement and software execution optimization, the Intel® Quark™ microcontroller D2000 is a modern microcontroller with a reliable history.

To support software development with this new microcontroller, Intel partnered with the Linux Foundation* to build an open-source real-time operating system (RTOS). Based on source code developed by Wind River*, the Zephyr* RTOS is built for resource constrained microcontrollers with less than 512kB of system memory. The Zephyr RTOS comes in two sizes and is highly configurable, allowing the user to choose an appropriate feature set and enable only necessary software features to minimize Zephyr’s memory footprint. The Zephyr RTOS includes an Application Programming Interface, or API, with tools and drivers that make working with embedded devices, like sensors and radios, a relatively simple process. If you’re new to working with an RTOS, you’ll find that writing applications with Zephyr will shorten processor bring-up, reduce software issues during hardware validation, and streamline multi-threaded development. If you’re experienced with an RTOS, you’ll find that Zephyr provides all the tools of a world-class RTOS in a fresh package, custom designed to meet the needs of modern designers of IoT.

Zephyr RTOS Fundamentals

Zephyr is a multi-threaded operating system, meaning that Zephyr can effectively perform multiple operations at the same time. Functional blocks of code are executed in turn, according to priorities that you assign to tasks and fibers. Separate blocks of code aren’t actually running simultaneously. Since the Intel® Quark™ microcontroller D2000 has only one processor core, it executes functions one at time, handling higher priority code first and executing lower priority code when higher priority code is idle. You decide which functions are most important and Zephyr will prioritize their execution to meet critical timing requirements.

In Zephyr, functional blocks of code can be executed in your choice of three execution contexts: task, fiber, or interrupt. A task is for larger pieces of code that take longer to execute and aren’t as time sensitive. A fiber is for smaller operations with stricter timing requirements like hardware drivers. An interrupt is for the smallest operations which are the most time critical, like responding to a hardware or software event. Tasks are the lowest priority and can be preempted whenever a higher priority task, a fiber, or an interrupt needs to execute. Fibers always interrupt a task when they need to execute. They can only be preempted by interrupts, not by tasks or other fibers, even higher priority ones. As you can see, interrupts are the highest priority and they always interrupt a task or fiber when they need to run. This article covers tasks and fibers but not interrupts. For more information on interrupts, consult the Zephyr Project* Documentation.

Kernels

The core of the Zephyr RTOS is called the kernel, which contains the software system for scheduling code execution. The kernel also contains software subsystems like device drivers and networking software. The Zephyr kernel is comprised of the nanokernel and the microkernel.

Nanokernel

The nanokernel is the lighter of the two kernels, with a reduced feature set to achieve a smaller memory footprint. It’s designed for microcontrollers with less than 50kB of system memory, like the Intel® Quark™ microcontroller D2000. The nanokernel is better suited to handle simpler applications, like reading a small number of sensors and communicating over a single radio.

The nanokernel is only allowed to have a single task, usually the main function. Nanokernel applications are not restricted in the number of fibers they can use, up to the limits of their memory. For most applications with the Intel® Quark™ microcontroller D2000, the nanokernel should be the best kernel option. While it’s possible to compile a microkernel application for the Intel® Quark™ microcontroller D2000, you’ll have less room for your application code. Only use the microkernel with the Intel® Quark™ microcontroller D2000 if you absolutely need microkernel features only (multiple tasks, sophisticated memory management tools, etc.).

Microkernel

Everything the nanokernel can do, the microkernel can do and more. The microkernel is the full-featured version of the Zephyr RTOS. Geared toward complex applications, the microkernel can coordinate multiple tasks, like handling reading sensors and performing data analysis while communicating with the cloud over multiple radio channels. The microkernel can run more than one task as well as an unlimited number of fibers. The microkernel has more available features for managing data flow and memory and synchronizing execution. 

Scheduling with Zephyr

The Tick Timer

Everything in the Zephyr RTOS marches to the timing of the Zephyr tick timer. The tick timer is derived from a 64-bit system clock in the Intel® Quark™ microcontroller D2000 which takes its count from a 32-bit hardware timer. Zephyr’s tick timer defines the granularity of timing in your application. The default step size of the tick timer is 10 milliseconds. The period of the tick timer determines the minimum resolution you can achieve with software timers in your application. It also determines the shortest interval in which the Zephyr RTOS will change between equally prioritized tasks and fibers. A longer tick timer period can potentially make your code less responsive. On the other hand, a shorter tick timer period increases the operating system overhead because changing tasks takes time and resources. You can reduce the tick timer period if you need finer timing resolution or increase it if you want to reduce processor activity. If your application doesn’t require it, the tick timer is best left alone.

Tasks

Now, let’s take a look at how to configure and use tasks and fibers to build your application. You need to be aware of differences between the nanokernel and the microkernel. There are differences in how each handles tasks and fibers. First, let’s look at tasks in each of the kernels.

Nanokernel Tasks

In the nanokernel, you’re only allowed one task. Zephyr requires at least one task to operate and uses your main() function as that one task. Zephyr refers to the main() task in a nanokernel application as the background task. As the name suggests, your main() function will only execute when no fiber or Interrupt Service Routine (ISR) needs to run. This fact has some implications for how you write your code and how you structure your application.

Unlike a main() function in a non-RTOS application, the Zephyr background task doesn’t end when execution reaches the end of the function; the task runs again in a loop. To avoid running initialization code again, your main function should have an endless loop at its end.

The next consideration is to place code in the main task that is not time critical. Since any other application code can interrupt it at any time, your main task may not always execute with consistent timing. It’s also possible that you could construct an application where the main task doesn’t ever get to execute at all. If fibers and interrupts monopolize the processor by taking too long to execute or by containing prolonged delays, you can cause what’s referred to as task starvation. Chances are that simple applications won’t encounter task starvation. It’s just something that you need to be aware of now that you’re working with an operating system.

Your main() function/task with Zephyr’s nanokernel should look like this:

void main(void){
	
/*Hardware initialization here*/
	
while(1){
		/*Endless loop*/
	}
}

If you want the main task to pause operation, you can use timers and wait for them to expire, or you can use the task_sleep() function to idle the task for a specified length of time. Using the tick timer with the task_sleep() function, you tell the task to sleep for a certain number of ticks. The task will then sleep for a length of time of the number of ticks times the tick timer period. For example, if you would like to put the main() task to sleep for 10 timer ticks, or 100 milliseconds with a 10 millisecond tick timer, use this:

task_sleep(10);

Using this basic timing functionality, you can create software events which occur at regular intervals.

Microkernel Tasks

With the microkernel, as with the nanokernel, your main() function is a task. However, unlike the nanokernel, the microkernel can handle multiple tasks. When you design your software system architecture, tasks should contain longer and more complex code functions that like the main task are too lengthy to be performed by a fiber.

Microkernel tasks are very different from nanokernel tasks in terms of how they are initialized and how they work. Tasks in the microkernel are given priorities to determine which is the most important. Priorities for microkernel tasks can range from 0, the highest priority, down to a configurable minimum priority which defaults to 15. Your minimum task priority should be one less than the lowest priority which is reserved for the microkernel’s idle task which runs when nothing else needs to execute. The Zephyr RTOS will run whichever task has the highest priority first. If two tasks have the same priority, it will run the one that has been waiting the longest.

Like the main() task in the nanokernel, tasks normally run forever in a loop. It’s your responsibility to set the priorities, determining which code needs to always execute on time and which code can handle more interruptions and longer delays.

Microkernel tasks require: a defined memory region to store the task’s stack, a function to be invoked when the task starts executing, a priority, and what’s called a task group. Also, the microkernel requires an extra file, called an MDEF file, in your project directory. The MDEF file is a text file in which you’ll declare all your microkernel objects, including tasks.

Declaring a microkernel task

Microkernel tasks are declared in the MDEF file with all the necessary information conveyed in this order: name, priority, function, stack memory size, and task group. In the MDEF file, comment lines, starting with the % symbol, are not interpreted by the Zephyr build system. Task declarations start with the keyword TASK. Tasks that should execute immediately when the application starts should be assigned to the EXE group. Using task groups, you can start and stop a group of tasks together. Tasks that should not execute immediately, like ones which handle sensors that may require a start up delay, can be assigned to a different task group, or the group can be left empty as in the example below.

% TASK NAME           PRIO  ENTRY          STACK   GROUPS
% ===================================================================
  TASK MAIN_TASK        6   main                    1024   [EXE]
  TASK SENSOR_TASK 2   sensor                 400     []

In this example, two tasks are defined. The “MAIN_TASK” is defined with a priority of 6, the main function as its entry point, a stack memory region of 1024 bytes, and assigned to the EXE group to start immediately. The “SENSOR_TASK” is defined with a higher priority of 2, a function called sensor() as its entry point, a stack region of 400 bytes, and no assigned task group.

Starting a Microkernel Task

Tasks in the EXE group will start automatically but tasks that don’t start right away need to be started by another task using the task_start() function. To start a task, you only need to know the task’s name as it appears in the MDEF file:

task_start(SENSOR_TASK);

Fibers

Unlike tasks, fibers are handled the same with the nanokernel and the microkernel. Fibers are intended to be used for shorter performance-critical pieces of code. Fibers cannot be interrupted by another task or another fiber, so execution timing is more consistent and reliable. You should use fibers for device driver code or for communications requiring precise timing. Interrupt service routines can interrupt a fiber, so you should still account for this in writing your code.

Fibers are scheduled for execution by the RTOS based upon priority. Fiber priorities range from 0, which is the highest priority, down to 232-1. If two fibers have the same priority, the fiber which has been idle the longest is executed first. If no fibers need to execute, then the highest priority task that needs to run is scheduled for execution. Of course, a fiber can interrupt any task if it needs to run.

Initializing a Fiber

Fibers are declared in your source code and then initialized and started from a task or another fiber. The process is slightly more complex than that for a task. Fibers require that you declare a stack memory region for storing fiber variables and context data that is used when the task is idled and restarted. You also need to create a function which will be used as the function entry point where the fiber starts execution. The function can take up to two arguments although it’s not necessary that you use them. The function arguments can be used to supply initialization information for the fiber. You also need to specify the fiber’s priority and have the option of passing some options to the fiber. The options don’t apply to the Intel® Quark™ microcontroller D2000. To declare the stack memory region, do something like this:

#define STACKSIZE 2000
char __stack fiberStack[STACKSIZE];

This declaration uses the C preprocessor __stack identifier to declare a memory array of size set by the STACKSIZE constant. The function that will serve as the fiber entry point requires no special declaration, using a prototype like this:

void fiberEntry(int arg1, int arg2);

If your application doesn’t have a need for the function arguments, you are free to use the function prototype:

Void fiberEntry(void);

For your application, you should change “fiberEntry” to something appropriate and meaningful.

With a stack memory region and an entry point function, you have everything you’ll need to use the task_fiber_start function to enable the fiber. The fiber start function takes as arguments: a pointer to the stack memory, an integer defining the size of the stack, the name of the entry function, the two integer arguments, the priority as an integer, and any fiber options. In practice, it looks like this:

task_fiber_start(fiberStack, STACKSIZE , fiberEntry, arg1, arg2, priority, options);

In this case, arg1, arg2, priority, and options are integer variables. Since fibers take priority over tasks, if a task executes this function, the fiber will start to execute immediately after the processor finishes executing the task_fiber_start function. The task will be idled and the RTOS will switch over to the fiber. If immediately starting the fiber is undesirable, then you can use the task_fiber_delayed_start() function instead. This function takes one extra argument which is the number of ticks that the fiber should be delayed in starting up. The other arguments are the same and in the same order. To delay the startup of a fiber by 10 ticks, you would change the above function call to this one:

task_fiber_delayed_start(fiberStack, STACKSIZE , fiberEntry, arg1, arg2, priority, options, 10);

If you are starting a fiber from another fiber, then you need to use a different function designed to be used from fibers. The form is the same; just the name is different. Function calls from fibers substitute the word “fiber” for “task” in the function name. Use this function to start a fiber immediately:

fiber_fiber_start(fiberStack, STACKSIZE , fiberEntry, arg1, arg2, priority, options);

And this function to start a fiber with a delay:

fiber_fiber_delayed_start(fiberStack, STACKSIZE , fiberEntry, arg1, arg2, priority, options, 10);

Idling a Fiber

Like a task, a fiber normally executes forever once it’s started. Unlike a task, a fiber cannot be interrupted except by an interrupt. Other fibers, including higher priority fibers cannot interrupt an actively processing fiber. Any fiber that monopolizes the processor with long processing times may cause delays with other fibers, including higher priority ones. In this case, it may be necessary for a task to deliberately pause execution to allow time for other fibers and tasks to execute. There are two Zephyr functions for this purpose, each with slightly different behaviors. The fiber_yield() function will idle a fiber so that higher or equal priority fibers have an opportunity to execute. As an argument, the fiber_yield() function requires the number of ticks for which the task will yield control of the processor. To yield for 10 timer ticks, use this:

fiber_yield(10);

For a more general relinquishment of processor control, you should use fiber_sleep(), which will surrender control of the processor without condition for a specified number of ticks. Unlike the yield function, the sleep function allows tasks and lower priority fibers to execute. To idle a fiber for 10 ticks, use this:

fiber_sleep(10);

The Microkernel Server Fiber

Fibers play a larger role in nanokernel applications since the nanokernel doesn’t allow multiple tasks. In microkernel applications, fiber usage should be reserved for the highest priority activities when being preempted could compromise performance. Nanokernel applications should use fibers for driver interactions and time sensitive processing.

The microkernel automatically runs one task, the microkernel server fiber, which handles the scheduling of all microkernel fibers, determining which fiber needs to execute first. The microkernel server fiber defaults to the highest priority, 0. You can change the microkernel server fiber to a lower one if you have high priority critical code that can’t tolerate any delay, like time sensitive device drivers. In general, it won’t be necessary to change the microkernel server priority, but if you’re curious, consult the Zephyr Microkernel Fiber Documentation.

Next Steps

With basic knowledge of scheduling with the Zephyr RTOS and an understanding of fibers and tasks you can now make quality applications with Zephyr. For further information, check out the advanced RTOS mechanisms in the Zephyr Project Documentation.

 

Download    Get Started    Code Sample

Resources