QNX Book
QNX Book
Elad Lahav
Preface 7
1 Introduction 9
1.1 What is a Real-Time Operating System? . . . . . . . . . . . . . . . . . . . 9
1.2 A Brief History of QNX . . . . . . . . . . . . . . . . . . . . . . . . . . 9
1.3 QNX RTOS FAQ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
2 Getting Started 13
2.1 Installing the QNX SDP . . . . . . . . . . . . . . . . . . . . . . . . . . 13
2.2 Creating The SD Card Image . . . . . . . . . . . . . . . . . . . . . . . . 13
2.2.1 Generate the Image . . . . . . . . . . . . . . . . . . . . . . . . . 13
2.2.2 Copy the Image: The Linux Way . . . . . . . . . . . . . . . . . . 13
2.2.3 Copy the Image: The Windows Way . . . . . . . . . . . . . . . . 14
2.3 Booting The Raspberry Pi . . . . . . . . . . . . . . . . . . . . . . . . . . 14
2.4 Connecting to The System . . . . . . . . . . . . . . . . . . . . . . . . . 14
2.5 Writing Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
2.6 Troubleshooting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
3
4 CONTENTS
4.4 PWM . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
4.4.1 Background . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
4.4.2 Fading LED . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
4.4.3 Servo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
4.5 I2C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
4.5.1 Background . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
4.5.2 PCF8591 Digital/Analog Converter . . . . . . . . . . . . . . . . . 41
4.5.3 PCA9685 16 Channel PWM Controller . . . . . . . . . . . . . . 42
4.6 Towards Robotics: Motor Control . . . . . . . . . . . . . . . . . . . . . 44
4.6.1 DC Motor with an H-Bridge . . . . . . . . . . . . . . . . . . . . 44
4.6.2 Encoders . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
4.7 How Does It Work? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
5 Real-Time Programming in C 55
5.1 Building C Programs . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
5.1.1 Command Line . . . . . . . . . . . . . . . . . . . . . . . . . . 55
5.1.2 Recursive Make . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
5.1.3 VSCode . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
5.2 Inter-Process Communication . . . . . . . . . . . . . . . . . . . . . . . 57
5.3 Threads and Priorities . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
5.3.1 What are Threads? . . . . . . . . . . . . . . . . . . . . . . . . . 62
5.3.2 Thread Scheduling . . . . . . . . . . . . . . . . . . . . . . . . . 63
5.4 Timers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
5.5 Event Loops . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
5.6 Controlling Hardware . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
5.7 Handling Interrupts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
5.7.1 What is an Interrupt? . . . . . . . . . . . . . . . . . . . . . . . . 75
5.7.2 Processing an Interrupt . . . . . . . . . . . . . . . . . . . . . . . 76
5.7.3 Handling an Interrupt in the QNX RTOS . . . . . . . . . . . . . 77
5.7.4 What about ISRs? . . . . . . . . . . . . . . . . . . . . . . . . . 79
5.7.5 Example . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
5
6 LIST OF FIGURES
Preface
In a talk about micro-kernels given in February 2023 the QNX operating system was de-
scribed as “historical”. 1 While the operating system has existed in various incarnations for
over 40 years (and thus represents one of the longest surviving OS lines), and while many
people have unknowingly interacted with it by using computer systems based on QNX, it is
still unknown to most people, even those who are otherwise proficient in the field. Those
who are familiar with QNX often remember only the 1.44MB demo disk from the 90s, or its
use in the commercially-unsuccessful BB10 devices from BlackBerry.
And yet the operating system is still very much in use in various fields. In recent years
it has been very popular in the automotive market, first to power infotainment systems and
then transitioning more to ADAS, instrumentation clusters and other safety-critical subsys-
tems of the car. It is also used as the operating system of various medical devices, robots and
industrial systems.
These use cases may suggest that the QNX operating system is by nature a deeply em-
bedded one, requiring specialized knowledge and tools. In fact, QNX retains its origins as
a general-purpose operating system that can be used anywhere and by anyone familiar with
UNIX-like operating systems such as Linux and FreeBSD. Using a shell with standard tools,
a C/C++ compiler or a Python interpreter, anyone familiar with those other systems can
write and run programs on a QNX system. Code written to standard interfaces such as C11,
C++17 or POSIX will, in most cases, require just re-compilation for QNX. The API for pro-
cesses, threads, file systems, sockets and more, while implemented very differently from other
systems, still looks the same for those used to system-level programming on other UNIX-like
operating systems. Perhaps ironically, it is embedded programmers used to small, restricted,
environments who may find things unfamiliar, with processes running in their own virtual,
512GB address spaces (randomized with ASLR), 32,000 threads per process, dynamically-
loaded libraries, the use of a standard compiler and linker (optionally on the target system),
etc.
The emergence of the Raspberry Pi as a (very) cheap, yet capable, single-board computer,
makes it much easier than before for anyone to run QNX on the type of hardware that is used
by its customers. Unlike a PC, the Raspberry Pi is built to control various devices in a simple,
easy to to follow manner. A plethora of accessories, examples, tutorials, books and forums
exist to help both the novice and the expert in making the most out of this tiny computer.
This book makes use of the Raspberry Pi 4 as a platform for introducing people to the
QNX real-time operating systems. It is primarily directed at people with some program-
1 https://archive.fosdem.org/2023/schedule/event/microkernel2023/
7
8 LIST OF FIGURES
ming experience both in Python and C. The book assumes the reader is at least familiar with
opening a programming text editor, writing code, building and running programs (be it on
the command line or via an integrated development environment). The examples presented
in this book have been purposefully kept very simple. The Python examples in Chapter4
will look very familiar to anyone who has followed tutorials or books on using Python with
the Raspberry Pi. The goal of this chapter is to highlight the similarities in user (or, rather
programmer) experience with other systems. For people not familiar with the Raspberry Pi
it serves as a brief introduction to the way external devices can be controlled using simple
Python scripts. Chapter 5 takes the opposite approach of highlighting QNX-specific inter-
faces, allowing programmers to make the most out of the operating system.
The book is accompanied by a QNX system image that can be copied to a micro-SD card
and used with a Raspberry Pi 4. The image contains the tools necessary to follow the exam-
ples in the book, and experiment further with programming for QNX. The image is free for
non-commercial use only. It is designed for simplicity, lowering the barriers for people fa-
miliar with other operating systems to get started with QNX. However, it does not represent
a QNX product. You may notice that the system has not been hardened for security in any
way: a root account, password-based logins, a writable system partition, the lack of a secu-
rity policy and a debug service open to the world are all examples of very poor design for real
systems, that are nevertheless useful for an introductory image.
Chapter 1
Introduction
9
10 CHAPTER 1. INTRODUCTION
still receiving the occasional request for support to this day (which prompts a search for that
ancient computer under somebody’s desk that is still capable of running it).
A few years later work began on the next version of the operating system. One of the
main goals for this version, which became QNX Neutrino 1.0, was to create a multi-platform
operating system, whereas all versions before that were only able to run on x86 computers.
At the behest of Cisco, who wanted to use QNX for some of its routers, the new version
was implemented for MIPS, with PPC, ARM and SH versions following over the years (in
addition to x86). SMP support was also introduced.
In the early 2000s QNX started gaining popularity with companies building infotain-
ment systems for car manufacturers. Eventually QNX Software Systems was sold to one of
these companies, Harman International. At this point Gordon Bell left QNX.
In 2010 Research In Motion, then a leading smartphone company of BlackBerry fame,
was looking to replace the home-grown operating system on its devices. After meeting Dan
Dodge, RIM bosses Mike Lazaridis and Jim Balsillie decided to purchase QNX from Har-
man International. The first RIM product to use QNX was the PlayBook tablet, released
that year. Soon afterwards work began on a new generation of smartphones running QNX,
which became the basis for the BB10 operating system. Neither the tablet nor the smart-
phones were commercially successful, and by 2015 BlackBerry (as RIM was now called) switched
to Android-based phones, and then shut down its smartphone business completely. A year
later Dan Dodge, along with several QNX veterans, left for Apple.
Despite this failure, the QNX operating system remained popular in certain markets,
primarily automotive but also medical and industrial. While the smartphone business was
grinding to a halt, the infotainment business, and then a focus on more safety-oriented sys-
tems in these fields, kept the company going. It does so to this day.
The version of the system that accompanies this book represents the first major redesign
of key components of the system since Neutrino 1.0. The operating system is now 64-bit
only (supporting the AArch64 and x86_64 architectures) and can run on systems with up
to 64 processors and 16TB of RAM.
user-mode processes that are completely separated from the kernel, i.e., run in a non-
privileged mode, each with its own address space. That said, the kernel is bundled
with a few services that do run in privileged mode and share the same address space as
the kernel - the memory manager, path manager and process manager. Micro-kernel
purists may object to such a design choice, but it was felt that running these specific
services as stand-alone processes would complicate the system and incur a significant
performance penalty.
Is QNX an automotive operating system? No, QNX is a general-purpose operating sys-
tem. It can be used anywhere such a system is required. In recent years the automotive
market has been the largest adopter of the QNX RTOS, and the system abides by var-
ious automotive standards. Nevertheless, QNX is used in other fields, and, as you will
discover in this book, is versatile enough for any task.
12 CHAPTER 1. INTRODUCTION
Chapter 2
Getting Started
Á Warning
It is crucial that you determine the correct device name! Copying the image to the
wrong device can cause permanent damage to other storage devices, including the
one used for your computer’s file system.
First, insert the card to a card reader connected to your computer. The operating system
may mount the card automatically, presenting an existing file system under some path. We
13
14 CHAPTER 2. GETTING STARTED
are not interested in this file system or path. Instead, we want to establish the name of the
device.
The lsblk command can be used to list the available block devices. Find the device that
matches the size of the card you have just inserted. In the example below, this is /dev/sdb.
Note that the numbered devices appearing below it are partitions on the device, but we want
the primary device name in order to write the image to the entire card. If you have doubt,
remove the card, run lsblk again to confirm that device is no longer listed (or is listed with
a 0 size), and repeat the exercise after re-inserting the card. Alternatively, use the dmesg
command to see which device was detected when the SD card was inserted.
$ lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
...
sda 8:0 1 0B 0 disk
sdb 8:16 1 14.5G 0 disk
|-sdb1 8:17 1 1G 0 part /media/elahav /1945 -029C
|-sdb2 8:18 1 1G 0 part
|-sdb3 8:19 1 12.5G 0 part
nvme0n1 259:0 0 1.9T 0 disk
|-nvme0n1p1 259:1 0 260M 0 part /boot/efi
...
You are now ready to copy the image. Run the following command from the directory
that contains the image file:
Make sure to replace <device name> with the name of the card block device discovered
above (e.g., /dev/sdb).
Your system may require root privileges for this command, in which case you will need
to run the command as root. On most modern Linux systems this is done by prefixing the
sudo command:
and real-time responsiveness. In both cases you will need to create and edit source files.
There are a few options for writing code for use with the QNX Raspberry Pi image. The
first is to use the Vim editor included with the image. For that you log in via SSH and run
the vim command. While Vim is a very popular editor, people who first encounter it can be
baffled by the interface, especially by the split between a command mode and an insert (or
edit) mode. There are many guides on the Internet for getting started with Vim and you can
follow these. If you have already started Vim and would really like to quit, press the ESC key
to go back to command mode, and then :q (colon followed by the letter q) and ENTER to
quit.
A second option is to write the code on your computer, using your preferred text editor1 .
In the case of Python such code can then be run on the Raspberry Pi using the included
Python interpreter (see Section 4.1). Code written in C needs to be compiled first into an
executable using the QNX C compiler. The resulting executable can then be copied over to
the Raspberry Pi.
When using an editor on your computer the source files are typically saved on the com-
puter’s own file system. In the case of Python code these files will have to be copied over
to the Raspberry Pi before they can be run with the Python interpreter. An alternative to
copying is to mount a directory from the Raspberry Pi file system into your computer, using
SSHFS, which is available both for Linux and Windows. The following example mounts
the /data/home/qnxuser/python directory on a Linux user’s qnxrpi directory (assuming
both directories exist):
Once this command is executed any file saved on qnxrpi from the computer will be seen
under /data/home/qnxuser/python, and updates will be kept in sync. This way you can
write Python code on your computer and run it on the Raspberry Pi without constantly
copying the files after every update.
Finally, it is possible to use VSCode2 with the QNX plugin to edit, build, deploy and de-
bug code. The plugin also allows you to inspect the system, e.g., by listing running processes,
analyzing memory usage and collecting trace logs for system activity.
2.6 Troubleshooting
qnxuser@qnxpi :~$
The shell is an interactive command interpreter: you type in various commands and the
shell executes these, typically by creating other processes. Anyone who has ever used a *NIX
system is familiar with the basic shell commands, such as echo, cat and ls.
The default shell on QNX systems is a variant of the Korn Shell, called pdksh, but our
Raspberry Pi image uses the more familiar bash (the Bourne Again Shell), which is the de-
fault shell on many modern *NIX systems.
Many of the simple (and some not so simple) shell utilties are now provided by a single
program call toybox. This program uses the name by which it is invoked as indication of
which utility it should run. The system uses symbolic links from various utility names to
toybox, which allows the user to use standard shell commands. For example, we can use ls
to list files:
The sequence above shows that ls is a symbolic link under /proc/boot to toybox.
17
18 CHAPTER 3. EXPLORING THE SYSTEM
qnxuser@qnxpi :~$ df
ifs 26344 26344 0 100% /
/dev/hd0t178 26212320 1288784 24923536 5% /data/
/dev/hd0t177 2097120 600088 1497032 29% /system/
/dev/hd0t11 2093016 31256 2061760 2% /boot/
/dev/hd0 30408704 30408704 0 100%
/dev/shmem 0 0 0 100% (/dev/shmem
)
The first entry shows the Image File System (IFS). This is a read-only file system that is
loaded into memory early during boot, and remains resident in memory. The IFS contains
the startup executable, the kernel and various essential drivers, libraries and utilities. At a
minimum, the IFS must contain enough of these drivers and libraries to allow for the other
file systems to be mounted. Some QNX-based systems stick to this minimum, while other
use the IFS to host many more binaries, both executables and libraries. There are advantages
and disadvantages to each approach. The IFS in this image is mounted at the root directory,
with most of its files under /proc/boot. It is possible to change this mount point.
3.3. PROCESSES 19
Next are two QNX6 file systems, mounted at /system and /data, respectively. This sepa-
ration allows for the system partition to be mounted read-only, potentially with added verifi-
cation and encryption, while the data partition remains writable. In this image both partition
are writable, to allow for executables and libraries to be added later and for configuration files
to be edited.
Under the /system mount point you will find directories for executables (/system/bin),
libraries (/system/lib), configuration files (/system/etc) and more. The data partition holds
the users’ home directories (/data/home), the temporary directory (/data/tmp, also linked
via /tmp) and a run-time directory for various services (/data/var).
The FAT file system mounted at /boot is used by the Raspberry Pi firmware to boot
the system. It also holds a few configuration files to allow for easy setup before the system is
booted for the first time (see Section 2.3).
The next entry shows the entire SD card and does not represent a file system.
Finally, /dev/shmem is the path under which shared memory objects appear. The reason
it is listed here is that it is possible to write to files under /dev/shmem, which are kept in
memory until the system is shut down (or the file removed). It is not, however, a full file
system, and certain operations (e.g., mkdir) on it will fail. Do not treat it as a file system.
At this point people familiar with previous versions of QNX, with Linux or with other
UNIX-like operating systems, may be wondering what happened to the traditional paths,
such as /bin or /usr/lib. The answer is that you can still create a file system on QNX with
these paths and mount at it the root directory. There are two reasons this image employs a
different strategy:
1. The separation of /system and /data makes it easier to protect the former, as men-
tioned above.
2. This layout avoids union paths, which have both performance and security concerns.
Union paths are an interesting feature of the operating system that allows multiple file
systems to provide the same path. For example, both the IFS and one (or more) of the QNX6
file systems can provide a folder called etc. If both are mounted under the root path then the
directory /etc is populated as a mix of the files in the folders of both file systems. Moreover,
if both file system have a file called etc/passwd then the file visible to anyone trying to access
/etc/passwd is the one provided by the file system mounted at the front. This feature can
be quite useful in some circumstances, such as providing updates to a read-only file system
by mounting a patch file system in front of it, but it complicates path resolution, can lead to
confusion and, under malicious circumstances, can fool a program into opening the wrong
file.
3.3 Processes
A process is a running instance of a program. Each process contains one or more threads,
each representing a stream of execution within that program. Any QNX-based system has
multiple processes running at any given point. The first process in such a system is the process
that hosts the kernel, which, mainly for historical reasons, is called procnto-smp-instr.
The program for this process comprises the Neutrino micro-kernel, as well as a set of basic
20 CHAPTER 3. EXPLORING THE SYSTEM
services, including the process manager (for creating new processes), the memory manager
for managing virtual memory and the path manager for resolving path names to the servers
that host them.
The micro-kernel architecture means that most of the functionality provided by a mono-
lithic kernel in other operating systems is provided by stand-alone user processes in the QNX
RTOS. Such services include file systems, the network stack, USB, graphics drivers, audio and
more. A QNX system does not require all of these to run. A headless system does not need
a graphics process, while a simple controller may do away with a permanent file system.
Finally, a system will have application processes to implement the functionality that the
end user requires from it. An interactive system can have processes for the shell and its utili-
ties, editors, compilers, web browsers, media players, etc. A non-interactive system can have
processes for controlling and monitoring various devices, such as those found in automotive,
industrial or medical systems.
The pidin command can be used to list all processes and threads in the system. The
following is an example of the output of this command. Note, however, that the output
was trimmed to remove many of the threads in each process for the purpose of this example
(indicated by “...”):
3.3. PROCESSES 21
Each process has its own process ID, while each thread within a process has its own thread
ID. The output of pidin lists all threads, grouped by process, along with the path to the
executable for that process, the priority and scheduling policy for the thread, its state and
some information relevant to that state. For example,
22 CHAPTER 3. EXPLORING THE SYSTEM
shows that thread 3 in process 118802 belongs to a process that runs the io-usb-otg
executable (the USB service). Its priority is 10 while the scheduling policy is round-robin. It
is currently blocked waiting on a condition variable whose virtual address within the process
is 0x59709193a0.
It is possible to show other type of information for each process with the pidin com-
mand: pidin fds shows the file descriptors open for each process, pidin args shows
the command-line arguments used when the process was executed, pidin env shows the
environment variables for each process and pidin mem provides memory information. It
is also possible to restrict the output to just a single process ID (e.g., pidin -p 208908) or
to all processes matching a given name (e.g., pidin -p ksh).
3.4 Memory
RAM, like the CPU, is a shared resource that every process requires, no matter how simple.
As such all processes compete with each other for use of memory, and it is up to the system
to divide up memory and provide it to processes. In a QNX system control of memory is
at the hands of the virtual memory manager, which is a part of the procnto-smp-instr
process (recall that this is a special process than bundles the micro-kernel with a few services).
A common problem with the use of memory is that the system can run out of it, either
due to misbehaving processes or poor design that does not account for the requirements of
the system. Such situations naturally lead to two frequently-asked questions:
The first question is easy to answer. The second, perhaps surprisingly, is not.
A quick answer to the first question can be obtained with pidin:
The first line shows that the system has 4GB of RAM (depending on the model, yours
may have 1GB, 2GB, 4GB or 8GB), with the system managing 4032MB, out of which 3689MB
are still available for use. The QNX memory manager may not have access to all of RAM on
a board if some of it is excluded by various boot-time services, hypervisors, etc.
A more detailed view is available by looking at the /proc/vm/stats pseudo-file. You will
need to access this file as the root user, e.g.:
3.4. MEMORY 23
The output provides quite a bit of information, some of it only makes sense for people
familiar with the internal workings of the memory manager. However, a few of the lines are
of interest:
The remaining pages_* lines represent allocated pages in different internal states.
The breakup of physical memory into various areas can be observed with the following
command
All entries that end with sysram are available for the memory manager to use when ser-
vicing normal allocation requests from processes. Entries that are part of RAM but outside
the sysram ranges can be allocated using a special interface known as typed memory.
In order to attempt an answer for the second question, we will use the mmap_demo
program that is included with the Raspberry Pi image. Start this program in the background,
so that it remains alive while we examine its memory usage.
1 Theword active here is not a redundancy, as a process in the middle of its creation or after termination but
before being claimed by its parent (i.e., a zombie) does not have an address space.
24 CHAPTER 3. EXPLORING THE SYSTEM
This program creates different types of mappings (though it doesn’t come close to ex-
hausting all the different combinations of options that can be passed to mmap()). The first
two mappings are anonymous, which means that the resulting memory is filled with zeros.
The next two are file-backed, which means that the memory reflects the contents of a file (the
program uses a temporary file for the purpose of the demonstration). Finally, the last map-
ping just carves a large portion of the process’ virtual address space, without backing it with
memory. The output shows the virtual addresses assigned to each of these mappings. The
start address is chosen by the address space layout randomization (ASLR) algorithm, while
the end address of each range reflects the requested size. The output you see will therefore
be different than the example above for the start addresses, but the sizes will be the same.
To get a view of the memory usage by this process we can examine all the mappings it
has. This is done by looking at the /proc/<pid>/pmap file, where <pid> should be replaced
by the process ID (1617946 in the example above):2
Note that the output was truncated to fit the page. Here is a complete line that will be
examined in detail:
2 Confusingly, every process also has a /proc/<pid>/mappings file, which provides the state of every page as-
signed to this process (both from the virtual and physical point of view). This file can be huge and is rarely of
interest.
3.5. RESOURCE MANAGERS 25
The first two columns show the virtual address (0x209b3a6000) and size (0x4000, or 4
4K pages) of the mapping. You should find matching entry in the output of mmap_demo.
The next two columns show the flags and protection bits passed to the mmap() call. This
line corresponds to a private mapping with read and write permissions. The next 4 fields are
not important for this discussion.
Next comes the reservation field, which is crucial. This field shows how many pages were
billed to this process for the mapping. In this case the value is 0x4000, which is the same as
the size of the mapping. The reason is that this is a private mapping of a shared object, and
such a mapping requires that the system allocate new pages for it with a copy of the contents
of the object (in this case the temporary file). By contrast, a read-only shared mapping of the
same file will show a value of 0.
Of the last two fields, the first shows the mapped object. This can be a file, a shared-
memory object, or a single anonymous object that is used to map the process’ stacks and
heaps. The second field shows the typed memory object from which memory was taken
(recall that “sysram” refers to generic memory that the memory manager can use to satisfy
most allocations).
Given the information provided here, why is it hard to answer the question about per-
process memory consumption? The complexity comes from shared objects. Every process
maps multiple such objects, including its own executable and the C library. As mentioned
above, shared mapping of such objects (that is, mappings that directly reflect the contents
of the object, rather than creating a private copy) show up in the pmap list as though they
consume no memory. And yet the underlying object may3 require memory allocation that
does affect the amount of memory available in the system. Additionally, there may be shared
memory objects that are not mapped by any process: one process can create such an object
with a call to shm_open() and never map it. That process may even exit, leaving the shared
memory object to linger (which is required for POSIX semantics). A full system analysis of
memory consumption thus requires a careful consideration of all shared objects, along with
which processes, if any, map them.
An example of a resource manager on the QNX Raspberry Pi image is the GPIO server,
rpi_gpio. You can see it running on the system by using the pidin command, as described
above:
The resource managed by this process is the 40-pin GPIO header on the Raspberry Pi
(See Appendix A for more information). It does so by memory-mapping the hardware regis-
ters that control the header, and then handling requests by various client programs to control
GPIOs, e.g., to turn an output GPIO pin on or off. The advantage of this design is that it
allows only one process to have access to the hardware registers, preventing GPIO users from
interfering with each other, either by accident or maliciously.
The rpi_gpio resource manager registers the path /dev/gpio (though it can be config-
ured to register a different path, if desired, via a command-line argument). Under this path
it creates a file for each GPIO pin, with a name matching that of the GPIO number, as well
as a single msg file.
The numbered files can be read and written, which means that the resource manager
handles messages of type _IO_READ and _IO_WRITE on these files. Consequently, we can
use shell utilities such as echo and cat on these files. To see this at work, follow the instruc-
tions for building a simple LED circuit, as described in Section 4.2. Do not proceed to write
Python code at this point. Once the circuit is built, log in to the Raspberry Pi and run the
following shell commands:
The first command configures GPIO 16 as an output. The second turns the GPIO on
(sets it to “HIGH”) and the third turns the GPIO off (sets it to “LOW”).
Let us consider how these shell commands end up changing the state of the GPIO pin.
1. The command echo on > /dev/gpio/16 causes the shell to open the file /dev/g-
pio/16, which establishes a connection (via a file descriptor) to the rpi_gpio resource
manager.
2. The shell executes the echo command, with its standard output replaced by the con-
nection to the resource manager.
3.5. RESOURCE MANAGERS 27
3. echo invokes the write() function on its standard output with a buffer containing
the string “on”. The C library implements that call by sending a _IO_WRITE message
to the resource manager.
4. The resource manager handles the _IO_WRITE message sent by echo as the client. It
knows (from the original request to open the file 16 under its path) that the message
pertains to GPIO 16, and it knows from the buffer passed in the message payload that
the requested command is “on”. It then proceeds to change the state of the GPIO pin
by writing to the GPIO header’s registers.
The msg file can be used for sending ad-hoc messages, which provide better control of
GPIOs without the need for the resource manager to parse text. Such messages are easier to
handle and are less error prone. The structure of each message is defined in a header file that
clients programs can incorporate into their code. We will see in Section 4.7 how a Python
library uses such messages to implement a GPIO interface.
For information on how to write resource managers see the QNX documentation.
28 CHAPTER 3. EXPLORING THE SYSTEM
Chapter 4
In this chapter we will get the Raspberry Pi to do some useful (and fun!) work, by control-
ling various external devices. Such control will be achieved by writing code in the Python
programming language and running it on the Raspberry Pi. To complete the exercises you
will need a few components, including:
• breadboard
• jump wires
• LEDs
• Buttons
• Motors
• Servos
• Sensors
You may already have some or all of these components. If not, there are many startup
kits available for the Raspberry Pi that include all of these. Such kits often come with their
own Python tutorials, and these tutorials can be much more comprehensive than what is
available in this book. There are also books dedicated to maker projects and robotics with
the Raspberry Pi, and many of those also use Python. Feel free to use any of these resources
on top of, or in lieu, of the information in this chapter. The Python libraries for GPIO and
I2C included with the QNX image for Raspberry Pi have been designed to be as compatible
as possible with those available for other operating systems. You can then come back to Sec-
tion 4.7 to learn how the Python code you write allows the Raspberry Pi to control external
devices.
29
30 CHAPTER 4. CONTROLLING I/O WITH PYTHON
Á Warning
Never power a high-current device (such as a motor or a servo) directly from the
Raspberry Pi’s GPIO header. Such devices should get their power from an external
source that matches their voltage and current requirements.
1 print("Hello World!")
We can now invoke the python interpreter to run the program. If you haven’t done so
already, log into the Raspberry Pi with SSH and type the following commands:
Ď Note
Make sure to specify -t as part of the SSH command. Without it, the program may
continue running on the Raspberry Pi even after the SSH command exits.
4.1.3 VSCode
TBD
Ď Note
The jumper wires can be connected to any other pin in the GPIO header, as long as
the short leg of the LED is connected to a ground pin and the resistor is connected
to a GPIO pin. If you decide to use a different GPIO pin make sure to adjust the
code such that it uses the correct number.
Next, we will write the code for making the LED blink:
led.py
Save this file as led.py, and run the program as described in the previous section. For
example, if using the command line while connected via SSH, run the command:
4.3. BASIC INPUT (PUSH BUTTON) 33
If all goes well the LED should turn on and off at half second intervals. You can press
Ctrl-C to stop it.
If the program doesn’t work, try the following steps to determine what has gone wrong:
We will now take a closer look at the code. The first two lines make the necessary libraries
available to the program. The rpi_gpio library provides the functionality for working with
the GPIO pins and will be used. It is made visible using an alias GPIO so that the code matches
examples found on the Internet for similar libraries written for other operating systems, such
as the Raspbian. The time library is used to add the necessary delays via the time.sleep
call.
Next, the program prepare the GPIO to be used as an output. Every GPIO can function
as either an output or an input. Many GPIO pins also have other functions, some of which
will be explored in the next sections. The program then enters an infinite loop in which the
LED’s state is toggled between high (on) and low (off), using a delay of 500 milliseconds
between each state change.
Ď Note
The code in this program was stripped to the bare minimum, eschewing functional-
ity that may be considered as best practice. For example, it does not ensure that the
GPIO state is set back as low when the program is terminated.
ground pins from the Raspberry Pi. The LED’s anode is connected to GPIO 16 as before.
The push button is connected to ground on one leg and to GPIO 20 on another. The per-
manently connected legs straddle the trough in the middle of the breadboard.
Save the following program as button.py:
button.py
The first lines are identical to those in the previous exercise. Line 7 defines GPIO 20 as
input. The GPIO.PUD_UP argument tells the Raspberry Pi to use its internal pull-up resis-
tor on this pin. Without a pull-up resistor the state of the pin is undetermined (or “float-
ing”), which means that the connection to ground established by pushing the button may
not be detected. Pulling up means that the pin detects a high state by default, and then
4.3. BASIC INPUT (PUSH BUTTON) 35
a low state (from the connection to ground) when the button is pressed. We could have
replaced the connection of the button to ground with a connection to a 3V pin and used
GPIO.PUD_DOWN, in which case the default state of GPIO 20 would have been low, and
would change to high when the button is pressed. The internal pull resistors in the Rasp-
berry Pi avoid the need for using discrete resistors in the circuit to achieve the same effect.
The loop on lines 9-15 checks the state of GPIO 20 every 10 milliseconds. If the state is
high (the default, due to the pull-up resistor) then the LED is turned off. If the state is low
that means that the button is pressed and the LED is turned on.
Having a check run every 10 milliseconds can waste processor cycles, while at the same
time miss some events (especially if the button is replaced by some other detector for events
that happen at a higher frequency). Instead of polling for button state changes, we can ask
the system to detect such changes and notify our program when they happen:
button_event.py
4.4 PWM
4.4.1 Background
The GPIO pins on the Raspberry Pi are only capable of discrete output - each pin, when
functioning as an output, can be either in a high state or a low state. Sometimes, however, it
is necessary to have intermediate values. e.g., to vary the brightness of an LED or to generate
sound at different pitch levels. One option for accomplishing such tasks is to use a digital to
analog converter, which is a separate device that can be connected to the Raspberry Pi over
a bus such as I2C or SPI (see below). Alternatively, we can use Pulse Width Modulation
(PWM) to approximate an intermediate state.
PWM works by changing the state of a GPIO output repeatedly at regular intervals.
Within a period of time, the state is set to high for a certain fraction of that time, and to
low for the remainder. If the period is long enough the result is a noticeable pulse. With a
short period the effect is perceived as an intermediate value. What constitutes “short” and
“long” depends on the device and human perception.
PWM is specified using two values: the period, which is the repeating interval, and the
duty cycle, which is the fraction of the period in which the output is high. The period can be
specified in units of time (typically milliseconds or microseconds), or in terms of frequency
(specified in Hz). Thus a period of 20ms is equivalent to a frequency of 50Hz. The duty
cycle can be specified as a percentage of the period, or in time units. A duty cycle of 1ms for
a period of 10ms is thus 10%. Period and duty cycle are demonstrated in Figure 4.3.
High
Low
High
Low
hardware implementation, albeit on just four GPIO pins: 12, 13, 18 and 19. Note, however,
that only two PWM channels are available: channel 1 for GPIOs 12 and 18, and channel 2
for GPIOs 13 and 19. This means that if both GPIO 13 and 19 are configured for hardware
PWM then they will have the same period and duty cycle. Moreover, both channels share
the same clock, further limiting their independence. Expansion boards are available that al-
low for more PWM channels with finer control, and these are recommended for applications
that require multiple PWM sources, such as robotic arms.
In addition to the period and duty cycle, each PWM pin can be configured to work in
one of two modes of operation. The first mode, referred to in the manual as M/S mode,
works exactly as depicted in Figure 4.3, i.e., the GPIO is kept high for the duration of the
duty cycle and then low for the remainder. The second mode spreads the duty cycle more or
less evenly across the period, with several transitions from low to high and back. This mode
more closely emulates an analog output and is preferred when such output is desired. On the
other hand, M/S mode is used with devices that expect a signal with exact characteristics as
a mode of communication. This is the case for servo motors, which will be described below.
Now save this program as pwm_led.py and run it. You should see the LED fade in and
out.
38 CHAPTER 4. CONTROLLING I/O WITH PYTHON
pwm_led.py
Line 4 creates a PWM object using GPIO 12 with a frequency of 1KHz and the standard
PWM mode (not M/S). The loop on lines 7-9 changes the duty cycles from 0% to 100% in
increments of 5% every 100 milliseconds, while the loop on lines 11-13 does the opposite.
4.4.3 Servo
Next, we will use PWM to control a servo motor. A servo is an electric motor that has a
control mechanism that allows for exact positioning of the shaft. The angle of the shaft is
communicated to the motor via PWM, where the period is fixed and the duty cycle is used
to determine the angle.
There are many different types of servo motors, but one of the most common for begin-
ners is the SG90, which is marketed under different brand names. This is an inexpensive,
albeit weak, servo, but will do for the purpose of this exercise. The shaft can only be rotated
180 degrees between a minimum position and a maximum position.
The servo has three wires: brown (ground), red (power) and orange (control). Connect
the servo as in the diagram:
Note that we have added an external power source (the diagram shows 1.5V batteries con-
nected in series, but you can substitute a different DC source). The required voltage for the
SG90 servo is between 4.8V and 6V. As mentioned before, do not use the Raspberry Pi 5V
pin to power the servo, as it may exceed the current limit for this pin and damage the Rasp-
berry Pi. It is important, however, to make sure that the ground terminal of the external
power source is connected to a ground pin on the Raspberry Pi (in this case both are con-
nected to the ground bus on the breadboard).
The orange wire is connected to GPIO 12 for control via PWM. To control the servo
we have to use a period of 20 milliseconds (or a frequency of 50Hz). The servo’s motor is
in its minimum position (0°) when the duty cycle is 0.5 milliseconds (or 2.5%), and at its
maximum position (180°) when the duty cycle is 2.5 milliseconds (or 12.5%). Intermediate
duty cycle values position the motor in between these two angles.
4.4. PWM 39
Ď Note
Some datasheets claim that the minimum duty cycle is 1ms and the maximum is 2ms.
It is not clear whether that is a mistake, or that different vendors manufacture SG90
servos with different control parameters. You can experiment with various values to
see that you get a full 180°motion. Just be careful not to exceed the minimum or
maximum values for too long, to avoid strain on the gears.
Save this program as pwm_servo.py and run it. It should cause the servo’s arm to rotate
to one side and then the other, going through several angle changes.
40 CHAPTER 4. CONTROLLING I/O WITH PYTHON
pwm_servo.py
Line 4 creates a PWM object using GPIO 12, with a frequency of 50Hz. Note that the
mode must be set to M/S, as the control mechanism expects a continuous high level for the
duty cycle in each period. Lines 8-12 move the shaft from 0°to 180°in regular increments every
500 milliseconds, and lines 14-17 do the opposite.
4.5 I2C
4.5.1 Background
All of the examples so far have only used a few GPIO pins. Once you start building less
trivial systems you may find that a 40 pin header is simply not enough. Consider an LED
matrix with dozens of diodes, a robotic arm that requires multiple servos (remember that the
Raspberry Pi has only two PWM channels) or a sensor that provides a wide range of values.
Instead of using a GPIO pin for each bit of data, the Raspberry Pi can connect to external
devices using a communication bus. The simplest of these is the Inter-Integrated Circuit
bus, commonly referred to as I2C.
The I2C bus requires just two pins - a serial data pin (SDA) and a serial clock pin (SCL).
The data pin is used to communicate data to the device, by switching between low and high
states, while the clock pin synchronizes these changes such that the target device can interpret
them as a stream of bits. The device can respond with its own stream of bits, for bi-directional
communication. Pin 3 on the Raspberry Pi is the SDA pin, while pin 5 is the SCL pin.
Each I2C device has its own interpretation of this bit stream, of which the controlling de-
vice needs to be aware. This establishes a device-specific protocol on top of the I2C transport
layer.
How does I2C solve the problem of replacing multiple GPIO pins? For one, the bit
stream can be used to convey much more information between the Raspberry Pi and a device
than a simple on/off state, or even PWM. For example, an LED matrix can be told to change
the state of an LED at a given row and column, by interpreting the bit stream as command
packets. An analog to digital converter replies to requests from the Raspberry Pi with bits
that are interpreted as a value in a certain range.
4.5. I2C 41
Moreover, multiple devices can be connected to each other, forming a chain. Each device
has an address (typically 7 bits, for a maximum of 128 addresses in a chain), and will respond
only to communication that is preceded by its address (which is part of the bit stream sent
by the Raspberry Pi).
The following sections show two examples of I2C devices and how to communicate with
them. You may not have these devices (though both are cheap and readily available), but the
principles should be the same for all I2C devices.
PCF8591
Pin 1 from the Raspberry Pi, which provides a 3.3V source, is connected to one of the
positive buses in the breadboard. That bus is then connected to the second positive bus on
the other side of the breadboard. Similarly, pin 14 (ground) is connected to the first negative
bus, which is then connected to the second negative bus. This configuration makes it easy to
connect multiple pins from the PCF8591 chip to the source voltage and to ground.
The yellow and green wires in the diagram are connected to the SDA and SCL pins,
respectively, on both the Raspberry Pi and the PCF8591 chip. These pins on the chip are also
connected via 10KΩ pull-up resistors to the source voltage (or else they will not be able to
detect changes on these pins). All of the address pins are connected to ground, which means
that the device address is 72 (48 hex).
Finally, a 10KΩ potentiometer is connected to the source voltage, analog input 0 on the
PCF8591, and ground.
Run the following program, and then turn the potentiometer to see the output.
adc.py
1 import smbus
2 import time
3
4 adc = smbus.SMBus (1)
5
6 prev_value = 0
7 while True:
8 value = adc.read_byte_data (0x48 , 0x40)
9 if prev_value != value:
10 print('New value: {}'.format(value))
11 prev_value = value
12
13 time.sleep (0.1)
Line 4 establishes a connection to the I2C resource manager. The number 1 in the argu-
ment to smbus.SMBus() matches the I2C device number. If you get an error on this line
check which device was registered by the resource manager:
If the result is different than /dev/i2c1 change the code to match the number in the
device name.
Line 8 sends the command 0x40 on address 0x48 in order to request the value for analog
input 0 from the device. It then prints the value if it has changed since the last time it was
read. Turning the potentiometer all the way to one end should result in the value 0, the other
end in the value 255, and intermediate positions in values between these two.
value determines the time in which it is turned off. The period is determined by a common
frequency value, which is derived from the internal oscillator running at 25MHz and a pro-
grammable pre-scale value. For example, for a frequency of 1KHz (i.e., a period of 1ms), if the
first value is 300, and the second value is 1600, then the channel will turn on 73 microseconds
after the beginning of the period and turn off 390 microseconds after the beginning of the
period, resulting in a duty cycle of 31.7%.
Several vendors provide a board that contains a PCA9685 chip and 16 3-pin headers for
connecting servos. Such a board is an easy and cheap way to control multiple servos. As
mentioned before the Raspberry Pi only provides two PWM channels, which prevents the
use of more than two independently-controlled servos at the same time.
The board connects to the Raspberry Pi using 4 wires: 3.3V, ground, SDA and SCL. An
independent power source is connected to the two power terminals, which is used to power
the servos (recall that you cannot use the 5V output from the Raspberry Pi as it cannot sustain
the current requirements of the servos). Finally, up to 16 servos can be connected to the 3-pin
headers. Figure 4.7 illustrates a circuit with two servo motors.
The chip is controlled by writing to I2C address 0x40 by default. It is possible to change
this address by soldering one or more of the pads in the corner of the board, allowing for mul-
tiple devices to be connected on the same I2C bus. The two channel values are programmed
with two bytes each, the first for the lower 8 bits, the second for the upper 2 bits. This means
that each channel requires 4 bytes. These start at command number 6 for channel 0, and
44 CHAPTER 4. CONTROLLING I/O WITH PYTHON
increment by four for each channel up to 15. Commands 0 and 1 are used to configure the
chip, while command 254 configures the pre-scalar value to determine the frequency.
The following program controls the two servos, which are connected to channels 0 and
8, respectively, as in the diagram:
pca9685.py
1 import smbus
2 import time
3
4 smbus = smbus.SMBus (1)
5
6 # Configure PWM for 50 Hz
7 smbus. write_byte_data (0x40 , 0x0 , 0x10)
8 smbus. write_byte_data (0x40 , 0xfe , 0x7f)
9 smbus. write_byte_data (0x40 , 0x0 , 0xa0)
10 time.sleep (0.001)
11
12 # Servo 1 at 0 degrees
13 smbus. write_block_data (0x40 , 0x6 , [0x0 , 0x0 , 0x66 , 0x0])
14
15 # Servo 2 at 180 degrees
16 smbus. write_block_data (0x40 , 0x26 , [0x0 , 0x0 , 0x0 , 0x2])
17
18 time.sleep (1)
19
20 # Servo 1 at 0 degrees
21 smbus. write_block_data (0x40 , 0x6 , [0x0 , 0x0 , 0x0 , 0x2])
22
23 # Servo 2 at 180 degrees
24 smbus. write_block_data (0x40 , 0x26 , [0x0 , 0x0 , 0x66 , 0x0])
Lines 7-10 set the pre-scalar for a frequency of 50Hz.1 This requires putting the chip
to sleep first, and then restarting it, followed by a short delay. When restarting, the chip
is also configured for command auto-increment, which means that we can write the per-
channel 4-byte values in one I2C transaction. Line 13 writes the four byte values for channel
0, starting at command 6. These values say that the channel should turn on immediately at
the beginning of a period, and turn off after 102 (0x66) steps out of 4096, giving a duty cycle
of 2.5%. Line 16 configures channel 8, starting at command 38 (0x26), to turn on immediately
at the beginning of the period and turn off after 512 steps out of 4096, giving a duty cycle of
12.5%. After one second each servo is turned the opposite way.
with an oscilloscope revealed that a value of 0x7f is closer to the required frequency.
2 Some servos have their internal gearbox modified to allow for continuous motion and then used for driving
wheels. However, at that point they are really just standard motors.
4.6. TOWARDS ROBOTICS: MOTOR CONTROL 45
is with a DC motor. The motor itself has no control mechanism: once connected to a voltage
source and ground it moves at a constant speed in one direction. For a robot we would like
to have control over the direction and the speed of the motor.
Ď Note
The typical DC motor is too fast and too weak to move a robot. It is therefore cou-
pled with a gear box the slows the motor down while increasing its torque.
Controlling the direction can be done with a device called an H-Bridge, a fairly simple
circuit that allows for the polarity of the connection to the motor to be switched. Thus,
instead of one terminal being permanently connected to a voltage source and the other to
ground, the two can change roles, reversing the direction of the motor.
While you can build an H-bridge yourself with a few transistors, it is easier to use an inte-
grated circuit, such as L293D. This integrated circuit provides two H-bridges, which means
it can control two motors at a time. Another popular choice is L298, which can work with
higher voltage and is more suitable for bigger robots.
Figure 4.8 shows a circuit that combines a L293D chip with a 5V DC motor. The chip
has two voltage source connections, one for the chip’s logic, connected to a 5V pin on the
Raspberry Pi, and the other for the motor, connected to an external power source. Inputs 1
and 2 of the L293D chip are connected to GPIOs 20 and 16, respectively, while outputs 1 and
2 are connected to the DC motor. GPIO 19 is connected to the pin that enables outputs 1
and 2. Note that the two ground buses are connected (by the left-most black wire) to ensure
a common ground between the Raspberry Pi and the external power source.
The motor is at rest when both inputs are the same (either high or low), rotates one way
when input 1 is high and input 2 is low, and the other way when input 1 is low and input 2 is
high. The following program demonstrates this.
l293.py
Line 8 enables the outputs. Lines 10-12 cause the motor to rotate one way for 1 second.
46 CHAPTER 4. CONTROLLING I/O WITH PYTHON
L293D
Lines 13-16 reverse the motor’s direction for one second. Then both outputs, as well as the
enable pin, are turned off.
So far we have only used the L293D chip to control the direction of the motor. We can
also control the speed by using PWM on the enable pin, which in our circuit is connected
to GPIO 19. A duty cycle of 100% works as though the pin is always enabled, which means
that the motor rotates at full speed. Lower values of the duty cycle slow the motor down.
The next program starts the motor at full speed one way, slows it down to a halt, switches
direction and then speeds up to full speed.
4.6. TOWARDS ROBOTICS: MOTOR CONTROL 47
l293_pwm.py
4.6.2 Encoders
The level of control provided above is not sufficient for accurate positioning of a robot. Even
if we set the speed of each wheel to a desired value and measure for a specific amount of time
there is no guarantee that the robot will move exactly as desired. The motors may take some
time to reach the desired speed, and friction may affect movement. To solve this problem we
can use encoders, which tell us exactly how much each wheel has moved.
It is possible purchase DC motors with built-in encoders. However, in the spirit of this
book, we will create one from scratch, using a pair diodes: an infrared LED and an infrared
photodiode. First, construct a basic circuit to see these diodes in work, as depicted in Figure
4.9.
The clear diode is the infrared (henceforth IR) LED, while the black one is the photo-
diode. The IR LED is connected to 3.3V via a small resistor (27Ω in the example), which
makes it permanently on. (An alternative is to connect the LED to a GPIO output and turn
it on as needed). The photodiode’s cathode (short leg) is connected to 3.3V (unlike an LED),
while the anode (long leg) is connected via a large resistor (10KΩ in the example) to ground.
A connection to GPIO 4 is made between the anode and the resistor.
If the photodiode does not detect IR light (which it should not in the configuration
depicted in the diagram) then it does not pass any current. In this case the voltage level read
by GPIO 4 is 0, as it is connected to ground via the resistor. However, when the photodiode
detects IR light, which can be done by placing a white piece of paper over the heads to the
two diodes, it passes current, which creates a voltage difference across the resistor. GPIO 4
detects this difference as a high input.
The next program reports how many time the photodiode detected IR light in the span
of 5 seconds. Once you run it, move a white piece of paper back and forth at a small dis-
48 CHAPTER 4. CONTROLLING I/O WITH PYTHON
tance over the diodes, such that it alternates between covering and not covering these. The
program should report how many times it transitioned between the non-covered and the
covered states.
ir.py
To control a motor with IR light we add an encoder wheel to the motor. The encoder
wheel is a disc with slots that can be detected by the photodiode as the motor rotates. Count-
ing the slots allows the motor to be stopped by a program once the desired angle has been
reached (which can be more than 360°). Using the angle and the circumference of the robot
wheel attached to the motor (not the encoder disc) we can determine the distance that wheel
has traversed. Figure 4.10 shows an encoder wheel with 9 slots.
4.6. TOWARDS ROBOTICS: MOTOR CONTROL 49
The slots can be holes in the disc, in which case the IR diodes are arranged as in Figure
4.10. Alternatively, the disc can have black stripes on a white background (or vice versa), in
which case the diodes should be placed on the same side and use the reflection of the IR
light for detection. For the purpose of this exercise you can use a 3D printer for creating an
encoder wheel, or just make one out of cardboard. If opting for a slotted wheel, make sure
that whatever material you use blocks IR light (printed plastic may need to be painted).
We will now combine the last two circuits for a full motor control device. The circuit,
depicted in Figure 4.11 is somewhat complicated by the need to supply three different voltage
sources: 5V from the Raspberry Pi to power the L293D chip, 3.3V from the Raspberry Pi for
the IR LED and photodiode, and external power for the motor.3
The program for this circuit starts the motor at a 40% PWM duty-cycle (you can change
this value depending on the rotation speed of your motor), and then stops it once 9 tran-
sitions from low to high have been detected. Assuming the encoder has 9 slots, as in the
example, this code should stop the motor after one full rotation of the wheel.
3 I was able to power the L293D from the 3.3V output of the Raspberry Pi, which simplifies the circuit. Never-
L293D
encoder.py
When running this program you may find that the motor is stopped well after the time
it is supposed to. There are a couple of reasons for that:
1. Python is not very efficient. It is an interpreted language, with poor support for con-
currency. By the time the thread that runs the callback function handles the events
sent by the GPIO resource manager they may have been queued for quite some time.
2. Python’s threads execute at the system’s normal priority, which means that their schedul-
ing is affected by that of other threads in the system (as well as each other).
We can alleviate the problem by using the match argument to GPIO.add_event_detect(),
which allows the GPIO resource manager’s event thread to keep track of changes, and no-
tify the Python program only once the requested number of changes have occurred. This
does not solve the problem fully, as the callback that stops the motor still runs at normal
system priority. A better solution will be provided by running the same program using a
high-priority thread for motor control, a subject that will be revisited in Section 5.3.
encoder_match.py
1 static PyObject *
2 rpi_gpio_output(PyObject *self , PyObject *args)
3 {
4 unsigned gpio;
5 unsigned value;
6
7 if (! PyArg_ParseTuple(args , "II", &gpio , &value)) {
8 return NULL;
9 }
10
11 gpio = get_gpio(gpio);
12 if (gpio == 0) {
13 set_error("Invalid GPIO number");
14 return NULL;
15 }
16
17 if (( value != 0) && (value != 1)) {
18 set_error("Invalid GPIO output value");
19 return NULL;
20 }
21
22 rpi_gpio_msg_t msg = {
23 .hdr.type = _IO_MSG ,
24 .hdr.subtype = RPI_GPIO_WRITE ,
25 .hdr.mgrid = RPI_GPIO_IOMGR ,
26 .gpio = gpio ,
27 .value = value
28 };
29
30 if (MsgSend(gpio_fd , &msg , sizeof(msg), NULL , 0) == -1) {
31 set_error("RPI_GPIO_WRITE: %s", strerror(errno));
32 return NULL;
33 }
34
35 Py_RETURN_NONE;
36 }
Lines 4-20 get the arguments passed to the function (the GPIO pin number and value,
4 https://docs.python.org/3/extending/extending.html
4.7. HOW DOES IT WORK? 53
which can be either 1 for “HIGH” or 0 for “LOW”), and validate those. Lines 22-28 define a
message, composed of a header and a payload. The header tells the resource manager that this
message wants to write to a GPIO, while the payload tells it which GPIO pin to update and
to what value. Finally, lines 30-33 send the message, wait for the reply and check for errors.
The resource manager runs a loop around the MsgReceive() call, which waits for client
messages. When it gets such a message it uses the header to determine what type of message it
is and then handles it appropriately. In the case of a RPI_GPIO_WRITE message the resource
manager writes to the GPIO hardware registers a value that matches the GPIO pin specified
in the payload of the message. If that value is 1 then it sets a bit in one of the GPSET registers,
while for a value of 0 it sets a bit in one of the GPCLR registers. These registers are associated
with physical addresses that the resource manager maps into its address space when it starts.
More information about mapping hardware registers is provided in Section 5.6.
54 CHAPTER 4. CONTROLLING I/O WITH PYTHON
Chapter 5
Real-Time Programming in C
1 # include <stdio.h>
2
3 int
4 main ()
5 {
6 printf("Hello World !\n");
7 return 0;
8 }
This command will build the code in hello.c into the executable hello. Once built, the
executable can be copied to the Raspberry Pi and run:
55
56 CHAPTER 5. REAL-TIME PROGRAMMING IN C
Just as with other compilers, multiple source files can be specified on the command line
to be linked together. Alternatively, the -c option can be used just to compile one source file
into an object, and then the objects linked together.
$ tree hello
hello
|-- Makefile
|-- aarch64
| |-- Makefile
| `-- le
| `-- Makefile
|-- common.mk
`-- hello.c
The common.mk file includes the files necessary for the recursive make system, along
with definitions for the project as a whole (such as the name of the executable). A bare-bones
file for our example looks like this:
1 For simplicity, the OS-level directory was omitted, as all of our code is going to target QNX systems.
5.2. INTER-PROCESS COMMUNICATION 57
common.mk
1 ifndef QCONFIG
2 QCONFIG=qconfig.mk
3 endif
4 include $(QCONFIG)
5
6 NAME=hello
7 USEFILE=
8
9 include $(MKFILES_ROOT)/qtargets.mk
The top-level make files simply tell make to continue down the hierarchy, while the bot-
tom level file (the one under hello/aarch64/le) includes common.mk. It is the names of the
directories that tell the recursive system what is the target for the binaries built under them.
We can now go into any of these directories and type make to build the executables for
the targets covered by this level of the hierarchy. As we have only one target it doesn’t matter
which one we use.
5.1.3 VSCode
To use VSCode for QNX C/C++ development you can open an existing folder with pre-
created make files (as in the example above), or you can create a new project directly in VS-
Code. To do that, open the command palette (Ctrl-Shift-P) and type “QNX: Create New
QNX Project” (the command will be completed soon after you start typing). Choose a name,
the make system to use and a default language. A project with a simple main() function is
initialized and ready to be built.
Next, make sure that the Raspberry Pi is set as the default target. The list of targets is
available in the QNX pane, which is made visible by clicking on the QNX plugin button.
Once the default target is set you can launch the program on the Raspberry Pi. From the
command palette choose “QNX: Run as QNX Application”. The output should be visible
in the debug console below. You can also use VSCode to debug the program.
The most basic form of IPC in the QNX RTOS is synchronous message passing, also
referred to as Send/Receive/Reply. Almost all of the scenarios mentioned above for the use
of IPC in a micro-kernel operating system are implemented in QNX by the use of such mes-
sages. In fact, some of the other IPC mechanisms are built on top of synchronous message
passing.
With synchronous message passing, one process, known as the client, requests a service
from another process, known as the server, by sending it a message, and then waiting for a
reply. The server receives the message, handles it, and then replies to the waiting client. Note
that when we say that the client is waiting we are only referring to one thread in the client that
has sent the message to the server. Other threads in the client process can continue running
while one thread is waiting for a reply.
The main advantage of synchronous message passing over other forms of IPC is that it
does not queue messages. Consequently, there are no limits on the size of messages.2 Also,
synchronous message passing is self-throttling, making it harder for the client to flood the
server with IPC requests at a higher rate than the server can handle. Finally, QNX message
passing provides a scatter/gather feature, in which all the buffers used during the message
pass (client’s send and reply buffers, and the server’s receive buffer) can be specified as arrays
of sub-buffers, known as I/O vectors (IOV). Each of these sub-buffers consists of a base ad-
dress and a length. IOVs allow messages to be assembled without the need first to copy the
components into a single buffer.
Another feature of synchronous message passing, which is absent from most other forms
of IPC, is priority inheritance. When a client thread at priority 20 sends a message to a server,
then the server thread that receives the message will have its priority changed to 20, for the
duration of the time it handles the message. For more information on priorities see Section
5.3.
Before a client can send a message to a server it needs to establish a communication con-
duit to that server. The server creates one or more channels, on which threads are waiting to
receive messages. The use of channels as the end points of IPC rather than the server process
itself allows the server to have multiple such end points, which it can then use either for dif-
ferent services (e.g., a file system process can have a separate channel for each mount point)
or for different quality of service (e.g., separating services to privileged clients from those to
non-privileged clients). Once a channel is created, via the ChannelCreate() kernel call,
the client can establish a connection to that channel, using the ConnectAttach() kernel
call.
The ConnectAttach() call takes the server’s process ID and the channel ID, and re-
turns a connection ID for use with calls to MsgSend(). However, the requirement to know
the identifiers of the server process and its channel make it hard to write client code: these
identifiers may be different on different systems, or even across boots of the same system.
Consequently, we need a mechanism by which a server can advertise itself, and which the
client can use to discover it. That mechanism is provided by paths: a server can associate its
channel with a unique path name, which is registered and handled by the path manager (see
Section 3.2). The client can then call open() on that path, which performs the following
actions:
2 Well, there are limits. A single part in a message is limited to the size of the virtual address space, which is 512GB,
and a message is limited to 512,000 parts, for a total limit of 256PB. If you run into this limit, please contact QNX
support. We would love to see your system.
5.2. INTER-PROCESS COMMUNICATION 59
1. Sends a message to the path manager to inquire about the path. The path manager re-
sponds with the process and channel identifiers for the corresponding server channel.
2. Calls ConnectAttach() using the provided identifiers.
led_client.c
1 # include <stdio.h>
2 # include <stdlib.h>
3 # include <fcntl.h>
4 # include <sys/neutrino.h>
5 # include <sys/rpi_gpio.h>
6
7 int
8 main(int argc , char ** argv)
9 {
10 // Connect to the GPIO server .
11 int fd = open("/dev/gpio/msg", O_RDWR);
12 if (fd == -1) {
13 perror("open");
14 return EXIT_FAILURE;
15 }
16
17 // Configure GPIO 16 as an output .
18 rpi_gpio_msg_t msg = {
19 .hdr.type = _IO_MSG ,
20 .hdr.subtype = RPI_GPIO_SET_SELECT ,
21 .hdr.mgrid = RPI_GPIO_IOMGR ,
22 .gpio = 16,
23 .value = RPI_GPIO_FUNC_OUT
24 };
25
26 if (MsgSend(fd , &msg , sizeof(msg), NULL , 0) == -1) {
27 perror("MsgSend(output)");
28 return EXIT_FAILURE;
29 }
30
31 // Set GPIO 16 to high.
32 msg.hdr.subtype = RPI_GPIO_WRITE;
33 msg.value = 1;
34
35 if (MsgSend(fd , &msg , sizeof(msg), NULL , 0) == -1) {
36 perror("MsgSend(high)");
37 return EXIT_FAILURE;
38 }
39
40 usleep (500000);
41
42 // Set GPIO 16 to low.
43 msg.value = 0;
44
45 if (MsgSend(fd , &msg , sizeof(msg), NULL , 0) == -1) {
46 perror("MsgSend(low)");
47 return EXIT_FAILURE;
48 }
49
50 return EXIT_SUCCESS;
51 }
Line 11 establishes the connection to the GPIO server. Line 18 declares a message structure
with the type expected by this server. The first message, which is sent on line 26, uses the
RPI_GPIO_SET_SELECT subtype and the value RPI_GPIO_FUNC_OUT to tell the server
to configure GPIO 16 as an output. Lines 32-35 reuse the same message structure to tell the
server to set GPIO 16 to high (note that the other structure fields are unchanged), while lines
43-45 use the same structure to set the GPIO to low.
It may seem from this example that messages must have a specific structure. The QNX
5.2. INTER-PROCESS COMMUNICATION 61
kernel does not impose any such structure on messages, and treats these as raw buffers to be
copied from client to server and back. It is only the server that imposes semantics on these
raw buffers, and each server can define its own expected structures for the various types of
messages it handles.
As mentioned above, many of the standard library functions in QNX are implemented
as message passes to the appropriate servers. The following example shows how to read a
file directly with a message to the server that provides the file. When the call to MsgSend()
returns the read bytes are in the reply buffer. Of course, in this example there is no advantage
to the direct use of MsgSend(), and code would normally call read() instead.
read_file.c
1 # include <stdio.h>
2 # include <stdlib.h>
3 # include <fcntl.h>
4 # include <sys/neutrino.h>
5 # include <sys/iomsg.h>
6
7 int
8 main(int argc , char ** argv)
9 {
10 if (argc < 2) {
11 fprintf(stderr , "usage: %s PATH\n", argv [0]);
12 return EXIT_FAILURE;
13 }
14
15 // Open the file.
16 int fd = open(argv [1], O_RDONLY);
17 if (fd == -1) {
18 perror("open");
19 return EXIT_FAILURE;
20 }
21
22 // Construct a read request message .
23 struct _io_read msg = {
24 .type = _IO_READ ,
25 .nbytes = 100
26 };
27
28 // Buffer to fill with reply .
29 char buf [100];
30
31 ssize_t nread = MsgSend(fd , &msg , sizeof(msg), buf , sizeof(buf));
32 if (nread == -1) {
33 perror("MsgSend");
34 return EXIT_FAILURE;
35 }
36
37 printf("Read %zd bytes\n", nread);
38 return EXIT_SUCCESS;
39 }
62 CHAPTER 5. REAL-TIME PROGRAMMING IN C
1. Time Sharing Different programs can make use of different threads in order to exe-
cute concurrently. The operating system alternates the execution of threads from all
running programs, giving the illusion that the programs are running in parallel.3
2. Processor Utilization A program often needs to wait for some even to occur (e.g.,
a block read from a hard drive, user input, or a sensor detecting activity). Threads
allow for a different execution stream to proceed while one is waiting, preventing the
processor from going idle while the system has work to do.
3. Non-Blocking Execution Closely related to the previous point, a single program
can make use of threads to turn a blocking operation into a non-blocking one. For
3 Note that true parallelism is not possible on a single processor, but to a human observer fast-switching concur-
example, a device that does not respond to a request until it is ready can be interacted
with using one thread, while another thread in the same program continues.
4. Low Latency Threads are crucial for a real-time operating system to provide tim-
ing guarantees on low-latency operations. When an event occurs, such as an inter-
rupt raised from an external device, or a timer expiring, the current thread can be sus-
pended, and a new one put into execution to handle the event. Such replacement of
one thread by another in response to an event is called preemption. Without preemp-
tion event handling would have to wait until the current thread suspends itself, which
may interfere with the timely response to the event.
Ď Note
The discussion so far has made no reference to processes. Some textbooks refer
to threads as light-weight processes, but that is an anachronism: old UNIX (and
UNIX-like) systems had one stream of execution per process, and new processes had
to be created for each of the use cases described above. The introduction of multi-
ple such streams per process helped reduce the overhead in some of these cases, as
the system no longer needed to allocate the full resources for a process just to have
another stream of execution within the same logical program. But such a defini-
tion of a thread misses the essential point of this construct. Even in systems that do
not support multiple threads per process there are still multiple threads, one for each
process, and these threads are distinct entities. When the operating system scheduler
puts a stream into execution it schedules a thread, not a process.
1. FIFO (first-in, first-out), in which the thread that has been ready for the longest time
executes next, and then runs until it has to wait; and
2. round-robin, which alternates between threads, allowing each a fixed time period to
execute (its time slice).
Neither of these policies by itself is a good choice for a complex operating system. FIFO
depends on a thread to relinquish the processor before any other activity can proceed, which
means that other threads may have to wait indefinitely. Round-robin alleviates the problem
4 Putting threads into execution is the one task that even the smallest, purest micro-kernel has to do itself.
64 CHAPTER 5. REAL-TIME PROGRAMMING IN C
only to a degree, but critical system work still has to wait for all the time slices of all the ready
threads ahead of it.
QNX, like other real-time operating systems, supports priority scheduling. Each thread
is associated with a priority value, which is a number between 0 (lowest) and 255 (highest).
When a scheduling decision is performed, the scheduler chooses the thread with the highest
priority that is ready to run. Within each priority level threads are scheduled according to
which has been ready for the longest time.
Priority 0 is reserved for the idle threads. These are the threads that run when the pro-
cessor has nothing else to do. Typically these will execute the architecture-specific halt in-
struction, which can reduce the power consumption of the processor when it does not need
to do anything. Priority 255 is reserved for the inter-processor interrupt (IPI) thread. IPIs
are used in a multi-processor system to communicate between the different processors, and
are required for distributing work. Additionally, one priority level is assigned to the clock
interrupt handler, which handles timers. By default this priority is 254, but can be lowered
with a command-line option to the kernel. If lowered, then the priorities between the clock
thread and the IPI thread can be used for the lowest-latency threads, as long as these do not
require software timers.
The assignment of priorities to threads is one of the most important tasks when designing
a complete system. Clearly this cannot be a free-for-all, or all programs would assign the
highest priority to all (or most) of their threads. 5 The following are some best practices
when deciding on priorities:
• The threads with the highest priority in the system are those that require the shortest
response time to events (i.e., the lowest latency). These are not (necessarily) the most
important threads in the system, and are certainly not the most demanding in terms
of throughput.
• The higher the priority of a thread, the shorter its execution time should be before it
blocks waiting for the next event. High-priority threads running for long periods of
time prevent the processor from doing anything else.
• The above restriction should be enforced by the system. In QNX the priority range is
divided into a non-privileged and a privileged range (by default 1-63 and 64-254). Only
trusted programs should be given the ability to use privileged priorities. Also, the use
of a software watchdog to detect a misbehaving high-priority thread is recommended.
The following example shows a program that creates 11 threads, in addition to the main
thread. Ten of these threads are worker thread, which calculate the value of π, while another
thread sleeps for one second and then prints the number of microseconds that have elapsed
since the last time it woke up. Running this example should show that the high-priority
thread executes consistently within one millisecond of the expected time.6 Now comment
out the call to pthread_attr_setschedparam() so that the high-priority thread is re-
duced to the default priority, and observe the effect.
5 Convincing programmers that the threads in the applications or drivers they work on should not have the
highest priority in the system is one of the toughest jobs a system architect has to deal with.
6 The one millisecond granularity is the result of the standard timer resolution. If needed, this example can be
threads.c
1 # include <stdio.h>
2 # include <stdlib.h>
3 # include <string.h>
4 # include <time.h>
5 # include <unistd.h>
6 # include <pthread.h>
7
8 static void *
9 worker(void *arg)
10 {
11 double pi = 0.0;
12 double denom = 1.0;
13
14 for ( unsigned i = 0; i < 100000000; i++) {
15 pi += 4.0 / denom;
16 pi -= 4.0 / (denom + 2.0);
17 denom += 4.0;
18 }
19
20 printf("pi=%f\n", pi);
21 return NULL;
22 }
23
24 static void *
25 high_priority(void *arg)
26 {
27 unsigned long last = clock_gettime_mon_ns ();
28 for (;;) {
29 sleep (1);
30 unsigned long now = clock_gettime_mon_ns ();
31 printf("Slept for %luus\n", (now - last) / 1000 UL);
32 last = now;
33 }
34
35 return NULL;
36 }
66 CHAPTER 5. REAL-TIME PROGRAMMING IN C
1 int
2 main(int argc , char ** argv)
3 {
4 // Attribute structure for a high - priority thread .
5 pthread_attr_t attr;
6 pthread_attr_init (& attr);
7 pthread_attr_setinheritsched (&attr , PTHREAD_EXPLICIT_SCHED );
8
9 struct sched_param sched = { .sched_priority = 63 };
10 pthread_attr_setschedparam (&attr , &sched);
11
12 // Create the high - priority thread .
13 pthread_t tids [11];
14 int rc;
15 rc = pthread_create (& tids [0], &attr , high_priority , NULL);
16 if (rc != 0) {
17 fprintf(stderr , "pthread_create: %s\n", strerror(rc));
18 return EXIT_FAILURE;
19 }
20
21 // Create worker threads .
22 for ( unsigned i = 1; i < 11; i++) {
23 rc = pthread_create (& tids[i], NULL , worker , NULL);
24 if (rc != 0) {
25 fprintf(stderr , "pthread_create: %s\n", strerror(rc));
26 return EXIT_FAILURE;
27 }
28 }
29
30 // Wait for workers to finish .
31 for ( unsigned i = 1; i < 11; i++) {
32 pthread_join(tids[i], NULL);
33 }
34
35 return EXIT_SUCCESS;
36 }
5.4 Timers
Keeping track of time is a requirement of many programs: refreshing the screen, re-transmitting
network packets, detecting deadlines and controlling devices with strict timing restrictions
are all examples of such a requirement. Given that hardware timers are few and demand is
high, the operating system needs to multiplex software timers on top of those provided by the
hardware. A program can create multiple timers and associate each of those with an event to
be emitted whenever that timer expires. The program can then ask the system, individually
for each of the timers it created, to expire the timer at some point in the future.
The QNX RTOS (as of version 8.0) makes use of a per-processor hardware timer. When
a program requests that one of its software timers expire in the future, that request is en-
queued for the hardware timer associated with the processor on which the request was made.
When the expiration time is reached that hardware timer raises an interrupt, which is han-
dled by the clock interrupt thread on the same processor. That thread then emits the event
associated with the software timer, which notifies the program that the timer has expired.
Timers are created with the timer_create() function, which takes the clock ID and
an event to be associated with the timer. The clock ID represents the underlying clock to
5.5. EVENT LOOPS 67
which expiration times for the timer correspond. Two common clocks are CLOCK_MONOTONIC
and CLOCK_REALTIME. Both of these are tied to the hardware clock, which ticks constantly,
but the latter is affected by changes to the system’s notion of the time of day, while the for-
mer is not. Another option for a clock is the runtime of a thread or a process (the latter is the
accumulation of thread runtime for all threads in the process). This option is less common,
and is typically used to monitor thread execution. The event associated with the timer can
be any of the types that are allowed by the sigevent structure, including a pulse, a signal,
posting a semaphore, creating a thread7 or updating a memory location.
The standard timer resolution in a QNX system is the tick length, which is by default one
millisecond. Software timers that use this resolution are referred to as tick-aligned timers.
This means that these timers will expire on the next logical tick after their real expiration
time. Aligning timers on a logical tick avoids excessive context switches as multiple timers
expire in close succession.
Ď Note
As of QNX 8.0 the system is tickless by default, which means that there is no re-
curring tick if no timer expires within the next tick period. Standard timers are still
aligned to a logical tick, which is a multiple of the number of clock cycles in a tick
period since the system’s boot time.
The system also provides high-resolution timers, which expire as soon as the specified
time was reached (subject to the granularity of the hardware timer). High-resolution timers
should be used sparingly8 and with care, as frequent expiration of such timers can prevent
the system from making forward progress. For this reason the creation of such timers is a
privileged operation.
Once a timer has been created it can be programmed using timer_settime(). The
function can be used to set the expiration time in either absolute or relative terms (both cor-
respond to the clock used to create the timer), and as either a single-shot or a recurring timer.
An example of using a timer will be given in the next section.
1 void
2 event_loop ()
3 {
4 event_t event;
5
6 for (;;) {
7 wait_for_event (& event);
8 switch (event_type(event)) {
9 case EVENT_TYPE_1:
10 handle_event_1(event);
11 break;
12
13 case EVENT_TYPE_2:
14 handle_event_2(event);
15 break;
16
17 ...
18 }
19 }
20 }
(This example does not reflect a real API, but simply illustrates the structure common to
various event loop implementations. The switch statement may be replaced by if-else blocks,
or by a table of function pointers, but such choices do not alter the fundamental structure.)
Various operating systems have come up with different types of API data structures and
functions for implementing event loops. In UNIX and UNIX-like operating systems the
two common interface functions have traditionally been select(), and poll(). These
functions have been recognized as deficient for a long time, both in terms of scalability and
race conditions. Systems such as Linux and FreeBSD attempt to overcome their inherent
limitations with less-standard approaches, such as epoll and kqueue, respectively.
While select() and poll() are available from the QNX C library, the inherent limi-
tations of these functions make them less than ideal choices for implementing event loops. A
much better choice is to build the loop around a call to MsgReceivePulse() and use pulses
as the event notification mechanism. A pulse is a fixed-sized structure with a small payload (a
one byte code and an 8-byte value) that can be emitted in response to various events, includ-
ing, but not limited to, an interrupt firing, a timer expiring, a pipe changing its state from
empty to non empty, and a socket being ready to deliver the next packet. The program that
implement the event loop requests to be notified via pulses by associating a pulse sigevent
structure with each event source. We have seen in the previous section that a sigevent
can be associated with a timer when that timer is created. Other functions that take such
structures include InterruptAttachEvent(), ionotify() and mq_notify(). Vari-
ous servers accept sigevent structures embedded in messages, which allow them to deliver
these events when the conditions for delivery are met.
5.5. EVENT LOOPS 69
Ď Note
As of QNX 7.1 all sigevent structures must be registered first with calls to
MsgRegisterEvent(). Registration prevents server processes from delivering un-
expected (or even malicious) events to clients.
Pulses are queued for delivery by priority first and order of arrival second. Using priorities
allows certain events to be handled first, resulting in lower latency for those events.
For the next example build the circuit depicted in figure 5.1.
The program sets three event sources, one for each button and a timer that fires 5 seconds
after the last event that was received. Each event is assigned a different pulse value to distin-
guish it. The timer event is used with a call to timer_create(). The button events are
passed to the GPIO resource manager to be delivered by that server whenever a rising edge is
detected on the respective GPIO.
The pulses are delivered to a local channel. As the channel is only used for pulse delivery
within the process, the channel can be, and should be, declared as private. This prevents
other processes from connecting to this channel and sending messages or pulses.
The program creates a connection to the channel, which is then used by the system to
deliver pulses. Note that while the SIGEV_PULSE_INIT() macro takes the connection ID
used to deliver the pulse, MsgRegisterEvent() takes a connection ID that identifies the
server that is allowed to emit the event. In the case of a timer the event is coming from the
kernel, but in the case of the buttons the events are delivered by the GPIO resource manager.
Any other process trying to deliver this event to the process will be prevented from doing so.
70 CHAPTER 5. REAL-TIME PROGRAMMING IN C
event_loop.c
1 # include <stdio.h>
2 # include <stdlib.h>
3 # include <stdbool.h>
4 # include <fcntl.h>
5 # include <sys/neutrino.h>
6 # include <sys/rpi_gpio.h>
7
8 enum {
9 EVENT_TIMER ,
10 EVENT_BUTTON_1 ,
11 EVENT_BUTTON_2
12 };
13
14 static int chid;
15 static int coid;
16 static timer_t timer_id;
17
18 static bool
19 init_channel(void)
20 {
21 // Create a local channel .
22 chid = ChannelCreate(_NTO_CHF_PRIVATE);
23 if (chid == -1) {
24 perror("ChannelCreate");
25 return false;
26 }
27
28 // Connect to the channel .
29 coid = ConnectAttach (0, 0, chid , _NTO_SIDE_CHANNEL , 0);
30 if (coid == -1) {
31 perror("ConnectAttach");
32 return false;
33 }
34
35 return true;
36 }
37
38 static bool
39 init_timer(void)
40 {
41 // Define and register a pulse event .
42 struct sigevent timer_event;
43 SIGEV_PULSE_INIT (& timer_event , coid , -1, _PULSE_CODE_MINAVAIL ,
44 EVENT_TIMER);
45 if (MsgRegisterEvent (& timer_event , coid) == -1) {
46 perror("MsgRegisterEvent");
47 return false;
48 }
49
50 // Create a timer and associate it with the first pulse event .
51 if (timer_create(CLOCK_MONOTONIC , &timer_event , &timer_id) == -1) {
52 perror("timer_create");
53 return false;
54 }
55
56 return true;
57 }
5.5. EVENT LOOPS 71
1 static bool
2 init_gpio(int fd , int gpio , int event)
3 {
4 // Configure as an input .
5 rpi_gpio_msg_t msg = {
6 .hdr.type = _IO_MSG ,
7 .hdr.subtype = RPI_GPIO_SET_SELECT ,
8 .hdr.mgrid = RPI_GPIO_IOMGR ,
9 .gpio = gpio ,
10 .value = RPI_GPIO_FUNC_IN
11 };
12
13 if (MsgSend(fd , &msg , sizeof(msg), NULL , 0) == -1) {
14 perror("MsgSend(input)");
15 return false;
16 }
17
18 // Configure pull up.
19 msg.hdr.subtype = RPI_GPIO_PUD;
20 msg.value = RPI_GPIO_PUD_UP;
21 if (MsgSend(fd , &msg , sizeof(msg), NULL , 0) == -1) {
22 perror("MsgSend(pud 16)");
23 return false;
24 }
25
26 // Notify on a rising edge.
27 rpi_gpio_event_t event_msg = {
28 .hdr.type = _IO_MSG ,
29 .hdr.subtype = RPI_GPIO_ADD_EVENT ,
30 .hdr.mgrid = RPI_GPIO_IOMGR ,
31 .gpio = gpio ,
32 .detect = RPI_EVENT_EDGE_RISING ,
33 };
34
35 SIGEV_PULSE_INIT (& event_msg.event , coid , -1, _PULSE_CODE_MINAVAIL ,
36 event);
37 if (MsgRegisterEvent (& event_msg.event , fd) == -1) {
38 perror("MsgRegisterEvent");
39 return false;
40 }
41
42 if (MsgSend(fd , &event_msg , sizeof(event_msg), NULL , 0) == -1) {
43 perror("MsgSend(event)");
44 return false;
45 }
46
47 return true;
48 }
72 CHAPTER 5. REAL-TIME PROGRAMMING IN C
1 int
2 main(int argc , char ** argv)
3 {
4 if (! init_channel ()) {
5 return EXIT_FAILURE;
6 }
7
8 if (! init_timer ()) {
9 return EXIT_FAILURE;
10 }
11
12 int fd = open("/dev/gpio/msg", O_RDWR);
13 if (fd == -1) {
14 perror("open");
15 return false;
16 }
17
18 if (! init_gpio(fd , 16, EVENT_BUTTON_1)) {
19 return EXIT_FAILURE;
20 }
21
22 if (! init_gpio(fd , 20, EVENT_BUTTON_2)) {
23 return EXIT_FAILURE;
24 }
25
26 struct itimerspec ts = { .it_value.tv_sec = 5 };
27 timer_settime(timer_id , 0, &ts , NULL);
28
29 for (;;) {
30 struct _pulse pulse;
31 if (MsgReceivePulse (chid , &pulse , sizeof(pulse), NULL) == -1) {
32 perror("MsgReceivePulse ()");
33 return EXIT_FAILURE;
34 }
35
36 if (pulse.code != _PULSE_CODE_MINAVAIL ) {
37 fprintf(stderr , "Unexpected pulse code %d\n", pulse.code);
38 return EXIT_FAILURE;
39 }
40
41 switch (pulse.value.sival_int) {
42 case EVENT_TIMER:
43 printf("Press a button already !\n");
44 break;
45
46 case EVENT_BUTTON_1:
47 printf("Thank you for pressing button 1!\n");
48 break;
49
50 case EVENT_BUTTON_2:
51 printf("Thank you for pressing button 2!\n");
52 break;
53 }
54
55 timer_settime(timer_id , 0, &ts , NULL);
56 }
57
58 return EXIT_SUCCESS;
59 }
5.6. CONTROLLING HARDWARE 73
Á Warning
In the next exercise we will map the GPIO control registers and uses those directly to
change the state of a pin. First, rebuild the simple LED circuit from Figure 4.1. Compile the
following program.
9 https://datasheets.raspberrypi.com/bcm2711/bcm2711-peripherals.pdf
74 CHAPTER 5. REAL-TIME PROGRAMMING IN C
gpio_map.c
1 # include <stdio.h>
2 # include <stdlib.h>
3 # include <stdint.h>
4 # include <unistd.h>
5 # include <sys/mman.h>
6 # include <aarch64/rpi_gpio.h>
7
8 static uint32_t volatile *gpio_regs;
9
10 int
11 main(int argc , char ** argv)
12 {
13 // Map the GPIO registers .
14 gpio_regs = mmap(0, __PAGESIZE , PROT_READ | PROT_WRITE | PROT_NOCACHE ,
15 MAP_SHARED | MAP_PHYS , -1, 0xfe200000);
16 if (gpio_regs == MAP_FAILED) {
17 perror("mmap");
18 return EXIT_FAILURE;
19 }
20
21 // Configure GPIO 16 as an output .
22 gpio_regs[ RPI_GPIO_REG_GPFSEL1 ] &= ~(7 << 18);
23 gpio_regs[ RPI_GPIO_REG_GPFSEL1 ] |= (1 << 18);
24
25 int led_state = 0;
26 for (;;) {
27 if (led_state == 0) {
28 // Set GPIO 16 to high.
29 gpio_regs[ RPI_GPIO_REG_GPSET0 ] = (1 << 16);
30 } else {
31 // Set GPIO 16 to low.
32 gpio_regs[ RPI_GPIO_REG_GPCLR0 ] = (1 << 16);
33 }
34
35 led_state = !led_state;
36 sleep (1);
37 }
38
39 return EXIT_SUCCESS;
40 }
Note that you will need to run this program as root, as a normal user does not have the
necessary privileges to map physical memory:
Lines 14-15 map the registers at physical address 0xfe200000 and then assign these to an
array of 32-bit integers called gpio_regs (the control registers are 32-bit and need to be
accessed as such). Note the use of PROT_NOCACHE: if the virtual address assigned by the
memory manager for this mapping is marked as cached then reads and writes will not prop-
agate immediately to the registers.10 Also note the use of the MAP_PHYS flag, which tells the
memory manager that the last argument is actually a physical address rather than an offset for
10 Since the physical address is not part of RAM, the use of PROT_NOCACHE results in strongly-ordered device
memory (or nGnRnE, in AArch64 terminology), which also ensures that writes are not reordered.
5.7. HANDLING INTERRUPTS 75
the underlying object. Finally, MAP_SHARED is crucial here: a private mapping would create
a copy of the register contents in memory rather than back the mapping with the registers
themselves.
Lines 22-23 configure GPIO 16 as an output, first by clearing bits 18-20 in the GPFSEL1
register and then setting these bits of 0b001. Lines 27-33 set GPIO 16 to high by setting bit 16
in GPSET0 or to low by setting bit 16 in GPCLR0. These registers are described in GPIO
chapter of the datasheet.
1. the device that generates the interrupt (a timer, a serial device, a network card, a graph-
ics processor, etc.);
2. one or more interrupt controllers, connected both to the device and to the processor;
3. the processor.
76 CHAPTER 5. REAL-TIME PROGRAMMING IN C
4. the processor to which the interrupt is routed allows for interrupts to be delivered.
When referring to a specific interrupt being disabled or enabled in the interrupt con-
troller we say that the interrupt is masked or unmasked, respectively. For a processor, we talk
about interrupts (in general) being enabled or disabled.
to the entry routine. Such blocking can occur either at the processor level (in which case
all interrupts to that processor are blocked), or at the controller level (which allows for the
masking of a specific interrupt).
Notes:
the latter provides lower overhead and better control over interrupt handling. The sole pur-
pose of the event API is to allow for easy migration of code from previous versions of the
QNX RTOS that did not provide the thread attaching API.
5.7.5 Example
For the next example, rebuild the button and LED circuit depicted in Figure 4.2. In order
to run the program you will first need to kill the GPIO resource manager, which itself at-
taches to the GPIO interrupt. While it is possible for more than one process to attach to the
same interrupt, the result is often interference, and in fact the resource manager will reset the
hardware’s event registers before our example program sees the interrupt.
gpio_interrupt.c
1 # include <stdio.h>
2 # include <stdlib.h>
3 # include <stdint.h>
4 # include <unistd.h>
5 # include <sys/mman.h>
6 # include <sys/neutrino.h>
7 # include <aarch64/rpi_gpio.h>
8
9 static uint32_t volatile *gpio_regs;
10
11 static bool
12 init_gpios ()
13 {
14 // Map the GPIO registers .
15 gpio_regs = mmap(0, __PAGESIZE , PROT_READ | PROT_WRITE | PROT_NOCACHE ,
16 MAP_SHARED | MAP_PHYS , -1, 0xfe200000);
17 if (gpio_regs == MAP_FAILED) {
18 perror("mmap");
19 return false;
20 }
21
22 // Configure GPIO 16 as an output .
23 gpio_regs[ RPI_GPIO_REG_GPFSEL1 ] &= ~(7 << 18);
24 gpio_regs[ RPI_GPIO_REG_GPFSEL1 ] |= (1 << 18);
25
26 // Configure GPIO 20 as a pull -up input .
27 gpio_regs[ RPI_GPIO_REG_GPFSEL2 ] &= ~(7 << 0);
28 gpio_regs[ RPI_GPIO_REG_GPPUD1 ] &= ~(3 << 8);
29 gpio_regs[ RPI_GPIO_REG_GPPUD1 ] |= (1 << 8);
30
31 // Clear and disable all events .
32 gpio_regs[ RPI_GPIO_REG_GPEDS0 ] = 0xffffffff;
33 gpio_regs[ RPI_GPIO_REG_GPEDS1 ] = 0xffffffff;
34 gpio_regs[ RPI_GPIO_REG_GPREN0 ] = 0;
35 gpio_regs[ RPI_GPIO_REG_GPREN1 ] = 0;
36 gpio_regs[ RPI_GPIO_REG_GPFEN0 ] = 0;
37 gpio_regs[ RPI_GPIO_REG_GPFEN1 ] = 0;
38 gpio_regs[ RPI_GPIO_REG_GPHEN0 ] = 0;
39 gpio_regs[ RPI_GPIO_REG_GPHEN1 ] = 0;
40 gpio_regs[ RPI_GPIO_REG_GPLEN0 ] = 0;
41 gpio_regs[ RPI_GPIO_REG_GPLEN1 ] = 0;
42
43 // Detect falling and rising edge events on GPIO 20.
44 gpio_regs[ RPI_GPIO_REG_GPREN0 ] |= (1 << 20);
45 gpio_regs[ RPI_GPIO_REG_GPFEN0 ] |= (1 << 20);
46
47 return true;
48 }
5.7. HANDLING INTERRUPTS 81
1 int
2 main(int argc , char ** argv)
3 {
4 if (! init_gpios ()) {
5 return EXIT_FAILURE;
6 }
7
8 // Attach to the GPIO interrupt without unmasking .
9 int intid = InterruptAttachThread (145, _NTO_INTR_FLAGS_NO_UNMASK );
10 if (intid == -1) {
11 perror(" InterruptAttachThread ");
12 return EXIT_FAILURE;
13 }
14
15 for (;;) {
16 // Unmask the interrupt and wait for the next one.
17 if (InterruptWait( _NTO_INTR_WAIT_FLAGS_FAST |
18 _NTO_INTR_WAIT_FLAGS_UNMASK , NULL) == -1) {
19 perror("InterruptWait");
20 return EXIT_FAILURE;
21 }
22
23 // Check for an event on GPIO 20.
24 if (( gpio_regs[ RPI_GPIO_REG_GPEDS0 ] & (1 << 20)) != 0) {
25 if (( gpio_regs[ RPI_GPIO_REG_GPLEV0 ] & (1 << 20)) == 0) {
26 // GPIO 20 is low , set GPIO 16 to high.
27 gpio_regs[ RPI_GPIO_REG_GPSET0 ] = (1 << 16);
28 } else {
29 // GPIO 20 is high , set GPIO 16 to high.
30 gpio_regs[ RPI_GPIO_REG_GPCLR0 ] = (1 << 16);
31 }
32 }
33
34 // Clear any detected events before unmasking the interrupt .
35 gpio_regs[ RPI_GPIO_REG_GPEDS0 ] = 0xffffffff;
36 gpio_regs[ RPI_GPIO_REG_GPEDS1 ] = 0xffffffff;
37 }
38
39 return EXIT_SUCCESS;
40 }
The example attaches interrupt 145, which is the GPIO interrupt on Raspberry Pi 4,
to the main thread. While it is common to have a dedicated IST in a program, this exam-
ple is simple enough to have the main thread service the interrupt. After configuring the
GPIOs such that the hardware detects both rising and falling edges on GPIO 20 (the but-
ton), the program goes into an infinite loop, waiting for the interrupt. Once the call to
InteruptWait() returns, indicating that interrupt 145 has fired, the code checks that the
reason for the interrupt was indeed a change to GPIO 20. If so, it reads the current value of
the GPIO, and updates the output on GPIO 16 (the LED) accordingly. It is important to
reset the event registers before going back to InterruptWait(), to prevent the interrupt
from firing again immediately.
82 CHAPTER 5. REAL-TIME PROGRAMMING IN C
Appendix A
3.3V 1 2 5V
GPIO2 3 4 5V
GPIO3 5 6 GND
GPIO4 7 8 GPIO14
GND 9 10 GPIO15
GPIO17 11 12 GPIO18
GPIO27 13 14 GND
GPIO22 15 16 GPIO23
3.3V 17 18 GPIO24
GPIO10 19 20 GND
GPIO9 21 22 GPIO25
GPIO11 23 24 GPIO8
GND 25 26 GPIO7
GPIO0 27 28 GPIO1
GPIO5 29 30 GND
GPIO6 31 32 GPIO12
GPIO13 33 34 GND
GPIO19 35 36 GPIO16
GPIO26 37 38 GPIO20
GND 39 40 GPIO21
83