Getting Started With QNX For Free!
Getting Started With QNX For Free!
Raspberry Pi
Elad Lahav
Contents
Preface 1
1 Introduction 3
1.1 What is a Real-Time Operating System? . . . . . . . . . . . . . . . . . 3
1.2 A Brief History of QNX . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.3 QNX RTOS FAQ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.4 Conventions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
2 Getting Started 7
2.1 Shopping List . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
2.2 Installing the QNX SDP . . . . . . . . . . . . . . . . . . . . . . . . . . 7
2.3 Creating The SD Card Image . . . . . . . . . . . . . . . . . . . . . . . 9
2.3.1 Generate the Image . . . . . . . . . . . . . . . . . . . . . . . . 9
2.3.2 Copy the Image: Raspberry Pi Imager . . . . . . . . . . . . . 9
2.3.3 Copy the Image: Linux Command Line . . . . . . . . . . . . . 9
2.4 Booting The Raspberry Pi . . . . . . . . . . . . . . . . . . . . . . . . . 11
2.5 Connecting to The System . . . . . . . . . . . . . . . . . . . . . . . . 12
2.5.1 Display and Keyboard . . . . . . . . . . . . . . . . . . . . . . 12
2.5.2 Serial Connection . . . . . . . . . . . . . . . . . . . . . . . . . 12
2.5.3 Secure Shell . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
2.6 Writing Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
2.7 Troubleshooting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
iii
iv CONTENTS
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 . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
5.3.1 What are Threads? . . . . . . . . . . . . . . . . . . . . . . . . 61
5.3.2 Thread Scheduling . . . . . . . . . . . . . . . . . . . . . . . . 63
5.4 Timers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
5.5 Event Loops . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
5.6 Controlling Hardware . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
5.7 Handling Interrupts . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
5.7.1 What is an Interrupt? . . . . . . . . . . . . . . . . . . . . . . . 76
5.7.2 Processing an Interrupt . . . . . . . . . . . . . . . . . . . . . 77
5.7.3 Handling an Interrupt in the QNX RTOS . . . . . . . . . . . . 78
5.7.4 What about ISRs? . . . . . . . . . . . . . . . . . . . . . . . . . 80
5.7.5 Example . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
In a talk about microkernels given in February 2023 the QNX operating system was
described as “historical”.1 While the operating system has existed in various incar-
nations 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 sys-
tems 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 subsystems 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
embedded 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 any-
one 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 processes, 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 ad-
dress 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 com-
puter, makes it much easier than before for anyone to run QNX on the type of hard-
ware that is used by its customers. Unlike a PC, the Raspberry Pi is built to control
1 https://archive.fosdem.org/2023/schedule/event/microkernel2023/
1
2 CONTENTS
Introduction
3
4 CHAPTER 1. INTRODUCTION
was grinding to a halt, the infotainment business, and then a focus on more safety-
oriented systems 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 re-
design 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.
1.4 Conventions
The following conventions are used in this book:
• File names and paths are written in a bold typeface.
• Shell commands and code snippets are written in a teletype face.
6 CHAPTER 1. INTRODUCTION
Getting Started
7
8 CHAPTER 2. GETTING STARTED
linkers, header files, etc.). The SDP is freely available for non-commercial use.
Obtaining the SDP involves the following steps:
1. Register for a myQNX account.
2. Request a free licence.
3. Download QNX Software Centre.
4. Install the SDP on your computer.
To get started, visit qnx.com/getqnx. You will be prompted for your myQNX account
credentials. If you do not have an account, create one first. Once you have logged in,
you can request a free QNX SDP licence to be associated with your account. Follow
the steps on the web page for getting the licence, activating it, and associating it with
your account.
The next step is to download the QNX Software Centre (QSC). Pick the version that
matches your host operating system. Windows users can run the installer directly,
while Linux users will have to make the file executable first. Refer to the installation
instructions for further information.
With QSC installed, we can now get the SDP. Run QSC, choose Add Installation, and
then select the SDP version to install. As this book was written for QNX 8.0, use this
version of the SDP. Follow the prompts to complete the installation. We will assume
that the SDP was installed in a folder called qnx800 under your home directory, but
you are free to choose any location.
Take a look at the installation directory.
$ ls ~/qnx800
host qnxsdp-env.bat qnxsdp-env.sh target
The two scripts can be used to set up the environment for development (one for Win-
dows and one for Linux). The host folder is where you will find the tools necessary
for building programs on your computer, while the target folder contains all of the
files that can go on your QNX system (though you will only ever likely need a small
subset of these).
The base installation contains only a small portion of the content that is available as
part of the SDP. Additional packages can be installed with QSC, some of which are
free and some require a commercial licence. To develop for a particular board, you
will need to add the relevant board support package (BSP), which contains the source
code and binaries for the specific hardware.
The SDP provides everything you need in order to build your own QNX system. How-
ever, to get you up and running quickly with Raspberry Pi, we will use a pre-defined
image that you can just copy to an SD card. Open QNX Software Centre, and install
the “QNX® SDP 8.0 Quick Start image for Raspberry Pi 4” package. The image file is
now located in the images folder under SDP installation path:
2.3. CREATING THE SD CARD IMAGE 9
$ ls ~/qnx/sdp/8.0/images/
qnx_sdp8.0_rpi4_quickstart_20240920.img
(The name of the image will likely be different for you, as it includes the version
number and date.)
Á 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 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.
2.4. BOOTING THE RASPBERRY PI 11
$ 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 discov-
ered 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:
Ď Note
Make sure you update these passwords the first time you log in. Passwords
can be changed using the passwd shell command. Note that root access over
SSH is disabled by default.
Ď Note
In order to get a login prompt on the serial console you must edit the
qnx_config.txt file in the boot partition to remove (or comment out) the line
that says CONSOLE=/dev/con1. This can be done either from the Raspberry
Pi itself, or when the SD card is plugged into your computer.
The connection involves three pins on the Raspberry Pi 40-pin GPIO header (see Chap-
ter 6). Pin 8 is the transmit pin, pin 10 the receive pin and the third is any of the
ground pins (though usually either 6 or 14 is used due to their proximity to the trans-
mit/receive pins). Which of the converter connections goes to which pin on the Rasp-
berry Pi depends on the converter itself. If you need to buy one, make sure it has
instructions for the Raspberry Pi.
2.5. CONNECTING TO THE SYSTEM 13
Á Warning
Never connect a USB-TTL converter to any of the power pins on the Raspberry
Pi. Some converters have a fourth connector that should be left unattached.
Such converters tend to be very susceptible to current surges and once dam-
aged are beyond repair.
>
Once the USB-TTL converter is connected to the computer you can use a terminal
program to connect to the Raspberry Pi. Terminal programs include minicom, GNU
Screen and c-kermit for UNIX-like systems, or PuTTY for Windows. There are
many tutorials on the Internet on how to use such a program with the Raspberry Pi
and you should follow one of those. Next, boot the Raspberry Pi and look for output
in the terminal.
$ ssh [email protected]
qnxuser@qnxpi:~$
14 CHAPTER 2. GETTING STARTED
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 VSCode4 with the QNX plugin to edit, build, deploy and
debug code. The plugin also allows you to inspect the system, e.g., by listing run-
ning processes, analyzing memory usage and collecting trace logs for system activity.
Install the QNX Toolkit extension, which is available on the VSCode Extension Mar-
ketplace.
3 Just remember that Microsoft Word is NOT a text editor!
4 https://code.visualstudio.com
2.7. TROUBLESHOOTING 15
2.7 Troubleshooting
What if the system doesn’t boot, or it does but you cannot connect to it? Before we
start diagnosing any issues with the QNX image for Raspberry Pi, ensure that the
board itself is properly connected and powered.
Does the red LED on the board turn on? If not, the board is not powered. Check
that the board is connected to a proper power supply. The official power supply
for Raspberry Pi is 5.1V and 3A.
Does the firmware detect the SD card? Connect the Raspberry Pi to a display, us-
ing one of the micro-HDMI connectors on the board. If the SD card is not
properly inserted you will see a message on the display that no system image
was found.
Does the firmware detect the image? A sign of the firmware starting the system
image is a multi-coloured rectangle displayed on the screen.
Any of the above issues are indicative of a problem with the setup of the board (or, if
you are very unlucky, with the board itself), and not with the QNX image. Assuming
the firmware appears to boot fine, we need to examine what is not working with the
QNX image.
Is there any output? If not, try to use the official Raspbian image instead, enabling
its UART output option. Again, follow tutorials on how to do that, ensuring
that you have connected the USB-TTL converter correctly, that the terminal
program is connected to the right device (e.g., /dev/ttyUSB0 on Linux) and
with the correct settings: a baud rate of 115200, 8 bits, no parity, 1 stop bit and
no control flow. On Windows, make sure that the driver for the converter is
installed by checking the device manager.
Do you see errors from the QNX image about a failure to mount file systems?
Some SD cards are not recognized by the SD driver. Try a different card, prefer-
ably from a different manufacturer.
Does the system boot to a command prompt, but there is no network connection?
Run ifconfig and check that the bcm0 interface shows up and is associ-
ated with an IP address. If not, ensure that the network configuration in
wpa_supplicant.conf are correct.
Ď Note
Most logs on a QNX system are accessible by running the slog2info com-
mand. This command will dump all logs from all processes. You can refine
it with the -b <component> command, where <component> should be re-
placed by the name of the process. For example, devb_sdmmc_bcm2711 is the
SD card driver.
16 CHAPTER 2. GETTING STARTED
Chapter 3
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 default 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:
qnxuser@qnxpi:~$ ls /tmp
encoder.py robot.py
qnxuser@qnxpi:~$ which ls
/proc/boot/ls
qnxuser@qnxpi:~$ ls -l /proc/boot/ls
lrwxrwxrwx 1 root root 6 2023-10-14 15:18 /proc/boot/ls -> toybox
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/hd0t17 26212320 1288784 24923536 5% /data/
/dev/hd0t17 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
3.2. THE FILE SYSTEM 19
this image is mounted at the root directory, with most of its files under /proc/boot.
It is possible to change this mount point.
Next are two QNX6 file systems, mounted at /system and /data, respectively. This
separation allows for the system partition to be mounted read-only, potentially with
added verification and encryption, while the data partition remains writable. In this
image both partitions 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.4).
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 it at 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
mentioned above.
2. This layout avoids union paths, which have both performance and security con-
cerns.
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 systems 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.
20 CHAPTER 3. EXPLORING THE SYSTEM
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 microkernel, as well as a set of basic 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 microkernel architecture means that most of the functionality provided by a
monolithic kernel in other operating systems is provided by stand-alone user pro-
cesses 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 utilities, 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
qnxuser@qnxpi:~$ pidin
pid tid name prio STATE Blocked
1 1 /proc/boot/procnto-smp-instr 0f READY
1 2 /proc/boot/procnto-smp-instr 0f READY
1 3 /proc/boot/procnto-smp-instr 0f RUNNING
1 4 /proc/boot/procnto-smp-instr 0f RUNNING
1 5 /proc/boot/procnto-smp-instr 255i INTR
1 6 /proc/boot/procnto-smp-instr 255i INTR
...
1 20 /proc/boot/procnto-smp-instr 10r RECEIVE 1
12291 1 proc/boot/pipe 10r SIGWAITINFO
12291 2 proc/boot/pipe 10r RECEIVE 1
12291 3 proc/boot/pipe 10r RECEIVE 1
12291 4 proc/boot/pipe 10r RECEIVE 1
12291 5 proc/boot/pipe 10r RECEIVE 1
12292 1 proc/boot/dumper 10r RECEIVE 1
12292 2 proc/boot/dumper 10r RECEIVE 2
12293 1 proc/boot/devc-pty 10r RECEIVE 1
12294 1 proc/boot/random 10r SIGWAITINFO
12294 2 proc/boot/random 10r NANOSLEEP
12294 3 proc/boot/random 10r RECEIVE 1
12294 4 proc/boot/random 10r RECEIVE 2
12295 1 proc/boot/rpi_mbox 10r RECEIVE 1
12296 1 proc/boot/devc-serminiuart 10r RECEIVE 1
12296 2 proc/boot/devc-serminiuart 254i INTR
12297 1 proc/boot/i2c-bcm2711 10r RECEIVE 1
12298 1 proc/boot/devb-sdmmc-bcm2711 10r SIGWAITINFO
12298 2 proc/boot/devb-sdmmc-bcm2711 21r RECEIVE 1
12298 3 proc/boot/devb-sdmmc-bcm2711 21r RECEIVE 2
...
12298 16 proc/boot/devb-sdmmc-bcm2711 21r RECEIVE 4
36875 1 proc/boot/pci-server 10r RECEIVE 1
36875 2 proc/boot/pci-server 10r RECEIVE 1
36875 3 proc/boot/pci-server 10r RECEIVE 1
49166 1 proc/boot/rpi_frame_buffer 10r RECEIVE 1
61455 1 proc/boot/devc-bootcon 10r RECEIVE 1
61455 2 proc/boot/devc-bootcon 10r CONDVAR (0x50d562a3cc)
73744 1 proc/boot/rpi_thermal 10r RECEIVE 1
86033 1 proc/boot/rpi_gpio 10r RECEIVE 1
86033 2 proc/boot/rpi_gpio 200r INTR
118802 1 proc/boot/io-usb-otg 10r SIGWAITINFO
118802 2 proc/boot/io-usb-otg 10r CONDVAR (0x5970918880)
118802 3 proc/boot/io-usb-otg 10r CONDVAR (0x59709193a0)
...
118802 13 proc/boot/io-usb-otg 10r RECEIVE 2
139283 1 system/bin/io-pkt-v6-hc 21r SIGWAITINFO
139283 2 system/bin/io-pkt-v6-hc 21r RECEIVE 1
139283 3 system/bin/io-pkt-v6-hc 22r RECEIVE 2
...
139283 9 system/bin/io-pkt-v6-hc 21r RECEIVE 7
163860 1 ystem/bin/wpa_supplicant-2.9 10r SIGWAITINFO
163861 1 system/bin/dhclient 10r SIGWAITINFO
192535 1 system/bin/sshd 10r SIGWAITINFO
196630 1 system/bin/qconn 10r SIGWAITINFO
196630 2 system/bin/qconn 10r RECEIVE 1
208908 1 proc/boot/ksh 10r SIGSUSPEND
270349 1 proc/boot/pidin 10r REPLY 1
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 types of information for each process with the pidin
command: pidin fds shows the file descriptors open for each process, pidin
arguments shows the command-line arguments used when the process was exe-
cuted, 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 in the hands of the virtual memory manager, which is a part of
the procnto-smp-instr process (recall that this is a special process that bundles the
microkernel 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 to poor design that does not account for the
requirements of the system. Such situations naturally lead to two frequently-asked
questions:
1. How much memory does my system use?
2. How much does each process contribute to total system use?
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, hy-
pervisors, etc.
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:
• page_count=0xfc000 shows the number of 4K RAM pages available to the
memory manager. The total amount of memory in the pidin info output
should match this value.
• vmem_avail=0xe67f6 is the number of pages that have not been reserved by
allocations, and are thus available for new allocations. This value corresponds
to the free memory number provided by pidin info.
• vm_aspace=30 is the number of address spaces in the system, which corre-
sponds to the number of active processes.1
• pages_kernel=0x4347 is the number of pages used by the kernel for its own
purposes. Note that the value includes the page-table pages allocated for pro-
cesses.
• pages_reserved=0x9012 is the number of pages that the memory manager
knows about, but cannot allocate to processes as regular memory. Such ranges
are typically the result of the startup program, which runs before the kernel,
reserving memory for special-purpose pools, or as a way to reduce boot time
(in which case the memory can be added to the system later).
The remaining pages_* lines represent allocated pages in different internal states.
The breakup of physical memory into various areas can be observed with the follow-
ing command
All entries that end with sysram are available for the memory manager to use when
servicing 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.
We will now attempt to answer the question of how much does a process contribute
to total memory use in the system. To answer this 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 The word 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
exhausting 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 re-
flects the contents of a file (the program uses a temporary file for the purpose of the
demonstration). Finally, the last mapping just carves a large portion of the process’
virtual address space, without backing it with memory. The output shows the vir-
tual 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 assigned 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
0x000000209b3a6000,0x0000000000004000,0x00000002,0x03,0x0f,0x0000040c,
0x000000000000001c,0x0000000000000000,0x0000000000004000,0x00000000,
0x00000004,0x00000002,/data/home/qnxuser/mmap_demo.9ubgxv,{sysram}
The first two columns show the virtual address (0x209b3a6000) and size (0x4000, or
4 4K pages) of the mapping. You should find a matching entry in the output of
mmap_demo. The next two columns show the flags and protection bits passed to
the mmap() call.3 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 (new in version 8.0 of the OS) 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
may4 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.
the server. Resource managers typically handle some standard message types, such
as read, write, stat and close, but can also handle ad-hoc messages that are relevant
to the specific service they provide.
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 Chapter 6} for more information). It does so by memory-mapping the hard-
ware registers 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 reg-
isters, 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 con-
figured 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.
qnxuser@qnxpi:~$ ls /dev/gpio
0 12 16 2 23 27 30 34 38 41 45 49 52 8
1 13 17 20 24 28 31 35 39 42 46 5 53 9
10 14 18 21 25 29 32 36 4 43 47 50 6 msg
11 15 19 22 26 3 33 37 40 44 48 51 7
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 instructions 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/gpio/16, which establishes a connection (via a file descriptor) to the
3.5. RESOURCE MANAGERS 27
In this chapter we will get the Raspberry Pi to do some useful (and fun!) work, by
controlling 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
• jumper wires
• various resistors
• LEDs
• breadboard push buttons
• DC motor
• two or more servos
• infrared LED and a photodiode
• PCF8591 Analog-to-digital converter
• 10KΩ potentiometer
• PCA9685-based 16-channel PWM board
• L293 or L298 H-bridge
• external DC power supply (a battery pack will do)
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 Section 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
If you write the code directly on the Raspberry Pi, or if you use SSHFS to mount a
directory from the Raspberry Pi into your image, then the file is now stored in the
target directory. On the other hand, if you saved it on your local computer then it
needs to be copied to the Raspberry Pi:
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:
qnxuser@qnxpi:~$ cd ~/python
qnxuser@qnxpi:~/python$ python hello.py
Hello World!
4.2. BASIC OUTPUT (LED) 31
Ď 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.
Ď 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
1 import rpi_gpio as GPIO
2 import time
3
4 GPIO.setup(16, GPIO.OUT)
5 GPIO.output(16, GPIO.LOW)
6
7 while True:
8 GPIO.output(16, GPIO.HIGH)
9 time.sleep(.5)
10 GPIO.output(16, GPIO.LOW)
11 time.sleep(.5)
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:
1. Double-check all connections.
2. Ensure the jumper wires are connected to the correct GPIO header pins.
3. Connect the positive jumper wire to a 3v pin instead of a GPIO pin (e.g., pin
number 1). The LED should turn on. If it doesn’t, try a different LED (in case
this one is burnt out).
4. Check your code to ensure that it is exactly the same as in the listing. If you
connected the positive wire to a different GPIO pin, make sure that all references
to 16 have been replace by the right number.
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. 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 prepares the GPIO to be used as an output. Every GPIO can func-
tion 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 (of), using a delay
of 500 milliseconds between each state change.
Ď Note
The code in this program was stripped to the bare minimum, eschewing func-
tionality 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.
bus. This allows us to have a common ground for all components, without using more
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
permanently connected legs straddle the trough in the middle of the breadboard.
Save the following program as button.py:
button.py
1 import rpi_gpio as GPIO
2 import time
3
4 GPIO.setup(16, GPIO.OUT)
5 GPIO.output(16, GPIO.LOW)
6
7 GPIO.setup(20, GPIO.IN, GPIO.PUD_UP)
8
9 while True:
10 if GPIO.input(20) == GPIO.LOW:
11 GPIO.output(16, GPIO.HIGH)
12 else:
13 GPIO.output(16, GPIO.LOW)
14
15 time.sleep(.01)
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
resistor on this pin. Without a pull-up resistor the state of the pin is undetermined
(or “floating”), which means that the connection to ground established by pushing
4.3. BASIC INPUT (PUSH BUTTON) 35
the button may not be detected. Pulling up means that the pin detects a high state
by default, and then 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 con-
nection 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 Raspberry 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
1 import rpi_gpio as GPIO
2 import time
3
4 def buttonPressed(pin):
5 if GPIO.input(20) == GPIO.LOW:
6 GPIO.output(16, GPIO.HIGH)
7 else:
8 GPIO.output(16, GPIO.LOW)
9
10 GPIO.setup(16, GPIO.OUT)
11 GPIO.output(16, GPIO.LOW)
12
13 GPIO.setup(20, GPIO.IN, GPIO.PUD_UP)
14
15 GPIO.add_event_detect(20, GPIO.BOTH, callback = buttonPressed)
16
17 while True:
18 time.sleep(1)
(e.g., by adding a capacitor), or in code (e.g., by requiring the same value to be read
some number of times consecutively before deciding that the value has changed).
It may seem redundant, and somewhat excessive, to use a 4-core, multi-gigabyte com-
puter to achieve the same effect as connecting the button directly to the LED. In a
real project the button can be used to initiate a much more interesting activity. Also,
the input may not be a push button at all. Other common components that provide
discrete input (i.e., input that is either “on” or “of”) include magnetic reed switches,
photo-resistors detecting infrared light and various sensors.
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.
It is possible to implement PWM in software, by configuring any GPIO pin as an
output and using a repeating timer to toggle its state. However, the Raspberry Pi
provides a 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 allow for more PWM channels with finer control,
and these are recommended for applications that require multiple PWM sources, such
as robotic arms.
4.4. PWM 37
High
Low
High
Low
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.
pwm_led.py
1 import rpi_gpio as GPIO
2 import time
3
4 pwm = GPIO.PWM(12, 1000, mode = GPIO.PWM.MODE_PWM)
5
6 while True:
7 for dc in range(0, 100, 5):
8 pwm.ChangeDutyCycle(dc)
9 time.sleep(.1)
10
11 for dc in range(100, 0, -5):
12 pwm.ChangeDutyCycle(dc)
13 time.sleep(.1)
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.
4.4. PWM 39
There are many different types of servo motors, but one of the most common for be-
ginners is the SG90, which is marketed under different brand names. This is an inex-
pensive, 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
connected in series, but you can substitute a different DC source). The required volt-
age 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 Raspberry 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 connected 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.
40 CHAPTER 4. CONTROLLING I/O WITH PYTHON
Ď 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 man-
ufacture 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.
pwm_servo.py
1 import rpi_gpio as GPIO
2 import time
3
4 pwm = GPIO.PWM(12, 50, mode = GPIO.PWM.MODE_MS)
5
6 MIN = 2.5
7 MAX = 12.5
8 dc = MIN
9 while dc <= MAX:
10 pwm.ChangeDutyCycle(dc)
11 dc += 0.5
12 time.sleep(.5)
13
14 while dc >= MIN:
15 dc -= 0.5
16 pwm.ChangeDutyCycle(dc)
17 time.sleep(.5)
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 (re-
member 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
4.5. I2C 41
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
device 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.
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 avail-
able), but the principles should be the same for all I2C devices.
PCF8591
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.
4.5. I2C 43
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 ar-
gument to smbus.SMBus() matches the I2C bus number, as assigned by the resource
manager (not to be confused with the address assigned to the device, the ADC con-
verter in this case, attached to that bus). If you get an error on this line check which
device was registered by the resource manager:
qnxuser@qnxpi:~$ ls /dev/i2c*
/dev/i2c1
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.
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 multiple 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 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:
4.6. TOWARDS ROBOTICS: MOTOR CONTROL 45
pca9685.py
1 import smbus
2 import time
3
4 smbus = smbus.SMBus(1)
5
6 # Configure PWM for 50Hz
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 180 degrees
21 smbus.write_block_data(0x40, 0x6, [0x0, 0x0, 0x0, 0x2])
22
23 # Servo 2 at 0 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.
inspection 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.
46 CHAPTER 4. CONTROLLING I/O WITH PYTHON
Ď Note
The typical DC motor is too fast and too weak to move a robot. It is therefore
coupled 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 integrated 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 four ground pins of the L293D chip are connected to
the ground buses.
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
1 import rpi_gpio as GPIO
2 import time
3
4 GPIO.setup(20, GPIO.OUT)
5 GPIO.setup(16, GPIO.OUT)
6 GPIO.setup(19, GPIO.OUT)
7
8 GPIO.output(19, GPIO.HIGH)
9
10 GPIO.output(20, GPIO.HIGH)
11 GPIO.output(16, GPIO.LOW)
12 time.sleep(1)
13
14 GPIO.output(20, GPIO.LOW)
15 GPIO.output(16, GPIO.HIGH)
16 time.sleep(1)
17
18 GPIO.output(20, GPIO.LOW)
19 GPIO.output(16, GPIO.LOW)
20 GPIO.output(19, GPIO.LOW)
Line 8 enables the outputs. Lines 10-12 cause the motor to rotate one way for 1 second.
Lines 13-16 reverse the motor’s direction for one second. Then both outputs, as well
4.6. TOWARDS ROBOTICS: MOTOR CONTROL 47
L293D
l293_pwm.py
1 import rpi_gpio as GPIO
2 import time
3
4 GPIO.setup(20, GPIO.OUT)
5 GPIO.setup(16, GPIO.OUT)
6 pwm = GPIO.PWM(19, 20, mode = GPIO.PWM.MODE_MS)
7
8 GPIO.output(20, GPIO.HIGH)
9 GPIO.output(16, GPIO.LOW)
10
11 for dc in range (100, 0, -1):
12 pwm.ChangeDutyCycle(dc)
13 time.sleep(0.1)
14
15 GPIO.output(20, GPIO.LOW)
16 GPIO.output(16, GPIO.HIGH)
17
18 for dc in range (0, 100):
19 pwm.ChangeDutyCycle(dc)
20 time.sleep(0.1)
21
22 pwm.ChangeDutyCycle(0)
23 GPIO.output(20, GPIO.LOW)
24 GPIO.output(16, GPIO.LOW)
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 to 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 at work, as
depicted in Figure 4.9.
The clear diode is the infrared (henceforth IR) LED, while the black one is the photodi-
ode. 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 directly connected
to 3.3V (unlike an LED), while the anode (long leg) is connected via a large resistor
4.6. TOWARDS ROBOTICS: MOTOR CONTROL 49
(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 times 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 distance 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.
50 CHAPTER 4. CONTROLLING I/O WITH PYTHON
ir.py
1 import rpi_gpio as GPIO
2 import time
3
4 count = 0
5
6 def ir_detect(pin):
7 global count
8 count += 1
9
10 GPIO.setup(4, GPIO.IN, GPIO.PUD_DOWN)
11 GPIO.add_event_detect(4, GPIO.RISING, callback = ir_detect)
12
13 time.sleep(5)
14
15 print('Photodiode detected {} signals'.format(count))
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.
Counting 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 cir-
cumference 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.
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
3 I was able to power the L293D from the 3.3V output of the Raspberry Pi, which simplifies the circuit.
L293D
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
transitions 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.
encoder.py
1 import rpi_gpio as GPIO
2 import time
3
4 encoder_detect = 0
5
6 def stop_motor(pin):
7 global encoder_detect
8 encoder_detect += 1
9 if encoder_detect == 9:
10 GPIO.output(20, GPIO.LOW)
11 GPIO.output(16, GPIO.LOW)
12 print('Stopping motor')
13
14 GPIO.setup(4, GPIO.IN, GPIO.PUD_DOWN)
15 GPIO.add_event_detect(4, GPIO.RISING, callback = stop_motor)
16
17 GPIO.setup(20, GPIO.OUT)
18 GPIO.setup(16, GPIO.OUT)
19 pwm = GPIO.PWM(19, 20, mode = GPIO.PWM.MODE_MS)
20
21 GPIO.output(20, GPIO.HIGH)
22 GPIO.output(16, GPIO.LOW)
23
24 pwm.ChangeDutyCycle(40)
25
26 while encoder_detect < 9:
27 time.sleep(0.1)
28
29 pwm.ChangeDutyCycle(0)
30 GPIO.output(20, GPIO.LOW)
31 GPIO.output(16, GPIO.LOW)
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 concurrency. 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 scheduling 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 notify 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.
4.7. HOW DOES IT WORK? 53
encoder_match.py
1 import rpi_gpio as GPIO
2 import time
3
4 encoder_detect = False
5
6 def stop_motor(pin):
7 global encoder_detect
8 GPIO.output(20, GPIO.LOW)
9 GPIO.output(16, GPIO.LOW)
10 print('Stopping motor')
11 encoder_detect = True
12
13 GPIO.setup(4, GPIO.IN, GPIO.PUD_DOWN)
14 GPIO.add_event_detect(4, GPIO.RISING, match = 9, callback = stop_motor)
15
16 GPIO.setup(20, GPIO.OUT)
17 GPIO.setup(16, GPIO.OUT)
18 pwm = GPIO.PWM(19, 20, mode = GPIO.PWM.MODE_MS)
19
20 GPIO.output(20, GPIO.HIGH)
21 GPIO.output(16, GPIO.LOW)
22
23 pwm.ChangeDutyCycle(40)
24
25 while encoder_detect == False:
26 time.sleep(0.1)
27
28 pwm.ChangeDutyCycle(0)
29 GPIO.output(20, GPIO.LOW)
30 GPIO.output(16, GPIO.LOW)
implements the library’s output() method, which was used in the LED example to
turn the LED on and off:
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,
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.
Chapter 5
Real-Time Programming in C
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
or, from a QNX shell (after copying the executable to the Raspberry Pi):
qnxuser@qnxpi:~$ ./hello
Hello World!
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
ifndef QCONFIG
QCONFIG=qconfig.mk
endif
include $(QCONFIG)
NAME=hello
USEFILE=
include $(MKFILES_ROOT)/qtargets.mk
The top-level make files simply tell make to continue down the hierarchy, while the
bottom 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 bina-
ries 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 VSCode. 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 microkernel operating system are implemented in QNX by the
use of such messages. 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 ser-
vice 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 refer-
ring 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. Fi-
nally, 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 address and a length. With IOVs, messages can be
assembled without first copying 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 mes-
sage 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 informa-
tion on priorities see Section 5.3.
Before a client can send a message to a server, it needs to establish a communication
conduit 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 different services (e.g., a file system process can have a sepa-
rate 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
returns 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
2Well,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
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:
1. Sends a message to the path manager to inquire about the path. The path man-
ager responds with the process and channel identifiers for the corresponding
server channel.
2. Calls ConnectAttach() using the provided identifiers.
The resulting connection identifier is the QNX implementation of a file descriptor.
The following program demonstrates a client connecting to the GPIO server, and then
using messages to control a GPIO. Rebuild the circuit from Section 4.2 to see the pro-
gram turning an LED on and off.
60 CHAPTER 5. REAL-TIME PROGRAMMING IN C
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 kernel does not impose any such structure on messages, and treats these as raw
5.3. THREADS AND PRIORITIES 61
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 }
executes, it fetches the next instruction from memory based on the address stored in
the instruction pointer, decodes it, updates registers according to the instruction (e.g.,
adds the values from two registers and stores the result in a third register), and then
updates the instruction pointer such that it points to the next instruction. That next
instruction is typically the one that follows the current instruction in memory, but it
can also be in a different memory location if the processor has just executed a branch
instruction.
The values stored in the processor’s registers at any given point in time provide the
processor with its execution context. We can think of the processor as executing a
stream of instructions that updates this context as it goes along from one instruction to
the next. Many simple micro-controllers only have just one such stream of execution,
typically an infinite loop for the control of some device. The micro-controller follows
that loop and does nothing else.
Computer systems that are not simple micro-controllers require multiple streams of
execution. An example of the use of a separate execution stream is an exception: when
the processor encounters an instruction such as a trap call, it jumps to a predefined
location and executes the code in that location. It is expected that, once the exception
is handled, the previous stream of execution resumes from where it left (though po-
tentially the context is modified to reflect the results of exception handling). In order
to resume the previous execution stream, the exception handler needs first to save,
and then to restore, the execution context of the processor at the time the exception
occurred.
A stream of execution that can be suspended and then restored is called a thread. In
its most basic form, a thread provides storage for a processor’s execution context.
The system (typically the operating system kernel) copies the execution context from
the processor to the thread’s storage when a thread is suspended (such as when a
processor jumps to an exception handler), and copies back the stored context to the
processor when the thread is resumed.
The use of threads provides several important features in a computer system:
1. Time Sharing Different programs can make use of different threads in order to
execute 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 event 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 pro-
gram can make use of threads to turn a blocking operation into a non-blocking
one. For example, a device that does not respond to a request until it is ready can
3 Note that true parallelism is not possible on a single processor, but to a human observer fast-switching
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
timing guarantees on low-latency operations. When an event occurs, such as
an interrupt raised from an external device, or a timer expiring, the current
thread can be suspended, 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 preemption 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 re-
fer 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 multiple such streams per process helped reduce the overhead
in some of these cases, as the system no longer needed to allocate the full re-
sources for a process just to have another stream of execution within the same
logical program. But such a definition of a thread misses the essential point of
this construct. Even in systems that do not support multiple threads per pro-
cess 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.
alleviates the problem 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
processor has nothing else to do. Typically these will execute the architecture-specific
halt instruction, 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 de-
signing 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 (neces-
sarily) 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 range and a privileged range (by default
1-63 and 64-254). Only trusted programs should be given the ability to use priv-
ileged 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 threads, which calculate the value of 𝜋,
while another thread sleeps for one second and then prints the number of microsec-
onds 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
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
that the high-priority thread is reduced to the default priority, and observe the effect.
66 CHAPTER 5. REAL-TIME PROGRAMMING IN C
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) / 1000UL);
32 last = now;
33 }
34
35 return NULL;
36 }
37
38 int
39 main(int argc, char **argv)
40 {
41 // Attribute structure for a high-priority thread.
42 pthread_attr_t attr;
43 pthread_attr_init(&attr);
44 pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED);
45
46 struct sched_param sched = { .sched_priority = 63 };
47 pthread_attr_setschedparam(&attr, &sched);
48
49 // Create the high-priority thread.
50 pthread_t tids[11];
51 int rc;
52 rc = pthread_create(&tids[0], &attr, high_priority, NULL);
53 if (rc != 0) {
54 fprintf(stderr, "pthread_create: %s\n", strerror(rc));
55 return EXIT_FAILURE;
56 }
57
58 // Create worker threads.
59 for (unsigned i = 1; i < 11; i++) {
60 rc = pthread_create(&tids[i], NULL, worker, NULL);
61 if (rc != 0) {
62 fprintf(stderr, "pthread_create: %s\n", strerror(rc));
63 return EXIT_FAILURE;
64 }
65 }
66
67 // Wait for workers to finish.
68 for (unsigned i = 1; i < 11; i++) {
69 pthread_join(tids[i], NULL);
70 }
71
72 return EXIT_SUCCESS;
73 }
5.4. TIMERS 67
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 enqueued for the hardware timer associated with the processor on which the re-
quest was made. When the expiration time is reached, that hardware timer raises an
interrupt, which is handled 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 underly-
ing clock to 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 former 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
recurring 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
7 Provided for POSIX compatibility. Strongly discouraged, especially when used with timers.
68 CHAPTER 5. REAL-TIME PROGRAMMING IN C
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 correspond 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
8 There are only a few scenarios in which such timers are needed. As one safety expert at QNX is fond
of saying, even the fastest car travels very little distance in one millisecond.
5.5. EVENT LOOPS 69
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 sys-
tems 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 lim-
itations 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, including, 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 implements 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(). Various servers accept
sigevent structures embedded in messages, which allow them to deliver these events
when the conditions for delivery are met.
Ď Note
As of version 7.1 of the QNX OS, sigevent structures passed to servers must
first be registered with calls to MsgRegisterEvent(). Registration prevents
server processes from delivering unexpected (or even malicious) events to
clients. Events used with timers do not have to be registered, as these are
delivered by the microkernel. Nevertheless, it is good practice to do so, and
the example below does.
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 distinguish 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
70 CHAPTER 5. REAL-TIME PROGRAMMING IN C
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 connec-
tion 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.
5.5. EVENT LOOPS 71
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 }
72 CHAPTER 5. REAL-TIME PROGRAMMING IN C
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)");
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 }
5.5. EVENT LOOPS 73
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 }
74 CHAPTER 5. REAL-TIME PROGRAMMING IN C
Á Warning
In the next exercise we will map the GPIO control registers and use those directly
to change the state of a pin. First, rebuild the simple LED circuit from Section 4.2.
Compile the following program.
9 https://datasheets.raspberrypi.com/bcm2711/bcm2711-peripherals.pdf
5.6. CONTROLLING HARDWARE 75
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,
15 PROT_READ | PROT_WRITE | PROT_NOCACHE,
16 MAP_SHARED | MAP_PHYS, -1, 0xfe200000);
17 if (gpio_regs == MAP_FAILED) {
18 perror("mmap");
19 return EXIT_FAILURE;
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 int led_state = 0;
27 for (;;) {
28 if (led_state == 0) {
29 // Set GPIO 16 to high.
30 gpio_regs[RPI_GPIO_REG_GPSET0] = (1 << 16);
31 } else {
32 // Set GPIO 16 to low.
33 gpio_regs[RPI_GPIO_REG_GPCLR0] = (1 << 16);
34 }
35
36 led_state = !led_state;
37 sleep(1);
38 }
39
40 return EXIT_SUCCESS;
41 }
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:
qnxuser@qnxpi:~$ su -c ./gpio_map
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 propagate 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 the underlying object. Finally, MAP_SHARED
is crucial here: a private mapping would create a copy of the register contents in
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.
76 CHAPTER 5. REAL-TIME PROGRAMMING IN C
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.
2. one or more interrupt controllers, connected both to the device and to the pro-
cessor;
3. the processor.
In a multi-processor system, the interrupt controller can be configured to deliver spe-
cific interrupts to specific processors, or (depending on the controller) to a subset of
the processors, though a single processor is the usual case.
The following conditions must be met for an interrupt to be seen by the processor:
1. the device is configured to generate an interrupt upon meeting some condition;
2. the condition is met;
3. the specific interrupt is enabled in the interrupt controller;
4. the processor to which the interrupt is routed allows for interrupts to be deliv-
ered.
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.
has been determined the kernel can notify the device driver so that it can take the
necessary action.
When the device driver is done with the interrupt, it lets the interrupt controller know,
so that a new interrupt can be generated once the next event occurs. Note that the
specific interrupt is blocked between the time the processor jumps to the kernel’s
entry routine and the time the device driver is done handling it, to prevent a never-
ending sequence of jumps 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).
1 int id = InterruptAttachThread(interrupt_number,
2 _NTO_INTR_FLAGS_NO_UNMASK);
3
4 for (;;) {
5 if (InterruptWait(_NTO_INTR_WAIT_FLAGS_UNMASK, NULL) != -1) {
6 // Service interrupt
7 } else {
8 // Handle errors
9 }
10 }
Notes:
• The interrupt number passed to InterruptAttachThread() is the global in-
terrupt source identifier as provided by the BSP documentation. The value re-
turned from the call is a process-specific identifier that can then be passed to
calls to mask, unmask or detach an interrupt.
• Since the loop above uses the shortcut flag for unmasking an interrupt before
blocking, the call to InterruptAttachThread() must not itself unmask the
interrupt. The kernel keeps track of how many times an interrupt is masked
and unmasked, and it is an error to unmask too many times.
• Each thread can attach to at most one interrupt. It is possible to attach multi-
ple threads to the same interrupt. When the interrupt is delivered, the kernel
notifies all the threads attached to that interrupt. However, attaching multiple
threads to the same interrupt is rarely useful, and can be a source of trouble,
as the controller is only told to unmask the interrupt once all threads are done
handling it and unmask the interrupt. If one thread does not do that, or if it
takes a long time to handle the interrupt, the other threads will not be able to
handle new events from the device.
• The IST is a regular thread. Other than the requirement to unmask the interrupt
when it is done there are no special restrictions imposed upon it. The IST can
invoke any kernel call or any library routine. An IST that takes a long time to
service an interrupt affects only that particular device and not the system as a
whole.
• The flag _NTO_INTR_WAIT_FLAGS_FAST can be used in a call to InterruptWait()
to reduce the overhead of the kernel call. The downside of this flag is that it
cannot be used in combination with a timeout on the blocking call. If a device
driver does not need to enforce a timeout on the call then this flag provides
lower latency.
• The latency of handling a specific interrupt is determined by the priority and
processor affinity of the IST. As a general rule the IST should have its processor
affinity set to the same processor to which the interrupt is routed. The higher
the thread’s priority the lower the latency, though a high priority also requires
shorter work bursts from the thread to reduce the impact on the system.
A second method for handling interrupts is by attaching an action represented by a
signal event to the interrupt. The device driver then waits for the event and processes
the interrupt. The actions associated with signal events include delivering pulses to
a channel, emitting a signal, posting a semaphore, creating a thread and changing a
80 CHAPTER 5. REAL-TIME PROGRAMMING IN C
memory value. In practice, however, only pulse and semaphore events should be asso-
ciated with interrupts. A call to InterruptAttachEvent() associates an interrupt
number with a signal event.
Earlier we said that each interrupt handled by the system must be associated
with a thread. This is true for the case of attaching an event to an inter-
rupt. A call to InterruptAttachEvent() creates a thread that itself calls
InterruptAttachThread() and then runs a loop that dispatches the requested
event whenever InterruptWait() returns. It should be clear that using events with
interrupts does not provide any functionality that using ISTs cannot, and the latter
provides lower overhead and better control over interrupt handling. The sole purpose
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 described in Section 4.3. In
order to run the program, you will first need to kill the GPIO resource manager, which
itself attaches 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,
16 PROT_READ | PROT_WRITE | PROT_NOCACHE,
17 MAP_SHARED | MAP_PHYS, -1, 0xfe200000);
18 if (gpio_regs == MAP_FAILED) {
19 perror("mmap");
20 return false;
21 }
22
23 // Configure GPIO 16 as an output.
24 gpio_regs[RPI_GPIO_REG_GPFSEL1] &= ~(7 << 18);
25 gpio_regs[RPI_GPIO_REG_GPFSEL1] |= (1 << 18);
26
27 // Configure GPIO 20 as a pull-up input.
28 gpio_regs[RPI_GPIO_REG_GPFSEL2] &= ~(7 << 0);
29 gpio_regs[RPI_GPIO_REG_GPPUD1] &= ~(3 << 8);
30 gpio_regs[RPI_GPIO_REG_GPPUD1] |= (1 << 8);
31
32 // Clear and disable all events.
33 gpio_regs[RPI_GPIO_REG_GPEDS0] = 0xffffffff;
34 gpio_regs[RPI_GPIO_REG_GPEDS1] = 0xffffffff;
35 gpio_regs[RPI_GPIO_REG_GPREN0] = 0;
36 gpio_regs[RPI_GPIO_REG_GPREN1] = 0;
37 gpio_regs[RPI_GPIO_REG_GPFEN0] = 0;
38 gpio_regs[RPI_GPIO_REG_GPFEN1] = 0;
39 gpio_regs[RPI_GPIO_REG_GPHEN0] = 0;
40 gpio_regs[RPI_GPIO_REG_GPHEN1] = 0;
41 gpio_regs[RPI_GPIO_REG_GPLEN0] = 0;
42 gpio_regs[RPI_GPIO_REG_GPLEN1] = 0;
43
44 // Detect falling and rising edge events on GPIO 20.
45 gpio_regs[RPI_GPIO_REG_GPREN0] |= (1 << 20);
46 gpio_regs[RPI_GPIO_REG_GPFEN0] |= (1 << 20);
47
48 return true;
49 }
82 CHAPTER 5. REAL-TIME PROGRAMMING IN C
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 example 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 button), 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.
Chapter 6
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
84 CHAPTER 6. THE GPIO HEADER