This article introduces you to the the Zephyr* RTOS and explains how to configure it for the Intel® Quark™ microcontroller D2000.
Zephyr* RTOS with the Intel® Quark™ microcontroller D2000
Welcome to The Zephyr* RTOS with the Intel® Quark™ microcontroller D2000! Intel is now building embedded microcontrollers. They’ve taken the Pentium® processor and taken it down to microcontroller size to be the heart of small battery-powered devices. The Intel® Quark™ microcontroller D2000, based on Intel’s lowest power Pentium® processor, is designed to control battery-powered electronics like wireless sensors and wearables. To support development with the Intel® Quark™ microcontroller D2000 and to make it easy to build devices with other Intel® Quark™ microcontrollers and beyond, Intel worked with the Linux Foundation* to build a real-time operating system (RTOS), called Zephyr*. Zephyr is an open-source RTOS designed to operate in microcontrollers with limited memory. The Zephyr RTOS is a software platform that simplifies software development, freeing you up to focus more on algorithms and less on hardware.
The Zephyr RTOS includes driver libraries to:
- Talk to sensors
- Keep track of time
- Send messages to the internet
- Communicate using radios, like Bluetooth® technology or Wi-Fi
- Manage power consumption to extend battery life
The Zephyr RTOS is compatible with an array of processors, not just those in Intel® Quark™ microcontrollers. The description in this article additionally applies to the use of the Zephyr RTOS with an array of available microcontrollers from other manufacturers.
What is an RTOS?
An RTOS is an operating system with a focus on real-time applications. Zephyr is similar to operating systems you find on desktop computers and laptops. The difference is that an RTOS performs tasks in a predictable, scheduled manner with a focus on getting the most important tasks done on time. In an embedded device, timing is critical. On a desktop, it doesn’t matter much if your computer decides to check for new emails before it starts playing a video. The operating system has a running list of tasks and decides which tasks are most important. Chances are user software is not the highest priority task. An extreme example is an embedded system in a car. It matters if the microcontroller decides to check for email when the microcontroller should be triggering the airbag. An RTOS is an operating system that you control completely.
Why Use an RTOS?
As the Internet of Things expands, formerly unconnected devices are getting “smarter” (e.g., able to send data to the cloud) and increasingly complicated. As complexity grows, software becomes more difficult to manage. Simple devices with single purposes probably don’t need to run an RTOS. But, complicated devices with multiple sensors and radios that need to be smart (connected and responsive) are easier to build and maintain with an RTOS.
The RTOS manages software complexity by encapsulating all the activities the microcontroller needs to perform into individual tasks. Then, the RTOS provides tools to prioritize the tasks, determining which tasks always need to execute on time and which tasks are more flexible. Some applications, like communication over radios to the internet, have strict timing requirements and complicated communication protocols. You can rely on the Zephyr RTOS to make sure that communications happen on time and that your microcontroller responds appropriately without you having to write any software to make it happen.
Writing software with an RTOS is a familiar process for developers coming to microcontrollers from desktop programming. For embedded developers with a background in bare metal firmware on microcontrollers, an RTOS is a powerful new tool. The structure of the RTOS improves encapsulation, isolating different functional pieces of software from each other and provides tools to exchange information between different functional code blocks. This prevents one of the greatest hazards of microcontroller firmware development: memory management. For developers to take advantage of the Zephyr RTOS, it’s important to understand how it works. In the next section, we take a look at the features and capabilities of the Zephyr RTOS.
Zephyr Kernel Fundamentals
What’s a Kernel?
The core functionality of the Zephyr RTOS is the Zephyr kernel. The kernel is software that manages every aspect of hardware and software functionality. The Zephyr kernel is designed to be small, requiring little program and data memory. There are two main components to the Zephyr kernel: a microkernel and nanokernel. Each has different memory requirements and features.
The nanokernel is the smaller of the two. It’s designed to operate on smaller microcontrollers in devices with less functionality (e.g., a sensor measuring only temperature). It requires as little as two kiloBytes of program memory, which means it can be used in all but the very smallest microcontrollers.
The microkernel is a full-featured kernel for more complex devices: a smartwatch with a display, multiple sensors, and multiple radios. The microkernel is designed for larger microcontrollers with memory between 50 and 900 kiloBytes. Every feature of the nanokernel is available to the microkernel, but not the other way around. With 32 kiloBytes of memory, the Intel® Quark™ microcontroller D2000 is ideal for the nanokernel. A simple microkernel project may fit, but if you don’t need any particular microkernel functions, pick the nanokernel which is better suited to the size of the microcontroller’s memory. The core functionality of the kernels is the same either way, you just won’t be able to use advanced memory management features that aren’t supported by the nanokernel.
Three Contexts in the Zephyr RTOS
The Zephyr kernel provides three main tools for organizing and controlling software execution: tasks, fibers, and interrupts. In the Zephyr documentation, these tools are called contexts because they provide the context within which software executes and each context has different capabilities.
In the Zephyr RTOS, major software functionality is encapsulated in a task. A task is a piece of software that performs processing that takes a long time or is complicated (e.g., interacting with a server on the internet over Wi-Fi* or analyzing sensor data looking for patterns).
Tasks are assigned priorities with more important activities assigned higher priorities. Lower priority tasks can be interrupted if a higher priority task needs to take action. When a higher priority task interrupts a lower priority task, the lower task’s data and state is stored and then the higher priority task’s data and state is invoked. When the higher priority task finishes its work, the lower priority task is restored and starts again at the point it was interrupted. Tasks take over the processor, perform their function, and then go to sleep to wait until they are needed again. The Zephyr kernel contains a scheduler that determines which task needs to run at any time. You can precisely control when a task executes, based on the passage of time, in response to a hardware signal, or based on the availability of new data to analyze. If it’s important that a task responds quickly to the trigger, it should be assigned a higher priority. Tasks execute in an endless loop, sleeping most of the time waiting to be called to perform their function.
Fibers are smaller than tasks, used to perform a simple function or just a portion of the processing. Fibers cannot be interrupted by other tasks or fibers. They should be used for performance critical work that requires immediate action. Fibers are defined and started by tasks. Fibers are prioritized over tasks. Tasks can only operate when no fiber needs to execute, so you need to make sure that fibers don’t monopolize the system. Fibers are prioritized like tasks, but no fiber can interrupt a running fiber. However, fibers always interrupt tasks. Fibers should be used for timing sensitive operations, like communicating with sensors where the timing of responses could cause a problem. Fibers should not be used for processing which takes a long period of time.
Interrupts are the highest priority context. The execution of interrupts takes precedence over fibers and tasks. They allow for the fastest possible response to an event, whether it’s a hardware signal from a safety mechanism or the reception of critical communications. Interrupts are prioritized like tasks and fibers, so that higher priority interrupts can take over the processor from lower priority interrupts. When the higher priority interrupt finishes, the low priority interrupt is re-entered. Interrupts are handled with software functions called Interrupt Service Routines (ISRs). Each interrupt has an ISR that runs whenever the interrupt occurs. Interrupts, as a rule, should be kept as short as possible so that they don’t interfere with the schedule for the rest of the system. Commonly, an ISR sends a message to a task or fiber, passing data, or telling it to run. This keeps the interrupt service routine short and offloads longer processing to parts of the application that can be preempted.
Kernels and Tasks
The nanokernel, as described above, is the smaller of the two kernels. The nanokernel has only one task, known as the background task, which can only execute when no fiber or interrupt needs to execute. The background task is the main() function. The nanokernel can have zero fibers or as many as your application needs. The nanokernel also has no limits on the number of interrupts up to the limits imposed by the hardware of the microcontroller and the size of your program memory. As we said before, the nanokernel is better suited to the Intel® Quark™ microcontroller D2000 because of the size of its program memory.
The microkernel is more powerful than the nanokernel. It also requires more memory resources. The microkernel supports having more than one task and allows you to group tasks to work together to perform a larger function. Microkernel fibers and interrupts are the same as in the nanokernel. The microkernel has more sophisticated functionality for handling memory, for sending data between tasks and fibers, and for managing the microcontroller’s power consumption.
Advanced Zephyr Kernel Functionality
You can make complete applications with just the features described above, but to get the most out of Zephyr, you should get familiar with some of its advanced features. The Zephyr kernel includes functionality to synchronize operations, to pass data between tasks, and to trigger execution of tasks based on external events. It’s beyond the scope of this introduction to go into detail on all of the features. For more information about the deeper features of the Zephyr RTOS, consult the Zephyr Project* Documentation.
Getting Started with the Zephyr Kernel
To get started with Zephyr, you’ll need to download the Zephyr kernel and follow the instructions to set up the Zephyr Development Environment on your computer. With Zephyr installed, it’s a good idea to start with a sample project like the hello world project, which you can find in the samples directory where you installed the Zephyr Project code. Follow the instructions for building an application shown in the Getting Started guide. You’ll then understand how to compile your own application and verify that you’ve gotten everything set up correctly. Take a look at the hello world sample application directory. We’ll go through it to understand what all the files are, their purpose, and how you can modify them to build your own applications.
Nanokernel or Microkernel?
The first thing you’ll notice is that there’s a directory for the nanokernel and another for the microkernel. You can build either and they will look the same from the outside. As mentioned earlier, the nanokernel is the more likely kernel for the Intel® Quark™ microcontroller D2000. Still, it’s important to understand the differences to know which kernel is right for your application. Let’s start with the microkernel.
A microkernel project consists of at least five files:
- A configuration file that instructs the kernel to enable features you want to use in your application. Based on the instructions in the configuration file, the kernel will enable hardware functionality and include the appropriate drivers in your project.
- A microkernel object definition file that initializes RTOS features, like tasks and interrupts.
- An application makefile that informs the Zephyr kernel about which processor you are using, which kernel you are using, whether nano or micro, the name of the project configuration file, and the name of your microkernel object definition file.
- Your source code, contained in a subfolder in the project folder.
- A makefile that instructs the compiler how to build your source code.
Let’s take a look at each of these files and how you can modify them for your application.
Kernel Configuration File
The Zephyr RTOS is highly configurable with a huge array of options for tailoring the kernel to meet your application’s needs. In the configuration file, commonly named prj.conf, you determine which Zephyr features you’ll use in your application. By only turning on the features that you need, you control the size of the Zephyr libraries included alongside your application code. Every feature that you intend to use in your application needs to be explicitly enabled using definition statements like the one you see in the prj.conf file in the ‘Hello World’ project.
This statement tells Zephyr to include the driver for the standard output console, which you use to send statements to be displayed on your computer. Other configuration options take the same form. Many drivers have an array of options for setting up hardware to operate exactly how you need it to work. The list of available options and configurations is quite extensive. To see all the available configurations and options, see the Zephyr Configuration Options Reference Guide.
Microkernel Object Definition File
The microkernel object definition file contains definitions of tasks and any other kernel objects your project needs. You should define any objects in this file that you want to be available to your entire application, across any number of source files. In the definition of a task, you need to give the task a name, a priority, a size of memory to use, and to assign that task to a group. In the ‘Hello World’ project, the prj.mdef definition file contains the following task definition:
% TASK NAME PRIO ENTRY STACK GROUPS % ================================== TASK TASKA 7 main 2048 [EXE]
The lines with “%” at the beginning are just comments to clarify the code for the reader. They are not read by the Zephyr compiler. The task name is a Zephyr name for the task, not the name of the function. The priority is what it sounds like. In Zephyr, the lowest priority is 7 and the highest priority is 1. Main is the name of the function that the task will call as its entry point to start running. 2048 is the size of memory, in bytes, allocated to the task. This may seem like a lot, but this memory stores Zephyr data that keeps track of the tasks state and allows for the task to be suspended and restarted. [EXE] is the name of the executable group. In your own project, you can use this same structure to create tasks, as well as defining all of the more advanced features of the microkernel.
The Application Makefile
The application makefile informs the Zephyr kernel which files to use in building your application. It specifies the name of the kernel configuration file, the name of the microkernel configuration file, which processor architecture you are using, and the name of your source code application file. Generally, you won’t need to make changes to the standard Zephyr application makefile.
The Source Code Makefile
The source code makefile is necessary to inform the compiler how to build your source code. Underneath, the Zephyr Development uses an open source compiler to convert your software into machine instructions. The source code makefile tells the compiler which files to include in the process and conveys compiler configuration instructions.
Your Source Code
The source code includes your application, structured in as many files as you need or prefer. To use any Zephyr functionality, a file must include zephyr.h as well as header files for any drivers that you intend to use. Source code files are generally written in C, although the Zephyr compiler allows the use of C++ outside of tasks, fibers, interrupts, and other Zephyr RTOS code. If you look at main.c in the hello world project, you’ll see a standard C file using Zephyr functions.
The nanokernel configuration is broadly the same as the microkernel configuration, except nanokernel projects don’t have microkernel object definition files. Since the nanokernel only uses one task, it’s automatically generated by Zephyr and will use your main function as the entry point. Fibers and interrupt routines are defined inside your source code. If you compare the microkernel project to the nanokernel project, there isn’t much difference. The prj.mdef file is not necessary, so it’s been removed, and the reference to it in the makefile has also been changed. Otherwise, the nanokernel and microkernel from this perspective are largely the same.
The Zephyr API
The Zephyr API is the toolset you will use to build your application code. It’s full of functions for very quickly prototyping hardware. There are libraries to define and use Zephyr functionality like tasks, fibers, interrupt routines, and timers and drivers for communication buses or to talk to specific pieces of hardware. With the Zephyr API, you can hook up a development board and a sensor shield and be up and running, gathering data in no time. For more information about the Zephyr API, consult the API Documentation.
As you build applications and become familiar with Zephyr, you’ll start to think of writing software in different and new ways. Structuring your software according to tasks and their priority helps to make your software more responsive, more compact, and better organized.