Getting Started With Secure Embedded Systems - Developing IoT Systems For Micro-Bit and Raspberry Pi Pico Using Rust and Tock - 2022
Getting Started With Secure Embedded Systems - Developing IoT Systems For Micro-Bit and Raspberry Pi Pico Using Rust and Tock - 2022
Alexandru Radovici
Ioana Culic
Getting Started with Secure Embedded Systems: Developing IoT Systems for
micro:bit and Raspberry Pi Pico Using Rust and Tock
Alexandru Radovici Ioana Culic
Wyliodrin, Bucharest, Romania Wyliodrin, Bucharest, Romania
iii
Table of Contents
iv
Table of Contents
v
Table of Contents
Lifetimes�����������������������������������������������������������������������������������������������������������108
Who Is Responsible for Deallocation?���������������������������������������������������������108
Lifetime Elision Rules����������������������������������������������������������������������������������113
Generics and Trait Objects��������������������������������������������������������������������������������114
A Kind of Inheritance�����������������������������������������������������������������������������������114
Traits������������������������������������������������������������������������������������������������������������120
Generic Structures���������������������������������������������������������������������������������������131
Associated Types�����������������������������������������������������������������������������������������134
Null Values and Error Reporting������������������������������������������������������������������������135
Rust Concepts Used in Tock������������������������������������������������������������������������������141
Interior Mutability����������������������������������������������������������������������������������������141
Global Variables�������������������������������������������������������������������������������������������149
Buffer Lifetimes�������������������������������������������������������������������������������������������150
Unwrapping Values��������������������������������������������������������������������������������������152
Transforming Values������������������������������������������������������������������������������������155
Summary����������������������������������������������������������������������������������������������������������159
vi
Table of Contents
vii
Table of Contents
viii
Table of Contents
ix
Table of Contents
x
Table of Contents
xi
Table of Contents
Sticky Applications��������������������������������������������������������������������������������������506
Fault Policies�����������������������������������������������������������������������������������������������������506
Write a Custom FaultPolicy��������������������������������������������������������������������������509
Override the Fault Handler��������������������������������������������������������������������������512
System Information�������������������������������������������������������������������������������������������514
Inspecting Processes����������������������������������������������������������������������������������������515
System Status���������������������������������������������������������������������������������������������516
List Processes���������������������������������������������������������������������������������������������517
Control the System Processes���������������������������������������������������������������������518
Kernel Memory��������������������������������������������������������������������������������������������520
Summary����������������������������������������������������������������������������������������������������������520
Index�������������������������������������������������������������������������������������������������523
xii
About the Authors
Alexandru Radovici has a PhD in the field of mobile computing and
works as an Assistant Professor at the Politehnica University of Bucharest,
teaching subjects related to operating systems, compilers, and Internet of
Things. Alexandru believes in the power of education and teaching is his
passion, so 14 years ago he founded an NGO that focuses on organizing
IT educational events. Alexandru is also the co-founder and CEO of
Wyliodrin, being in touch with the latest IoT technologies. Alexandru has
been a contributor to Tock, adding boards such as the STM32 Discovery
Kit and the micro:bit and adding support for screens and touch screens.
xiii
About the Technical Reviewer
Sai Yamanoor is an embedded systems engineer working for an industrial
gases company in Buffalo, NY. His interests, deeply rooted in DIY and
open source hardware, include developing gadgets that aid behavior
modification. He has published two books with his brother, and in his
spare time, he likes to build things that improve quality of life. You can find
his project portfolio at http://saiyamanoor.com.
xv
Acknowledgments
We would like to thank the Tock Core Team for their support, especially
to Leon Schürmann (University of Stuttgart) and Branden Ghena
(Northwestern University). Their feedback has been very important and
has helped us greatly improve the book’s contents.
A special thank you goes to our colleagues Ștefan Dan Ciocîrlan and
Jan Alexandru Văduva from the University Politehnica of Bucharest for
their suggestions that helped us improve this book.
Thank you all for your support.
xvii
CHAPTER 1
Embedded Systems
and Architectures
Computers have been around us for many years. Even though most of
us think of computers as boxes attached to a keyboard, a mouse, and a
display, most of the computers used today are nothing like that. They are
machines that crunch numbers, and they are everywhere. All the home
appliances or even our cars have a computer inside; some have even
more than one (e.g., in the case of a car, there is a computer that is driving
the engine and another one that displays a friendly interface inside your
vehicle and allows you to listen to your favorite song while driving). The
Cloud we often refer to consists of large computers that look like boxes with
blinking lights stored in large cool-aired rooms. The personal computer
that we use daily, a laptop or a phone, is just the tip of the iceberg. Like
most icebergs, the great majority of computers are not visible as they are
embedded into devices, performing computations needed for the devices’
functioning. These are called embedded computers or embedded systems.
www.draper.com/
1
Computer-Architecture-and-Operation.pdf
2
Chapter 1 Embedded Systems and Architectures
Figure 1-1. The Apollo Guidance Computer (AGC, left) next to the
display and keyboard (DSKY, right) interface
Note We will discuss the Hello World problem a little bit further.
Just keep in mind that computers were much simpler at the time,
and security was not a pressing issue as most of the things were not
available for the public and were designed behind closed doors.
The user interface of the AGC, the DSKY, was specially designed for
the Apollo spacecraft mission. Figure 1-2 shows the functions it provided.
It is a little different from what we call today a display and a keyboard.
It had several status lights like Altitude (ALT); Velocity (VEL); Tracker;
a numbereh yrfd keyboard; some functional buttons like VERB, NOUN,
or ENTER; a processing indicator; and some seven segment displays for
displaying numbers.
If you think of it, this is very similar to today’s buildings’ alarm system
interfaces that have a status display and a keypad for entering the alarm
password and some functional buttons. In fact, both computers have more
or less the same specific purpose, to control an extensive system according
to particular needs. If we think further, this kind of interface is still used in
many appliances, like an oven or a coffee machine.
3
Chapter 1 Embedded Systems and Architectures
4
Chapter 1 Embedded Systems and Architectures
are pretty clear, we have smarter devices that can work with much more
information and offer us faster access to information. The downside is still
pretty much unknown. One of the significant issues the IoT field is facing is
security.
As embedded systems were usually small computers with a specific
purpose, software was written with functionality in mind rather than
security. Most systems were not accessible from the outside, so there was
no real danger of tampering. However, things are changing, and once these
devices get connected to the Internet, a remote access path is opened.
In this context, when building an IoT system, it has become the duty to
assume that anyone with access to a computer or phone connected to
the Internet may get access to the device. This brings changes to the way
software for these devices has to be designed and built. With an increasing
number of security attacks, the industry slowly acknowledges this problem
and looks for more and more ways to deploy secure software.
Throughout this book, we will tackle the security issue related to the
IoT field and focus on building and securing embedded systems. To this
end, we will look into a new operating system for embedded devices,
called Tock. The main characteristics of Tock are that it is one of the few
operating systems written in a programming language different from C and
that it has been built from the ground up with security in mind.
Now that we have a better idea of what an embedded system is and
how these devices get integrated into our lives, let’s look at the main
components of a basic embedded system.
5
Chapter 1 Embedded Systems and Architectures
• A central bus
• An Interrupt Controller
Figure 1-3 shows how these components interact with each other.
6
Chapter 1 Embedded Systems and Architectures
Further on, let’s get into the details about each of the components and
take a look at some examples of actual existing devices.
7
Chapter 1 Embedded Systems and Architectures
Before we dive into each of them, we have to define what ISA stands
for. ISA, or Instruction Set Architecture, is an instruction definition, a
standard. It defines what the instructions or commands known to a
processor are. You can think of it as a programming language specific to
each processor architecture. For instance, both Intel and AMD CPUs know
how to process the sysenter instruction as they both implement the x86
ISA. ARM processors do not know this instruction as they implement a
different ISA.
AVR
The AVR family of processors is probably one of the most used educational
and prototyping embedded systems as it contains the ATMega and ATtiny
chips that power the Arduino board. Designed in 1996 by Atmel, these
chips are 8-bit microcontrollers capable of running at around 20 MHz.
When they were created, these CPUs were more than enough for running
alarm systems or automotive computers.
Tip As a fun fact, the AVR chips, the ones that are the most popular
in education, were themselves designed by two students, Alf-Egil
Bogen and Vegard Wollan, at the Norwegian Institute of Technology
in Trondheim. The name AVR is most probably an abbreviation for Alf
and Vergard’s RISC processor.
8
Chapter 1 Embedded Systems and Architectures
PIC
The Programmable Interface Controller or PIC has been around for many
years. It was designed in 1975 and was intended for the PDP general-
purpose computers. PIC’s popularity decreased as chips like the Intel
8080 or Apple’s, IBM’s, and Motorola’s PowerPC became the standard for
general-purpose computers. Finally, it was acquired by Microchip in 1985
and repurposed as an embedded systems’ microprocessor.
PIC chips come in several flavors, ranging from 8-bit CPUs to 24-bit
CPUs. With speeds up to a few MHz, PIC chips are frequently used in alarm
systems and simple automation. Similar to AVRs, PIC CPUs are being
replaced by more powerful architectures like ARM and RISC-V.
Until the appearance of Arduino, the PIC architecture was the most
popular for embedded systems. Microchip, the manufacturer of PIC,
has recently acquired Atmel and is now producing both PIC and AVR
processors.
ARM
Formerly known as Acorn RISC Machine, now known as Advanced RISC
Machine, ARM is the most commonly used ISA for embedded systems.
Several producers like Apple, Samsung, Broadcom, NXP, Qualcomm,
STMicroelectronics, or Nordic Semiconductor provide ARM CPUs. Of
course, all of them have to pay royalties to Arm Holdings, the owner of the
ARM ISA.
9
Chapter 1 Embedded Systems and Architectures
MIPS
Designed in 1985 by MIPS Technologies, a Silicon Graphics spin-off,
MIPS CPUs have been widely used in servers, home appliances, and
automobiles. Sony’s PlayStation and PlayStation 2 used MIPS, and Renault
10
Chapter 1 Embedded Systems and Architectures
x 86/x64
The x86 ISA has been designed by Intel and licensed to AMD. This is what
is commonly known as a 32-bit system. Desktop and server computers
have been running based on these CPUs for many years. They are
being used as high-end embedded systems, mostly where Windows is a
prerequisite. In 2014, Intel designed an x86 processor series especially for
embedded systems, named the Intel Curie. Arduino 101 was powered by
this CPU.
The x64 ISA, officially known as AMD64, has been designed by AMD
and licensed to Intel. Most CPUs in desktops and servers run using this
architecture. These CPUs are a newer and more powerful version of the
x86.
As these CPUs are designed for high-power embedded systems, we
will not be discussing them in this book. We suggest reading an Embedded
Linux3 book for further details.
R
ISC-V
Probably the hottest topic in the field of embedded systems is RISC-V.
Designed by the University of California, Berkeley, RISC-V represents
Berkeley’s fifth generation of ISA. Unlike the ISAs described earlier,
RISC-V is an open ISA that does not require licensing or royalty fees. Any
manufacturer can freely implement a CPU with the RISC-V ISA.
3
Sally Gene, Pro Linux Embedded Systems, www.apress.com/gp/
book/9781430272274
11
Chapter 1 Embedded Systems and Architectures
The base standards are enough to have a fully functional CPU. A quick
note here regarding the integer numbers instruction set: it contains only
addition and subtractions. This has been specifically designed this way
to support very low-end implementations. Several extensions have been
discussed and adopted. Table 1-2 shows the extensions and their version
and status.
12
Chapter 1 Embedded Systems and Architectures
13
Chapter 1 Embedded Systems and Architectures
4
www.sifive.com/boards/hifive1-rev-b
5
www.sparkfun.com/products/15594
14
Chapter 1 Embedded Systems and Architectures
Intel and AMD devices use the motherboard bus for communication.
The bus is divided into two components: the North Bridge, used to
interconnect the CPU to the memory and video adapter, and the South
Bridge, used to interconnect the lower-speed devices, like hard drives,
network cards, or keyboard and mouse.
RISC-V has defined its open standard bus named TileLink. It is
simpler to implement than AMBA, has higher performance, and is a fully
open standard. While it was designed for RISC-V, it can be used by other
architectures.
The bus system also defines a device priority, allowing devices with
higher priority to interrupt existing transfers, if needed.
The Memory
Running software means processing data through multiple operations.
As this processing can be divided into several sequential phases, a lot
of intermediary data is generated and has to be stored temporarily. As
there is only a limited amount of storage space inside the CPU (in the
form of registers and, optionally, CPU cache memory), the CPU requires
a memory space where it can store and load data that is in process. This
is the working memory unit (called RAM – random access memory). This
memory communicates with the CPU using a high-speed bus component,
usually having the highest bus access priority. Except for some very
specialized systems, all embedded platforms have RAM.
15
Chapter 1 Embedded Systems and Architectures
Input/Output Devices
Input/output (I/O) devices are components that interact with the actual
hardware. The AGC described previously had an I/O device for
16
Chapter 1 Embedded Systems and Architectures
17
Chapter 1 Embedded Systems and Architectures
To better understand this, we need to dig a little bit into how the CPU
works. Under standard operation, the CPU reads software commands,
called instructions, one after the other, and executes them. Now let’s
18
Chapter 1 Embedded Systems and Architectures
6
These numbers are described in each system’s datasheet.
19
Chapter 1 Embedded Systems and Architectures
20
Chapter 1 Embedded Systems and Architectures
the debugger is located, can be ripped off before using the platform in
production, thus disabling any external debug systems.
The debug interface has to be disabled by hardware to prevent end
users and unauthorized technicians from tampering and modifying
devices in production environments. From the security point of view, this
debug interface is a significant security risk. Disabling it in the production
system has to be a top priority.
21
Chapter 1 Embedded Systems and Architectures
M
icrocontrollers
Microcontrollers are less powerful processing units that run at speeds
ranging from a few MHz to a few tens of MHz and have a limited amount
of RAM and a small amount of flash memory. What they lack in processing
power they make in having the ability to interface many I/O devices
out of the box. They are found in systems that have the task of precisely
controlling hardware. Usually, these systems are named microcontroller
unit (MCU). Examples of such systems are AVR, PIC, ARM Cortex-M, ARM
Cortex-R, and RV32IAM.
Usually, microcontrollers are used inside simple devices that need
to fulfill one specific task (e.g., alarm system, measure humidity, etc.).
Inside IoT systems, microcontrollers are mainly used at the sensing layer,
inside complex sensors that gather various environment information and
transmit it to the gateway via multiple protocols. The applications run are
quite simple, mostly focusing on collecting and transmitting data.
C
omputers
On the other side of microcontrollers, we have computers. These have
CPUs that are very similar and sometimes identical to standard computers.
They run at high frequencies, around 1 or 2 GHz, but they lack the ability
to control hardware directly. The Raspberry Pi7 is a perfect example: it has
four cores that run at 1.2 GHz and has up to 8 GB of RAM, so it can perform
a lot of data processing, but it needs a lot of extra I/O device connectors to
be able to interface hardware properly.
7
The Raspberry Pi foundation, www.raspberrypi.org/
22
Chapter 1 Embedded Systems and Architectures
The main difference from standard computers is that these systems are
packaged in what is called a System on a Chip (SoC). In a typical computer,
a mainboard connects the CPU with the RAM and the other hardware
inside. For the embedded system, vendors embed the whole mainboard,
CPU, RAM, and other devices into a single chip, hence the name.
In most systems’ documentation, you will find the name of the CPU
or MCU and the name of the SoC. The CPU is the actual processing unit,
while the SoC is the processing unit bundled into a single chip with all the
peripherals. Figure 1-6 shows the SoC used for the first Raspberry Pi. The
SoC’s name is BCM2835; it contains an ARM 1176JZF-S CPU, a VideoCore
GPU, a Samsung RAM memory, and other devices.
Examples of such systems are ARM Cortex-A and RV64IAM.
23
Chapter 1 Embedded Systems and Architectures
Inside an IoT system, the embedded computers are used at the edge or
fog layer, where data coming from the peripherals based on microcontrollers
is processed and some simple system logic is implemented. What is more,
these devices are used to communicate with the Cloud, both for sending
data and for receiving commands (e.g., turn on lights controlled from a
smartphone app). Because of the advanced capabilities, these devices can
implement complex protocols and security policies.
24
Chapter 1 Embedded Systems and Architectures
Hybrids
Lately, a lot of very powerful microcontrollers have emerged. We are
talking about microcontrollers that run at speeds close to 1 GHz and have
a few hundred MB of RAM. The authors of this book call them hybrids:
they have all the features of microcontrollers, which means they can fully
control hardware but have a decent amount of processing power. Examples
of such systems are the iMX-RT platform from NXP or the STM32H747XI
from STMicroelectronics found in the Arduino Portenta H7 platform.
These devices have the capability to run complex programs that
simulate an operating system and multiple processes being run at once.
25
Chapter 1 Embedded Systems and Architectures
Summary
As this book will focus on building secure applications for embedded
systems, this chapter is designed to give us an overview of the existing
hardware platforms and their capabilities. Although we will focus on the
software side from this point on, the first step in building an efficient and
secure system is to be aware of the hardware capabilities and adapt the
software accordingly.
The main characteristics of the embedded devices lie in the CPU
architecture and capabilities, memory, and the supported I/O devices.
Based on this, we can place any embedded device in one of the three
categories: microcontroller, computer, or hybrid. While microcontrollers
are simple devices, having reduced capabilities that can run only
one application at once, embedded computers can run full-fledged
operating systems and multiple processes. Finally, hybrid devices are
microcontrollers with advanced capabilities, enabling us to build IoT
systems at a reduced cost.
From the CPU point of view, we notice ARM-based devices are by far
the most popular. Therefore, in the following chapters, we will use two
ARM-based platforms to run the applications we develop: micro:bit v2 and
Raspberry Pi Pico.
26
CHAPTER 2
Embedded Systems
Software Development
Hardware systems are very important but pretty much useless nowadays
without the proper software to make them run. Hardware is more or
less like a framework that provides functions. At the same time, the
software does most of the heavy lifting and actions, and with all these
new programming languages available, developing applications seems to
be easy. If we take a closer look, we can see that it is not. In this chapter,
we will focus on presenting the characteristics of the software platforms
designed for embedded systems.
The AGC (see Chapter 1) that landed a man on the moon had 11 basic
instructions and 4 extended ones. In contrast, nowadays, CPUs support
thousands of instructions.
D
evelopment Languages
With an increasing number of programming languages being developed,
programmers nowadays can choose from a great variety based on their
needs. When it comes to embedded systems, it is essential to consider that
these devices are limited in resources. What is more, energy consumption
is also an important factor as embedded devices are designed to be
deployed and work autonomously for long periods. Further on, we will
explore the development languages used for programming embedded
devices.
A
ssembly Language
Writing software for microprocessors implies writing code using assembly
language. The instruction set together with a specific set of rules forms the
assembly language of a microprocessor. Each microprocessor has its own
instruction set and functioning rules, so each microprocessor’s assembly
language is very different.
28
Chapter 2 Embedded Systems Software Development
29
Chapter 2 Embedded Systems Software Development
Structured Programming
As writing software in assembly language turned out to be difficult,
computer system designers came up with what we call today structured
programming. Instead of writing in assembly language, that is,
instructions for the CPU, programmers could write in a very restricted
language format based on the English language. This language is then
transformed by a program called compiler into the assembly language.
Besides allowing the usage of a more familiar format, programs were
30
Chapter 2 Embedded Systems Software Development
31
Chapter 2 Embedded Systems Software Development
4. C#
5. Rust
6. Assembly language
While the first four languages have been in last year’s top five, Rust is
the newcomer. Rust is growing more and more in popularity, and in the
authors’ opinion, it will eventually replace C/C++.
P
ython
Python is a high-level programming language, allowing fast development
and easy debugging. While these features are great, it does introduce some
overhead, resulting in higher resources usage. However, Python is the
recommended language for the Raspberry Pi, an embedded computer
having advanced capabilities. We strongly recommend using it for any
embedded system that is powered by x86/x64 or ARM Cortex-A CPUs.
Using any low-level programming language like C/C++ will increase
development time and most probably introduce security issues.
1
IEEE Top Programming Languages – https://spectrum.ieee.org/
top-programming-languages/ (select only embedded)
2
Even though these languages have been take into account separately by the IEEE
article, we can safely state that they are more or less the same, so we decided to
place them together.
3
Arduino is just a set of libraries on top of C/C++.
32
Chapter 2 Embedded Systems Software Development
C/C++ and Arduino
For most production systems, C/C++ is still the preferred language. This is
mainly because most of the embedded operating systems and software are
already written in C/C++. Also, when it comes to resource constraints, C/
C++ applications can be very efficient and not waste resources. However,
writing applications using C/C++ brings a high development overhead,
as the time taken to write and debug the programs is significantly higher
when compared to other languages such as Python. What is more, many
security issues in embedded systems arise from C/C++ applications that
were poorly implemented.
Arduino is a set of libraries on top of C++ that was released together
with the Arduino hardware platform. The simplicity of the device and
the library made the Arduino the most popular educational embedded
platform. When talking about implementing simple devices, such as
sensors, Arduino is one of the most used solutions.
Assembly Language
As we have already mentioned, writing applications using the assembly
language enables us to use the system’s resources most efficiently.
However, the downside of a significant development time and the difficulty
of writing and reading the assembly code make the assembly language
unpopular.
33
Chapter 2 Embedded Systems Software Development
C
#
A high-level language, similar to Java, C# is mostly used for the
development of Windows applications. Embedded computer systems,
such as ATMs, that provide a user interface and run Windows are
developed using C#. Microsoft provides a lightweight .NET Micro
Framework which enables C# on microcontrollers. Just like Python, it
provides an easy way of developing applications while introducing a
considerable overhead. MCU systems designed to run C# are provided by
Wilderness Labs.4
R
ust
Rust is a very different story. While it offers most of the features of a high-
level language like Python, it has no overhead and allows the same level of
control as C/C++. This is why Rust is the second highest-growing language
on GitHub.5 More and more developers are now switching from C/C++ to
Rust. Rust is so powerful that developers have started using it for writing
whole operating systems. Tock is one of these systems.
There are a lot of other languages supported on low-end embedded
systems, a few examples being
4
www.wildernesslabs.co/
5
The language with the highest growth is Dart, but this is due to the fact that
Flutter, a cross-platform framework for mobile devices, is written using Dart.
34
Chapter 2 Embedded Systems Software Development
Having discussed these languages, we think that Rust is one of the best
languages for embedded systems. Designed with safety in mind, it offers
all the advantages of a high-level language while still allowing developers
full control over the hardware and software.
35
Chapter 2 Embedded Systems Software Development
C Programming and Safety
As most of the programs for embedded systems are written in C, it is
important to analyze the impact this programming language has on the
security of these platforms.
Here are a few safety issues that, from the authors’ point of view,
emerge due to the C programming language:
37
Chapter 2 Embedded Systems Software Development
The list is way longer, and we could debate a lot about these safety
features (or lack thereof ). One thing is sure, C is fast and, so far, the only
language that allows writing operating systems.
Beware of Rust
The Mozilla Foundation has always focused on providing tools for open
and secure access to the Internet. While developing and improving their
main product, the Firefox browser, they quickly realized that they had to
write it in C/C++ due to performance issues. The downside of writing the
browser engine in C/C++ is security issues. Even by following the best
development patterns available, Firefox, and browsers in general, had a
large amount of exploitable code.
38
Chapter 2 Embedded Systems Software Development
6
The Rust Foundation, https://foundation.rust-lang.org/
7
Ferrocene: Rust for critical systems, https://ferrous-systems.com/ferrocene/
8
www.drdobbs.com/jvm/the-rise-and-fall-of-languages-in-2013/240165192
39
Chapter 2 Embedded Systems Software Development
Note Firefox Quantum was the first version to use the new browser
engine.
40
Chapter 2 Embedded Systems Software Development
C
MSIS
The Cortex Microcontroller Software Interface Standard started as a
standard specification for writing libraries for ARM Cortex-M devices. If
you are planning to use ARM platforms, this is the library recommended
by the manufacturer. Several embedded operating systems such as
FreeRTOS and Mbed are based upon it.
This is a mature library and is the standard for ARM bare metal
programming. The downside is that it is limited to ARM platforms.
O
penCM3
An alternative to the CMSIS is OpenCM3. This is an open source library
for ARM platforms. Even though it is not production ready, it is a good
starting point for understanding how bare metal programming works and
how the ARM platform hardware-software interaction works. We do not
recommend using it in production.
9
The Rust Embedded Book, https://docs.rust-embedded.org/book/
41
Chapter 2 Embedded Systems Software Development
This is a very good starting point for understanding how to use Rust
with embedded systems. For each supported platform, there are usually
three crates (libraries):
10
Real-Time Interrupt-driven Concurrency Book, https://rtic.rs/0.5/book/en/
42
Chapter 2 Embedded Systems Software Development
FreeRTOS
This used to be the best option if you needed a production-ready
RTOS. Now supervised by Amazon Web Services, FreeRTOS has been one
of the first embedded operating systems. Released in 2003, this is the most
mature RTOS. It lives up to its name and is a real-time operating system
designed to run on time-sensitive systems. It is open source, written in C,
and licensed under the MIT license terms.
43
Chapter 2 Embedded Systems Software Development
As far as its architecture goes, the kernel is built together with all the
applications into a single binary. Applications are more like threads than
actual processes. From the security point of view, it has limited support for
hardware memory protection.
As for the commercial aspect, it does have a long-term support (LTS)
release cycle, meaning it is guaranteed that it will receive regular security
updates. Out of the systems presented, it supports the most extensive set of
hardware platforms.
44
Chapter 2 Embedded Systems Software Development
Mbed OS
Being more a framework than an actual operating system, Mbed OS is
ARM’s software platform for its Cortex-M chips. Open source, written in C,
and released under the Apache 2.0 license, Mbed OS works only on ARM
systems.
Its architecture allows its usage either as a set of libraries for bare
metal applications or as an operating system. Similar to FreeRTOS and
Zephyr Project, it is compiled together with all the applications into a
single binary. The main downside is that it does not support any memory
hardware security mechanism.
From the business side, it is fully released under an LTS, having
probably the best support among the systems described so far. If you
are planning to use ARM platforms, it is definitely something worth
considering.
RIOT
Even though it is far less popular than the previous systems, RIOT is
one of the best academic embedded operating systems. Engineered
initially as a research project between Freie Universität Berlin, Institut
National de Recherche en Sciences et Technologies du Numérique, and
Fortbildungsakademie der Wirtschaft Hamburg, it is a very good testing
ground for new technologies, efficiency algorithms, and new platforms. It
is one of the best-documented academic embedded systems.
From a technical standpoint, it is an open source project written in C
and released under the LGPLv2 license. The kernel is compiled together
with the applications into a single binary. It is one of the most space-
efficient systems, running with as low as 1.5 KB of RAM and 5 KB of flash
memory. Security-wise, it does make limited use of the memory hardware
protection system.
45
Chapter 2 Embedded Systems Software Development
Tock
Tock is neither the most popular nor the most efficient embedded
operating system. Still, it is the only one that can be called a complete
operating system, with a clear separation between the kernel and
applications. It is also the only one written in a different language than
C/C++. It is written in Rust. Started as a research collaboration project
between the University of Michigan; University of California, Berkeley; and
Stanford University, Tock paved the way to a new generation of embedded
operating systems that are not written in C/C++ and are built to run
untrusted applications. These applications are not directly shipped with
the kernel and may be built by third parties.
Tock has been designed from the ground up with security in mind,
and it uses all the hardware security features out of the box. The Rust
programming language enforces drivers’ security, while the clear
separation between the kernel and applications, and memory hardware
security features are enforcing application security.
Tock runs on top of ARM Cortex-M0+, M3, M4, and M7 and RISC-V
IMC and IAMC architectures.
The Tock kernel is compiled as a separate binary and may be uploaded
to embedded systems separately. Each application is compiled separately
and talks to the kernel via a real system call interface. This allows
developers to write applications in any language that is able to compile
into machine language. For now, Tock officially supports C/C++ and Rust,
while an SDK for the D language is underway.
46
Chapter 2 Embedded Systems Software Development
Summary
In this chapter, we dealt with the software side of the embedded devices
by emphasizing the security aspect. With an increasing number of
embedded systems becoming part of our lives, securing these devices
becomes of paramount importance. As many security threats are located
at the software level, analyzing the operating systems and programming
languages used for embedded devices can help us prevent a large number
of attacks.
With C being a powerful language and one of the most used in the
embedded world, some of its characteristics can lead to writing less
secure code. As a result, a new systems programming language built with
security in mind is gaining momentum. Developed initially by the Mozilla
Foundation and handed over to the Rust Foundation, Rust has become the
first programming language after C/C++ used to write an operating system,
making it even more interesting. Among the operating systems written in
Rust, Tock has gained popularity and is also used by important companies
such as Google and Western Digital.
In the following chapters, we will dig into the internals of Tock and
Rust programming for building secure embedded systems. We will analyze
both the kernel and the application space of Tock and will build basic
programs that can be easily integrated into a larger IoT infrastructure.
47
CHAPTER 3
W
hy Tock?
Tock is an embedded operating system that has emerged out of an
academic research project. Even though it might not be production ready
(yet), it has one significant advantage: it is the first full-fledged embedded
operating system written in another language but C. What is more, the
industry is also interested in using it (Google for OpenSK and OpenTitan).
It might not be the widely used operating system of the future. Still,
certainly, it is the one that will open the way to other operating systems
written in modern languages.
The authors of this book believe in the importance of having a new and
updated perspective on the embedded operating systems. All the current
production OSes are based on the same principles developed 50 years
ago. They are solid and robust but not adapted for the next generation of
devices.
With this book, we try to offer you, our reader, a new perspective.
The Kernel
The operating system per se is called the kernel. This is a piece of software
that runs along with your applications and manages all the system’s
hardware and software resources. The kernel is the one that decides which
programs have to run, which programs have to wait, which programs are
allowed to access resources like files or network connections. It is the
kernel that performs most of the tasks on a system, such as reading and
writing data from and to files, sending and receiving data to and from the
network, reading keystrokes, etc.
From the developers’ perspective, the kernel interacts with the
hardware and exposes two standard interfaces, called Application Binary
Interface (ABI) and Application Programming Interface (API), toward the
programs that run on top of it. This makes the developer’s life easy. Were it
not for the kernel, a developer would have to write a lot of lines of code to
be able to interact with the hardware. Each piece of hardware is different
and has to be interfaced differently.
50
Chapter 3 The Tock System Architecture
Just to give you an example, think about a sound card. Based on its
manufacturer, each sound card exposes a different interface which means
that it is controlled using different functions. When using a kernel, the
kernel knows the type of the sound card and how to interface it or, in
other words, what functions to call. It then exports the sound API to the
developer. As long as it is a sound card, developers will use the same kernel
API and don’t have to worry about the actual hardware. The same program
will run on any system that runs a kernel that exports the same sound ABI.
As long as the system works, the kernel is entirely invisible to the
developer and the user. The user notices the kernel only in the case of a
complete failure, such as the Blue Screen of Death (Windows) or Panic
(Linux and macOS).
The Drivers
Now that we have established what the kernel does, the following question
will pop up: how does the kernel know how to interface every possible
piece of hardware? The answer is simple: it doesn’t. The kernel has a
set of standard device interfaces that it wants to support. Examples of
such interfaces are disk drives, sound cards, graphic cards, network, etc.
Depending on the actual kernel used, these so-called device interfaces
vary a lot.
For each of these interfaces, the kernel allows developers to load
plugins, also called drivers. These drivers are the ones that are capable
of interfacing with the hardware and report to the kernel. As long as
51
Chapter 3 The Tock System Architecture
the kernel has a driver for a specific piece of hardware, developers and
programs are able to use it. If there is no driver loaded, the kernel will not
be able to interface it and export it to the developers and programs.
Think of the kernel as an extensive framework that connects drivers to
programs using standard interfaces for both of them: the driver interface
toward the drivers and the API toward the developers.
The Applications
The actual programs that are running on a system and interact with
the user are called applications. They use the kernel ABI and API for
interacting with the hardware and are usually portable. That means that
as long as we have the same operating system (kernel), the application
should run, regardless of the hardware platform.
While some applications are shipped together with the operating
system (but are not an actual part of it), most applications are installed by
the user.
Services
A service is a program that usually does something in the background and
does not interact with the user. Examples of such programs are web servers
or backup systems. These interact with several applications and provide
services to them.
52
Chapter 3 The Tock System Architecture
protection unit (MPU). Most existing OSes, like FreeRTOS and Zephyr,
do support these new features, but developers have to do a lot of work to
enable them as plugins. Tock offers all these features out of the box.
Another exciting feature that makes Tock unique is the separation
between the kernel and the applications. While for standard (computer)
OSes, this seems something normal – you have an OS like Windows, Linux,
or macOS on top of which you run several applications that you install
separately – for an embedded OS, this is not. The general idea for MCUs
is that the operating system comes as a single binary that bundles all the
applications and services. When building an application, you are actually
building the whole operating system.
When it comes to Tock, the kernel is built separately and uploaded to
the device. Afterward, each application is individually built and uploaded
separately to the board. This process mimics the behavior of a regular OS
and makes application and service updates more manageable.
53
Chapter 3 The Tock System Architecture
Figure 3-1 shows the full Tock stack. At the bottom, in red, we have the
hardware device, the MCU. It is composed out of a processing unit (CPU)
and several peripherals that add functionality (random number generator,
RNG; encryption, AES) and allow it to interact with other hardware (GPIO,
USB, I2C, ADC, SPI, UART).
54
Chapter 3 The Tock System Architecture
User Space
At the top of the Tock stack, we have the applications and services. These
are the programs that run on top of Tock. Each of them can be written in
any programming language (as long as it can be compiled for the device’s
MCU architecture) and is built separately, just like a typical computer
application.
The applications and services are running in a restricted mode, which
means that they have access only to the resources that they have been
granted by the OS and cannot directly interact with the hardware. They
can only use the OS system calls interface as defined by the ABI. Processes
may also only access memory provided to them by the kernel and may not
access memory belonging to the kernel or other applications.
As they run with some restrictions, it is said that they run in user mode.
As such, all these applications and services are sometimes called the
operating system’s user space.
While Tock does run on some MCUs without memory protection, like
some Cortex-M0 devices, it cannot restrict memory access for applications
when no MPU is present.
55
Chapter 3 The Tock System Architecture
As it can be seen, the Tock kernel does not know how to access
hardware but exposes a Hardware Interface Layer (HIL). Low-level drivers
(Figure 3-1 – in orange) provide an interface to the hardware for the upper-
level drivers (Figure 3-1 – in blue), called capsules. As we will detail later,
the Tock kernel does not actually broke these services, but it only provides
a common interface for the capsules and the drivers to talk to each other.
As the kernel requires complete device and memory access, its code
runs without any memory or access restrictions. Technically, it could
perform any action, which could be a security issue. As it is written in Rust,
it must comply with Rust’s compiler memory safety rules. This means that
56
Chapter 3 The Tock System Architecture
the kernel’s code is being checked and validated for memory errors at
compile time. This is a considerable increase in security compared to any
code written in C.
There is a catch, though. Since the kernel has to perform some specific
system management tasks, it needs to access memory directly from time
to time. In Rust, this can be done using the unsafe block. Inside this block,
the Rust compiler does not perform any checks. The Tock team has tried
to minimize the number of unsafe blocs in the kernel, but they do exist,
and they will not go away. It is simply not possible to perform some tasks
without this feature.
H
ardware Drivers
Depicted in orange in Figure 3-1, hardware drivers are small plugins that
interact directly with hardware components. They implement a HIL,
expose functionality to the kernel, and talk directly with the hardware
interface on the other side.
As for the kernel, interfacing hardware requires specific memory access
that in Rust translates to unsafe blocks. To minimize the number of unsafe
blocks, the kernel exposes a register interface that drivers can use. This
allows all drivers to use more or less the same method and code to access
the hardware directly. From a security standpoint, while a bug in the register
interface affects all drivers, the fix is also applied to all drivers at once.
57
Chapter 3 The Tock System Architecture
C
apsules
The upper-level drivers, called capsules, are responsible for offering the actual
ABI toward the applications and services running in the user space. They use
hardware drivers and other capsules via the HIL interface and provide system
calls. To make things clearer, capsules are divided into two categories:
58
Chapter 3 The Tock System Architecture
Application
SyscallDriver trait
NineDoF Capsule
NineDoF trait
Hardware Registers
Hardware
On the other hand, the Fxos800 capsule knows how to interface with
the Fxos8700 sensor located on the board. This sensor communicates
via I2C, so the Fxos8700 capsule sends and receives data to and from the
sensor using the I2C Mux capsules via the I2C HIL.
59
Chapter 3 The Tock System Architecture
S
ecurity Facts
Security is becoming an increasingly important subject as most devices
today are connected to a network or the Internet. While in the early days
of computing, security issues would result only in some computers not
running and the loss of some documents, nowadays security can have a
much greater impact on the real world.
Tock has taken a few interesting approaches when it comes to security.
A quick view of the security methods used is illustrated in Table 3-1. The
basic security rules that Tock is based on are the following:
60
Chapter 3 The Tock System Architecture
Even though the kernel uses some unsafe blocks, the amount of code
within them has decreased in time, as the Tock developers have found
ways to avoid some of them or optimize them. Unlike kernels written in C,
the Rust compiler can guarantee memory safety and consistency for most
of the source code. This is a huge advantage, and you can think of it as a
computer that proofreads the kernel before it builds it.
61
Chapter 3 The Tock System Architecture
P
rocess States
The kernel stores the state for each process. Table 3-2 describes all the
possible process states. While there are several states available, the
essential ones are Running and Yielded. These two states are the ones that
are visible to application developers.
62
Chapter 3 The Tock System Architecture
63
Chapter 3 The Tock System Architecture
State Definition
Tock processes are single threaded, which means that they can run
only one code instance at a time. The process execution can be interrupted
by the kernel or by interrupts, but execution continues from the same
point where it was interrupted. A process can schedule functions that
should be called when several events happen, but they only get called
when the process asks for them.
Note The process states in Tock are a little bit different from the
classical process states. In classical OS like Linux, the Running
state is called Ready, while Yielded is called Waiting or Blocked.
There is no corresponding state for Linux’s Running process state.
This is because Tock is a single-threaded OS, and no kernel action
can be executed in parallel with any process. Table 3-3 shows the
correspondence.
64
Chapter 3 The Tock System Architecture
S
ystem Calls
A Tock process asks the kernel for services using system calls and receives
back information through scheduled upcalls. The kernel provides seven
system calls: memop, readwriteallow, readonlyallow, subscribe, command,
yield, and exit. Table 3-4 describes each one in detail. All of these system
calls are asynchronous, which means that they return a value, usually
success or failure, immediately. If the kernel needs some time to process
the request and cannot send back a response immediately, the process
needs to subscribe and wait for an upcall from the kernel. These callbacks
are scheduled and only occur after the process has called yield. In classic
OS terminology, all the Tock system calls are asynchronous.
A process can only be interrupted in three specific cases:
1. The time slice has expired, meaning that the process
has executed without yielding more than the kernel
has it allowed to do.
2. An interrupt has arrived; the kernel will have to
handle the interrupt.
3. The process faults, and the kernel puts it in the Stopped state.
65
Chapter 3 The Tock System Architecture
0 Yield Puts the process in the Yielded state and waits for any
scheduled upcall to be run.
After the upcall returns, the process resumes as if it
was returning from the call to yield
1 Subscribe Allows a process to register a function that should be
called when a requested action has finished
This function will be run only when the process is in
the Yielded state, meaning it has previously called
the yield system call and is waiting for a scheduled
upcall. Otherwise, the callback will be queued for a
later yield call
2 Command Requests an action from a driver. The command
system call returns success or failure and up to three
numeric values
The return value usually means that the action request
has been received, and the driver will try to fulfill it
3 ReadWriteAllow Allows a process to share a buffer with a driver for
reading and writing purposes. This is usually used for
returning larger amounts of data from within a driver
It is used in conjunction with subscribe and command
system calls. Filling the buffer usually does not begin
until a command is issued. The process of reading
the result in the process occurs after the scheduled
callback function has been called
(continued)
66
Chapter 3 The Tock System Architecture
Note Linux has a few hundred system calls while Tock only has
seven. Linux has a different system call for every task, meaning that
developers usually need to call a single system call for a task. For
Tock, developers need to combine the five system calls to achieve
the same result. This is very similar to CISC vs. RISC processor
architectures, Tock being equivalent to a RISC.
67
Chapter 3 The Tock System Architecture
68
Chapter 3 The Tock System Architecture
69
Chapter 3 The Tock System Architecture
70
Chapter 3 The Tock System Architecture
error. The process can now continue doing something else, and the driver
will notify it when it has the data available.
Now here comes the tricky part. The driver will do its work in the
background while all the other processes are executing. Once it finishes
reading the data, it will schedule an upcall to the process (considering that
the process has actually subscribed to it). The process will not be notified out
of the blue, but it has to ask for an upcall. We need to get into more details
here. Tock upcalls are performed in a synchronous way. This means that
the process will never be abruptly interrupted so that a callback function
can be called. To receive an upcall, a process needs to yield. This is done by
calling the yield system call, which will stop it and place it into the Yielded
state. When a process is yielded, the kernel will check if it has any pending
(scheduled) upcalls in its task queue. If so, the kernel will place the process
into the Running state and call the first scheduled upcall in the queue. Then
the callback function registered for the upcall returns, and the process
will continue its execution as if the yield system call had returned. In other
words, the yield system call returns when a callback function returns.
71
Chapter 3 The Tock System Architecture
72
Chapter 3 The Tock System Architecture
S
hared Buffers
Looking at Figure 3-3, you might see an optional extra step in the system
call pattern: the allow system call. Subscribed callback functions have
three unsigned 32-bit (or 64, depending on the MCU word size) integers as
parameters. This means that results reported using upcalls have to fit into
three numbers. This is enough for our sensor, as it most probably reports
two numbers but might not be enough for several other drivers, such as a
network interface.
Allow RW or RO
(optional)
Buffer
Subscribe
Command
not usable by
the application
Yield
Yes
Is Yielded? Run Upcall
Upcall
no No
Callback Ran?
Postpone
yes
UnAllow RW or RO
(optional)
Buffer
73
Chapter 3 The Tock System Architecture
To get more complex data, processes can share buffers with a driver.
Unlike standard drivers in Linux or similar operating systems, Tock drivers
have to obey all the Rust compiler rules while not being allowed to use the
unsafe keyword. This means that, unlike standard drivers, Tock drivers
cannot access the process’ memory (even though drivers run with all
memory privileges). Drivers can access the process’ memory only through
a kernel interface and only as long as these processes have allowed it.
This is done through the allow system call. This system call comes in two
flavors: allow_readonly and allow_readwrite, the first one allowing drivers
to read data from that buffer and the second one allowing drivers to read
and write data from and in that buffer.
If processes need to send or receive a large amount of data from or
to the driver, they need to allow a buffer with the driver. The process still
needs to subscribe and wait for a callback from the driver. Failure to do
so may result in reading incorrect or incomplete data from the buffer, as
the process has no control over when the driver reads or writes data into
the buffer.
The application is required to unallow the buffer before accessing it.
This means either allowing a different buffer or allowing a NULL buffer.
For several functions, like sensors and I/O, Tock provides generic
drivers with a standard API. For instance, there are drivers for temperature,
humidity, motion, etc. When adding a new hardware sensor to the Tock
kernel, one does not need to write a new temperature driver, instead just
74
Chapter 3 The Tock System Architecture
The RAM component of the process memory hosts the data, stack, and
heap sections. Additionally, it has an extra area, grant, that is not accessible
to the process. The first thing that can be seen is the placement of the
stack. Instead of being placed at the top of the memory, it is placed at the
bottom. This design decision has been made to enable the detection of
75
Chapter 3 The Tock System Architecture
stack overflows. The process is faulted if it tries to write outside its memory
region. As the stack is at the bottom, an overflow results in a write outside
the process’ memory, triggering a fault. The downside of this approach
is that all processes have to specify the maximum stack size they use at
compile time, and they have to stick to it. Moreover, the stack memory
cannot be shared with other sections.
Above the stack, we find the data section. This is where all the global
variables are kept. The process itself manually initializes this section
before the main function is called. The data section size is computed
automatically at compile time by the compiler.
On top of the data section, there is the heap. This is where all the
dynamically allocated variables are kept (malloc). When starting a process,
Tock needs to know how much memory it needs to allocate for it.
76
Chapter 3 The Tock System Architecture
77
Chapter 3 The Tock System Architecture
0x0040000
Task Queue
0x003FFC8
Grant 2
0x003FFC0
Grant 1
Figure 3-5. The Grant Memory Space that stores the list of pointers
toward driver grants, the task queue, and the Process Control Block
78
Chapter 3 The Tock System Architecture
Dynamic Grants
We stated before that drivers are not able to allocate memory dynamically.
This is only partially true. Some drivers do need to allocate some
extra memory that cannot be predicted at compile time. Tock offers a
DynamicGrant structure for that. This works the same way as regular
grants but allows drivers to ask for some extra space in the process’
grant region.
79
Chapter 3 The Tock System Architecture
80
Chapter 3 The Tock System Architecture
(b)
8 12 16
Flags Checksum
Padding
Bit 0 - Enabled, Bit 1 - Sticky XOR each 4 bytes of header (excl. checksum)
(c)
(a)
TLV Element
Application Binary
0 2 4 8
Required
(d)
Figure 3-6. The Tock Binary Format and TBF Header Format
81
Chapter 3 The Tock System Architecture
Each one of these TLV elements has a type, a length, and a data
payload. The type defines how the kernel should interpret the information
from the data payload, and the length provides the size of the data payload.
Table 3-5 describes the types that can be used.
There are two types of Tock applications: relocatable and fixed address.
The first ones are the actual Tock standard applications that can be loaded
into any address in memory and flash. They are built in a particular way,
allowing the applications to work correctly regardless of where they are
loaded. The second category is mostly a fallback. Due to some compiler
issues, some applications cannot be built in this way and have to know
the load memory and address at compile time. For the time being, Rust
applications, due to an LLVM issue, and RISC-V, due to the compiler’s lack
of proper relocation, are static address apps.
82
Chapter 3 The Tock System Architecture
83
Chapter 3 The Tock System Architecture
F lashing the System
An essential step in embedded systems development is writing
the software to the device. This process is usually called flashing or
programming the device. For Tock, it means loading the binary code of the
kernel and the application’s TBF files to the MCU’s flash memory. While
for most embedded systems, the two terms flashing and programming are
usually interchangeable, the terminology used in Tock clearly separates
them. In Tock terminology, flashing means to load code via JTAG, while
programming means to load code through a serial connection to a
bootloader.
For the time being, we will use the term upload to define the process
of writing the software parts to the MCU’s flash. Figure 3-7 shows the two
ways in which software can be uploaded: flashing or programming.
84
Chapter 3 The Tock System Architecture
Application 2
Application 2
Application 1
openocd
Application 1 (or similar) tockloader
Tock kernel
Tock kernel
JTAG / SWD JTAG / SWD
Tock bootloader
Debugger Debugger
a) Flashing b) Programming
Figure 3-7. Flashing (a) and programming (b) the Tock kernel and
applications
85
Chapter 3 The Tock System Architecture
F lashing
When it comes to Tock, flashing means writing Tock software, kernel, and
apps, using the hardware debug interface. This requires special software,
like OpenOCD,1 and special hardware, like JLink or ST-Link. This method
is not specific to Tock, as any executable code can be uploaded via this
method. Figure 3-7 (a) displays this method.
However, there are two advantages when flashing:
1
OpenOCD (Open On-Chip Debugger) is an open source on-chip debugger that
links a software debugger like gdb to the hardware debugger on the device.
86
Chapter 3 The Tock System Architecture
The second point is very important. Things can go wrong when writing
software to the flash memory. For instance, you can have a power failure
that leaves the flash only half-written. This will prevent the microcontroller
from functioning properly. When using a hardware debugger, there is no
problem. You can simply try again.
Programming
Programming a microcontroller means writing software onto its flash
without the need for a hardware debugger. This lowers the cost of the
system but does imply some restrictions. The microcontroller has to run
a small piece of software called a bootloader. The bootloader works in
the following way: when the microcontroller starts, it quickly checks if
there is an update request. The way the update request is signaled differs
from design to design. Some bootloaders check if a button is pressed,
others check if there is data on the serial port, etc. If an update request
is detected, the bootloader starts reading data from a communication
port and performs several actions requested by the computer. Think of
the bootloader as a small server receiving commands from a computer.
If there is no update request, the bootloader simply runs the standard
microcontroller software placed on top of the bootloader.
Tock has its own bootloader, tock-bootloader, together with dedicated
software, tockloader, that perform these management tasks. Tockloader
is able to update the Tock kernel, load, delete, update, activate and
deactivate applications, and read board information.
This method of uploading software to the microcontroller is called
programming. It does not need any special hardware, but it does need the
small piece of bootloader software.
87
Chapter 3 The Tock System Architecture
MCU's Flash
tockloader
Application 2
openocd
Application 1 (or similar)
Tock kernel
JTAG / SWD
Debugger
88
Chapter 3 The Tock System Architecture
Summary
In this chapter, we have described the general architecture of the Tock
embedded operating system, pointing out several differences from
classical operating systems like Linux. Throughout this book, we will
discuss in detail each of the subjects presented here and provide several
development examples about their usage.
We will start with setting up the development environment, uploading
the Tock kernel, and uploading example apps written in C and Rust.
Further on, we will discuss how to write capsules and add functionality to
the Tock kernel and how to use these from the C and Rust user spaces.
Nevertheless, we will provide some guidelines on designing and
implementing complex services and applications, providing examples for
ARM, using micro:bit v2 and the Raspberry Pi Pico.
89
CHAPTER 4
Rust for Tock
We have reached the point where it’s time to start writing code for the Tock
kernel. As Tock is an operating system written in Rust, this will require
some basic Rust programming knowledge. Therefore, this chapter focuses
on some of the important aspects that the Rust programming language
brings to the table. When describing Rust particularities, we assume
that the reader is familiar with other compiled programming languages
like C/C++ and knows how pointers and memory allocation work. Some
basic knowledge about Rust’s language constructs like data types, structs,
enums, and traits is also required.
This chapter details only essential aspects of Rust’s basic data types,
structures, enums, and traits. We will focus on Rust’s references and
lifetimes.
I ntroduction to Rust
First of all, we have to get through some basic Rust notions. Please keep in
mind that this is a very brief introduction. We strongly recommend you to
read the introduction of The Rust Book.1
Rust is a programming language that focuses on being highly secure
without compromising performance. While several programming
languages like Java or Python provide a high degree of safety, it comes
1
The Rust Book - https://doc.rust-lang.org/book
Table 4-1. Rust primary data types and their value range
Name Description Values
92
Chapter 4 Rust for Tock
Special attention has to be paid to the character (char) type. Rust uses
UTF-8 characters, while C uses ASCII and Java uses UTF-16. While in C a
character is 1 byte long and in Java 2 bytes long, a character can be up to
4 bytes long in Rust. The char data type is always 4 bytes long to be able to
accommodate any Unicode character encoded using UTF-8.
93
Chapter 4 Rust for Tock
M
utability
Rust defines a concept that is not available in C or Java: mutability. By
default, all declared variables are considered immutable. This means
that once assigned, their value cannot be modified. Listing 4-1 outlines
this concept. Variable v is immutable and cannot be reassigned. On the
other hand, variable v2 is declared as mutable (using let mut) and is
reassignable.
fn main() {
let v = 1;
v = v + 1; // ERROR: v is immutable
println!("v {}", v);
let mut v2 = 1;
v2 = v2 + 1;
println!("v2 {}", v2);
}
Rust infers the actual data type of variables v1 and v2. The Rust
compiler can determine that the data type of the two variables is u32 by
looking at the value assigned to them. If we want to use different data
types, we have to either declare the variable’s data type or add a data type
to the constant. Listing 4-2 displays a rewrite of Listing 4-1, with v having a
declared type and v2 having a constant with a data type.
fn main() {
let v: u8 = 1;
v = v + 1; // ERROR: v is immutable
println!("v {}", v);
94
Chapter 4 Rust for Tock
Note For integers, Rust will automatically use the u32 data type,
while for floats, it will use f64.
Another interesting aspect is that reference data types (&Type and &mut
Type) have mutability information. For instance, in let vr = &v, the type
of vr is &u32, while in let vr2 = &mut v2, the data type of v2 is &mut u32.
95
Chapter 4 Rust for Tock
fn main() {
let v: u32 = 1; // v owns the value
let v2 = v; // who owns the value, v2 or v?
}
First, the value 1 is assigned to the variable v. That means that v owns
the value. In fact, it owns any value that is stored at that memory location.
Now what happens when v2 is assigned the value of v? Rust applies the
following rules:
The question that now follows is: how does Rust know if it can copy or
not a value? This brings us to tackling the traits aspect. In simple words,
traits describe a set of functions that can be applied to a data type. If you
are familiar with Java, traits are similar to interfaces. A class is said to
implement a particular interface if it defines all the methods described in
that interface. In contrast to Java, traits can be described for any data type.
We will cover Rust traits in depth later in this chapter.
When it comes to a copy or move, the Rust compiler will check if
the data type implements the Copy trait. If it does, it will copy the value.
Otherwise, it will move it. All primitive data types shown in Table 4-1
implement the Copy trait, and this means that the example described in
Listing 4-3 will perform a copy of the value.
96
Chapter 4 Rust for Tock
fn main() {
// v owns the value
let v: u32 = 1;
// who owns the value, v2 or v?
let mut v2 = v;
v2 = v2 + 1;
To make it clear that Rust performs a copy, Listing 4-4 goes further with
the example and changes the value of v2. When printed, v will have the
value 1, while v2 will have the value 2 as it was incremented.
While primitive data types implement Copy by default, complex types
do not. Listing 4-5 shows an example where we define a complex data
type, a structure containing a number. We assign a structure (the value) to
the variable v, then assign variable v to v2. The Rust compiler will check
whether the Number structure implements Copy. As it does not, it will
move the value. When trying to print the number within the variable v, the
compiler issues an error as the variable v does not own the value anymore
and cannot be used.
struct Number {
n: u32
}
fn main() {
let v = Number {n:1};
let v2 = v;
97
Chapter 4 Rust for Tock
S
trings
Being able to store text is a very important feature of any language. Rust
provides two distinct data types to handle Strings: the String data type,
and the String slice (&str). The String data type is defined in Rust’s
standard library (std) and uses heap allocation. On the other hand, a
String slice is a reference to a memory location of an array ([u8]) that
contains a text and that has a fixed length.
struct String {
buffer: &[u8],
len: usize,
}
98
Chapter 4 Rust for Tock
Further on, let's take a closer look at the elements of the String
structure. The buffer property is the actual storage space. This is
somehow similar to the C Strings, as it is a simple character array that
holds bytes. The useful length of the Strings is stored by len. This means
how many useful characters the String has. Remember that each character
is a UTF-8 code point, so a character might be 1 up to 4 bytes.
Now, what about the third value? We mentioned earlier that the String
stores three values, but the structure defines only two. The third value, the
capacity of the String, is the array size in bytes. Unlike C arrays, Rust arrays
and array slices (references) are described by two values: the actual data
they hold and their length. By using buffer.len(), we can retrieve the
array’s length.
An essential difference between Rust Strings and C or Java Strings is
how individual characters are accessed. Both C and Java use array access
by s[position]. This works in C as Strings are just arrays, and in Java, as
Strings store Unicode characters, each of them occupying precisely 2 bytes.
As Rust stores UTF-8, the character length is variable. Accessing a random
element in the String’s buffer might not be at a character boundary, it
might be in the middle of a UTF-8 code point. This is why Rust Strings
provide an iterator that is able to go through all the characters. Using
standard iterator functions, we can get a specific character. Listing 4-7
illustrates such an example.
99
Chapter 4 Rust for Tock
fn main() {
let s = String::from("Hello from Rust, 😄 how are you?");
println!("{}", s[10]);
// ERROR - does not implement the Index trait
println!("{}", s.chars(). nth(10).unwrap());
// uses an iterator
println!("{}", s);
}
fn main() {
let title = String::from("The Title");
let the_title = title;
// ERROR
// title does not own the value anymore
println!("title {}", title);
println!("the_title {}", the_title);
}
100
Chapter 4 Rust for Tock
fn main() {
let title = String::from("The Title");
S
tring Slices
Slices are a special kind of reference mostly used for arrays and String
variables. In the case of String, a slice is a reference towards the inner
buffer of the String. Unlike a reference to a String, slices only store a
length parameter. Their size cannot be changed.
The String slice has its type, &str. In most cases, this is what functions
will take as parameters. Rust types constant Strings as &str. This is why we
used let a = String::from ("...") instead of let a = "..." to define
a String, like in Listing 4-10.
101
Chapter 4 Rust for Tock
Tock does not use String as this data type is part of the standard
library (std) which is not used. The String data type is not available unless
the standard library is included because it uses heap allocation. This
requires a heap allocator, which is available in the standard library. On the
other hand, Tock does use String slices (&str) that are allocated in the data
section (constant Strings ".....").
B
orrowing
Now that we understand value ownership, we have to discuss an important
usage of values: passing them as arguments to a function. Let’s take
the example in Listing 4-11, which describes a function that adds two
numbers. The question that pops up is what happens with the ownership
of the values of a and b. The rules applied by the Rust compiler for function
102
Chapter 4 Rust for Tock
arguments are the same as for assignments. If the data types implement
the Copy trait, values will be copied. Otherwise, they will be moved.
fn main() {
let a = 1;
let b = 2;
let s = sum(a, b);
103
Chapter 4 Rust for Tock
fn main() {
let a = String::from("The");
let b = String::from(" Title");
let s = string_sum(a, b);
// ERROR
// the value has been moved to string_sum
println!("a {}", a);
}
The question is, how can we solve this problem? One idea would
be to clone the arguments before we send them, but this is not efficient.
Moreover, the function only needs the arguments temporarily while it
creates a new String with the contents of the parameters. When the
function has finished executing, the two variables should be usable again.
This is where the concept of borrowing comes into play. Rust allows us
to borrow the values to the function while it needs them. This means that
we still have ownership of the values. We just might have some restrictions
on using them while they are borrowed.
104
Chapter 4 Rust for Tock
105
Chapter 4 Rust for Tock
This might not seem very different from other languages, but there is
more to discuss about borrowing and references and how Rust can provide
security guarantees. We will discuss this in the Lifetimes section.
S
lices
A slice is a reference towards a part of the array. Besides the actual
reference, the slice stores its length. Listing 4-14 defines a function that
returns the highest number in an array. If we take a better look, instead
of receiving an array, it receives an array slice. From the function’s point
of view, this is the whole array. The slice has a fixed length, and its index
starts at 0.
106
Chapter 4 Rust for Tock
fn main() {
let v = [5, 4, 1, 2, 3];
The example uses the function twice. First, it creates a slice from the
whole array, which is defined exactly the same as a reference to the array.
The second time, it creates a slice starting from the 4th element in the array
(index 3) to the 5th (up to, but without, position 5).
By using slices that store the length, the Rust compiler is able to
generate code that immediately panics if an access beyond the slice’s
length is made.
As Rust data types are inferred mainly by the compiler, it is sometimes
difficult to understand which data type the compiler has inferred. To print
the actual data type of a variable, we can use a trick. Declare a variable _ of
type () and assign it the variable whose type we are trying to find out. As
long as the variable type is not (), the compiler will print an error pointing
out the actual data type. Listing 4-14 has a comment using this trick to find
out the variable type of n. From the error message, we can see the data type
of n is &u32. Listing 4-15 shows such an example.
107
Chapter 4 Rust for Tock
L ifetimes
Handling references is one of the most important tools that Rust provides.
This allows the compiler to offer several memory guarantees and
automatic memory allocation at compile time. For this section, we assume
that the reader is familiar with C language memory allocations (malloc
and free). We will compare all the examples only to C examples as Java
has garbage collection and automatically handles memory safety and
allocation (with a heavy performance toll).
108
Chapter 4 Rust for Tock
int main ()
{
char * s = strdup ("We love Rust");
char *wfw = without_first_word (s);
// free (s); <-- before printf ?
printf ("%s\n", wfw);
// free (wfw); <-- after printf ?
}
By analyzing this example, two questions pop up. First, who has to
deallocate the returned value? Second, does the function keep a reference
to the supplied value s after it returns? Such an example is the strtok
function.
In the case of the first question, is the returned value wfw a pointer
within the allocated String s, or is it a copy of the word that the function
has allocated? Listing 4-17 presents the version where the returned value
is a pointer within the supplied value s. In this case, freeing the returned
value will fail, as that value has never been allocated using malloc.
Moreover, deallocating the initial value s before the returned value is used
will result in a dangling pointer. The returned value will point towards an
area of memory that has been deallocated.
109
Chapter 4 Rust for Tock
Listing 4-17. Case one: the returned value is a pointer within the
original value s in C
char * without_first_word (char *s) {
int pos = 0;
for (unsigned int i=0; i < strlen (s); i++) {
if (s[i] != ' ') pos = pos + 1;
else break;
}
return &s[pos];
}
Listing 4-18. Case two: the returned value is a pointer within the
original value s in C
char * without_first_word (char *s) {
int pos = 0;
for (unsigned int i=0; i < strlen (s); i++) {
if (s[i] != ' ') pos = pos + 1;
else break;
}
return strdup (&s[pos]);
}
Regarding the second question, whether the function keeps the pointer
for further use after it returns, even though we cannot be sure, judging by the
function’s name, most probably it will not keep a pointer to the variable value
s after it returns. We will have another example where this is not deductible.
110
Chapter 4 Rust for Tock
Now, here comes the important part. We were able to decide how to
handle the deallocation of s and wfw based on the contents of the function.
If we had only the function declaration, as shown in Listing 4-18, we would
not be able to decide what to do. The problem is that the C compiler has
access only to this declaration, so it does not know how to handle it. This is
left at the developers’ decision. Moreover, as a developer usually includes
a lot of libraries in the application, it is almost impossible to read all the
source code of all the used functions.
Rust solves this problem by using lifetime annotations. Every reference
used in Rust has to be annotated with a lifetime. This concept might be
the key difference between Rust and any other compiled programming
language. In a nutshell, giving each reference a lifetime allows the Rust
compiler to observe and impose the connections between a function’s
input parameters and its output. Listing 4-19 shows the same example
as Listing 4-18, but written in Rust. In this case, the compiler knows that
the output is connected to the input parameter and the input parameter
cannot be freed until the function’s output is freed.
fn main() {
let s = String::from("We love Rust");
let wfw = without_first_word (&s);
// drop(s); equivalent of free (s)
println! ("{}", wfw);
// drop(s); equivalent of free (s)
}
111
Chapter 4 Rust for Tock
112
Chapter 4 Rust for Tock
To solve this problem, Listing 4-21 displays the append function with
the correct lifetime annotations. The returned value and the s parameter
have the same lifetime annotation as they are connected. The n parameter
has a different lifetime annotation has the returned value of the function
has nothing to do with it. In this way, the main function can drop s2
without having to drop title first.
113
Chapter 4 Rust for Tock
// Rule 1
fn ex(&p1, &p2);
fn ex<'a, 'b>(&'a p1, &'b p2);
// Rule 2
fn ex(&p) -> (&p1, &p2);
fn ex<'a>(&'a p) -> (&'a p1, &'a p2);
// Rule 3
fn ex(&self, &p1) -> (&p1, &p2);
fn ex<'a, 'b>(&'a self, &'b p1) -> (&'a p1, &'a p2);
A Kind of Inheritance
Rust does not support inheritance. This is a deliberate design choice.
Instead of inheriting a structure, Rust favors structure composition.
Listing 4-23 provides an example, written in Java, where MicroBit
inherits Device.
114
Chapter 4 Rust for Tock
class Device {
String mcu;
int pins;
int getPinsCount () {
return this.pins;
}
impl Device {
pub fn get_pins_count(&self) -> usize {
self.pins
}
115
Chapter 4 Rust for Tock
struct MicroBit {
device: Device,
leds: usize
}
impl MicroBit {
pub fn get_pins_count(&self) -> usize {
self.device.get_pins_count ()
}
Similar to Python and unlike Java, Rust requires functions to define the
self parameter. While in Java, the compiler automatically adds this as a
reference to the current object, Rust provides three ways of defining self:
self, &self, and &mut self. These three have different meanings and are
used in different situations:
116
Chapter 4 Rust for Tock
The downside of this is that, yes, we have to write some more lines
of code. For the MicroBit, we have to call the functions from the device
manually. This is done automatically in Java. The advantage is a little
more subtle. To avoid the Diamond Problem, Java allows only simple
inheritance. That means that a class cannot inherit more than one class
(Listing 4-25). On the other hand, Rust does not allow inheritance and
makes the developer explicitly include the two inherited structures as
elements in the new structure. When calling a method that should have
been inherited, the developer has to explicitly call one or the other, as
shown in Listing 4-26.
class Device {
protected:
string mcu;
int pins;
117
Chapter 4 Rust for Tock
public:
Device (int pins) {
this->pins = pins;
}
int getPinsCount () {
return this->pins;
}
};
}
};
}
};
};
int main () {
DevelopmentBoard board = DevelopmentBoard ();
printf ("pins %d\n", board.getPinsCount());
return 0;
}
118
Chapter 4 Rust for Tock
impl DevelopmentBoard {
pub fn get_pins_count() {
self.microbit.get_pins_count() + self.raspberry_pi.get_
pins_count()
}
}
119
Chapter 4 Rust for Tock
T raits
Despite the advantages, composition has a limitation. While in Java, any
value of type Device can be assigned an object of type MicroBit, this is
not possible in Rust. For instance, let’s write a free-standing function (in
Java, this will be a status method in the class Main) that sets all the pins of a
device to zero. Listings 4-27 and 4-28 show the functions in Java and Rust.
Listing 4-28. Setting all the pins to zero in Rust. We use a reference
to Device as the function only borrows Device
fn set_pins_zero(device: &Device) {
for pin in 0..device.get_pins_count() {
device.set_pin(i, 0);
}
}
In Java, we can call the method setPinsZero on any object that extends
Device. In Rust, this is not possible. Rust uses composition, so each
structure is a different data type. To solve this issue, Rust implements traits.
Whenever we work with a structure, we usually interact with its
methods or functions, never directly with the properties. It is very seldom
that we have a public property. Rust is based on this assumption.
A trait is similar to (but not the same as) Java interfaces. It is a
definition of methods. It can be considered similar to (but in no way the
same as) a C header. Whenever a structure states that it implements a trait,
120
Chapter 4 Rust for Tock
it has to implement all the methods that the trait defines. Listing 4-29
illustrates an example of a Pins trait that provides pin manipulation
functions.
trait Pins {
fn get_pins_count(&self) -> usize;
fn set_pin(&self, pin: usize, value: usize);
}
Now that we have a trait, let’s implement it for our structures. Unlike
Java, where interfaces are implemented directly inside the structure, in
Rust, each trait implementation has a separate block (Listing 4-30).
121
Chapter 4 Rust for Tock
122
Chapter 4 Rust for Tock
self.raspberry_pi.set_pin(
pin -
self.microbit.get_pins_count(),
value
);
}
}
}
Once we have implemented the Pins trait, all our structures have a
common ground. The next step is to rewrite the set_pins_zero function.
Listing 4-31 displays two examples of how any programmer would think
to implement the function. However, both are not correct and cannot be
compiled.
As the trait is not an actual structure but just a definition, the code
above does not work. The Rust compiler needs to know at compile-time
the size of all parameters for a function. In the first example, as Pins might
be replaced with any structure that implements the trait, the compiler has
123
Chapter 4 Rust for Tock
no way of knowing its size. In the second example, the compiler knows the
exact size as &Pins is a reference (pointer to the data and pointer to vtable)
and only throws a warning requiring us to use the dyn keyword. In future
editions of Rust, this will be an error.
In this context, there are two ways of writing the function: one is by
using generics, and the other one uses trait objects. Further on, we detail
both approaches, each one with advantages and disadvantages.
G
enerics-Based Implementation
Listing 4-32 displays the implementation based on generics. We define a
function template rather than an actual function. As the example shows,
we use the data type P, where P can be any data type that implements
the Pins trait. All three variants of the template function are identical.
Which to choose is just a matter of preference and has no impact upon the
compiled code.
fn set_pins_zero<P>(device: &P)
where P: Pins
{
for pin in 0..device.get_pins_count() {
device.set_pin(i, 0);
}
}
124
Chapter 4 Rust for Tock
When the template function is used, the compiler will create and
compile the actual function by replacing P with the actual type that
is being supplied. Listing 4-33 shows the function calls. For the first
one, the compiler will generate a set_pins_zero function where the
type P is replaced by MicroBit. For the second call, the compiler will
generate another set_pins_zero function where the type P is replaced
by RaspberryPiPico. In the compiled code, there will be two differently
declared set_pins_zero functions.
set_pins_zero(µbit);
set_pins_zero(&raspberrypi);
fn set_pins_zero__raspberrypipico(
device: &RaspberryPiPico
) {
125
Chapter 4 Rust for Tock
126
Chapter 4 Rust for Tock
While this approach can make the code more readable and generate
one single function, it does add some space overhead due to the vtable.
Before calling any of the two functions, get_pins_count and set_pin, the
compiled code has to search the vtable for the actual function offset and
then call it. This results in a small overhead in execution time.
127
Chapter 4 Rust for Tock
Caution If a data type that implements a trait or the trait itself uses
Self in one of its function’s signatures (parameters or within the
return type), using it as a trait object is not possible.
struct Complex {
re: f32,
im: f32
}
fn main() {
let number = Complex {re: 1.0, im: 2.0};
128
Chapter 4 Rust for Tock
While there are several format specifiers in Rust, two of them are of
great interest: {} - the standard format and {:?} - the debug formatter. To
be able to format a data type using the standard formatter ({}), the type
needs to implement the Display trait, while to be able to format it using
the debug formatter ({:?}), it needs to implement Debug.
129
Chapter 4 Rust for Tock
type and their values. Listing 4-36 shows a modified version of Listing 4-35
where Complex is derived using Debug, and Display is implemented.
#[derive(Debug)]
struct Complex {
re: f32,
im: f32,
}
130
Chapter 4 Rust for Tock
fn main() {
let number = Complex { re: 1.0, im: 2.0 };
G
eneric Structures
Just like functions, type definitions (structures and enums) can contain
generic types. Listing 4-37 illustrates an example implementation for
MicroBit. As there are several types of micro:bit boards having different
MCUs, we define a property named mcu. As MCUs can be very different, we
only need a data type that implements the Mcu trait. Listing 4-37 provides
an example of this approach using generics. The drawback of this is that
the code becomes more difficult to read, as each impl block has to declare
the generics.
131
Chapter 4 Rust for Tock
trait Mcu {
// ...
}
struct MicroBit<M:Mcu> {
mcu: M
// ...
}
impl<M:Mcu> MicroBit<M> {
// ...
}
On the other hand, we can use trait objects at the cost of the vtable
and indirect function calls (Listing 4-38). This makes the code more
readable. As trait objects have to be borrowed, we have to define a lifetime
parameter for the structure.
struct MicroBit<'a> {
mcu: &'a dyn Mcu
}
impl<'a> MicroBit<'a> {
//
}
132
Chapter 4 Rust for Tock
impl MicroBit {
pub const fn version() -> usize {
VERSION
}
// ...
}
fn main () {
let microbit: MicroBit<2, 20> = MicroBit{}
}
133
Chapter 4 Rust for Tock
A
ssociated Types
Another kind of generic type is the Associated Type. Let us take the
example of the MicroBit structure. Instead of using generics for the MCU,
we can define the MCU as an associated type, as shown in Listing 4-40.
Using associated types restricts the implementations of a structure to one.
If we use generic types, we can define multiple implementations for a data
type based on the trait bounds that we set. For instance, if we define the
MicroBit structure in Listing 4-37, we can write two implementations:
impl<M: Nrf> MicroBit<M> {/* ... */} and impl<MCU: Stm>
MicroBit<M> {/* ... */}. This means that the compiler will use
different functions depending on which trait bound the actual data type
M will implement. In the case of associated types, we cannot have several
implementations depending on the actual M data type.
Another small advantage of using the approach is the improved code
readability. The associated type does not have to be declared as a structure
parameter or its default and trait implementations.
struct MicroBit {
type M: Mcu;
mcu: Self::M
// ...
}
impl MicroBit {
// ...
}
fn main() {
let microbit: MicroBit<M=Nrf52833> =
MicroBit{}
}
134
Chapter 4 Rust for Tock
135
Chapter 4 Rust for Tock
136
Chapter 4 Rust for Tock
Listing 4-43. Example of usage for the unwrap and expect functions
137
Chapter 4 Rust for Tock
Listing 4-44. The Result enum used for error reporting in Rust
Unwrapping the actual value is very similar to Option. The first way is
by using the match statement as presented in Listing 4-46. The second way
of verifying whether there was an error or not is by using the is_ok and
is_err functions. These functions return true depending on the Result
138
Chapter 4 Rust for Tock
variant that is being used. The third way of unwrapping values is by using
the unwrap and unwrap_or functions. These work the same way as for
Option, meaning they panic if the value stored is Err(E).
match value {
Ok(innver_value) => {
// use inner value
}
Err(error) => {
// use error
}
}
As Result is a type that wraps values, the map, map_or, and map_or_else
functions are also available. These functions will be discussed in detail in
the Rust concepts used in Tock section.
Using Option and Result usually tends to complicate code a lot due to
the if let and match statements used to check for errors (Listing 4-47).
To streamline code writing and make code more readable, Rust introduced
the ? operator. In most cases, if a function call returns an error, the function
where the call happens should also return the same error. This is more or
less how exceptions work in Java. If a called function throws an exception,
the caller function either catches it or throws it further down the stack.
139
Chapter 4 Rust for Tock
for n in numbers {
match division (s, *n) {
Ok (nr) => s = s + nr,
Err (error) => return Err (error)
}
}
Ok(s)
}
The example in Listing 4-47 illustrates the usage of the ? operator. The
mathematics function receives a slice of numbers and uses the division
function to perform some computation. On the other hand, the division
140
Chapter 4 Rust for Tock
I nterior Mutability
This is a heavily used feature of Rust within Tock. Tock has defined all
the HILs using functions that receive an immutable, &self, reference
to the structure they are implemented for. This means that drivers
that implement these traits are not able to modify any value inside
their structure. Listing 4-48 displays an example for the ft6x06 touch
panel driver.
141
Chapter 4 Rust for Tock
S
imple Values
The driver’s structure defines a variable num_touches that memorizes
the number of touchpoints that the panel can handle. This information
depends from panel to panel, so the driver has to ask the hardware for
it. This is done using an I2C command. The I2CClient trait that is used
to receive the I2C response defines the command_complete function with
the first parameter as an immutable reference &self. This prevents the
driver from modifying the num_touches value. To be able to modify it, the
function should have been defined with a mutable reference &mut self.
The question that pops up is why the HILs have been designed like this?
The answer is simple: due to the asynchronous design of Tock. Several
entities need to hold references to the same driver that implements a HIL.
To solve these kinds of problems, Rust uses the concept of interior
mutability. As Listing 4-49 displays, instead of defining a variable of a
type T, we use generics and define a variable of type Cell<T>. When using
an immutable reference &self, the variable num_touches is immutable,
but it provides two functions get and set. These two functions allow
developers to modify the value that is stored inside the Cell type. The
value Cell<T> is immutable from the outside, but inside, the T type is
mutable through the set function.
142
Chapter 4 Rust for Tock
Tip One may say that using the get and set functions generate
overhead. However, this is, on most occasions, optimized by the
compiler.
143
Chapter 4 Rust for Tock
O
ptional Values
Sometimes drivers need to store values that might or not have an actual
value. Listing 4-50 shows an example using the same touch panel driver.
The driver receives touch events and forwards them to its client. The
client is a data structure that gets the processed touch information from
the driver. From the driver’s point of view, this client may or may not be
set. Based on the previous example, the straightforward way is to use a
Cell<Option<T>>. The Cell wrapping offers interior mutability, while the
Option provides the possibility of not having an actual value.
144
Chapter 4 Rust for Tock
// ...
}
B
uffers
Another important discussion focuses on buffers. In most cases, drivers
receive buffers at initialization and pass them up or down to several other
components. For instance, we take the example presented in Listing 4-52,
which is part of the serial port driver. At initialization, inside the new
function, the driver does not receive a buffer. Whenever someone wants to
write a buffer to the serial port, it calls the transmit_buffer function. This
is the function that receives a buffer that the driver stores.
145
Chapter 4 Rust for Tock
impl<'a> UartDevice<'a> {
pub const fn new(
mux: &'a MuxUart<'a>,
receiver: bool
) -> UartDevice<'a> {
UartDevice {
tx_buffer: TakeCell::empty(),
// ...
}
}
// ...
}
As the example shows, the driver does not use the Cell or
OptionalCell for storing the buffer. From Rust’s point of view, it could
easily do that. There is a problem though, types using these two wrappers
need to implement the Copy trait. This means that each time data is taken
out or put in, the actual data is copied. When using buffers, this approach
146
Chapter 4 Rust for Tock
has two problems: copying buffers is not ideal as it takes time, and most
importantly, we do not want to have several copies of a buffer as they
occupy a lot of memory.
To overcome these, Tock introduced two special wrappers: TakeCell
and MapCell. They have the same semantics as Cell and OptionalCell
but do not require the inner value to implement the Copy trait. The internal
value can be accessed either by taking it out by using the take function
or by using it in the map function’s closure. Listing 4-53 shows an example
using the same touch panel driver.
147
Chapter 4 Rust for Tock
This is precisely what the example in Listing 4-53 does. It calls take,
which returns an Option, and then calls map on the Option to use the inner
value. Within the map closure, it now passes the buffer to the I2C driver.
148
Chapter 4 Rust for Tock
This driver will now own the buffer. When the I2C driver completes the
request, it returns the buffer through the command_complete function. The
touch panel driver uses the data returned and then puts back the buffer
into the original TakeCell using the replace function.
All Tock drivers and the kernel rely on this pattern of using buffers.
G
lobal Variables
Tock does not allow the usage of dynamically allocated data. This means
that all the references, including drivers, have to be statically allocated.
This is in contrast with Rust’s philosophy that does not encourage the use
of mutable global variables. In Rust, these global variables are called static
variables. Safe Rust code is allowed to use only immutable static variables.
Any use of a mutable static variable is considered to be unsafe.
Just like other Rust Embedded projects, Tock provides a macro called
static_init. This allows the definition and initialization of global variables
and returns a ‘static mutable reference to them. The usage is shown in
Listing 4-54 that presents an example for the definition and initialization of
the Tock kernel.
149
Chapter 4 Rust for Tock
The first argument of the macro is the data type, while the second
argument is the initialization value. In Rust, macros have their own
context. This means that each time a macro is called, the Rust compiler
will build a new namespace. The macro defines a new variable having the
provided type with this new context and assigns the provided value to it.
There is a caveat when using this method. The actual initialization
value is first allocated on the current function’ stack and then moved
to the macro. Actually, this pattern is present in most of Rust’s libraries.
Developers rely on the fact that the Rust compiler will optimize this and
place the initialization value directly in its destination place.
Note The micro:bit will not work if Rust does not perform this kind
of optimization as the kernel stack will overflow. To make sure that
this optimization is done, Tock provides some specific arguments to
the compiler. These can be found in the Cargo.toml file placed in the
Tock’s folder.
Using this macro, developers can create static variables from anywhere
in the code. One of the most common mistakes is to consider these
values local. In Listing 4-55, the board_kernel variable is local, but it is a
reference to a global value.
B
uffer Lifetimes
The most important difference between Tock and other embedded
operating systems is that the kernel does not use any heap. All the used
memory has to be statically allocated. From the programming language’s
point of view, this means that all the used buffers must have a ‘static
lifetime. Whenever the kernel or a capsule requires a buffer, its lifetime has
to be ‘static.
150
Chapter 4 Rust for Tock
151
Chapter 4 Rust for Tock
0x38
).finalize(
components::i2c_component_helper!()
);
static mut ft6x06: MaybeUninit<
Ft6x06<'static>
> = MaybeUninit::uninit();
(
&i2c, &mut ft6x06,
&mut BUFFER, &mut EVENTS_BUFFER
)
};};
}
U
nwrapping Values
So far, we have discussed several ways to wrap mutable values
inside immutable containers. All these wrapping types, like Cell,
OptionalCell, MapCell, and TakeCell, have a set of common functions
that are heavily used throughout the kernel. We are talking here about the
map functions family, map, map_or, and map_or_else. Listing 4-56, 4-57,
and 4-58 show an example for each of these.
value.map(|inner_value| {
// use inner value
});
152
Chapter 4 Rust for Tock
The simplest of all is map. This function tries to unwrap the inner
value and call the provided closure with the inner value as the argument.
Listing 4-56 displays the definition of the function. The first parameter
is the actual wrapper (self ). This means that the map function consumes
the wrapper. In other words, it will not be available anymore after the map
function call. Of course, if the wrapper is still needed, the map’s closure
can return the value.
The function returns Option for a reason. Wrapper types might contain
or not an actual value. If there is no value within the wrapper, the map
function returns None.
One drawback of the map function is that it always returns None if there
is no value wrapped. There might be situations where we would like to
return a default value instead of None. This is what the map_or function
shown in Listing 4-57 does. It adds an extra parameter of type U, the
same type as the return type of the closure. Instead of returning None, the
function returns the default parameter if there is no inner value.
153
Chapter 4 Rust for Tock
154
Chapter 4 Rust for Tock
T ransforming Values
One of the important features of Rust is typecasting. Rust does not perform
any typecasts automatically. Developers have to specify the typecast
manually. For instance, adding a u32 value to an i32 value will generate an
error unless one of them is typecasted manually. While the compiler can
cast number types between each other, complex types require additional
code that the developers have to write.
Rust provides an interesting trait pair: From and Into. The
implementation of From and TryFrom for usize and ErrorCode is illustrated
in Listing 4-60. These traits provide a standard way to convert between
data types. Using these traits, developers can simply write value.into()
whenever a conversion is needed. In most cases, Rust will be able to infer
the data type that is required and call the appropriate into function.
An interesting fact is that only the From trait must be implemented.
Rust offers a blanket implementation for the Into trait for all types. As long
as From is implemented, Rust is able to use the inverse.
The example in Listing 4-59 implements the From trait for generating
a usize from an ErrorCode. Since the ErrorCode enum is represented as
a usize, this conversion does never fail. On the other hand, converting a
Result<(), ErrorCode> to ErrorCode might fail as ErrorCode can only
represent errors. If the Result type returns Ok(()), there is no way to
represent this as an ErrorCode. In this case, the trait used is TryFrom. This
trait tries to convert the values and returns a Result. If the conversion
can be done, it returns the value within an Ok, otherwise, it returns an
error type.
155
Chapter 4 Rust for Tock
156
Chapter 4 Rust for Tock
Tock uses the From trait a lot, primarly for converting error types.
A very good example is the I2C driver. This driver can return several
custom errors that are specific to the I2C bus. However, these errors have
to be converted to ErrorCode if they have to be sent upwards the driver
stack. Listing 4-60 displays this implementation.
157
Chapter 4 Rust for Tock
/// connected.
AddressNak,
158
Chapter 4 Rust for Tock
S
ummary
This chapter has briefly presented some of the most useful features of
Rust that Tock is using. The features presented here are the ones we
encountered during our work with Tock and believe are most relevant. At
first, some of them might seem difficult to understand and use, but the
more readers use them, the better they understand them. This chapter’s
purpose is to present the minimal Rust requirements to understand how
Tock works.
We strongly recommend our users to read additional Rust
documentation, such as The Rust Programming Language2 and The
Little Book for Macros3. Our own experience is that writing simple Tock
drivers is a great way of getting started with Rust and most of the features
presented here.
2
https://doc.rust-lang.org/book/
3
https://danielkeep.github.io/tlborm/book/index.html
159
CHAPTER 5
Getting Started
with Tock
This chapter will get you started with running the Tock operating system
and a simple application on our devices. We will focus on running the
classical “hello world” app on the micro:bit and the Raspberry Pi Pico. This
will give us a head start to building more complex secure applications and
modules in the following chapters of this book.
H
ardware Requirements
To implement the project in this chapter, you need the following hardware
components, based on the device you use:
• Micro:bit
• 1 x micro:bit v2 board
• Raspberry Pi Pico
• 1 x Raspberry Pi board.
As Tock and all the additional modules are still under development,
it is important to keep in mind the versions used when developing the
applications and pay attention to new releases and updates.
At the time of writing, the Tock operating system release is at version 2.0.
However, each week new capsules are being developed and new devices
being integrated, so if at a certain point you need a module that is not
implemented, we suggest you pay attention to the commits and the pull
requests as they might be under development and on the verge of being
released.
162
Chapter 5 Getting Started with Tock
1
Tock 2.0 Release, https://github.com/tock/tock/releases/tag/release-2.0
163
Chapter 5 Getting Started with Tock
164
Chapter 5 Getting Started with Tock
Figure 5-1 displays the correspondence between the main source code
folders.
Table 5-1. The correspondence between the Tock stack and the folder
structure
Tock project folder Tock stack layer color Implementation rules
165
Chapter 5 Getting Started with Tock
In some future chapters of this book, we will work in our own capsules
folder, as we will go through the steps necessary to add support for a new
peripheral in the Tock kernel. What is more, we will generate a simpler
project structure to work with.
Here we can find the libraries that enable the control of peripherals
such as GPIO, buttons, LED, etc. What is more, both repositories contain
plenty of application examples. The README file is also very useful in
understanding how to build and deploy the applications on the devices.
Further on, we will get through the steps necessary for downloading
the source code for the Tock kernel and the supporting libraries and
frameworks.
Environment Setup
To get started with compiling and running the Tock kernel on our devices,
we first need to install all necessary tools and libraries. This process
varies based on the system that you are working on. While the setup is
straightforward for Linux and macOS systems, things get a bit complicated
for Windows machines. This is why we will go through two possible
approaches:
166
Chapter 5 Getting Started with Tock
• Gcc for Arm - the C/C++ compiler for ARM, as both the
micro:bit and the Raspberry Pi Pico are built with ARM
processors.
167
Chapter 5 Getting Started with Tock
Tip You can use the virtual machine for Linux and macOS systems
if you prefer not to install the necessary tools on the physical
machine. However, this might bring an overhead to the setup and
device flashing processes.
168
Chapter 5 Getting Started with Tock
Linux Systems
To install the tools for Linux, open a terminal and type the commands in
Listing 5-1.
Tip For Ubuntu 14.04 and 16.04 or distributions other than Debian,
gdb-multiarch is replaced with gdb-arm-none-eabi.
169
Chapter 5 Getting Started with Tock
ACTION!="add|change", GOTO="openocd_rules_end"
SUBSYSTEM!="usb|tty|hidraw", GOTO="openocd_rules_end"
LABEL="openocd_rules_end"
MacOS Systems
To install the tools for macOS, open a terminal and type the commands in
Listing 5-3.
170
Chapter 5 Getting Started with Tock
Windows Systems
The tools necessary to run Tock on a device are not available for Windows
systems.
However, there are a couple of ways of running a Linux machine inside
the Windows environment.
171
Chapter 5 Getting Started with Tock
file containing all the necessary source code and libraries for both the Tock
kernel and the applications. The machine runs a Linux distribution and
has all the files and tools needed for running applications on top of Tock
2.0 already cloned and installed.
To download the virtual machine image, we need to access the
following link: https://tock-book.s3.us-west-1.amazonaws.com/
VM/TockDev.ova. A .ova file will be downloaded. Further on, we need
to download the tools necessary to run the virtual machine. In our case,
this would be the VirtualBox application. In addition, we have to install
the VirtualBox Extension Pack that brings extra functionalities. This is
necessary to enable the USB connection between the virtual machine and
the device.
2
https://www.virtualbox.org/wiki/Downloads
172
Chapter 5 Getting Started with Tock
Once downloaded, open the file using the VirtualBox application, and
you will be prompted for permission to install all the necessary tools. The
final step is to hit the install button (Figure 5-3).
Now that we have the setup in place, we can start a generic Linux
machine or the machine prepared for this book.
173
Chapter 5 Getting Started with Tock
If you decide to use a generic Linux machine, you need to follow the
instructions in the Linux Systems section.
To launch the prepared virtual machine, we just need to download the
.ova file and import it by selecting the File ➤ Import Appliance options in
the VirtualBox menu.
Once the Linux machine is imported, we can launch it and get started
with developing Tock applications as all the necessary tools are already
installed.
When it comes to the source code files, these are available in the
/home/tock directory, where we can find the necessary folders: tock and
libtock-c.
Tip To make sure the repositories are up to date with the latest
changes, we recommend running git pull for each of them and
switching to the tags specified below.
3
https://www.putty.org/
174
Chapter 5 Getting Started with Tock
4
Expanding the Raspberry Pi’s file system, https://elinux.org/
RPi_Resize_Flash_Partitions
175
Chapter 5 Getting Started with Tock
176
Chapter 5 Getting Started with Tock
dtoverlay=disable-bt
enable_uart=1
While most of the installed applications are from the main Raspbian
(Debian) repositories, OpenOCD needs to be installed from the source
code as we need the version adapted for the Raspberry Pi.
177
Chapter 5 Getting Started with Tock
Listing 5-5. The commands that install the necessary tools on the
Raspberry Pi
$ sudo apt update
$ sudo apt install automake autoconf
build-essential texinfo libtool libftdi-dev
libusb-1.0-0-dev git
$ git clone https://github.com/raspberrypi/openocd.git
--recursive --branch rp2040 --depth=1
$ cd openocd
$ ./bootstrap
$ ./configure --enable-ftdi --enable-sysfsgpio --enable-
bcm2835gpio
$ make -j4
$ sudo make install
$ cd ~
$ curl https://sh.rustup.rs -sSf | sh
# Proceed with installation (default)
# logout and login again after install
$ sudo apt install gcc-arm-none-eabi
$ sudo apt install gdb-multiarch
$ pip3 install --upgrade tockloader --user
$ grep -q dialout <(groups $(whoami)) || sudo usermod -a -G
dialout $(whoami)
# Note, will need to reboot if prompted for #password
$ sudo apt install minicom
178
Chapter 5 Getting Started with Tock
Note Installing all these tools can take a lot of time, do not worry if
the terminal seems to be stuck at a certain time.
Tip Using Visual Studio Code5 together with the Remote SSH
extension is a powerful solution when developing and deploying from
a Raspberry Pi that has no display.
5
Visual Studio Code, https://code.visualstudio.com/
179
Chapter 5 Getting Started with Tock
Linux Systems
For Linux systems, the commands that install the necessary tools are
described in Listing 5-6.
Tip For Ubuntu 14.04 and 16.04 or distributions other than Debian,
gdb-multiarch is replaced with gdb-arm-none-eabi.
180
Chapter 5 Getting Started with Tock
Listing 5-6. The commands that install the necessary tools on Linux
computers
$ sudo apt-get update
$ sudo apt install automake autoconf
build-essential texinfo libtool libftdi-dev libusb-1.0-0-dev
$ curl https://sh.rustup.rs -sSf | sh
#Note, will need to restart terminal after installation
$ sudo apt install gcc-arm-none-eabi
$ sudo apt install gdb-multiarch
MacOS Systems
For macOS systems, we need to open a new terminal window and type the
commands in Listing 5-7.
181
Chapter 5 Getting Started with Tock
Tip We recommend you create a new folder where you clone the
two repositories: tock and libtock-c.
182
Chapter 5 Getting Started with Tock
public, no other changes are made. Therefore, to make sure you run your
applications on top of a stable system, we recommend that you use one of
the releases of the Tock kernel. While this implies that all the new features
will not be integrated into your system, the advantage is that you can rely
on a stable version of Tock. Of course, once a new version is released, we
recommend that you switch to it.
To check the current release, we select the Releases option on the Tock
Github page6, redirecting us to a page where each release, together with its
details, is listed (Figure 5-5). Another important aspect is the name of the
release, which is required to clone the appropriate source code. At the time
of this writing, the release is tagged as release-2.0.
Figure 5-5. The release information for the Tock source code
6
Tock, https://github.com/tock/tock
183
Chapter 5 Getting Started with Tock
You are in 'detached HEAD' state. You can look around, make
experimental
changes and commit them, and you can discard any commits you
make in this
state without impacting any branches by performing another
checkout.
184
Chapter 5 Getting Started with Tock
interface with the kernel and access the peripherals. Libtock-c and libtock-
rs are the two repositories that contain the userland libraries for that, and
they enable us to build c and rust applications for Tock.
We can now explore the main directories that libtock-c consists of:
185
Chapter 5 Getting Started with Tock
7
LVGL, https://lvgl.io/
186
Chapter 5 Getting Started with Tock
187
Chapter 5 Getting Started with Tock
188
Chapter 5 Getting Started with Tock
Ulinkpro. We just need to use the OpenOCD application, which makes use
of the board’s hardware debugger to flash the device.
Next, we need to take into account that there are two main options of
deploying the kernel on the micro:bit device, which we mentioned above:
use OpenOCD directly, or use gdb on top of OpenOCD.
To get started, no matter the approach, we first need to connect the
device to the computer via USB.
189
Chapter 5 Getting Started with Tock
MEMORY
{
# with bootloader
# rom (rx) : ORIGIN = 0x00010000, LENGTH = 192K
# without bootloader
190
Chapter 5 Getting Started with Tock
Now we can build the kernel by running the make command in the
same folder. As Listing 5-12 shows, a microbit_v2.bin file is generated.
This is the kernel binary that needs to be flashed on the device. However, if
we check the release folder where the file was generated, we notice that a
microbit_v2.elf file is also there. The ELF file has the same binary code as
the BIN file but has some additional information that can be interpreted by
gdb. This is the one we will use further on.
$ cd tock/boards/microbit_v2
$ make
info: downloading component 'rust-std' for 'thumbv7em-none-eabi'
info: installing component 'rust-std' for 'thumbv7em-none-eabi'
info: using up to 500.0 MiB of RAM to unpack components
Compiling tock-tbf v0.1.0 (/Users/tock/tock/libraries/tock-tbf)
Compiling tock-registers v0.6.0 (/Users/tock/tock/libraries/
tock-register-interface)
......
Compiling nrf52_components v0.1.0 (/Users/tock/tock/boards/nordic/
nrf52_components)
Finished release [optimized + debuginfo] target(s) in 21.93s
text data bss dec hex filename
106497 0 16384 122881 1e001 /Users/
tock/tock/target/thumbv7em-none-eabi/release/microbit_v2
4121b1f7ba7cb43321dd977d393923bdbbad8100e9e9febe2bdb84169476a7ab
/Users/tock/tock/target/thumbv7em-none-eabi/release/microbit_v2.bin
191
Chapter 5 Getting Started with Tock
$openocd
Open On-Chip Debugger 0.10.0
Licensed under GNU GPL v2
For bug reports, read
http://openocd.org/doc/doxygen/bugs.html
Info : auto-selecting first available session transport "swd".
To override use 'transport select <transport>'.
cortex_m reset_config sysresetreq
adapter speed: 1000 kHz
Info : CMSIS-DAP: SWD Supported
Info : CMSIS-DAP: Interface Initialised (SWD)
Info : CMSIS-DAP: FW Version = 0255
Info : SWCLK/TCK = 1 SWDIO/TMS = 1 TDI = 0 TDO = 0 nTRST = 0
nRESET = 1
Info : CMSIS-DAP: Interface ready
Info : clock speed 1000 kHz
Info : SWD DPIDR 0x2ba01477
Info : nrf51.cpu: hardware has 6 breakpoints, 4 watchpoints
192
Chapter 5 Getting Started with Tock
$ arm-none-eabi-gdb ../../target/thumbv7em-none-eabi/release/
microbit_v2.elf
GNU gdb (GNU Tools for Arm Embedded Processors 9-2019-q4-major)
8.3.0.20190709-git
Copyright (C) 2019 Free Software Foundation, Inc.
. . .
Reading symbols from ../../target/thumbv7em-none-eabi/release/
microbit_v2.elf...
(gdb) target remote localhost:3333
Remote debugging using localhost:3333
<microbit_v2::io::Writer as kernel::debug::IoWrite>::write (
self=<optimized out>, buf=...) at boards/microbit_v2/src/
io.rs:58
58 boards/microbit_v2/src/io.rs: No such file or directory.
(gdb) load
Loading section .text, size 0x19994 lma 0x0
Loading section .ARM.exidx, size 0x10 lma 0x19994
Loading section .storage, size 0x65c lma 0x199a4
Loading section .apps, size 0x4 lma 0x40000
Start address 0x0, load size 106500
Transfer rate: 4 KB/sec, 10650 bytes/write.
(gdb) continue
Continuing.
193
Chapter 5 Getting Started with Tock
194
Chapter 5 Getting Started with Tock
$ cd tock/boards/microbit_v2
$ make flash
info: syncing channel updates for 'nightly-2020-10-25-x86_64-
apple-darwin'
info: latest update on 2020-10-25, rust version 1.49.0-nightly
(ffa2e7ae8 2020-10-24)
info: downloading component 'cargo'
info: downloading component 'clippy'
info: downloading component 'rust-docs'
. . .
** Programming Finished **
verified 95181 bytes in 1.123071s (82.764 KiB/s)
shutdown command invoked
195
Chapter 5 Getting Started with Tock
To verify that the flashing was completed, we can use again the
tockloader tool to read data from the serial line. Although there is no
application running yet, when resetting the device, a simple Initialization
complete. message is emitted. Therefore, when running the tockloader
listen command in the terminal, the output in Listing 5-18 will appear.
$ tockloader listen
No device name specified. Using default "tock"
No serial port with device name "tock" found
Found 5 serial port(s).
[0] /dev/cu.MALS - n/a
[1] /dev/cu.SOC - n/a
[2] /dev/cu.XXXX - n/a
[3] /dev/cu.YYYY - n/a
[4] /dev/cu.usbmodem145102 - "BBC micro:bit CMSIS-DAP" -
mbed Serial Port
196
Chapter 5 Getting Started with Tock
$ cd libtock-c/examples/c_hello
$ make
. . .
DIR build/cortex-m0
CC main.c
LD build/cortex-m0/cortex-m0.elf
DIR build/cortex-m3
CC main.c
LD build/cortex-m3/cortex-m3.elf
DIR build/cortex-m4
CC main.c
LD build/cortex-m4/cortex-m4.elf
DIR build/cortex-m7
CC main.c
LD build/cortex-m7/cortex-m7.elf
Application size report for target cortex-m0:
text data bss dec hex filename
844 208 2396 3448 d78 build/cortex-m0/
cortex-m0.elf
197
Chapter 5 Getting Started with Tock
By examining the result, we notice that several elf files have been
generated in the build folder. If we inspect this folder, we also see that a
c_hello.tab file has been generated. This Tock Application Bundle, or TAB,
is nothing more than a tar archive containing a manifest and the same TBF
(Tock Binary Format) file compiled for several architectures (Cortex-M0,
M3, M4, M7). This is also the file that tockloader uses to deploy the
application on a device.
Therefore, the only remaining step is to run tockloader to install the
application, as shown in Listing 5-20.
198
Chapter 5 Getting Started with Tock
To actually see the message, we have to reset the device by pressing the
button on the back of the board after running tockloader listen.
Listing 5-21. Listen for messages from the device using tockloader
$ tockloader listen
[INFO ] No device name specified. Using default name "tock".
[INFO ] No serial port with device name "tock" found.
[INFO ] Found 5 serial ports.
Multiple serial port options found. Which would you
like to use?
[0] /dev/cu.SOC - n/a
[1] /dev/cu.MALS - n/a
[2] /dev/cu.XXXX - n/a
[3] /dev/cu.YYYY - n/a
[4] /dev/cu.usbmodem144102 - "BBC micro:bit CMSIS-DAP" -
mbed Serial Port
199
Chapter 5 Getting Started with Tock
200
Chapter 5 Getting Started with Tock
Once we power on both devices, we can get started with building and
deploying the Tock kernel.
201
Chapter 5 Getting Started with Tock
In both cases, we will use the serial connection to print data coming
from the Pico. Therefore, no matter the approach, the first step is to open a
new terminal and run the command in Listing 5-22. This will listen for data
coming from the Pico using the UART communication.
Listing 5-22. Open serial connection between the Pi and the Pico
OPTIONS: I18n
Compiled on Aug 13 2017, 15:25:34.
Port /dev/serial0
This shell instance will be used only for printing data coming on the
serial line.
202
Chapter 5 Getting Started with Tock
the first terminal we have opened and is running minicom. This means
that Tock is running on the Pico board.
$ cd tock/boards/raspberry_pi_pico/
$ make flash
Finished release [optimized + debuginfo] target(s) in 0.64s
text data bss dec hex filename
77828 0 8192 86020 15004 /home/pi/
tock/target/thumbv6m-none-eabi/release/raspberry_pi_pico
openocd -f openocd.cfg -c "program /home/pi/tock/target/
thumbv6m-none-eabi/release/raspberry_pi_pico.elf; verify_image
/home/pi/tock/target/thumbv6m-none-eabi/release/raspberry_pi_
pico.elf; reset; shutdown;"
Open On-Chip Debugger 0.10.0+dev-gf8e14ec97-dirty
(2021-06-09-13:10)
Licensed under GNU GPL v2
For bug reports, read
http://openocd.org/doc/doxygen/bugs.html
Info : Hardware thread awareness created
203
Chapter 5 Getting Started with Tock
if we check the release folder where the file was generated, we can notice
that a raspberry_pi_pico.elf file is also there. This has the same binary
code but includes additional information that can be interpreted by gdb,
and this is the one we will use further on.
$ cd tock/boards/raspberry_pi_pico
$ make
make
info: downloading component 'llvm-tools-preview'
. . .
Finished release [optimized + debuginfo] target(s) in 5m 04s
text data bss dec hex filename
57345 0 8192 65537 10001 /home/
pi/tock/target/thumbv6m-none-eabi/release/raspberry_pi_pico
6a3c56d4f78a98bcdf0f75e16d6571b3656c385a4b433675237410e6fbf11f16
/home/pi/tock/target/thumbv6m-none-eabi/release/raspberry_pi_
pico.bin
The next step is to open a new terminal window and type the
command in Listing 5-25. This will use OpenOCD to open a connection
to the Raspberry Pi Pico. If no error messages appear, the connection
has been successfully established. Next, we leave this terminal open and
switch to the previous one.
204
Chapter 5 Getting Started with Tock
In the same terminal where we have built the kernel, we run the
commands in Listing 5-26. By using gdb-multiarch, we can use the
OpenOCD connection to transfer information to and from the device.
We first run gdb with the elf file as a parameter, then initiate a remote
connection to localhost port 3333. This is the port where OpenOCD listens
for connections. Finally, we run load and continue, which will run the
kernel on the device.
$ gdb-multiarch ~/tock/target/thumbv6m-none-eabi/release/
raspberry_pi_pico.elf
GNU gdb (Raspbian 8.2.1-2) 8.2.1
. . .
Reading symbols from target/thumbv6m-none-eabi/release/
raspberry_pi_pico.elf...done.
(gdb) target remote localhost:3333
Remote debugging using :3333
0x10006ae4 in cortexm::support::atomic (f=...)
at arch/cortex-m/src/support.rs:30
30 asm!("cpsie i", options(nomem, nostack));
(gdb) load
Loading section .apps, size 0x1 lma 0x20020000
Loading section .text, size 0x100 lma 0x10000000
Loading section .text, size 0xd024 lma 0x10000100
Loading section .ARM.exidx, size 0x10 lma 0x1000d124
Loading section .storage, size 0xecc lma 0x1000d134
205
Chapter 5 Getting Started with Tock
As the kernel is run, the first terminal, where the serial connection
was established using minicom, will show the message Initialization
complete. Entering main loop. This means that the kernel is running on the
Raspberry Pi Pico device.
206
Chapter 5 Getting Started with Tock
$ cd tock/boards/raspberry_pi_pico
$ make
make
info: downloading component 'llvm-tools-preview'
. . .
Finished release [optimized + debuginfo] target(s) in 5m 04s
text data bss dec hex filename
57345 0 8192 65537 10001 /home/
pi/tock/target/thumbv6m-none-eabi/release/raspberry_pi_pico
6a3c56d4f78a98bcdf0f75e16d6571b3656c385a4b433675237410e6fbf
11f16 /home/pi/tock/target/thumbv6m-none-eabi/release/
raspberry_pi_pico.bin
If we check the release folder where the file was generated, we can
notice that a raspberry_pi_pico.elf file is also there. This has the same
binary code, but includes additional information that can be interpreted
by gdb and this is the one we will use further on.
207
Chapter 5 Getting Started with Tock
Caution If you are on a Linux machine, you will use the gdb-
multiarch command, while on macOS, the same is done using
arm-none-eabi-gdb. Listing 5-29 shows the commands necessary
for macOS. For Linux machines, replace arm-none-eabi-gdb with
gdb-multiarch.
208
Chapter 5 Getting Started with Tock
As the binary is flashed and Tock is run on the device, we can notice
the message Initialization complete. Entering main loop in the terminal
where minicom is running. This signals that Tock is running on the
Raspberry Pi Pico.
209
Chapter 5 Getting Started with Tock
$ cd libtock-c/examples/c_hello/
$ make
. . .
Application size report for target cortex-m0:
text data bss dec hex filename
844 208 2396 3448 d78 build/cortex-m0/
cortex-m0.elf
Application size report for target cortex-m3:
text data bss dec hex filename
840 208 2396 3444 d74 build/cortex-m3/
cortex-m3.elf
Application size report for target cortex-m4:
text data bss dec hex filename
840 208 2396 3444 d74 build/cortex-m4/
cortex-m4.elf
Application size report for target cortex-m7:
text data bss dec hex filename
840 208 2396 3444 d74 build/cortex-m7/
cortex-m7.elf
The make command generates an elf file for each of the supported
architectures. What the output of the make command does not show is
that a TBF (Tock Binary Format) file for each of the architectures has also
been generated. This is the file that we need to use. In our case, we will use
the build/cortex-m0/cortex-m0.tbf file.
To flash the application on the device, we navigate to tock/boards/
raspberry_pi_pico and run the command in Listing 5-31. This will
bundle the TBF file with the Tock kernel and deploy the resulted binary on
the device.
210
Chapter 5 Getting Started with Tock
Listing 5-31. Flash the Tock kernel and the application on the
Raspberry Pi Pico
$APP=../../../libtock-c/examples/c_hello/build/cortex-m0/
cortex-m0.tbf make program
Once the command finishes its execution, the Hello World! message is
printed in the terminal where minicom is running.
Listing 5-32. Deploy the kernel and application bundle using gdb-
multiarch
$ gdb-multiarch ~/tock//target/thumbv6m-none-eabi/debug/
raspberry_pi_pico-app.elf
GNU gdb (Raspbian 8.2.1-2) 8.2.1
Copyright (C) 2018 Free Software Foundation, Inc.
(gdb)target remote localhost:3333
211
Chapter 5 Getting Started with Tock
. . .
(gdb) load
Loading section .text, size 0x100 lma 0x10000000
. . .
(gdb) continue
Continuing.
The result is similar, in the third terminal where minicom runs, the
Hello World! message is displayed.
Tip The Pico does not have a reset button. A simple way to reset
it is to run the monitor reset init command followed by the
continue command in gdb.
212
Chapter 5 Getting Started with Tock
S
ummary
Starting with this chapter, we can use the Raspberry Pi Pico and micro:bit
v2 devices to deploy the Tock kernel and C applications on top of it. Based
on the purpose, we can choose between several deployment approaches.
The common ground for all of them is that we need a hardware and
software debugger. While the hardware debugger is different for the Pico
and the micro:bit, the software tool is the same for both devices, called
OpenOCD. Further on, we can choose to use OpenOCD on its own, or
together with gdb so we can make use of features such as breakpoints or
memory inspection to debug our software.
This chapter might seem a bit complex for a getting started guide,
but it deals with all the possible approaches of deploying Tock and its
applications on the devices so you can choose the one that fits your
requirements best.
213
CHAPTER 6
The Structure of a
Custom Tock System
In the previous chapter, we followed the necessary steps to build and
deploy the Tock kernel together with a simple single-process application
based on libtock-c. To achieve this, we cloned the tock and libtock-c
repositories, then we compiled and deployed the source code according to
the device’s characteristics and the specific use-case.
However, there is a major issue with this process. The Tock kernel
that we compiled has a different folder for each device it supports, and
many implemented capsules and HIL files. When we aim to run a simple
application on a micro:bit or a Raspberry Pi Pico device, many of the
existing files are unnecessary. Their complex structure can make the
project structure hard to follow. What is more, tampering with the Tock
kernel repository is not recommended. The repository should be used only
as a foundation on top of which we build our projects.
In a nutshell, we need to think of a better way of structuring our
projects, highlighting the files that we need to create and work with while
the tock and libtock-c repositories are only as support files.
This chapter will focus on how we can create a new project with a
straightforward structure and where the files we need to work with are
easily accessible.
To get started, we first create a new empty git repository called project.
This is the folder where we will create the rest of the project structure.
This structure can be divided into two main components: kernel and
application.
216
Chapter 6 The Structure of a Custom Tock System
Listing 6-1. Link the Tock kernel repository in the project folder
$ cd project
$ git init
$ git submodule add https://github.com/tock/tock.git
$ cd tock
$ git checkout tags/release-2.0
$ cd kernel
$ cp -r ../tock/boards/microbit_v2 microbit_v2
217
Chapter 6 The Structure of a Custom Tock System
Besides the source code and other configuration files necessary for
running the code on the device, this folder also contains the Makefile that
automates the build and deployment of the kernel, and the Cargo.toml file
necessary for building the source code.
B
uild Information
To ensure that the source code of the device package can be built, we need
to adapt the Cargo.toml file.
Note Cargo is the package (crate) manager for Rust, and Cargo.toml
is the manifest file that contains information such as name, version,
and dependencies, for the Rust crate where it resides.
The first lines in the file consist of general information about the crate,
which we can leave unchanged. The following section, [dependencies],
links the crates that need to be included in the build. However, since we
copied the folder from the tock directory, we need to change the paths to
target the appropriate crates in the kernel folder.
What is more, we added another crate called drivers. This points
towards a folder that we create, where the capsules that we write will be
placed (Listing 6-4). More information on this folder is presented in the
following section.
218
Chapter 6 The Structure of a Custom Tock System
[dependencies]
cortexm4 = {path = "../../tock/arch/cortex-m4"}
capsules = { path = "../../tock/capsules" }
kernel = { path = "../../tock/kernel" }
nrf52 = { path = "../../tock/chips/nrf52" }
nrf52833 = {path = "../../tock/chips/nrf52833"}
components = { path = "../../tock/boards/components" }
nrf52_components = { path = "../../tock/boards/nordic/nrf52_
components" }
Finally, we have to add two more target properties that specify details
about the build process. This is necessary because the tock repository has
a Cargo.toml file located in the root of the folder hierarchy, and the build
information is placed there. Now, as the hierarchy changes, we need to
copy that information in this configuration file.
The build target properties define how the build is done for a
development or a release binary. Listing 6-5 illustrates the two targets’
properties that need to be added to the file. They are the same no matter
what device you use.
Listing 6-5. The complete Cargo.toml file for the device package
[profile.dev]
panic = "abort"
lto = false
opt-level = "z"
debug = true
219
Chapter 6 The Structure of a Custom Tock System
[profile.release]
panic = "abort"
lto = true
opt-level = "z"
debug = true
codegen-units = 1
include ../../tock/boards/Makefile.common
INCLUDE ../../tock/boards/kernel_layout.ld
220
Chapter 6 The Structure of a Custom Tock System
Listing 6-8. The full Cargo.toml file for the drivers package
[package]
name = "drivers"
version = "0.1.0"
authors = ["Tock Project Developers <tock-dev@
googlegroups.com>"]
edition = "2018"
[dependencies]
kernel = { path = "../../tock/kernel" }
enum_primitive = { path = "../../tock/libraries/enum_primitive" }
tickv = { path = "../../tock/libraries/tickv" }
221
Chapter 6 The Structure of a Custom Tock System
#![forbid(unsafe_code)]
#![no_std]
The first line in the file specifies that no unsafe code can be used. The
second is used to exclude the standard Rust library from the crate. Usually,
the standard library is included by default. However, the standard library
depends on the operating system and since we are building one, using it is
not possible in this context.
222
Chapter 6 The Structure of a Custom Tock System
$ cd project
$ git submodule add https://github.com/tock/libtock-c.git
$ cd libtock-c
$ git checkout tags/release-2.0
223
Chapter 6 The Structure of a Custom Tock System
#pragma once
void example_driver_action(void);
The first line in example_driver.h specifies that this source file cannot
be included more than once in the same application. For instance, if we
create a file, peripheral.h, that includes example_driver.h, and another
file that includes peripheral.h and example_driver.h, the compiler will
ignore the second include of example_driver.h.
#include "tock.h"
#include "example_driver.h"
void example_driver_action(void) {}
When generating the Makefile, we need to consider that all the files
in drivers are exported as a library that we include in our applications.
In addition, we need to link libtock when compiling the source code
(Listing 6-13) to have access to the Tock API functions.
224
Chapter 6 The Structure of a Custom Tock System
override CFLAGS +=
-I$(TOCK_USERLAND_BASE_DIR)/libtock
include $(TOCK_USERLAND_BASE_DIR)/TockLibrary.mk
#include <stdio.h>
#include "example_driver.h"
int main(void) {
printf ("Hello World!\r\n");
example_driver_action ();
return 0;
}
225
Chapter 6 The Structure of a Custom Tock System
On the other hand, the Makefile takes into account all the necessary
libraries we need to link to our application, such as drivers and libtock-c
(Listing 6-15). We use one Makefile from the libtock-c/examples folder,
modify its paths and add the drivers library.
226
Chapter 6 The Structure of a Custom Tock System
$ cd project
$ ln -s tock/rust-toolchain .
B
uild the Project
Now that we have defined the project folder structure, all that is left is to
build it. The kernel and the applications are built separately.
B
uild the Kernel
To build the kernel, we go to the kernel/microbit_v2 or kernel/
raspberry_pi_pico folder and run the make. Next, we follow all the steps
that have been described in the previous chapter.
227
Chapter 6 The Structure of a Custom Tock System
B
uild an Application
We have to build each application individually. To build an application, we
go to its folder, for example, applications/example_app, and run make.
This will build the application together with all its dependencies.
S
ummary
In this chapter, we described all the steps necessary to build a new,
standalone project based on the Tock kernel. To ensure that you followed
all the steps correctly, Listing 6-17 displays the folder hierarchy starting
from the project folder.
228
Chapter 6 The Structure of a Custom Tock System
$ tree project
$ .
├── applications
│ ├── drivers
│ │ ├── Makefile
│ │ ├── example_driver.c
│ │ └── example_driver.h
│ └── example_app
│ ├── Makefile
│ └── main.c
├── kernel
│ ├── drivers
│ │ ├── Cargo.toml
│ │ └── src
│ │ └── lib.rs
│ └── microbit_v2
| ├── ...
│ ├── ...
├── libtock-c
│ ├── ...
│ ├── ...
├── rust-toolchain -> tock/rust-toolchain
└── tock
├── ...
├── ...
229
Chapter 6 The Structure of a Custom Tock System
Tip To use the project structure multiple times, you can upload it on
Github1 as a template project. You can also use the sample template
project that the authors of this book have prepared by accessing the
following link: https://github.com/WyliodrinEmbeddedIoT/
tock-project.
1
https://github.com
230
CHAPTER 7
Userspace
Applications
Development
For a process to interact with the Tock kernel, which includes the drivers,
it needs to make a system call. The Tock architecture is built around seven
such system calls: yield, subscribe, command, read-write allow, read-only
allow, memop, and exit. In a nutshell, any application that we aim to run
on top of the Tock kernel needs to make these calls to access the resources
like peripherals, storage space, or sensors.
However, including the system calls in the application is a tedious
process. This means that we have to carefully analyze each driver and
the necessary calls together with their parameters to obtain the desired
behavior.
As a result, each Tock driver has a C API library that we can use
to make writing applications easier. The API libraries expose generic
functions that enable the application developers to easily interact with the
peripherals. These functions offer an abstraction layer on top of the drivers
and the hardware that we use.
Hardware Requirements
To implement the project in this chapter, you need the following hardware
components, based on the device you use:
• Micro:bit
• 1 x micro:bit v2 board
• Raspberry Pi Pico
• 1 x Raspberry Pi board.
First of all, we take a look at the gpio.h and gpio.c files located
in libtock-c/libtock. These files expose the constants and functions
necessary to control and read values from the GPIO pins.
232
Chapter 7 Userspace Applications Development
The exposed functions are declared in the gpio.h file (Listing 7-1).
#pragma once
#include "tock.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef enum {
PullNone=0,
PullUp,
PullDown,
} GPIO_InputMode_t;
typedef enum {
Change=0,
RisingEdge,
FallingEdge,
} GPIO_InterruptMode_t;
233
Chapter 7 Userspace Applications Development
#ifdef __cplusplus
}
#endif
#include "gpio.h"
234
Chapter 7 Userspace Applications Development
return tock_command_return_u32_to_returncode
(rval, (uint32_t*) count);
}
235
Chapter 7 Userspace Applications Development
All these actions are enabled by the timer library that exposes the
following functions:
Tip All yield functions are used to make the process ask for any
incoming interrupts and handle them.
236
Chapter 7 Userspace Applications Development
Note When the main function ends, the process does not finish but
automatically calls yield. This is due to the implementation of the
libtock-c library. This means that if a callback function is registered,
it will fire even after the main function finishes its execution. Please
note that this might change in future versions.
237
Chapter 7 Userspace Applications Development
A
pplication Example
To make a simple application such as an LED blink, we use both the timer
and the GPIO libraries. In the application illustrated in Listing 7-3, we use
gpio_set and gpio_clear to turn the LED on and off and the delay_ms
function to make the process stop for 1 second so we can actually see the
LED blinking.
238
Chapter 7 Userspace Applications Development
#include <gpio.h>
#include <timer.h>
int main(void){
gpio_enable_output(25);
while (1){
gpio_set (25);
delay_ms (1000);
gpio_clear (25);
delay_ms (1000);
}
}
In the case of the Raspberry Pi Pico, the code above blinks the onboard
LED as this is connected on pin 25. However, the pin is not exposed by
default as it is mapped to the LED driver. Therefore, we need to expose the
LED, then run the application. This is done by adding pin 25 to the gpio
structure in the kernel/raspberry_pi_pico/src/main.rs file, around line
320. Listing 7-4 illustrates the new structure.
239
Chapter 7 Userspace Applications Development
24 => &peripherals.pins.get_pin(RPGpio::GPIO24),
25 => &peripherals.pins.get_pin(RPGpio::GPIO25),
For the micro:bit, we need to use one of the pins exposed by the device
as there is no default LED that we can control via GPIO. We recommend
connecting pin 0 to an LED via a micro:bit shield or crocodile clips and
changing the GPIO number from 25 to 0 in the application code.
Note The LEDs on the micro:bit are not directly connected one to
one to the GPIO pins and can only be used with the LED driver.
240
Chapter 7 Userspace Applications Development
A
pplication Example
As we specified in the section above, the Raspberry Pi Pico has an onboard
LED that we can control and which is connected on pin 25. On the other
hand, on the micro:bit, there are 25 such LEDs, numbered from 0 to 24.
Listing 7-5 displays an application that can be run on both devices to
make the LED 0 blink every 1 second.
#include <led.h>
#include <timer.h>
241
Chapter 7 Userspace Applications Development
int main(void){
tock_timer_t timer;
timer_every(1000, timer_cb, NULL, &timer);
while (1){
yield ();
// do other tasks
}
}
Note In this case, as the main function ends after the callback is
registered, calling yield is not necessary. We chose to add it to
the code to exemplify how this would work for other cases. Just
to be on the safe side, we recommend adding this to the end of all
applications that still have pending callbacks.
242
Chapter 7 Userspace Applications Development
Since buffered readings are not available when using the Raspberry Pi
Pico and micro:bit devices, in this book, we will use only the synchronous
functions that return the read value immediately:
Application Example
The best application example for the ADC library is in the libtock-c/
examples/adc directory. We recommend you copy the main.c file in the
applications/example_app folder and compile it for the micro:bit or the
Raspberry Pi Pico.
243
Chapter 7 Userspace Applications Development
To connect some sensors on one of the ADC pins, you can use
crocodile clips for the micro:bit and a breadboard with jumper wires for
the Pico. We also recommend connecting the Pico to the Pico Explorer
expansion if you have one.
Reading The Temperature
The temperature.h file exposes three main functions to be used for
synchronous or asynchronous temperature readings. To use this library,
the device needs to have a temperature sensor connected and the kernel to
provide a driver for it. Both the Raspberry Pi Pico and the micro:bit have a
temperature sensor and the driver for it. As such, both devices are capable
of running applications that include this library.
The library exposes both synchronous and asynchronous functions:
244
Chapter 7 Userspace Applications Development
Reading The Motion
Tock provides a motion sensor library that can be used with devices like
the micro:bit, which is equipped with a motion sensor. This translates
to three different readings on the X, Y, and Z axes. The library that
exposes the functions for this is called ninedof. The library’s name
comes from the ninedof (nine degrees of freedom) sensor that reads the
acceleration, gyroscope, and compass for the three axes, resulting in nine
different values.
Some devices might support only a part of the three values, like the
micro:bit that has an LSM303AGR motion sensor with an accelerometer
and a magnetometer. Therefore, we can use only part of the functions that
the library exposes:
245
Chapter 7 Userspace Applications Development
A
pplication Example
To exemplify the use of the sensors’ libraries, we create an application that
reads the values from all the sensors and prints them in the console using
printf (Listing 7-6). We use only synchronous functions for this.
The complete application can be run on the micro:bit. For the
Raspberry Pi Pico, only the temperature can be read, as the other sensors
are not integrated into the device. For the other sensors, the driver_
exists function will return false, and nothing will happen.
The gyroscope functions will return success for the micro:bit as the
motion driver is present. As there is no gyroscope hardware, the returned
values are only zeros.
#include <stdio.h>
#include <humidity.h>
#include <temperature.h>
#include <ninedof.h>
unsigned humi = 0;
int temp = 0;
int ninedof_accel_x = 0;
int ninedof_accel_y = 0;
int ninedof_accel_z = 0;
246
Chapter 7 Userspace Applications Development
int ninedof_magneto_x = 0;
int ninedof_magneto_y = 0;
int ninedof_magneto_z = 0;
int ninedof_gyro_x = 0;
int ninedof_gyro_y = 0;
int ninedof_gyro_z = 0;
int main(void){
if (driver_exists(DRIVER_NUM_TEMPERATURE)){
temperature_read_sync(&temp);
printf("Temperature: %d C\n", temp/100);
}
if (driver_exists(DRIVER_NUM_NINEDOF)){
bool rc = ninedof_read_acceleration_sync(
&ninedof_accel_x, &ninedof_accel_y,
&ninedof_accel_z);
if (rc == RETURNCODE_SUCCESS){
printf("Acceleration: X: %d Y: %d Z: %d\n",
ninedof_accel_x, ninedof_accel_y,
ninedof_accel_z);
}
rc = ninedof_read_magnetometer_sync(
&ninedof_magneto_x, &ninedof_magneto_y,
&ninedof_magneto_z);
if (rc == RETURNCODE_SUCCESS){
printf("Magnetometer: X: %d Y: %d Z: %d\n",
ninedof_magneto_x, ninedof_magneto_y,
ninedof_magneto_z);
}
rc = ninedof_read_gyroscope_sync(
&ninedof_gyro_x, &ninedof_gyro_y,
&ninedof_gyro_z);
247
Chapter 7 Userspace Applications Development
For the micro:bit, both the temperature and the ninedof sensors are
available. For the ninedof sensor, we store the return code and check
that it equals to RETURNCODE_SUCCESS. This is to make sure that all three
components of the sensor are working and return valid readings.
248
Chapter 7 Userspace Applications Development
Note For reading data from the serial line, only the getch function
can be used.
249
Chapter 7 Userspace Applications Development
A
pplication Example
Listing 7-7 illustrates an application example that prints data with the help
of the console library. While the code is more complex than using printf,
the execution is more efficient.
#include <console.h>
int main(void){
putnstr("Hello world from Tock", 21);
return 0;
}
250
Chapter 7 Userspace Applications Development
251
Chapter 7 Userspace Applications Development
252
Chapter 7 Userspace Applications Development
253
Chapter 7 Userspace Applications Development
Tip If the device you are working with has support for graphical
user interfaces, we recommend you include the lvgl library in your
application. This enables you to build complex user interfaces. An
example is provided in the examples/lvgl folder.
254
Chapter 7 Userspace Applications Development
A
pplication Example
In Listing 7-8, we adapted the button example application in libtock-c to
light on and off LED 0 when any onboard button of the device is pressed.
In the case of the micro:bit, this means that any of the two buttons A and B,
will trigger an interrupt when pressed and released.
255
Chapter 7 Userspace Applications Development
To make sure the LED changes its state only when the button is
pressed, we check that value equals to 1. This is necessary because an
interrupt is generated both when we press and release the button.
#include <button.h>
#include <led.h>
int main(void) {
int err;
return 0;
}
256
Chapter 7 Userspace Applications Development
257
Chapter 7 Userspace Applications Development
#include <app_state.h>
#include <stdio.h>
#define MAGIC 42
struct valuable_data {
uint32_t magic;
uint32_t my_value;
};
int main() {
int ret;
ret = app_state_load_sync();
if (ret != 0) {
printf("ERROR(%i): Could not read the flash
region.\n", ret);
}
258
Chapter 7 Userspace Applications Development
my_data.magic = MAGIC;
my_data.my_value = 20;
ret = app_state_save_sync();
if (ret != 0) printf("ERROR(%i): Could not write back to
flash.\n", ret);
}
No valid data can be retrieved from the flash when first running the
application in Listing 7-9. This is verified using magic. Upon reading from
the flash, we verify if the magic value that we have just read is the same as
the magic value that we expect. If so, we can assume we have read valid
data, or at least data that we have previously written. If not, this is probably
the first time this application has run, so no values will be printed. To
read the values, we have to reset the device. The second time we run the
application, the value will be printed on the screen.
The app state driver is not the best solution to storing persistent data in
the flash. Tock provides some other mechanisms to achieve this, but those
drivers are not fully functional yet. We recommend taking a look at the
TickV and SDCard libraries.
259
Chapter 7 Userspace Applications Development
Summary
This chapter focuses on how Tock applications are built. To create complex
applications that leverage the devices’ peripherals, libtock-c exposes
various libraries and functions from simple GPIO control to sensor-specific
libraries that enable the reading of temperature or motion.
In this chapter, we made an overview of the most important libraries
that can be used with the micro:bit and the Raspberry Pi Pico. Still, many
others are available to be integrated into your projects. The integration of a
new library is described in the following chapters.
Tip For all the libraries described in this chapter, you can find
application examples in the libtock-c/examples folder. While this
chapter provides some examples for the most important libraries, we
recommend you also inspect the libtock-c examples.
In the following chapters, we will focus on the way the Tock kernel
works and how to build custom capsules that enable us to easily build
secure applications on top of it.
260
CHAPTER 8
Synchronous Syscall
Capsules
The purpose of this chapter is to get through the steps necessary for
building a capsule. To exemplify this, we will focus on building a simple
capsule to light up an LED matrix.
H
ardware Requirements
To implement the project in this chapter, you need the following hardware
components, based on the device you use:
• Micro:bit
• 1 x micro:bit v2 board
• Raspberry Pi Pico
• 1 x KWM-R30881CUAB or KWM-R30881AUAB
LED matrix;
• 14 x jumper wires;
• 5 x 220 Ω resistors;
Caution While the hardware setup for the two devices differs,
the code is mostly the same. The chapter will present the code that
needs to be implemented for the micro:bit, while the final section
details the changes necessary to run it on the Raspberry Pi Pico.
Note Throughout this chapter, we will use the words capsule and
driver interchangeably. From the Tock point of view, these two mean
the same thing. There is a slight preference to call the lower-level
ones drivers and the upper-level ones capsules.
262
Chapter 8 Synchronous Syscall Capsules
Table 8-1. Tock capsule (driver) types and their security mechanisms
Driver Name Functionality Security Mechanism
Service An upper-level driver that provides The Rust type system makes
Capsule functionality to other capsules by sure at compile time that
implementing one or more kernel capsules access only data
HILs. they are allowed to.
Syscall The upper-level drivers that provide The Rust type system makes
Capsules functionality to userspace processes sure at compile time that
by implementing the Driver trait and capsules access only data
exposing system calls. they are allowed to.
Drivers The lower-level drivers that interact None, these drivers are part
directly with hardware components of the trusted code base as
and expose the hardware functionality interacting with hardware
by implementing kernel HILs. requires direct (unsafe)
memory access.
263
Chapter 8 Synchronous Syscall Capsules
Note We will use only the command system call for the capsule that
we are trying to build.
264
Chapter 8 Synchronous Syscall Capsules
265
Chapter 8 Synchronous Syscall Capsules
266
Chapter 8 Synchronous Syscall Capsules
Application
User Space
Kernel
command (capsule_number,command_number, arg1, arg2)
Syscall Capsule
Ok(())
found
Is command_number a CommandResult::failure
is 0 no
valid action? (ErrorCode::NOSUPPORT)
yes
Execute or
schedule action
yes
CommandResult::success... (...)
267
Chapter 8 Synchronous Syscall Capsules
Note The kernel does not authorize system calls and does not
keep a list of capsules. It uses the SyscallFilter and
SyscallDriverLookup traits to ask the board implementation
about the drivers.
Once the message reaches the destination, the capsule reads the
command_number and tries to perform or start the requested command.
Special attention is given to the command_number 0. Tock’s convention
states that all the capsules have to respond with a successful result for
command_number 0. By leveraging this rule, a process can ask the kernel
whether one or more capsules are present.
268
Chapter 8 Synchronous Syscall Capsules
A Capsule’s Architecture
Tock capsules can be split into two categories: syscall and service. This
chapter aims to write a capsule that provides a Syscall API to the userspace
for displaying digits and letters using system calls.
269
Chapter 8 Synchronous Syscall Capsules
Note The last section of this chapter details how to connect the
LED matrix to the Raspberry Pi Pico device.
All the other components are already present and are part of the Tock
architecture.
From a high-level point of view, the process in the userspace will send
a command to the DigitLetterDisplay Syscall capsule asking it to display
a specific digit or a letter. Upon receiving the command, the capsule will
talk to the LedMatrix and ask it to light up the LEDs so that they generate
the requested digit or letter. The Syscall capsule will use several instances
of the LedMatrixLed service capsule (one of each LED) through the Led
HIL, which in turn communicate with the LedMatrix.
270
Chapter 8 Synchronous Syscall Capsules
command system call. The process of defining the API involves defining the
known command numbers, their parameters, and their functionality.
Application
User Space
Kernel System Call
Kernel
Service Capsule
DigitLetterDisplay Syscall Capsule
Driver
Led trait
Hardware
LedMatrixLed
LedMatrixLed
LedMatrixLed LedMatrix Capsule
Capsule
Capsule
Capsule
Pin trait
GPIO Driver
Hardware Registers
271
Chapter 8 Synchronous Syscall Capsules
Table 8-3. The system call API for the DigitLetterDisplay capsule
Command
No Arg 1 Arg 2 Description Return
0 Not used Not Verifies if the capsule is CommandReturn::success( )
used available.
1 ASCII code Not Display the digit or letter CommandReturn::success( )
for digit or used represented by the ASCII CommandReturn:: failure
letter code sent in argument 1. (ErrorCode::INVAL)
W
riting the Capsule
A Tock API capsule can be represented by any data type that implements
the SyscallDriver trait presented in Listing 8-1. The trait provides the two
functions corresponding to the possible system calls: command and allow.
For our capsule, we will implement only the command function and use the
default implementation for allow.
The allocate_grant function is used by the kernel when a process
tries to subscribe to a driver. This function is detailed in the next chapter
that presents an asynchronous capsule implementation.
272
Chapter 8 Synchronous Syscall Capsules
Note If the process tries to use any of the two, subscribe and allow
system calls for our capsule, the kernel for subscribe and the default
implementation of allow will return the ErrorCode::NOSUPPORT error.
273
Chapter 8 Synchronous Syscall Capsules
274
Chapter 8 Synchronous Syscall Capsules
D
efining the Driver
The first step in writing our Syscall capsule is to create a new file in the
kernel/drivers project folder to store the source code. The driver is part of
the Rust drivers crate. We name the file digit_letter_display.rs. Next, we
need to ask the Rust compiler to parse it and include it in the drivers crate.
This is done by adding it as a module named digit_letter_display to
the main drivers crate file, lib.rs. Listing 8-2 shows the exact statement we
need to use.
Now that we have a file for the capsule and have declared it as a
module inside the drivers crate, we can start writing the actual Syscall
capsule.
First of all, we have to declare a data type that implements the
SyscallDriver trait. We could use any data type, including an empty
one, such as an empty tuple. To follow other capsules implementations
and because we might need to store some additional information for the
capsule later on, we choose to use a structure.
First of all, we define an empty structure called DigitLetterDisplay.
Next, we implement the SyscallDriver trait for the structure. The actual
functions that our driver has to implement are allocate_grant and
command. The first function just returns Ok(()) as our driver does not have
any grant defined.
275
Chapter 8 Synchronous Syscall Capsules
use kernel::syscall::{
CommandReturn, SyscallDriver
};
use kernel::ErrorCode;
use kernel::process::{Error, ProcessId};
fn command(
&self,
command_number: usize,
r2: usize,
276
Chapter 8 Synchronous Syscall Capsules
r3: usize,
process_id: ProcessId,
) -> CommandReturn {
match command_number {
0 => CommandReturn::success(),
1 => {
// TODO write the actual code to
//display a digit or a letter
CommandReturn::success ()
}
_ => CommandReturn::failure(
ErrorCode::NOSUPPORT
),
}
}
}
Defining the Font
The micro:bit has a 5 x 5 LED matrix capable of displaying 25 red points.
Using the LedMatrix driver, Tock exposes all the LEDs separately to the
userspace. The same driver exposes a small adaptation structure called
LedMatrixLed. This enables each LED to be used from within other drivers
inside the Tock’s kernel. As such, the kernel capsules and userspace
processes can control each LED individually by setting it on or off.
In this chapter, the DigitLetterDisplay driver that we create will use
the LedMatrixLed to display digits and letters. This means that we have to
define a font. To be more specific, we need to define which of the 25 LEDs
will be on and which will be off for each digit, from 0 to 9. Then, we need to
do the same for each letter, from A to Z.
277
Chapter 8 Synchronous Syscall Capsules
The LEDs in the matrix are numbered from 0 to 24, with LED 0 being
the upper left LED and 24 being the lower right LED.
The easiest way to define the font is to use for each glyph (character)
a u32 value. Each of the bits 0 to 24 of this value represents an LED. To
make the font more readable, an inverted bits order has been used. LED
0 is represented by the most significant bit (MSB), bit 24, and LED 24 is
represented by the least significant bit (LSB), bit 0. Listing 8-4 defines the
arrays for each digit.
278
Chapter 8 Synchronous Syscall Capsules
// 8
0b11111_10001_11111_10001_11111,
// 9
0b11111_10001_11111_00001_11111,
];
The same approach is taken for the letters. For simplicity, we will use
only capital letters. Whenever the userspace process provides a letter, we
uppercase it before displaying it.
The font defined in Listing 8-5 shows the definition for the letter A. We
leave it as an exercise to the user to define the whole set of letters.
C
onnecting the LEDs
The DigitLetterDisplay Syscall capsule has to control the micro:bit’s
25 LEDs. This is done using the Led Hardware Interface Layer, or simply
Led HIL. The capsule does not care how the LEDs are set up, connected,
or initialized. This is a task that the board interface (the main.rs file in
the board folder) has to perform. The capsule can only receive a slice
containing 25 items that implement the Led trait.
279
Chapter 8 Synchronous Syscall Capsules
A HIL is a Rust trait defined in the kernel::hil crate. Using these HILs
allows capsules to be loosely coupled and interchangeable. In our example,
the DigitLetterDisplay driver will use the Led HIL to control each of the
25 LEDs. In a simpler use-case, each LED used by this capsule would be
connected to a different GPIO pin. If this capsule were to use directly the
Gpio instead of the Led HIL, it would not work for the micro:bit as LEDs are
not directly connected to the GPIO pins. The 25 LEDs from the micro:bit
are controlled by a more complex LED matrix driver. On the other hand,
the matrix driver can expose each LED separately using the Led HIL.
To interact with the Led HIL, we have to change the definition of the
capsule to something similar to what is shown in Listing 8-6. The capsule’s
structure receives two parameters: a lifetime for the LEDs array reference
('a) and a generic type L with the Led trait bound. L can be replaced with
any concrete type that implements the Led trait.
use kernel::hil::led::Led;
use kernel::syscall::{
CommandReturn, SyscallDriver
};
use kernel::ErrorCode;
use kernel::process::{Error, ProcessId};
280
Chapter 8 Synchronous Syscall Capsules
Listing 8-8. The alternative that uses a slice instead of the array
281
Chapter 8 Synchronous Syscall Capsules
The drawback of using a slice is because the length of any slice is only
known at runtime. This has two implications in our code:
282
Chapter 8 Synchronous Syscall Capsules
Controlling the LEDs
Now that we have completed the structure definition, the next step is to
implement it. Inside the impl block, we define a function called new that
will create a new structure of type DigitLetterDisplay. The function takes
as a parameter a reference to an array of references towards L and returns a
new structure instance. We use the keyword Self for the function’s return
type as it makes the code more readable. We could have easily written the
whole data type as DigitLetterDisplay<'a, L>.
283
Chapter 8 Synchronous Syscall Capsules
Listing 8-10. The check required inside the new function if a slice is
used instead of an array
pub fn new(leds: &'a [&'a L]) -> Self {
if leds.len() != 25 {
panic!("Expecting 25 LEDs, {} supplied", leds.len());
}
DigitLetterDisplay { leds: leds }
}
284
Chapter 8 Synchronous Syscall Capsules
fn clear(&self) {
for index in 0..25 {
self.leds[index].off();
}
}
285
Chapter 8 Synchronous Syscall Capsules
self.print(LETTERS[
displayed_character as usize - 'A' as usize]);
Ok(())
}
_ => {
self.clear();
Err(ErrorCode::INVAL)
}
}
}
}
The first function is print. Its task is to print the glyph received as a
parameter to the LED matrix. The glyph is a u32 number, with its first 25
bits representing the LEDs assigned to a digit or a letter, in other words,
one item from the defined font. This function always succeeds, so there
is no need to return a value. Its functionality is really simple, it iterates an
index from 0 to 24, verifies the value of a bit in the glyph, and sets each
LED on or off accordingly. If the value is 0, the corresponding LED is
turned off. Otherwise, it is turned on.
Note While there might be some errors when setting on and off
an LED, the Led HIL does not return them. From our driver’s point of
view, the on and off functions will always succeed.
286
Chapter 8 Synchronous Syscall Capsules
287
Chapter 8 Synchronous Syscall Capsules
288
Chapter 8 Synchronous Syscall Capsules
first have to convert the usize into an u8, a byte, and then convert it back
to a UTF-8 character. All u8 values are valid UTF-8 code points as they are
the actual ASCII codes.
289
Chapter 8 Synchronous Syscall Capsules
Note All ASCII codes are valid UTF-8 code points. When converting
a usize value to u8, we might lose some information. If the number
stored in the usize value is larger than 255, part of it will be lost in
the conversion. In our case, this is not a problem as the userspace
should send us only ASCII character codes, codes that are always
represented using 8 bits. If we want to make sure that we do not
lose any information due to the conversion to u8, we can add an if
statement before and return an ErrorCode::INVAL if the value we
received is larger than 255.
Another important aspect is how the match is used upon the self.
display(...) function call. The display function returns Result<(),
ErrorCode>. Within the command function, we need to decide if the function
was successful and, in that case, return CommandReturn::success(). If it
failed, we return the error code that was wrapped into the returned Result.
This means we have to unwrap the Result value and transform it into a
CommandReturn. We use a two-branches match to perform this action.
The returned value of display is either Ok(()), which leads us
to CommandReturn::success(), or Err(err). In the case of an error,
match assigns the ErrorCode value wrapped into the Result to the
variable err and executes the code within that branch. We construct a
CommandReturn::failure(err) and return it to the userspace process.
290
Chapter 8 Synchronous Syscall Capsules
Registering the Capsule
Now that we have implemented that capsule, the next step is to register it
with the kernel. This allows the kernel to forward command, subscribe, and
allow system calls to it and send back a response to the process. The kernel
does not use a list of capsules but leverages the SyscallDriverLookup trait.
291
Chapter 8 Synchronous Syscall Capsules
• the watchdog.
292
Chapter 8 Synchronous Syscall Capsules
D
rivers Registration
If the system call is allowed, the kernel must find the driver to dispatch the
system call to. This is where the SyscallDriverLookup trait presented in
Listing 8-14 comes into play. The kernel asks the board implementation
by calling the syscall_driver_lookup function for a reference to a data
type that implements the SyscallDriverLookup trait. Now that it has a
reference to a syscall driver lookup data structure, it calls the with_driver
function with two arguments: the capsule_number and a function that
has to be called with an Option<&dyn SyascallDriver> as an argument.
The system call lookup implementation may use any means to find the
requested driver and call the supplied function. This function takes an
Option as an argument as the driver lookup system might not be able to
find a specific capsule. If it finds the capsule, it will call the function using
Some(driver). Otherwise, it will use None.
293
Chapter 8 Synchronous Syscall Capsules
294
Chapter 8 Synchronous Syscall Capsules
295
Chapter 8 Synchronous Syscall Capsules
// ...
scheduler: &'static RoundRobinSched<'static>,
systick: cortexm4::systick::SysTick,
// ...
}
Note Details on how fault policies work and are implemented will
be discussed in chapter 12.
Next, the implementation defines the concrete types for the scheduler
and the timer.
296
Chapter 8 Synchronous Syscall Capsules
297
Chapter 8 Synchronous Syscall Capsules
298
Chapter 8 Synchronous Syscall Capsules
S
tarting The Kernel
As the data structures required for starting the kernel are defined,
and the KernelResources trait and its associated data types are
implemented, the board implementation can initialize the drivers and
start the kernel. Listing 8-18 illustrates the initialization of the LedMatrix
capsule, the creation of the MicroBit structure, which implements the
SyscallDriverLookup trait, and the call to the kernel_loop function that
starts the kernel.
Initialization is different for each capsule. Each driver is a separate
structure with several generic parameters. What is more, their new function
takes several arguments that connect it to other drivers. Therefore, the
initialization line might be long and difficult. Tock leverages the Rust
macro system and recommends that each driver defines a set of macros
in the components crate. In the example presented in Listing 8-18,
the LedMatrix driver is initialized using the led_matrix_component_
helper macro.
299
Chapter 8 Synchronous Syscall Capsules
300
Chapter 8 Synchronous Syscall Capsules
// ...
ipc: kernel::ipc::IPC::new(board_kernel, &memory_
allocation_capability),
};
// ...
board_kernel.kernel_loop(
µbit,
chip,
Some(µbit.ipc),
scheduler,
&main_loop_capability,
);
pub fn kernel_loop<
KR: KernelResources<C>,
C: Chip,
301
Chapter 8 Synchronous Syscall Capsules
C
apabilities
Suppose you are asking yourself about the meaning of the last argument
of kernel_loop, the capability. In that case, this is part of a security
302
Chapter 8 Synchronous Syscall Capsules
303
Chapter 8 Synchronous Syscall Capsules
304
Chapter 8 Synchronous Syscall Capsules
The value of the constant is relevant. Each driver in Tock has to have a
unique identifier. Drivers provided by Tock have their identifiers defined
in capsules/src/driver.rs. Third-party drivers like the one we have just
created must have an identifier starting from 0xa0000. We have chosen the
0xa0001 (Listing 8-21) as this is our first (1) driver.
Tip Driver ids below 0xa0000 are reserved for the drivers that Tock
provides out of the box.
The next step that we have to take is to register the driver with the
MicroBit structure. We define a new field within the structure, called
digit_letter_display as shown in bold in Listing 8-22. Its type is more
complex. All the structure’s fields are static references to the actual driver.
This means that these references never go out of scope. All the drivers’
references will be valid for as long as the kernel is loaded. From Rust’s
point of view, drivers are global variables.
305
Chapter 8 Synchronous Syscall Capsules
capsules::virtual_alarm::VirtualMuxAlarm<
'static, nrf52::rtc::Rtc<'static>>,
>,
button: &'static
capsules::button::Button<'static,
nrf52::gpio::GPIOPin<'static>>,
ipc: kernel::ipc::IPC<NUM_PROCS>,
alarm: &'static capsules::alarm::AlarmDriver<
'static,
capsules::virtual_alarm::VirtualMuxAlarm<
'static, nrf52::rtc::Rtc<'static>>,
>,
// ...
digit_letter_display: &'static
drivers::digit_letter_display::
DigitLetterDisplay
// 'a is set to 'static
'static,
// L is set to LedMatrixLed<...>
LedMatrixLed<
'static,
nrf52::gpio::GPIOPin<'static>,
capsules::virtual_alarm::
VirtualMuxAlarm<'static,
nrf52::rtc::Rtc<'static>>,
>,
>,
}
306
Chapter 8 Synchronous Syscall Capsules
When defining the actual data type of our driver, we have to supply all
the concrete data types recursively. This is why the type definition is long.
If we take a closer look, the structure that implements the Alarm trait also
uses generics, which makes the data type even longer.
More precisely, for LedMatrixLed, L is replaced by
nrf52::gpio::GPIOPin<'static> and A is replaced by
capsules::virtual_alarm::VirtualMuxAlarm<'static,
nrf52::rtc::Rtc<'static>>.
Note The data type would have been much shorter if the
DigitLetterDisplay used the Led trait object (&dyn Led).
There is always a tradeoff between simplicity and performance.
307
Chapter 8 Synchronous Syscall Capsules
Now that the data type is out of the way, an easy step is to register
the capsule within the SyscallDriverLookup trait and implement the
with_driver function as shown in Listing 8-24. All we have to do is to add
another branch to the match statement. This is where we use the DRIVER_
NUM constant defined earlier.
The final step is to actually initialize the driver. This involves creating
a new instance of the DigitLetterDisplay structure. As Listing 8-25
outlines, we define a variable called digit_letter_display and assign it a
large and strange value that we analyze next.
308
Chapter 8 Synchronous Syscall Capsules
309
Chapter 8 Synchronous Syscall Capsules
(0, 3),
(1, 3),
(2, 3),
(3, 3),
(4, 3),
(0, 4),
(1, 4),
(2, 4),
(3, 4),
(4, 4)
))
);
310
Chapter 8 Synchronous Syscall Capsules
DigitLetterDisplay<
'static,
LedMatrixLed<
'static,
nrf52::gpio::GPIOPin<'static>,
capsules::virtual_alarm::VirtualMuxAlarm<'static,
nrf52::rtc::Rtc<'static>>,
>,
>
311
Chapter 8 Synchronous Syscall Capsules
The first two parameters are the data types that implement the Pin and
Alarm traits. These are necessary as the macro needs to know the exact
type of the LedMatrix (which in turn uses them). The third parameter, led,
is a reference to the LedMatrix driver initialized in Listing 8-15. The macro
takes a variable list of (column, row) coordinates for each LED that should
be placed into the array that it returns.
Note Rust macros are very different from the macros in C. They
allow developers to extend the Rust language. For a better
understanding of these Rust macros, we strongly recommend reading
the Little Book of Macros1.
1
https://danielkeep.github.io/tlborm/book/index.html
312
Chapter 8 Synchronous Syscall Capsules
313
Chapter 8 Synchronous Syscall Capsules
#pragma once
#include "tock.h"
#define DIGIT_LETTER_DISPLAY_DRIVER_NUM 0xa0001
#ifdef __cplusplus
extern "C" {
#endif
#ifdef __cplusplus
}
#endif
314
Chapter 8 Synchronous Syscall Capsules
T he Library
After defining the header for the library, the next step is to write the
definition for the two functions. For this, we create a new source file called
digit_letter_display.c. Listing 8-29 shows the complete source code.
#include "digit_letter_display.h"
#include "tock.h"
315
Chapter 8 Synchronous Syscall Capsules
if (ret.type == TOCK_SYSCALL_SUCCESS) {
return true;
} else {
return false;
}
}
We first include the previously created header file and tock.h. The
ladder defines the system call functions and system call return types.
The first function that we write is digit_letter_display_is_present.
This will issue a command system call towards the DigitLetterDisplay
driver and verify its return value. Based on that, the function will return
true if the driver is present and false otherwise. The command system call
is sent using the library function with the same name. The first argument of
the function represents the capsule number. In our case, it is the previously
defined constant value DIGIT_LETTER_DISPLAY_DRIVER_NUM. The second
argument is the command number, in other words, the action that we
require the driver to perform. In our case, it is the value 0, which in Tock’s
convention means checking if the driver is present.
The command function returns a syscall_return_t type presented in
Listing 8-30.
316
Chapter 8 Synchronous Syscall Capsules
The type field of the structure provides the type of the return value.
The data field has to be interpreted differently based on the value of the
type field. If type is set to one of the TOCK_SYSCALL_FAILURE... values, the
first value of data (data[0]) represents the ErrorCode, while the rest of the
values represent some additional data. Their encoding differs depending
on what kind of a TOCK_SYSCALL_FAILURE... the result encodes. For
instance, if it is TOCK_SYSCALL_FAILURE_U32, data[1] has a valid value.
On the other hand, if we have TOCK_SYSCALL_FAILURE_U64, data[1] and
data[2] have to be combined using the big endian encoding to extract the
valid value (data[1] + data[2] << 32).
The same system applies in the case of success, the only difference
being that success may have up to three 32 bits unsigned integers as there
is no need to encode an ErrorCode.
317
Chapter 8 Synchronous Syscall Capsules
The Tock C userspace provides two error types that encode errors
differently:
Listing 8-32. The error codes that respect the usual C standard:
positive numbers mean success, negative numbers mean errors
// ReturnCode type in libtock-c.
//
// 0 is success, and a negative value is an
// error (consistent with C
// conventions). The error cases are
// -1*ErrorCode values.
typedef enum {
RETURNCODE_SUCCESS = 0,
RETURNCODE_FAIL = -1,
RETURNCODE_EBUSY = -2,
RETURNCODE_EALREADY = -3,
RETURNCODE_EOFF = -4,
RETURNCODE_ERESERVE = -5,
RETURNCODE_EINVAL = -6,
RETURNCODE_ESIZE = -7,
RETURNCODE_ECANCEL = -8,
RETURNCODE_ENOMEM = -9,
RETURNCODE_ENOSUPPORT = -10,
RETURNCODE_ENODEVICE = -11,
RETURNCODE_EUNINSTALLED = -12,
318
Chapter 8 Synchronous Syscall Capsules
RETURNCODE_ENOACK = -13,
RETURNCODE_EBADRVAL = -1024
} returncode_t;
319
Chapter 8 Synchronous Syscall Capsules
320
Chapter 8 Synchronous Syscall Capsules
#include <stdio.h>
#include <timer.h>
int main(void) {
// if(driver_exists(DIGIT_LETTER_DISPLAY...)) {
if (digit_letter_display_is_present()) {
for (unsigned int index = 0;
index < strlen (DISPLAY_TEXT); index++) {
digit_letter_display_show_character (DISPLAY_TEXT[index]);
delay_ms (500);
}
} else {
printf ("DigitLetterDisplay Syscall Capsule is not present\n");
}
}
The first action that the process performs is to check if the display
driver is present. If so, the process will iterate over the DISPLAY_TEXT
variable and print it character by character, waiting 500ms in between.
If the driver is not present, it will show an error message in the console.
321
Chapter 8 Synchronous Syscall Capsules
All there’s left to do is build the kernel with the new driver and the
display_text process and flash both of them to the micro:bit board.
Note The LED matrix device that we use here is one without a
controller. It has 16 pins that directly control the LEDs. Each LED’s
anode (positive) pin is connected directly to one of the pins. All LEDs
from one row are connected to a common cathode (negative) pin.
322
Chapter 8 Synchronous Syscall Capsules
16 15 14 13 12 11 10 9
1 2 3 4 5 6 7 8
323
Chapter 8 Synchronous Syscall Capsules
Each cathode connection has a 220Ω resistor. This limits the current
flow through the LEDs and prevents damage to the Raspberry Pi Pico
board and the LED.
Note The LED matrix pins 7, 2, and 5 are connected to the 3.3V pin.
While this is not actually needed, it ensures that LEDs in rows 6 to 8
are not lighting up due to electrical interferences.
324
Chapter 8 Synchronous Syscall Capsules
Figure 8-4. The schematic for connecting the LED matrix to the
Raspberry Pi Pico
Setting Up the Driver
To use the LED matrix with the Raspberry Pi Pico and display digits and
letters, we have to enable two drivers on the device. First, we have to set up
the LED matrix driver, and secondly, set up the DigitLetterDisplay driver
and connect it to the LED matrix.
325
Chapter 8 Synchronous Syscall Capsules
326
Chapter 8 Synchronous Syscall Capsules
// ... drivers::digit_letter_
display::DRIVER_NUM =>
f(Some(self.digit_letter_display)),
_ => f(None),
}
}
}
The next step is to add a branch in the match statement within the
with_driver function from the SyscallDriverLookup trait. This is the
actual registration of the driver with the kernel.
All that is left now is to initialize the driver. This requires three steps
that modify the main function. First, we have to disable the GPIO pins that
the LED matrix uses from the Gpio driver. This prevents the userspace
processes from accidentally using the GPIO pins and overlapping with the
functionality of the LED matrix. We simply comment in the pins 2 to 11 as
presented in Listing 8-36.
327
Chapter 8 Synchronous Syscall Capsules
//0=>&peripherals.pins.get_pin(RPGpio::GPIO0),
//1=>&peripherals.pins.get_pin(RPGpio::GPIO1),
//2=>&peripherals.pins.get_pin(RPGpio::GPIO2),
//3=>&peripherals.pins.get_pin(RPGpio::GPIO3),
//4=>&peripherals.pins.get_pin(RPGpio::GPIO4),
//5=>&peripherals.pins.get_pin(RPGpio::GPIO5),
//6=>&peripherals.pins.get_pin(RPGpio::GPIO6),
//7=>&peripherals.pins.get_pin(RPGpio::GPIO7),
//8=>&peripherals.pins.get_pin(RPGpio::GPIO8),
//9=>&peripherals.pins.get_pin(RPGpio::GPIO9),
//10=>&peripherals.pins.get_pin(RPGpio::GPIO10),
//11=>&peripherals.pins.get_pin(RPGpio::GPIO11),
12=>&peripherals.pins.get_pin(RPGpio::GPIO12),
// ...
),
).finalize(components::gpio_component_buf!(
RPGpioPin<'static>));
The second step is to initialize the LED matrix driver (Listing 8-37).
The code is identical to the one used for the micro:bit, the only difference
being the data types of GPIO pins and the Alarm and the activation of the
LEDs. As the LED matrix that we use has a common cathode (ground), the
columns are ActiveHigh, meaning that individual LEDs are lightened up
by setting the pins to 1 (HIGH). The rows are ActiveLow, in other words,
activated by setting the pins to 0 (LOW).
Note The micro:bit uses a common anode. This means the LEDs
are activated by writing 0 (LOW) to the GPIO pins controlling the
LEDs and writing 1 (HIGH) to pins controlling the rows. This is the
configuration that we would have to use if our LED matrix were
KWM-R30881AUAB.
328
Chapter 8 Synchronous Syscall Capsules
Now that we have initialized the LED matrix driver, we can initialize
the DigitLetterDisplay driver that uses the individual LEDs. The code
presented in Listing 8-38 is the same as the one used for the micro:bit,
except that the GPIO pins and alarm data types are different.
329
Chapter 8 Synchronous Syscall Capsules
330
Chapter 8 Synchronous Syscall Capsules
U
sing the Driver
We have successfully set up the DigitLetterDisplay driver for the Raspberry
Pi Pico. Due to the fact that we have used an embedded operating system,
the changes that we had to make were minimal. All that is left to do is
connect the Raspberry Pi Pico to a regular Raspberry Pi board and upload
the kernel and the example application. Chapter 5 describes in detail all
the steps that are required to perform this action.
One key difference between the Raspberry Pi Pico and the micro:bit
is the type of the ARM MCU. Raspberry Pi Pico uses an ARM Cortex-M0+
MCU, while micro:bit uses an ARM Cortex M4 MCU. Tock allows the
distribution of binary applications in the format of a TBF file. Since the
Raspberry Pi Pico has a different MCU than the micro:bit, we cannot use
the same TBF file as the MCU would throw errors due to incompatible
instructions. This is where the Tock Application Bundle (TAB) file comes
into play. This is a tar archive that contains several TBF files, one for
each MCU architecture. While tockloader knows how to use this file, gdb
does not.
When loading processes to the Raspberry Pi Pico, we must be careful
to select the correct ELF file. We must make sure that we use the file in the
build/cortex-m0 folder.
331
Chapter 8 Synchronous Syscall Capsules
Summary
Even though we wrote a relatively simple driver, we have covered a lot of
ground in this chapter. We have identified the three types of capsules or
drivers that the kernel works with, namely Syscall capsules, service capsules,
and drivers.
For each of them, we detailed their meaning and their relevance
in Tock’s architecture. Furthermore, we presented the system call
infrastructure and how system calls are dispatched from the userspace
process to a syscall capsule.
Another important aspect we described is how Tock starts and how
drivers are initialized and registered with the kernel. You should be
familiar now with the API that the kernel provides for driver development.
Last but not least, we provided an example of how to build a library for
a Syscall capsule and how to use it in a process.
As writing Tock capsules for the first time can be tricky, we have
deliberately used only the command system call. In the following chapters,
we will improve our drivers and use the other system calls.
Probably one of the most important takeaways from this chapter is the
portability of the drivers and applications. We have two different boards,
the micro:bit and the Raspberry Pi Pico that run the same drivers and
applications, with no driver or application changes. The only changes that
we had to make were in the board implementation. When it comes to the
application, we did not even have to recompile it. We have directly used
the binary that was previously compiled.
332
CHAPTER 9
Asynchronous
Syscall Capsules
The previous chapter has covered all the details about Tock capsule types
and how to develop a simple Syscall capsule. This chapter aims to extend
the capsule from chapter 8 and allow it to write more than one character.
While the previous capsule version received one character and displayed
it, this version will receive a string of characters to display. The capsule will
display a digit or letter, wait for a small amount of time, then display the
next until it reaches the last character.
To get started, you will need the project built in the previous chapter.
R
equirements
This chapter requires the following hardware components based on the
device you use:
• Micro:bit
• 1 x micro:bit v2 board
• Raspberry Pi Pico
• 1 x KWM-R30881CUAB or KWM-R30881AUAB
LED matrix;
• 14 x jumper wires;
• 5 x 220Ω resistors;
E xtending the API
So far, for the first version of the DigitLetterDisplay capsule, we have used
only the command system call. For the second version presented in this
chapter, we make use of all the possible system calls. Table 9-1 illustrates
the proposed API.
Table 9-1. The system call API for the DigitLetterDisplay capsule
Command
No Arg 1 Arg 2 Description Return
0 Not used Not used Verifies if the CommandReturn
capsule is ::success( )
available.
1 The number The inter- Start CommandReturn ::success( )
of characters character displaying CommandReturn
that are to be delay value in all the ::failure(ErrorCode::RESERVE)
displayed from milliseconds. characters CommandReturn
the allowed from the ::failure(ErrorCode::NOMEM)
buffer. allowed CommandReturn
buffer. ::failure(ErrorCode::SIZE)
Subscribe
No Description Return
0 Upcall issued when the whole buffer has been The previous Upcall function
displayed. that was registered
(continued)
334
Chapter 9 Asynchronous Syscall Capsules
Allow
No Description Return
0 The buffer used to send the characters. The previous ProcessBuffer
that was shared
Allow RW or RO
(optional)
Buffer
Subscribe
Command
not usable by
the application
Yield
Yes
Is Yielded? Run Upcall
Upcall
no No
Callback Ran?
Postpone
yes
UnAllow RW or RO
(optional)
Buffer
335
Chapter 9 Asynchronous Syscall Capsules
336
Chapter 9 Asynchronous Syscall Capsules
Similar to command and subscribe, the allow system calls receive the
capsule number as the first parameter. This instructs the kernel to which
driver to send the system call.
The second argument is the allow number. Based on this number,
each driver knows what data should be read and written from and to the
buffer. This number gives each buffer a semantic meaning. For instance,
a driver that reads and writes information from and to a peripheral might
have two allow numbers: number 1 for the output buffer, data sent to
the peripheral, and number 2 for the input buffer, data read from the
peripheral. The buffer for number 1 is shared using allow_readonly as the
driver does not write to it, while the buffer for number 2 is shared using
allow_readwrite as the driver has to write to it.
The allow system calls require a little more processing on the kernel
side before they can be forwarded to a capsule. Figure 9-2 depicts the flow.
337
Chapter 9 Asynchronous Syscall Capsules
Application
User Space
allow_... (capsule_number, allow_number, buffer_ptr, len)
Kernel
Syscall Capsule
Ok(())
found
yes
Is allow_number SyscallReturn::Allow...Failure
no
valid? (buffer, ErrorCode::NOSUPPORT)
yes
Register buffer
SyscallReturn::Allow...Failure
Success? no
(buffer, ErrorCode::...)
yes
Ok (previous_buffer)
338
Chapter 9 Asynchronous Syscall Capsules
Similar to the command and subscribe system calls, the kernel first asks
the board implementation whether it should forward or not the system
call. Next, if the call should be sent to the driver, the kernel will request the
driver in the board implementation. If it is found, the kernel has to build
the shared buffer. This step is unique for the allow system call. It requires
the kernel to perform some verifications and generate a Rust type starting
from the pointer and length it received.
Before forwarding the system call to the driver, the kernel makes sure that
the process has provided a valid buffer. It verifies that the buffer’s pointer is
within the process flash or memory limits and its length does not exceed the
flash or memory allocated to the process. If the verification fails, the kernel
will stop the system call and return ErrorCode::INVAL to the process. If the
call can be forwarded, the kernel will construct an internal structure of type
ReadOnlyAppBuffer or ReadWriteAppBuffer, based on the allow type. This
structure, illustrated in Listing 9-1, ties the pointer and length to a process id.
339
Chapter 9 Asynchronous Syscall Capsules
The kernel’s next step is to call one of the allow functions within the
capsule’s SyscallDriver trait. This function receives the calling process
id, the allow number, and the buffer of type ReadOnlyAppBuffer or
ReadWriteAppBuffer.
Upon receiving an allow system call, the capsule verifies the allow
number. If it is valid, the capsule tries to store the received buffer and
return the previous one. If the capsule does not validate the allow number
or cannot store the received buffer, it returns an error together with the
received buffer.
The ReadOnlyAppBuffer and ReadWriteAppBuffer structures have an
optional process id parameter. This informs the kernel whether the buffer
is valid or not. A buffer without a process id (value None) is considered
invalid. This behavior is because allow system calls have to return the
previously shared buffer. When no buffers are shared, the capsule uses
ReadOnlyAppBuffer::default() or ReadWriteAppBuffer::default()
buffers that have the process id set to None. Returning one of these buffers
will make the kernel return a null value to the application.
340
Chapter 9 Asynchronous Syscall Capsules
C
apsule Architecture
Once we have established all the necessary system calls, it is time to
describe how the driver works. Figure 9-3 shows the flow necessary to
display a piece of text from a process.
First of all, the process sends system calls to the TextDisplay driver.
In turn, the driver uses the Led HIL to communicate with the LEDs in
LedMatrix. This is identical to the previous capsule that we have created.
What is different about this new driver is that it cannot perform the
printing task directly from within the command system call. In contrast, it
needs to print one character, wait for an amount of time, print the next
character, and so on. To be able to wait, the driver uses the Alarm trait.
First of all, the process shares with the driver the buffer containing
the text. The buffer is shared using allow_readonly as the driver does not
need to write to it, it only reads each character and prints it.
As the task of printing a text takes some time (each letter is printed
individually, and a delay between letters is used), the action cannot be
performed with the command system call. The process will subscribe to the
capsule to be notified when the printing has finished.
341
Chapter 9 Asynchronous Syscall Capsules
Application
User Space
Kernel System Call
Kernel
Service Capsule
TextDisplay Syscall Capsule
Driver
Hardware
Alarm trait Led trait
Alarm LedMatrixLed
LedMatrixLed LedMatrix Capsule
Capsule LedMatrixLed
Capsule
Capsule
Capsule
Hardware Registers
Hardware
(LED Matrix and Real Time Clck)
If allowing the buffer and subscribing to the done event are successful,
the process issues a command system call to ask the driver to start printing
the text. The driver receives the command system call, starts the printing,
and immediately returns to the process. In the background, the driver
performs the printing task. After the last letter or digit is printed, the driver
schedules an upcall for the process.
In turn, the process has two possible ways to continue: it does
something else in between and checks from time to time if the driver has
scheduled an upcall, or it waits until it receives the upcall from the driver.
342
Chapter 9 Asynchronous Syscall Capsules
A
synchronous Tock Drivers
Tock drivers use a split-phase or asynchronous implementation when
performing tasks. Figure 9-4 displays the functioning of the TextDisplay
capsule that we detail in this section.
Timer
Interrupt
Command System
generic_isr alarm()
Call
Return to Kernel
AlarmClient HIL no
Set display in Is display in
progress progress?
Alarm HIL
Alarm Capsule
TextDisplay Capsule
Schedule a Process
Kernel
The action starts when the process issues a command system call
and asks the driver to start printing the text from the shared buffer. First
of all, the driver verifies if it has another activity in progress. If so, the
command returns ErrorCode::BUSY and the process will have to retry the
command later.
If there is no other display in progress, the driver verifies that it has a
buffer to display from and, if so, starts the displaying. It flags that it now has
a displaying in progress and returns success to the process.
The driver does not perform any actions in the background as there is
no way a driver can run in parallel with a process. Tock is single-threaded.
The driver splits the action into several smaller actions and interleaves
them with the processes.
Just before it returns from the command system call, the driver checks
if it has any characters left to display (actually if the length of the supplied
buffer is greater than 0). If so, it displays a character, and asks the Alarm to
schedule a callback for the driver after several milliseconds. It then returns
from the command system call. In a nutshell, the driver displays a character
and asks the alarm to call the driver back after some time.
When the command system call returns, the process and the kernel
resume their regular activity. At some point later in time, the alarm system,
controlled by the MCU’s timer, will send an interrupt to the MCU. The
kernel has registered the generic_isr function for this interrupt. As soon
as the interrupt fires, the MCU will execute the generic_isr function,
suspending any running process, exiting the interrupt mode, and
transferring control to the kernel. In turn, the kernel reads the interrupt
information and forwards the interrupt to its alarm driver.
344
Chapter 9 Asynchronous Syscall Capsules
The alarm driver then calls the TextDisplay driver using the AlarmClient
HIL. When the TextDisplay driver receives the alarm function call, it verifies
if it has any characters left to display. If it does, it displays a new character and
schedules another alarm. It then returns control to the kernel, which in turn
returns control to the process that was running when the interrupt fired.
Note The kernel might not transfer the control back to the same
process that was running when the interrupt fired. It is the kernel’s
scheduler that will decide who gets the control back right away.
If all the text has been displayed, meaning that there are no more
characters left to show, the driver schedules an upcall for the process and
marks that it has no display action in progress.
The process that has requested the text display might not get notified
immediately. Whenever a driver schedules an upcall, it actually asks the kernel
to add an upcall to the queue. Each process has a queue of scheduled tasks.
Whenever the process yields (uses the yield system call), the kernel verifies
if there is any upcall in the processes’ task queue. If so, that upcall is fired, and
the process executes the function registered for it. Whenever that function
returns, the control returns in the process right after the yield system call.
If no upcall is scheduled in the queue, the process is suspended (placed
into the Yielded state) until an upcall is scheduled. When using the yield
system call, the process can ask the kernel not to suspend it if there is no
scheduled upcall. This is done using the yield’s system call argument.
Writing the Capsule
Now that we have described how our capsule works, we can start writing
the code. We create the new capsule, set up an alarm used for the delay
between each character, store an upcall to the process, and signal the
process when the display of the text is done.
345
Chapter 9 Asynchronous Syscall Capsules
use core::cell::Cell;
use core::mem;
use kernel::grant::Grant;
use kernel::hil::led::Led;
use kernel::hil::time::{
Alarm, AlarmClient, ConvertTicks
};
use kernel::process::{Error, ProcessId};
use kernel::processbuffer::{
ReadOnlyProcessBuffer,
ReadableProcessBuffer
};
use kernel::syscall::{
CommandReturn, SyscallDriver
};
use kernel::utilities::cells::OptionalCell;
use kernel::ErrorCode;
#[derive(Default)]
pub struct AppData {
buffer: ReadOnlyProcessBuffer,
position: usize,
346
Chapter 9 Asynchronous Syscall Capsules
len: usize,
delay_ms: usize,
}
impl<
'a, L: Led, A: Alarm<'a>
> TextDisplay<'a, L, A> {
pub fn new(
leds: &'a [&'a L; 25],
alarm: &'a A,
grant: Grant<AppData, 1>
) -> Self {
TextDisplay {
leds,
alarm,
grant,
in_progress: Cell::new(false),
process_id: OptionalCell::empty(),
}
}
// ...
}
347
Chapter 9 Asynchronous Syscall Capsules
T he leds Field
Similar to the previous driver, we define the leds reference to an array of
LEDs. This allows the capsule to control each LED separately.
T he alarm Field
The alarm field is a reference to a data type that implements the Alarm trait
illustrated in Listing 9-3. This allows the capsule to schedule a callback
from the underlying alarm system after a certain amount of time.
348
Chapter 9 Asynchronous Syscall Capsules
After each letter is displayed to the LED matrix, the driver uses the
alarm to ask for a callback after the inter-character time provided by the
process that has requested the display. Whenever the hardware alarm fires,
the underlying driver calls the alarm function from the AlarmClient trait.
349
Chapter 9 Asynchronous Syscall Capsules
350
Chapter 9 Asynchronous Syscall Capsules
0x0040000
Task Queue
0x003FFC8
Grant 2
0x003FFC0
Grant 1
351
Chapter 9 Asynchronous Syscall Capsules
352
Chapter 9 Asynchronous Syscall Capsules
impl<
T: Default,
const NUM_UPCALLS: usize
> Grant<T, NUM_UPCALLS> {
/// Create a new `Grant` type which allows a
/// capsule to store process-specific data for
/// each process in the process's memory
/// region.
pub(crate) fn new(
kernel: &'static Kernel,
grant_index: usize
) -> Grant<T> {
Grant {
kernel: kernel,
driver_num: driver_num,
grant_num: grant_index,
ptr: PhantomData,
}
}
353
Chapter 9 Asynchronous Syscall Capsules
354
Chapter 9 Asynchronous Syscall Capsules
By taking a look at its definition, we realize that the grant structure does
not actually own or keep any reference to the data type T. PhantomData
is just a placeholder for an empty data structure (size 0) that makes the
structure behave as if it were storing a data type T. This allows the compiler
to compute several safety properties. The compiler will complain about the
type T not being used otherwise.
355
Chapter 9 Asynchronous Syscall Capsules
The grant field is just an entry point to the memory stored in the grant
area. As shown in Listing 9-4, Grant exposes the enter and each functions,
each of them receiving a closure as an argument. The actual grant data is
shown in Figure 9-7. This stores a data type T, the generic type of Grant,
and a list with NUM_UPCALLS pairs storing the callback pointer and user data
for each possible upcall.
Padding
356
Chapter 9 Asynchronous Syscall Capsules
The grant exposes another important function called each. This allows
capsules to iterate over all the grants stored in every process and access
the data. This driver is not making use of this function. Similar to each, the
grant provides the iter function which returns an iterator performing
the same task as each, but that allows the grant to be used within Rust
standard iterations like a for statement.
357
Chapter 9 Asynchronous Syscall Capsules
H
ow subscribe Works
The subscribe system call is automatically implemented by the kernel.
When allocating the grant, the kernel allocates an array of NUM_UPCALLS
elements. Each element contains a function pointer and a usize user data.
Following the same flow as for the allow and command system calls, the
kernel asks the board implementation if it should allow the subscribe.
If the subscribe is allowed, the kernel searches for the requested driver.
If the driver is registered, the system call is not actually forwarded to the
driver, but the kernel tries to execute it.
Registering a callback function requires an allocated grant for the
driver. For technical reasons, the kernel is not able to allocate the grant
entirely itself. The problem is the size of the T data. The kernel has no
way of knowing the actual size of a grant, as it does not have access to its
generic data type. The grant is defined as Grant<T>, and while the kernel
has access to the Grant structure, it has no way of knowing the actual data
type that a driver has used for T. This is why the driver has to implement
the allocate_grant function.
The kernel checks if the driver’s grant for the requesting process has
been allocated. If not, it calls the driver’s allocate_grant function. All that
the driver has to do within that function is to enter the grant. As the enter
function is called with the actual Grant<AppData> type, the kernel now
knows the exact size of the grant and is able to allocate it.
If the grant has been allocated, the kernel will try to register the
callback. If not, it will report an error to the process. The complete flow
is displayed in Figure 9-8. The kernel actually performs some additional
verifications that have been omitted as they are not essential.
358
Chapter 9 Asynchronous Syscall Capsules
Application
User Space
Ok(())
found
allocate_grant (process_id)
SyscallReturn::SubscribeFailure
Grant allocated? Err(error) (error, upcall_ptr, user_data)
Yes
Is subscribe_number SyscallReturn::SubscribeFailure
less then no (ErrorCode::NOSUPPORT, upcall_ptr,
NUM_UPCALLS ? user_data)
yes
Register upcall
Ok (previous_upcall_ptr, previous_user_data)
359
Chapter 9 Asynchronous Syscall Capsules
360
Chapter 9 Asynchronous Syscall Capsules
When the capsule enters the grant the first time, the kernel will create
a new grant data and initialize all the fields of AppData with their default
value. For numbers, this value is 0. For upcall, it is a null function, and
for buffers, it is a null buffer. Scheduling a null upcall results in no action
from the kernel, so drivers can safely schedule it. The kernel simply does
not schedule anything.
Using a null buffer will result in an error for the driver, so drivers
have to ensure they have a valid buffer before using it. The way the
buffer is accessed is similar to the grant, so there is no risk of invalid
memory access.
Note The name of the data type used by the grant is not relevant.
Most drivers will name it AppData.
361
Chapter 9 Asynchronous Syscall Capsules
S
toring the Buffer
The first action a process has to perform to display a text is to write it into
a buffer and share it in a read-only manner with the capsule. This is where
we use the allow_readonly function to define the SyscallDriver trait.
Instead of using the default implementation that always returns an error,
our capsule defines its custom implementation (Listing 9-5).
362
Chapter 9 Asynchronous Syscall Capsules
app.len = 0;
app.position = 0;
app.delay_ms = 0;
}
);
match res {
Ok(()) => Ok(buffer),
Err(err) => Err((buffer, err.into())),
}
}
_ =>
Err((buffer, ErrorCode::NOSUPPORT)),
}
}
// ...
}
363
Chapter 9 Asynchronous Syscall Capsules
First of all, the capsule will check the allow number. As described
in Table 9-1, the only allow number recognized by this driver is 0. If the
process supplied another number, the driver simply returns an error
containing the buffer provided and ErrorCode::NOSUPPORT.
If the allow number is 0, the driver must store the provided buffer and
return the previous one. This is where the grant data is used. To access the
data stored for each process, we call the enter function for the grant with
the received process_id. The kernel tries to access the grant for us and, if it
succeeds, calls the closure that we supplied. Within the closure, we receive
a parameter called app. This indirectly represents a mutable reference to
the AppData structure stored within the calling process’ memory.
Note The memory inside the process’ grant data can only be
accessed from within the enter closure.
Upon accessing the grant, the driver swaps the previous buffer app.
buffer with the newly provided buffer. The swap function defined in the
core::mem module effectively swaps the memory contents stored at the
locations provided by the two arguments.
Besides storing the buffer, the driver resets all the other data stored
within the grant. This reset is a significant action as the driver might be
right in the middle of writing another buffer to the display. The process
might have shared another buffer earlier, might have issued a display
command, and might have decided not to wait for the upcall before
unsharing or sharing another buffer.
The driver’s state, in this case, is waiting for the alarm in between
characters. By resetting all the position and length values, whenever the
alarm callback arrives, the driver will realize that it has finished displaying
characters as position will have the same value as len. This will become
more clear later in the chapter as we discuss the actual process of
displaying characters.
364
Chapter 9 Asynchronous Syscall Capsules
365
Chapter 9 Asynchronous Syscall Capsules
The compiler infers the Ok(()) return type for enter due to the last
empty statement inside the closure. The last visible statement ends with ;.
E xecuting the Commands
Once the process shares a buffer and subscribes for the upcall, it is time
to send the actual command to the driver. This system call tries to start the
display action (Listing 9-7).
366
Chapter 9 Asynchronous Syscall Capsules
Listing 9-7. The command system call that starts the displaying of
the text
impl<
'a, L: Led, A: Alarm<'a>
> Driver for TextDisplay<'a, L, A> {
// ...
fn command(
&self,
command_number: usize,
r2: usize,
r3: usize,
process_id: ProcessId,
) ->
CommandReturn {
match command_number {
0 => CommandReturn::success(),
1 => {
if !self.in_progress.get() {
let res = self.grant.enter(
process_id,
|app, _| {
if app.buffer.len() > 0 {
if app.buffer.len() >= r2 {
app.position = 0;
app.len = r2;
app.delay_ms = r3;
Ok(())
} else {
Err(ErrorCode::SIZE)
}
367
Chapter 9 Asynchronous Syscall Capsules
} else {
Err(ErrorCode::NOMEM)
}
}
);
match res {
Ok(Ok(())) => {
self.process_id.set(process_id);
self.in_progress.set(true);
self.display_next();
CommandReturn::success()
}
Ok(Err(err))=>
CommandReturn::failure(err),
Err(err) =>
CommandReturn::failure(err.into()),
}
} else {
CommandReturn::failure(ErrorCode::BUSY)
}
}
_ => CommandReturn::failure(
ErrorCode::NOSUPPORT),
}
}
}
368
Chapter 9 Asynchronous Syscall Capsules
369
Chapter 9 Asynchronous Syscall Capsules
370
Chapter 9 Asynchronous Syscall Capsules
impl<
'a, L: Led, A: Alarm<'a>
> TextDisplay<'a, L, A> {
pub fn new(
leds: &'a [&'a L; 25],
alarm: &'a A,
grant: Grant<AppData>
) -> Self {
/* ... */
}
371
Chapter 9 Asynchronous Syscall Capsules
fn display_next(&self) {
if self.in_progress.get() {
self.process_id.map_or_else(
|| {
self.in_progress.set(false);
// panic!("..., no process id");
},
|process_id| {
// ... display next character
// shown in listing 9-9
},
);
}
}
fn clear(&self) {
/* ... */
}
372
Chapter 9 Asynchronous Syscall Capsules
373
Chapter 9 Asynchronous Syscall Capsules
Once inside the grant data closure, we must check whether we still
have some characters left to print. By comparing the current position and
the length of the text, we establish if we have displayed the whole text or if
there is still some processing to do. If there is still work to do, we need to
extract another character from the buffer, increment the current position
and display the character.
374
Chapter 9 Asynchronous Syscall Capsules
match res {
Ok(()) => {}
Err(_) => self.in_progress.set(false),
}
375
Chapter 9 Asynchronous Syscall Capsules
This is so complicated for a reason. The driver performs all the actions
asynchronously, meaning that the process can freely execute any code
while the driver is displaying characters. Nothing stops a process from
unallowing or swapping the buffer while the driver has a displaying action
in progress. Suppose the driver had direct access to the buffer and could
keep a reference to it while the action was carried out. There could be a
situation where the process has swapped the buffer, and the driver still
uses the older reference.
This is why the kernel enforces the use of the buffer only within the
closure. As long as the closure executes, as Tock is running on a single-
core, no other process can run in parallel. The reference to the buffer that
the closure receives has an anonymous lifetime valid only for as long as the
closure runs. In a nutshell, all the interaction with the buffer’s bytes has to
be done inside the closure.
As soon as we are inside the closure, we are ready to display the
character. We get the character at the current position from the buffer and
send it to the display function. The display function is the same as the
one presented in the previous chapter. If the character is a valid digit or
letter, the function will set up the LEDs to display it. If not, the function will
turn off all the LEDs, displaying no character.
The buffer that stores the characters is defined as a slice of Cell<u8>.
This is why we have to use the get function to access the u8 value. The
decision to store the buffer as [Cell<u8>] instead of mut [u8] is because
of Rust’s borrowing rules. These rules state that there may be only one
single mutable reference to a value at any given time. For shared process
buffers, this is not true. While the kernel has one single reference to the
mutable buffer, the process that shares the buffer keeps another mutable
reference. Wrapping the u8 values into Cell solves the problem by using
interior mutability.
For simplicity, we do not take into account if the character can
be displayed or not. We will just assume that the process sends valid
characters. To avoid a Rust warning, we store the display’s returned value
376
Chapter 9 Asynchronous Syscall Capsules
Note The compiler complains about not using the Result type to
force developers to handle errors.
Implementing the Delay
Now that we have displayed the character, all that we have to do is to ask
the alarm driver to send us a callback after the amount of time that the
application has requested. This is done using the set_alarm function from
the Alarm driver. This function receives two parameters: the time reference
and the time interval to send the callback. As the underlying alarm works
in ticks rather than a time unit, we have some convenience functions like
tick_from_ms that allow us to convert the time value. Ticks represents the
number of increments that the hardware timer performs and is dependent
on the hardware.
As the hardware implementation of the alarm is a timer, most
probably, there will be a small, but sometimes significant, time delay from
the moment we issue set_alarm until the function computes the absolute
time for the callback by adding the time interval to the current time. This is
why the function takes a time reference besides the time interval.
Another aspect that we have to detail is the Client trait, which is the
mechanism through which an underlying driver can signal another driver.
Our driver is using the Alarm HIL to get a service from an alarm driver.
Most of the HILs come in pairs, HILName and HILNameClient. As such, the
Alarm HIL has its counterpart AlarmClient.
Each service capsule, like the Alarm, has one single client that it
can signal. If you are familiar with Java, this is the observer pattern,
but with one single observer. This means that the Alarm HIL provides
several functions that can be called to control the alarm. In contrast, any
377
Chapter 9 Asynchronous Syscall Capsules
The AlarmClient HIL provides one single function named alarm. This
function is called by the alarm driver when an alarm that was previously
set expires. When the TextDisplay driver receives this function call, it
tries to display the next character using the display_next function that we
presented above.
S
ignaling the Application
A particular topic that we have to detail is how we signal the application
when the job is done or if there is an error. Tock provides the upcall
mechanism. Just as discussed before, processes can subscribe to events
from drivers. These events are called upcalls.
378
Chapter 9 Asynchronous Syscall Capsules
Note The name upcall is inspired from the Tock stack. The
processes live above the kernel, and as such, the kernel sends a call
upwards to the process.
Each upcall has three arguments of type u32. The actual meaning
of these arguments is not defined by Tock itself, but rather it is left up
to each capsule and its corresponding userspace library to agree upon
some rules. The unwritten rule of Tock that most capsules use is to
provide a StatusCode via the first argument. The data type StatusCode
does not exist, it is a transformation from Result<(), ErrorCode> to
usize (Listing 9-11). In other words, 0 means success. Any other number
different from 0 is the corresponding ErrorCode.
379
Chapter 9 Asynchronous Syscall Capsules
In our case, if we have finished displaying all the characters from the
buffer, we schedule an upcall to the process with the first argument 0. This
is shown in Listing 9-12. The only error we could encounter and that we
could send back to the process is the lack of a buffer. This can happen only
if the process has swapped the buffer while the driver is still displaying
characters. This is why we use enter(...).unwrap_or when accessing
the buffer. If the buffer is missing, the enter function will return Err(...).
If the buffer is accessible, it will execute the closure that has the last
expression true wrapped within a Result.
If the value returned by enter is Err(...), unwrap_or returns its
argument (in this case false). If the value is Ok(value) it returns value.
if res {
app.position = app.position + 1;
} else {
self.in_progress.set(false);
let _ = upcalls.schedule_upcall(0,
(
ErrorCode::NOMEM.into(), 0, 0
);
}
380
Chapter 9 Asynchronous Syscall Capsules
Note If you are using a Raspberry Pi Pico device, the driver needs
to be declared in the RaspberryPiPico structure, as shown at the end
of this chapter.
381
Chapter 9 Asynchronous Syscall Capsules
<'static, nrf52::gpio::GPIOPin<'static>>,
alarm: &'static capsules::alarm::AlarmDriver<
'static,
capsules::virtual_alarm::VirtualMuxAlarm
<'static, nrf52::rtc::Rtc<'static>>,
>,
// ...
text_display: &'static drivers::text_display
::TextDisplay<
'static,
LedMatrixLed<
'static,
nrf52::gpio::GPIOPin<'static>,
capsules::virtual_alarm::
VirtualMuxAlarm<'static,
nrf52::rtc::Rtc<'static>>,
>,
capsules::virtual_alarm::VirtualMuxAlarm
<'static, nrf52833::rtc::Rtc<'static>>,
>,
}
382
Chapter 9 Asynchronous Syscall Capsules
Now that we have declared the driver and registered it so that the
kernel can call its functions, we have to initialize it. This part is slightly
different from the previous version, as we have to add an alarm to it.
First of all, we have to create an alarm driver. As most of the drivers are
asynchronous, they receive a command request, either through a system call,
or a HIL function call, then start the action and return immediately. To be
able to signal the completion of the requested action, drivers need a client.
This is an entity that implements the driver’s Client HIL. For example, the
Alarm HIL has a counterpart AlarmClient HIL that the alarm’s client has to
implement.
383
Chapter 9 Asynchronous Syscall Capsules
In Tock, most of the drivers can have one single client. This means
that even if several other drivers were to have a reference to it, and all
these drivers can request actions from this driver, the driver itself can
only signal one single driver about the completion of a requested action.
This is an important limitation for the alarm, as many drivers rely on the
alarm driver.
To solve this issue, Tock provides virtual devices. Figure 9-9 illustrates
a comparison between using a virtual device and directly accessing the
actual device. The actual device drivers always talk to the hardware device.
As we have seen before, this device driver is able to report to one single
client. When using virtual devices, the device driver reports back to a
multiplexer (Mux).
384
Chapter 9 Asynchronous Syscall Capsules
Virtualized
Direct Access
Access
Mux
Device Driver
Device
Figure 9-9. Using a virtual device versus directly accessing the device
385
Chapter 9 Asynchronous Syscall Capsules
There is one caveat when using virtual devices. The HILs that are
used in virtual devices are not able to provide synchronous functions.
This is because the multiplexer queues requests. If there is no request in
progress from another virtual device, the multiplexer forwards the request
immediately. On the other hand, if the underlying driver is busy executing
another request, the multiplexer will delay the execution of the request
waiting for the driver to become free.
Just like many of the other drivers, our driver, too, requires the alarm.
This is why the alarm driver provides the virtualizing functionality. Before
instantiating our driver, we initialize a new virtual alarm device that our
driver will use with the help of the static_init macro (Listing 9-15). The
type for the virtual alarm is VirtualMuxAlarm. This is parametrized with a
'static lifetime and the actual alarm driver type, nrf52833::rtc::Rtc.
The new function receives as an argument the alarm multiplexer.
Listing 9-15. Initializing the virtual alarm for the TextDisplay driver
Now that we have initialized the virtual alarm, we can instantiate the
driver (Listing 9-16). We use the same code as for the previous version of
the driver. First, the driver type is changed to include the virtual alarm type
386
Chapter 9 Asynchronous Syscall Capsules
387
Chapter 9 Asynchronous Syscall Capsules
(4, 0),
(0, 0),
(1, 1),
(2, 1),
(3, 1),
(4, 1),
(0, 2),
(1, 2),
(2, 2),
(3, 2),
(4, 2),
(0, 3),
(1, 3),
(2, 3),
(3, 3),
(4, 3),
(0, 4),
(1, 4),
(2, 4),
(3, 4),
(4, 4)
),
virtual_alarm_text_display,
board_kernel.create_grant(
&memory_allocation_capability)
),
);
Before using the driver, we still have to perform one more step shown
in Listing 9-17. We have to connect the virtual alarm to the driver. This
means setting the driver as the alarm’s client by using the set_alarm_
client function. This step is essential, and failing to do so will render the
388
Chapter 9 Asynchronous Syscall Capsules
driver unusable. The driver will never receive the callback from the alarm
when the timeout expires. In other words, the driver will wait forever for
the alarm’s callback.
389
Chapter 9 Asynchronous Syscall Capsules
#pragma once
#include "tock.h"
#ifdef __cplusplus
extern "C" {
#endif
// Asynchronous API
void text_display_set_done_callback (
text_display_done_t callback,
void *callback_args
);
returncode_t text_display_show_text (
const char* text, unsigned int display_ms
);
// Synchronous API
returncode_t text_display_show_text_sync (
const char* text, unsigned int display_ms
);
#ifdef __cplusplus
}
#endif
390
Chapter 9 Asynchronous Syscall Capsules
D
efinitions
The userspace library implementation starts with the data type definitions
illustrated in Listing 9-19.
391
Chapter 9 Asynchronous Syscall Capsules
// asynchronous
static text_display_done_t *done_callback =
NULL;
static void * done_callback_args = NULL;
// synchronous
typedef struct {
bool done;
statuscode_t status;
} text_display_status_t;
392
Chapter 9 Asynchronous Syscall Capsules
393
Chapter 9 Asynchronous Syscall Capsules
upcall,
userdata
);
}
A
synchronous API
The driver that we have created in this chapter performs the displaying
of the text in an asynchronous way. From the process’s point of view, this
means that it can issue a request to the driver to display a text, do some
other work, and later on, check if the text displaying has been done. This is
what an asynchronous API means.
Our driver’s asynchronous API is represented by two functions and
a private callback. First we have the text_display_set_done_callback
function. This allows the user of the driver library to register a callback
to be called upon the completion of the text display process. Besides the
callback pointer, the function takes an additional pointer parameter that
will be sent as an argument when the driver calls the callback. As Tock
processes are single-threaded, we can safely store these two parameters in
global variables. Listing 9-21 presents the function.
void text_display_set_done_callback
(text_display_done_t callback, void
*callback_args) {
done_callback = callback;
done_callback_args = callback_args;
}
The function that sends the display command to the driver is text_
display_show_text, illustrated in Listing 9-22. At the beginning of the
function, we make sure that we have a valid pointer towards a string. If this
pointer is NULL, we return an INVALID error.
394
Chapter 9 Asynchronous Syscall Capsules
Listing 9-22. The text display function and its associated callback
returncode_t text_display_show_text (
const char* text,
unsigned int display_ms
) {
if (text == NULL) {
return RETURNCODE_EINVAL;
}
// allow the buffer
allow_ro_return_t allow_ret =
text_display_allow (
0, text, strlen (text)
);
if (allow_ret.success) {
// subscribe to the display finished event
395
Chapter 9 Asynchronous Syscall Capsules
subscribe_return_t subscribe_ret =
text_display_subscribe (
0, text_displayed, NULL
);
if (subscribe_ret.success) {
// execute command
syscall_return_t ret =
text_display_command (
1, strlen (text), display_ms
);
if (ret.type == TOCK_SYSCALL_SUCCESS) {
return RETURNCODE_SUCCESS;
} else {
// unallow the buffer
text_display_allow (0, NULL, 0);
// unsubscribe
text_display_subscribe (0, NULL, NULL);
return
tock_status_to_returncode(
ret.data[0]
);
}
} else {
// unallow the buffer
text_display_allow (0, NULL, 0);
return tock_status_to_returncode(
subscribe_ret.status
);
}
396
Chapter 9 Asynchronous Syscall Capsules
} else {
return tock_status_to_returncode(
allow_ret.status
);
}
}
Once we ensure that we have a valid string, we must share it with the
driver. This is done using the allow_readonly system call. The driver
receives read-only access to the text, as it only needs to read it, it never
modifies the text. Instead of using the allow_readonly system call directly,
we use the wrapper function defined earlier. We use the allow number is 0,
the text is the pointer for the buffer, and the length of the text is for the
buffer size. This function returns an allow_ro_return_t data type with
two fields: a boolean success field and an integer status representing
the error code in case of an error. If allow_readonly is successful, we can
continue. Otherwise, we just return the error status. If we cannot share the
buffer, the driver will have no way of reading and displaying the text.
The next step after sharing the buffer is to subscribe for an upcall from
the driver that will notify us when the text has been fully displayed. In
userspace, an upcall is represented by a function pointer and a pointer to
custom user data. The upcall API provided by Tock is generic and requires
a callback function that receives four parameters.
397
Chapter 9 Asynchronous Syscall Capsules
398
Chapter 9 Asynchronous Syscall Capsules
If the subscribe works and returns success, we can send the display
command to the driver. On the other hand, if an error is returned, we have
to clean up. This means that we have to unallow the buffer that we shared
previously with the driver. Tock does not have the concept of unallowing a
buffer, it only knows how to allow one. As long as we share another buffer
for the same allow number (we actually swap the buffer), we can use the
buffer that we have previously shared. The new buffer can be NULL, and
this is precisely what we do.
If both actions of sharing a buffer and subscribing are successful, we
send command number 1 to the driver to start displaying the text. The first
argument the command receives is the length of the text. The second one
is the time it should display each character expressed in milliseconds. If
the command is successful, the driver has started displaying the text and
will notify us using the text_displayed callback function. We can safely
return RETURNCODE_SUCCESS. The driver will eventually call our callback
when it finishes displaying the text or if it encounters an error.
In case of an error, the driver is not able to display the text, and we have
to clean up again. This means unsubscribing the callback function and
unallowing the buffer. Similar to allow, Tock does not provide a way of
unsubscribing a callback but instead uses the idea of swapping a callback.
Just like for buffers, providing a NULL function pointer for the callback does
the trick.
For command system calls, the actual error value is stored in the first
data item returned, data[0]. We return this value to report the error.
399
Chapter 9 Asynchronous Syscall Capsules
Synchronous API
So far, we have shown the asynchronous API that is very similar to how
drivers work. Many times, though, using an asynchronous API is difficult
for application developers. This is why most of the userspace libraries
provide synchronous APIs for the drivers. That is to say, the library
implements all the code necessary to wait for the callback.
Before we dive in deeper, we have to describe the yield system call
briefly.
Drivers may schedule upcalls anytime, regardless of what userspace
processes do. When a driver schedules an upcall, the kernel, instead of
immediately calling the registered function from userspace, enqueues the
upcall. Unlike Linux, which interrupts processes to deliver signals, Tock
never interrupts processes due to upcalls. The kernel will call the function
callback only when processes are in the Yielded state.
Processes ask the kernel to place them in the Yielded state by using
the yield system call. When the kernel receives a yield system call, it tries
to unqueue the process’ first upcall and call the callback function. If the
400
Chapter 9 Asynchronous Syscall Capsules
task queue is empty, the process is placed into the Yielded state until an
upcall is scheduled. Namely, callback functions can only be called as a
result of a yield system call.
A process can ask for several asynchronous actions from several
drivers. Each time the process uses the yield system call, one of the
callbacks will be called. The order in which these are called depends on
the order that drivers have scheduled the upcalls. No assumption should
be made about this order.
The libtock library provides an additional variant of the yield system
call, yield_no_wait. This works similarly but returns immediately if there
is no upcall scheduled. Unlike the yield function, which returns void,
yield_no_wait returns an integer. If the value is 1, it means that a callback
has been executed. A 0 value means that the task queue had no scheduled
upcalls.
The synchronous API shown in Listing 9-23 provides a function and
a private callback. The text_display_show_text_sync function works
similarly to its asynchronous counterpart, except that it returns only after
the whole text has been displayed.
First, it defines a text_display_status structure that contains two
fields. Its first field, done, is a boolean value that memorizes whether the
text has been fully displayed or not. Its second field, status, stores the
status value returned by the driver through the callback function when the
text has been fully displayed. The default values for the field are false and
TOCK_STATUSCODE_SUCCESS.
401
Chapter 9 Asynchronous Syscall Capsules
display_status->done = true;
display_status->status = status;
}
returncode_t text_display_show_text_sync (
const char* text, unsigned int display_ms
) {
text_display_status_t display_status;
display_status.done = false;
display_status.status = TOCK_STATUS_SUCCESS;
text_display_set_done_callback
(text_displayed_sync, &display_status);
returncode_t ret = text_display_show_text (
text, display_ms
);
402
Chapter 9 Asynchronous Syscall Capsules
while (expected_function_has_not_been_called) {
yield ();
}
403
Chapter 9 Asynchronous Syscall Capsules
404
Chapter 9 Asynchronous Syscall Capsules
about three characters per second (300 ms for each digit or letter) using
the text_display_show_text_sync function. Listing 9-25 displays the
complete source code.
405
Chapter 9 Asynchronous Syscall Capsules
int main(void) {
if (text_display_is_present()) {
bool done = false;
text_display_set_done_callback (
job_done, &done
);
text_display_show_text (
"Hello World from the Microbit", 300
);
if (text_display_show_text (
"Hello World from the Microbit", 300
) == RETURNCODE_SUCCESS)
{
while (
yield_no_wait() == 0 && done == false
) {
406
Chapter 9 Asynchronous Syscall Capsules
printf (".");
fflush (stdout);
delay_ms (1000);
}
}
} else {
printf ("Error: the text_display driver is
not present\n");
}
return 0;
}
407
Chapter 9 Asynchronous Syscall Capsules
finish. Within a while loop, we use yield_no_wait to ask the kernel to call
a scheduled callback if one exists. Eventually, this will make the kernel call
the job_done function when the driver finishes displaying the text.
The yield_no_wait function returns 0 if the kernel had no scheduled
callback to run and 1 otherwise. If there is no callback to run, our driver
is still working, and we can do something else in the meanwhile. In this
example, we print a dot, flush the print buffer so that it actually displays
the dot, and wait for one second. We then ask the kernel again to call one
of the scheduled callbacks.
Whenever yield_no_wait returns 1, the kernel has called a callback.
This might be job_done or another callback scheduled by another driver.
The kernel simply calls the first scheduled callback, regardless if it is the
callback that our driver sent or not. We use the done variable to verify if
our callback was called. Whenever the kernel calls our callback job_done,
this callback will set the done variable to true.
Using the asynchronous API gives us a significant advantage. We can
perform several actions in parallel. We do not have to wait for an action to
finish before starting another one. This is a key concept that has become
more and more important in standard computer applications.
408
Chapter 9 Asynchronous Syscall Capsules
Listing 9-27 shows all the changes to the main.rs file, highlighting
the differences between the micro:bit and the Raspberry Pi Pico
implementations.
409
Chapter 9 Asynchronous Syscall Capsules
VirtualMuxAlarm<
'static,
RPTimer<'static>
>,
>,
}
410
Chapter 9 Asynchronous Syscall Capsules
411
Chapter 9 Asynchronous Syscall Capsules
&peripherals.pins.get_pin(RPGpio::GPIO4),
&peripherals.pins.get_pin(RPGpio::GPIO5),
&peripherals.pins.get_pin(RPGpio::GPIO6),
@rows => kernel::hil::gpio::ActivationMode::ActiveLow,
&peripherals.pins.get_pin(RPGpio::GPIO7),
&peripherals.pins.get_pin(RPGpio::GPIO8),
&peripherals.pins.get_pin(RPGpio::GPIO9),
&peripherals.pins.get_pin(RPGpio::GPIO10),
&peripherals.pins.get_pin(RPGpio::GPIO11),
)
.finalize(
components::led_matrix_component_buf!(
RPGpioPin<'static>,
RPTimer<'static>
)
);
let virtual_alarm_text_display =
static_init!(
capsules::virtual_alarm::VirtualMuxAlarm<
'static, RPTimer<'static>
>,
capsules::virtual_alarm::VirtualMuxAlarm::new(mux_alarm)
);
412
Chapter 9 Asynchronous Syscall Capsules
capsules::virtual_alarm::VirtualMuxAlarm<'static,
RPTimer<'static>>,
>,
capsules::virtual_alarm::VirtualMuxAlarm<'static,
RPTimer<'static>>,
>,
drivers::text_display::TextDisplay::new(
components::led_matrix_leds!(
RPGpioPin<'static>,
capsules::virtual_alarm::VirtualMuxAlarm<'static,
RPTimer<'static>>,
led_matrix_driver,
(0, 0),
(1, 0),
// ...
),
virtual_alarm_text_display,
board_kernel.create_grant(
&memory_allocation_capability
)
)
);
virtual_alarm_text_display.set_alarm_client(
text_display
);
// ...
let raspberry_pi_pico = RaspberryPiPico {
// ...
text_display: text_display,
};
// ...
}
413
Chapter 9 Asynchronous Syscall Capsules
Summary
In this chapter, we have presented how capsules exposing asynchronous
API should be written. This is the fundamental way in which capsules work
in Tock. Instead of blocking the execution while an action finishes, Tock
takes an asynchronous approach. Drivers start the first action of a task
and hand back the execution control to the kernel. Whenever the action
is done, the kernel is signaled and hands back control to the capsule to
continue with the next task action.
While this approach makes driver writing more complex, it has
several advantages. First of all, it allows concurrency by efficiently using
the hardware resources. Most of the actions that hardware components
execute are asynchronous. The hardware device receives a command,
executes it completely separated from the main MCU, and signals the MCU
through an interrupt upon completion. In the meanwhile, the MCU can
perform several tasks that are not related to this particular device.
The second advantage of this approach is that it makes the kernel’s
code simpler. Modern operating systems have mechanisms to interrupt a
driver that is performing an action that takes a longer time. In other words,
drivers become a kind of kernel processes that have to be preempted and
scheduled. This adds a lot of complexity to the kernel’s source code. Linux
takes this approach.
Most modern software, programming languages, and libraries
take this asynchronous or collaborative approach in favor of using the
classical threads. This uses the hardware resources more efficiently. All
the time used for context switches is not used to perform valuable work.
Nginx, NodeJS and Swift are examples of software that encourage the
asynchronous model.
414
Chapter 9 Asynchronous Syscall Capsules
415
CHAPTER 10
Service Capsules
Tock has three types of capsules: syscall capsules, service capsules, and
low-level drivers. Syscall capsules extend the kernel’s functionality and
expose an API towards the userspace while service capsules and low-level
capsules interact with hardware. So far, we have discussed, designed,
and implemented synchronous and asynchronous syscall capsules. This
chapter presents an adaptation of the TextDisplay syscall capsules to
function as a service driver.
R
equirements
This chapter requires the following hardware components based on the
device you use:
• Micro:bit
• 1 x micro:bit v2 board
• Raspberry Pi Pico
• 1 x Raspberry Pi Pico board;
• 1 x KWM-R30881CUAB or KWM-R30881AUAB
LED matrix;
• 14 x jumper wires;
• 5 x 220Ω resistors;
• 2 x (or 1 x large) breadboards.
418
Chapter 10 Service Capsules
This separation between the API and the service allows developers to
use the same API with different drivers. In other words, an application can
use the NineDoF API with any motion sensor, regardless of the hardware
it uses, as long as the sensor service driver implements the NineDoF
HIL. Figure 10-1 illustrates the relationship between the NineDoF Syscal
Driver and the Fxos8700 Driver.
419
Chapter 10 Service Capsules
Application
SyscallDriver trait
NineDoF Capsule
NineDoF trait
Hardware Registers
Hardware
420
Chapter 10 Service Capsules
421
Chapter 10 Service Capsules
off to display the corresponding text. This is done in a very similar way to
what we did for each character using the LED matrix, just that the display,
compared to the LED matrix, can display more than 25 dots.
A Tock service driver controlling a text display has to copy the text data
it receives to the display’s buffer and send the display a few commands to
make the new data visible.
To access the text screen, processes have to interact with the TextScreen
driver. Its purpose is to provide a standard system call API to the userspace
and forward requests further to the actual service driver that directly
controls the hardware. Figure 10-3 shows how the TextScreen driver works.
The driver keeps an internal buffer, called an intermediate text frame
buffer. Its purpose is to store the data received from a process through a
read-only allowed buffer and send it to the hardware driver. As its name
says, it intermediates the data transfer between the process and the
hardware.
TextScreen Driver
All processes that want to display some text to the screen have to send
data to the screen driver using a read-only allowed buffer. In turn, the
TextScreen driver will copy the data to its intermediate buffer and send it to
the hardware.
422
Chapter 10 Service Capsules
TextScreen Driver
Text Buffer
Text Driver
423
Chapter 10 Service Capsules
424
Chapter 10 Service Capsules
Application
User Space
Kernel System Call
Kernel
Service Capsule
TextScreen Capsule
Driver
Hardware
TextScreen trait
Hardware Registers
Hardware
(LED Matrix and Real Time Clck)
425
Chapter 10 Service Capsules
426
Chapter 10 Service Capsules
427
Chapter 10 Service Capsules
Most of the trait’s functions are asynchronous. This means that upon the
successful return, the caller will receive another callback function when the
requested action is finished. In other words, if the function returns Ok(...),
that means that the requested action will be performed, and one of the
TextScreenClient trait’s functions will be called when the action is done.
The set_client and get_size functions are synchronous. The first
one sets the HIL’s client. This is any data structure that implements
the TextScreenClient trait, and that will be notified when actions are
finished. The second one returns the size of the screen in the (columns,
rows) format. This function is synchronous as text screens usually have a
fixed size, and drivers do not need to read or request it from the hardware.
All the other functions, except print, are considered command
functions. Upon completion of their action, they will call the
TextScreenClient’s command_complete function.
The print function is the one that copies the received buffer
to the hardware buffer. Upon completion, this function calls the
TextScreenClient’s write_complete function. This distinction is
necessary as the print function, in contrast with the other functions from
the trait, has to return the buffer it has received.
We will discuss all the functions in detail while we implement
the driver.
428
Chapter 10 Service Capsules
D
river Initialization
The first step in the driver’s implementation is to define the structure
together with all the necessary fields. Similar to the previous two
drivers that we wrote, we define the LedMatrixText structure. It takes
three arguments: a generic lifetime used for annotating the structure’s
references, a data type that implements the Led trait, and a data type that
implements the Alarm trait (Listing 10-3).
429
Chapter 10 Service Capsules
client: OptionalCell<
&'a dyn TextScreenClient
>,
speed: Cell<u32>,
status: Cell<Status>,
is_enabled: Cell<bool>,
The led and alarm fields should be familiar as we used them for
the previous drivers we wrote. The first one is a reference to an array of
references to LEDs and the second one is a reference to a virtual alarm.
Next, we have the client field that references a data structure that
implements the TextScreenClient trait. This is used to inform TextScreen
when the requested actions are done.
Buffers and Parameters
The buffer field represents our internal buffer. If this were an actual
hardware component, buffer would be the part of the display’s memory
where the displayable data is stored. Its type, TakeCell<'a, T>, is worth
some explanation. Similar to OptionalCell<T>, it wraps a value that may
430
Chapter 10 Service Capsules
or may not exist. The difference is in how that value is placed, used, and
taken out from the wrapper. When using OptionalCell, the value wrapped
has to be of Copy type. This means that each time the wrapped value is
used, Rust makes a copy of it. This is fine for primitive data types like
numbers or immutable references but is a problem for mutable references,
like our buffers.
As TakeCell does not require the interior data to be Copy, users have to
either modify the internal data within the map closure function or move out
the data from the TakeCell, making it empty.
The position field is used to store the current position within the
buffer. In other words, it is the index of the letter or digit that will be the next
one displayed. The len field shows us the useful length of the data stored in
the buffer. For instance, while the buffer may hold up to 100 letters or digits,
the actual useful length is given by the amount of data TextScreen sends
us. The initial value of len is 0, as the buffer is initially empty.
The following two fields, client_buffer and client_len, are
used to store and represent the useful length of the buffer supplied
by TextScreen’s print function. These two fields will store useful data
during the interval between the call to the print function and the write_
complete callback.
The speed at which the digits and letters are being displayed is stored
in the speed field as the number of milliseconds that each character should
be visible on the screen.
Our capsule has to implement several asynchronous functions. This
means that whenever it receives a function call from the TextScreen, it
needs to perform an action, return from the function, and at a certain later
time continue the action that it started. This will become more obvious
431
Chapter 10 Service Capsules
Idle The capsule is not executing any action and can take new
commands.
ExecutesCommand The capsule executes a command that requires at completion
a call of the client’s command_complete function.
ExecutesPrint The capsule executes a command that requires at completion
a call of the client’s write_complete function (this happens
when the print function is called).
D
eferred Calls
To simulate the hardware’s asynchronous behavior, we need to use
deferred calls. This is a mechanism that the Tock kernel provides to
capsules to be able to simulate hardware interrupts. Whenever a driver
receives a function call to instruct a piece of hardware to perform an
action, it sends a command to the hardware and then returns from the
function call. Whenever the hardware completes the task, it sends an
interrupt to the Tock kernel. In turn, the Tock kernel executes the interrupt
handler of the capsule that has requested the action. The Tock’s kernel
event loop is presented in Figure 10-6.
432
Chapter 10 Service Capsules
System Call
Execute generic_isr or
Fault
Hardware Interrupt
Return to Kernel
Process
Kernel
yes
Execute Syscall or
Interrupt Handler Is Syscall or Fault?
Fault Handler
Deferred Call Handler
no
Empty Deferred Call Handler
no
Deferred Caller
Deferred Call
Executed
yes
Has Scheduled
Calls?
no
Schedule Process
433
Chapter 10 Service Capsules
impl<
LedMatrixText {
leds: leds,
alarm: alarm,
434
Chapter 10 Service Capsules
buffer: TakeCell::new(buffer),
client_buffer: TakeCell::empty(),
client_len: Cell::new(0),
position: Cell::new(0),
speed: Cell::new(speed),
len: Cell::new(0),
status: Cell::new(Status::Idle),
is_enabled: Cell::new(false),
deferred_caller: deferred_caller,
deferred_call_handle:
OptionalCell::empty(),
client: OptionalCell::empty(),
}
}
pub fn initialize_callback_handle(
&self,
deferred_call_handle: DeferredCallHandle
) {
self.deferred_call_handle.replace(
deferred_call_handle
);
}
fn schedule_deferred_callback(&self) {
self.deferred_call_handle
.map(|handle|
self.deferred_caller.set(*handle)
);
}
/* ... */
}
435
Chapter 10 Service Capsules
impl<
'a, L: Led, A: Alarm<'a>
> LedMatrixText<'a, L, A> {
/* ... */
fn get_buffer_len(&self) -> usize {
self.buffer.map_or(
0, |buffer| buffer.len()
)
}
}
436
Chapter 10 Service Capsules
impl<
'a, L: Led, A: Alarm<'a>
> TextScreen<'a> for LedMatrixText<'a, L, A> {
fn set_client(
&self,
client: Option<&'a dyn TextScreenClient>
) {
if let Some(client) = client {
self.client.set(client);
} else {
self.client.clear();
}
}
fn print(
&self,
buffer: &'static mut [u8],
len: usize,
) -> Result<
(), (ErrorCode, &'static mut [u8])
> { /* ... */ }
fn set_cursor(
&self,
_x_position: usize,
_y_position: usize
) -> Result<(), ErrorCode> {
Err(ErrorCode::NOSUPPORT)
}
437
Chapter 10 Service Capsules
self.is_enabled.set(false);
self.schedule_deferred_callback();
Ok(())
} else {
Err(ErrorCode::BUSY)
}
}
439
Chapter 10 Service Capsules
The TextScreen capsule will have to know the resolution of our screen,
meaning the number of columns and rows that our screen can display.
For this, it will call the get_size function. Within this function, we have to
return a tuple of two usize variables synchronously: the first is the number
of columns, and the second is the number of rows. The number of columns
that we can display is the size of our buffer, and the number of rows is 1.
Asynchronous Functions
The following functions that we implement are display_on and display_
off. Just as their names suggest, these functions turn the display on and
off. In our case, all that they need to do is to set the value of the is_enabled
field. In the case of actual hardware, these functions send a command
to the display hardware and immediately return. Whenever the display
finishes performing the action, it notifies its driver using an interrupt, and
the driver, in turn, calls the command_complete function. We need to take a
close look at the two.
These are asynchronous functions, meaning that the function’s caller,
Tock’s TextScreen capsule, expects them to either fail or to start an action
and return and notify the capsule when the action is done. The functions
return Result<(), ErrorCode>. If the called function returns an error, the
caller understands that the requested action cannot be done and expects
nothing else from the driver. If the called function returns Ok(()), the
requested action can be done, or at least the capsule will try to do it. The
caller now must wait for a call of the command_complete function before it
can request another command.
440
Chapter 10 Service Capsules
Now let’s take a look at the code. First of all, in both functions we check
if the current status of the capsule is Idle. This means that the capsule
has no action in progress and can accept a new request. If the status is not
Idle, we have an action in progress and should not accept a new one. In
this case, we simply return ErrorCode::BUSY. If the current status is Idle,
we can start a new action.
First of all, we set the status to ExecuteCommand as we have started
to perform an action. Secondly, we set the is_enabled field to the
corresponding value. As our driver is not controlling an actual hardware
display, setting the display on or off is the synchronous action of setting
the is_enabled field. At this point, we have finished our action. If this were
hardware, we would schedule a data transmission towards the device.
As we have completed the requested action, we are tempted to call the
command_complete function right away. However, we cannot do this. Our
caller expects to receive a call to command_complete after our function has
returned. This means that before we can call command_complete, we have
to return Ok(()), which is obviously not possible. This is where deferred
calls come into play. Instead of directly calling command_complete, we
ask the kernel to schedule a deferred call for us. In other words, we ask
the kernel to call a function for us, after we have returned to our caller.
To do this, we use the schedule_deferred_callback function defined in
Listing 10-4.
441
Chapter 10 Service Capsules
442
Chapter 10 Service Capsules
Status::Idle => {
// panic! ("Error, callback Idle");
}
Status::ExecutesCommand => {
self.client.map(|client|
client.command_complete(Ok(())));
}
Status::ExecutesPrint => {
self.client.map(|client| {
self.client_buffer.take()
.map(|buffer|
client.write_complete(
buffer,
self.client_len.get(),
Ok(())
)
);
});
}
}
self.status.set(Status::Idle);
}
}
443
Chapter 10 Service Capsules
P
rinting the Text
The most important function that we have to implement is print. This
receives two arguments: a buffer and the number of useful characters
that the buffer stores. The function, displayed in Listing 10-8, copies the
useful data from the buffer to the driver’s internal buffer. Just like the other
functions that we have implemented, it is asynchronous, which means that
it either returns an error or success followed by a call to the client’s write_
complete function.
444
Chapter 10 Service Capsules
impl<
'a, L: Led, A: Alarm<'a>
> TextScreen<'a> for LedMatrixText<'a, L, A> {
/* ... */
fn print(
&self,
buffer: &'static mut [u8],
len: usize,
) -> Result<(), (ErrorCode, &'static mut [u8])> {
if self.status.get() == Status::Idle {
if len <= buffer.len() {
self.status.set(Status::ExecutesPrint);
let previous_len = self.len.get();
let printed_len = self.buffer.map_or(
0, |buf| {
let max_len = cmp::min
len, buf.len()
);
for position in 0..max_len {
buf[position] = buffer[position];
}
self.len.set(cmp::max(
max_len, self.len.get()
));
max_len
});
self.client_buffer.replace(buffer);
self.client_len.set(printed_len);
self.schedule_deferred_callback();
445
Chapter 10 Service Capsules
/* ... */
}
Let’s analyze step by step what the function does. First of all, it checks
if there is any other action in progress by reading the value of the status
field. If this is not Idle, another action is in progress, so the function
returns ErrorCode::BUSY together with the buffer. If the driver’s status
is Idle, the function proceeds and verifies if the received arguments are
valid. More specifically, it makes sure that the provided length of the useful
data, len, is less or equal to the size of the received buffer. If this is not the
case, the function stops and returns ErrorCode::INVALID and the buffer.
With this, the function signals the caller that the received arguments are
invalid.
If there is no other action in progress and the arguments are valid, the
print function can start a new action by setting the value of the status
field to ExecutePrint. Next, it stores the size of the useful data, len, in
the previous_len variable so it can later make a power consumption
optimization.
446
Chapter 10 Service Capsules
The following action is to copy the data from the received buffer to the
capsules’s buffer. For that, we have to access the internal buffer using the
map_or function. This function will return the number of characters that
have been copied.
If the TakeCell that wraps the capsule’s buffer is storing a buffer, it will
call the closure function, putting a reference to the buffer in buf argument.
If there is no buffer stored, it will return 0 as no characters could be copied.
For this capsule, this should never happen, as we never take away the
buffer. On the other hand, if real hardware were involved, the driver would
probably take away the buffer from its wrapper and send it to the transport
(I2C, SPI, etc.) driver.
The actual size of the data being copied, stored in max_len, is the
minimum between the requested data size, len, and the size of the
driver’s buffer, buf.len(). This makes sure that the capsule’s buffer does
not overflow. There can be two approaches to copying the data from
the received buffer to the capsule’s. We can either verify that the data
we need to copy fits into the buffer and return an error if it does not or
simply copy as much data as possible. The second solution is valid only if
we can inform the caller that only a part of the data has been copied. As
write_complete requires us to notify the caller of how much data has been
copied, we chose the second approach.
The new length of the useful data stored into the capsule’s buffer is the
maximum between the size that we received as an argument, len, and the
current size of the data within the buffer, buf.len().
447
Chapter 10 Service Capsules
448
Chapter 10 Service Capsules
impl<
'a, L: Led, A: Alarm<'a>
> LedMatrixText<'a, L, A> {
/* ... */
fn display_next(&self) {
if self.position.get() >= self.len.get() {
self.position.set(0);
}
if self.position.get() < self.len.get() {
if !self.buffer.map_or(false, |buffer| {
if self.position.get() < buffer.len() {
let _ = self.display(
buffer[self.position.get()] as char
);
self.position.set(
self.position.get() + 1
);
true
} else {
false
}
}) {
449
Chapter 10 Service Capsules
self.clear();
}
} else {
self.clear();
}
if self.len.get() > 0 {
self.alarm.set_alarm(
self.alarm.now(), self.alarm.ticks_from_ms(
self.speed.get()
)
);
}
}
impl<
'a, L: Led, A: Alarm<'a>
> AlarmClient for LedMatrixText<'a, L, A> {
fn alarm(&self) {
450
Chapter 10 Service Capsules
self.display_next();
}
}
The core function that does the heavy lifting is display_text. This
function is called whenever the alarm fires. Its purpose is to display the
next character from the capsule’s buffer. Within the capsule’s structure,
we have defined a field called position that stores the following position
within the buffer that has to be displayed, and a field called len that stores
the size of the actual data within the buffer. Before printing anything,
display_next has to determine if it can actually display the digit or letter
stored at position. The capsule might have already displayed the last
digit or letter from the buffer. If position is larger than len, position is
reset to 0.
Now, position should be less than len. The function still checks this
condition before continuing, as the buffer might be empty as a result of a
call to TextScreen::clear, and therefore it should not display anything
and just turn off the LEDs. If position is valid, the function tries to access
the buffer using the map_or function. In this case, this function returns
whether a digit or letter has been displayed or not. Suppose there is no
buffer, map_or returns false. If there is a buffer, the closure function is
executed. Just to be on the safe side, the closure function verifies that the
position is within the buffer’s size, asks the display function to display
the digit or letter, and increments position.
If there is no buffer available or the position is out of the buffer’s size,
the LEDs are turned off using the clear function.
This driver tries to perform a small power consumption optimization.
Tock will do its best to put the MCU into low power mode if there is no task
to perform. If the buffer is empty, there is no point in scheduling the next
alarm until there is data to display. This is why display_next schedules
the alarm only if there is data in the driver’s buffer. This is why the print
function in Listing 10-8 verifies if the previous length of the buffer was
451
Chapter 10 Service Capsules
empty and the current length is different from 0 and calls display_next
immediately. If the previous length was 0, there is no alarm scheduled to
call display_next. If the current buffer size is 0, there is no point in calling
display_next as there is nothing to display.
Table 10-2. The setup system call API for the LedMatrixText capsule.
Command
No Arg 1 Arg 2 Description Return
0 Not used Not used Verifies if the capsule is CommandReturn::success( )
available.
1 speed Not used The time in milliseconds CommandReturn::success( )
during which each digit or
letter is displayed.
452
Chapter 10 Service Capsules
Listing 10-10. The implementation of the setup system call API for
the LedMatrixText driver
pub const DRIVER_NUM: usize = 0xa0003;
impl<
'a, L: Led, A: Alarm<'a>
> SyscallDriver for LedMatrixText<'a, L, A> {
fn allocate_grant(&self, _: ProcessId) -> Result<(), Error> {
Ok(())
}
fn command(
&self,
command_number: usize,
r2: usize,
_r3: usize,
_process_id: ProcessId,
) -> CommandReturn {
match command_number {
0 => CommandReturn::success(),
453
Chapter 10 Service Capsules
1 => {
self.speed.set(r2 as u32);
CommandReturn::success()
}
_ => CommandReturn::failure(
ErrorCode::NOSUPPORT
),
}
}
}
C
apsule Registration
In the previous chapters, we implemented syscall capsules, which
provide an API to the user space processes. All syscall capsules have to be
registered with the kernel using the SyscallDriverLookup trait. On the
other hand, service capsules provide services to other capsules and do
not usually interact with the userspace directly. These capsules have to be
connected to other capsules, in most cases through a reference passed as
an argument to the other capsule’s new function.
In our case, the capsule that uses our services is TextScreen. First,
we register TextScreen with the kernel as illustrated in Listing 10-11
by adding the text_screen field to the MicroBit structure. Next, we
connect its ID with this field within the with_driver function of the
SyscallDriverLookup trait implementation. This is required only if we
want to be able to use the setup syscall API.
454
Chapter 10 Service Capsules
455
Chapter 10 Service Capsules
match driver_num {
/* ... */
456
Chapter 10 Service Capsules
let dynamic_deferred_call_clients =
static_init!(
[DynamicDeferredCallClientState; 4],
Default::default()
);
/* ... */
/* ... */
457
Chapter 10 Service Capsules
drivers::led_matrix_text::LedMatrixText::new(
components::led_matrix_leds!(
nrf52::gpio::GPIOPin<'static>,
capsules::virtual_alarm::VirtualMuxAlarm<'static,
nrf52::rtc::Rtc<'static>>,
led,
(0, 0),
(1, 0),
// ...
),
virtual_alarm_led_matrix_text,
led_matrix_buffer,
300,
dynamic_deferred_caller
),
);
virtual_alarm_led_matrix_text
.set_alarm_client(led_matrix_text);
led_matrix_text.initialize_callback_handle(
dynamic_deferred_caller
.register(led_matrix_text)
.expect("no deferred call slot available for led
matrix text"),
);
458
Chapter 10 Service Capsules
/* ... */
text_screen,
led_matrix_text,
};
/* ... */
}
459
Chapter 10 Service Capsules
C
apsule Usage
So far, we have designed and implemented an LED matrix text screen
service capsule. A service capsule means that it offers services to other
capsules, in our case, Tock’s TextScreen capsule. This means that a
process that wants to use our capsule can do so by using Tock’s text screen
API. Listing 10-13 shows an example.
Listing 10-13. Usage example for the text screen driver API
#include "text_screen.h"
#include "timer.h"
#include <stdio.h>
#define SCREEN_BUFFER_SIZE 50
460
Chapter 10 Service Capsules
int main(void) {
if (driver_exists(DRIVER_NUM_TEXT_SCREEN)) {
if (text_screen_init(SCREEN_BUFFER_SIZE) == RETURNCODE_
SUCCESS) {
char *buffer = (char*)text_screen_buffer ();
strcpy (
buffer, "Hello World from the Microbit"
);
text_screen_set_cursor (0, 0);
text_screen_write (strlen (buffer));
} else {
printf ("Error: failed to initialize text screen\n");
}
} else {
printf ("Error: text screen driver is not present\n");
}
return 0;
}
Note At least for now, Tock does not provide such higher-level
APIs for text screens. The printf function uses the serial console to
display text.
461
Chapter 10 Service Capsules
The text screen API is specialized in sending buffers of data to the text
screen driver. The text screen library allocates a buffer that holds the text
that will be sent to the text screen driver. A process that wants to display a
message has to copy the message’s characters into this buffer, then ask the
library to send the buffer over to the driver.
The text_screen_init function receives a number as an argument
and allocates an internal text buffer of that size. To access that buffer,
developers have to use the text_screen_buffer function, which returns a
pointer to it. The next step is to copy the desired message to the buffer.
Before sending the text to the driver, processes usually inform the
driver about the position where that text should be displayed using the
text_screen_set_cursor function. In our specific case, this function
returns an error as our driver does not have support for it. We can safely
ignore the error. The buffer is sent to the driver using the text_screen_
write function.
462
Chapter 10 Service Capsules
#include "led_matrix_text.h"
#include "tock.h"
bool led_matrix_text_set_speed (
unsigned int speed
) {
syscall_return_t ret = command (
DRIVER_NUM_LED_MATRIX_TEXT, 1, speed, 0
);
if (ret.type == TOCK_SYSCALL_SUCCESS) {
return true;
} else {
return false;
}
}
463
Chapter 10 Service Capsules
Listing 10-15. Using the setup API to set the speed at which digits
and letters are displayed
#include "led_matrix_text.h"
#include "text_screen.h"
#include "timer.h"
#include <stdio.h>
#define SCREEN_BUFFER_SIZE 50
int main(void) {
if (driver_exists(DRIVER_NUM_TEXT_SCREEN)) {
if (text_screen_init(SCREEN_BUFFER_SIZE) == RETURNCODE_SUCCESS) {
char *buffer=(char*)text_screen_buffer ();
strcpy (
buffer, "Hello World from the Microbit"
);
text_screen_set_cursor (0, 0);
text_screen_write (strlen (buffer));
if (driver_exists (
DRIVER_NUM_LED_MATRIX_TEXT
)) {
printf ("Setting speed to 500\n");
led_matrix_text_set_speed (500);
}
} else {
printf ("Error: failed to initialize text screen\n");
}
} else {
printf ("Error: text screen driver is not present\n");
}
return 0;
}
464
Chapter 10 Service Capsules
465
Chapter 10 Service Capsules
466
Chapter 10 Service Capsules
}
}
}
467
Chapter 10 Service Capsules
capsules::virtual_alarm::VirtualMuxAlarm
::new(mux_alarm)
);
let led_matrix_buffer = static_init!(
[u8; 50], [0; 50]
);
468
Chapter 10 Service Capsules
dynamic_deferred_caller
)
);
virtual_alarm_led_matrix_text
.set_alarm_client(led_matrix_text);
led_matrix_text.initialize_callback_handle(
dynamic_deferred_caller
.register(led_matrix_text)
.expect("no deferred call slot available for led
matrix text"),
);
469
Chapter 10 Service Capsules
S
ummary
In this chapter, we have presented an example of how to design and
implement a service capsule. Tock ships a series of standard capsules
that provide different high-level APIs to the userspace. Such capsules are
sensors, like temperature, humidity, motion, devices like screen and text
screen, touch panels, storage, etc. All these standard syscall drivers need
one or several underlying service capsules that interact with the actual
hardware. This separation is important as processes in the user space
can use a standard API, regardless of the underlying hardware. These
standard syscall capsules are similar to Android’s Hardware Abstraction
Layer1 or HAL.
Another important aspect discussed in this chapter is deferred calls.
As Tock has an asynchronous design, most Hardware Interface Layers or
HILs, like TextScreen, expect actions to be performed asynchronously.
In other words, a request either returns an error or starts an action and
immediately returns success. Upon the action’s completion, the requester
is notified through a callback. There are cases, though, where such
behavior is not possible. The driver that we presented in this chapter
simulates a hardware screen, and as such, all actions are synchronous.
Tock provides the mechanism of deferred calls that allows developers to
ask the kernel to run a callback during the next interrupt handling step.
Another word for deferred calls is software interrupts.
Standard drivers provide a limited set of setup APIs. The underlying
hardware might expose an extended set of setup possibilities that are
not covered by the standard API. In this case, service drivers can add a
small setup syscall API that processes can use. There is a downside to
this: processes using the setup API become dependent on the specific
hardware.
1
Android Architecture, https://source.android.com/devices/
architecture#hidl
470
Chapter 10 Service Capsules
Last but not least, we have exemplified the usage of the text screen
userspace API to display a text via our driver.
471
CHAPTER 11
Tock Userspace
Drivers
In the previous chapters, we focused on building a kernel capsule to
interface an LED matrix. This capsule is part of the Tock kernel and is
registered as a new driver. However, the Tock architecture allows us to
build drivers that run in the userspace.
The userspace drivers are deployed as regular processes that run on
the system. They leverage Tock’s inter-process communication mechanism
so other processes can interact with them and ask them to perform various
actions.
R
equirements
This chapter requires the following hardware components based on the
device you use:
• Micro:bit
• 1 x micro:bit v2 board
• Raspberry Pi Pico
• 1 x KWM-R30881CUAB or KWM-R30881AUAB
LED matrix;
• 14 x jumper wires;
• 5 x 220Ω resistors;
474
Chapter 11 Tock Userspace Drivers
To notify the service or the client, applications need to use other two
functions:
To call the above functions, the client needs to identify the service
process id (pid). The function that does this is int ipc_discover(const
char* pkg_name, int* svc_id). It receives the service’s process name as
a parameter and stores its pid in the svc_id variable. If no such service is
found, svc_id will store a negative value.
Finally, the ipc library exposes a function that allows processes to share
a buffer: int ipc_share(int pid, void* base, int len). This is how
processes can share information. The base parameter specifies the starting
point of the buffer. For the micro:bit and Raspberry Pi Pico, it must be
aligned to the value of len, while len must be a power-of-two value that is
larger than 32, and 256 respectively. These constraints are due to the way
the memory is shared on the ARM microcontrollers. Both the client or the
service can call this function.
475
Chapter 11 Tock Userspace Drivers
T he TextDisplay Service
The TextDisplay service is designed to implement the functions necessary
for lighting up LEDs to print characters on the matrix. To create the service
files, we start with a new tock-project structure where the kernel setup
is made for the device you choose to use: micro:bit or Raspberry Pi Pico.
From there, we navigate to the tock-project/applications directory and
create a new folder called text_display, together with a main.c file. This is
where we place the service’s code (Listing 11-1).
To implement the service, we first have to define the buffer where we
store the text to be displayed, and similar to the capsule, we define the
LEDs state for all letters and numbers. This is a 25-bit long binary code that
identifies the state of each of the 25 LEDs of the matrix. The code is then
interpreted by the display_code (uint32_t code) function, which turns
on or off each LED according to the binary value.
476
Chapter 11 Tock Userspace Drivers
#include <stdio.h>
#include <ctype.h>
#include <timer.h>
#include <led.h>
#include <ipc.h>
#define NUM_LEDS 25
#define BUFFER_LEN 50
477
Chapter 11 Tock Userspace Drivers
for(led_index=0;led_index<NUM_LEDS;led_index++)
{
if(((code>>(NUM_LEDS-1-led_index))&0x1)==1)
{
led_on(led_index);
}
else
{
led_off(led_index);
}
}
}
Further on, we implement the main functions necessary for writing the
characters: display and clear, as illustrated in Listing 11-2. While clear
simply turns off all the LEDs, display transforms the received character
to uppercase, then calls display_code to light up the matrix LEDs
accordingly. We clear the display if the character passed as a parameter is
neither a letter nor a digit.
478
Chapter 11 Tock Userspace Drivers
479
Chapter 11 Tock Userspace Drivers
480
Chapter 11 Tock Userspace Drivers
int main(void) {
int leds;
int position = 0;
int len = 0;
bool should_clear = true;
strcpy (BUFFER, "");
481
Chapter 11 Tock Userspace Drivers
if (should_clear){
should_clear = false;
clear ();
}
}
else if (position < len) {
display (BUFFER[position]);
should_clear = true;
position = (position + 1) % len;
}
else{
position = 0;
display (BUFFER[position]);
should_clear = true;
position = (position + 1) % len;
}
delay_ms(300);
}
}
else{
printf ("text_display: Expected 25 LEDs, available
%d\n", leds);
}
}
else{
printf ("text_display: LEDs driver is not available\n");
}
return 0;
}
482
Chapter 11 Tock Userspace Drivers
Finally, we create the Makefile for the service (Listing 11-5). Because
we want to identify the process under the name text_display.service, we use
the PACKAGE_NAME variable to call the package like that.
PACKAGE_NAME = text_display.service
483
Chapter 11 Tock Userspace Drivers
#pragma once
#include <tock.h>
#define DISPLAY_BUFFER_LEN 64
#ifdef __cplusplus
extern "C" {
#endif
// display a test
int text_display_print (const char *buffer);
484
Chapter 11 Tock Userspace Drivers
#ifdef __cplusplus
}
#endif
#include "text_display.h"
#include <tock.h>
#include <ipc.h>
Listing 11-8. The function that searches for the text_display service
485
Chapter 11 Tock Userspace Drivers
486
Chapter 11 Tock Userspace Drivers
487
Chapter 11 Tock Userspace Drivers
}
}
// stop sharing the buffer so that it becomes
// accesible to the application
ipc_share(text_display_service, NULL, 0);
}
}
return ret;
}
The Makefile for the library is fairly simple and includes the necessary
dependencies (Listing 11-10).
include $(TOCK_USERLAND_BASE_DIR)/TockLibrary.mk
488
Chapter 11 Tock Userspace Drivers
#include <stdio.h>
#include <timer.h>
#include "text_display.h"
int main(void) {
if (text_display_is_present()){
text_display_print ("Hello World from the Microbit");
} else {
printf ("Error: the text_display.service service is not
present\n");
}
return 0;
}
By compiling the two applications, the service and the client, and
deploying them, we can notice the text being displayed on the matrix.
S
ummary
Tock’s implementation supports a mechanism that enables processes
to exchange information. This is done using a client-server architecture,
where a process waits for others to issue commands. That process is called
a service, while the others are the clients. However, in contrast to the
classical client-server architecture, the service can also send notifications
to the clients, independently from their requests. Data sharing between the
clients and the service is implemented in a similar way to Linux’s shared
memory mechanism.
489
Chapter 11 Tock Userspace Drivers
490
CHAPTER 12
Tock Systems
Management
So far, the chapters in this book have focused on application and system
development using Tock. In a development environment, deploying a
custom Tock system involves building the source code, bundling the
applications with the kernel, and finally deploying the binaries in an easy
to debug and monitor manner. We use tools such as OpenOCD and gdb to
achieve this.
However, in the Getting Started with Tock chapter, we installed the
tockloader utility as an alternative tool for deploying the Tock kernel and
applications. Tockloader is best used in production environments for
managing Tock systems. It is a command-line tool that supports various
operations for inspecting and controlling the applications running on top
of the Tock operating system.
R
unning Tockloader
The instructions in chapter 5: Getting Started with Tock include the
installation of tockloader. To ensure that you have it installed on your
system, either a Raspberry Pi device or your computer, based on your
development setup, you need to open a terminal and run the following
command: tockloader --v. This will display the tockloader version.
In case you cannot run the command, tockloader is not installed on the
system. To install it, run the first command in Listing 12-1. The second
command is optional and is only used to enable tab completion in the shell.
Tockloader can be used both with the micro:bit and the Raspberry
Pi Pico, but each device requires some specific operations beforehand.
Further on, we will detail each of these necessary steps.
492
Chapter 12 Tock Systems Management
1
https://github.com/tock/tock-bootloader/releases/download/microbit_
v2-vv1.1.1/tock-bootloader.microbit_v2.vv1.1.1.bin
493
Chapter 12 Tock Systems Management
On the other hand, we can continue from the last step of Listing 12-2
and run make flash inside the tock-bootloader/boards/microbit_v2-
bootloader folder. This will deploy the generated binary on the device.
To verify if the Tock bootloader was correctly flashed on the device, we
press and hold the A button while pressing and releasing the reset button.
This will reset the device and start the bootloader. When this happens, the
microphone LED lights up.
494
Chapter 12 Tock Systems Management
Troubleshooting
When using the micro:bit, tockloader is able to communicate with
the device using two channels. It can either communicate with tock-
bootloader through what is called the serial channel, or it can use openocd
directly. The second way is faster as it allows tockloader to access the flash
directly but does require OpenOCD to be installed.
As speed might be critical, tockloader will always try to use the fastest
channel that is available. This means that it will first verify if OpenOCD
is installed, and if so, it will try to use it. Another advantage of using
OpenOCD is that the micro:bit does not need to run tock-bootloader.
While openocd has several versions available, depending on your
operating system, tockloader might encounter some errors. At the time of
writing, tockloader is not able to fallback to another channel if OpenOCD
495
Chapter 12 Tock Systems Management
fails. It will simply exit with an error. Add --serial to the command line to
force tockloader to use the serial channel. Make sure that the micro:bit is
in bootloader mode, meaning that the microphone LED is lightened up.
Depending on the size of the apps that you load, tockloader might
fail to write them correctly when using openocd. If your device does not
seem to function properly, make sure to verify the installed apps using
tockloader list. If some apps are not present, upload them again using
tockloader and adding --bundle-apps to the command line.
Flash the Kernel
The first operation we can make using tockloader is to flash the kernel on
the device. This is appropriate for a production environment with a stable
kernel version that needs to be flashed on many devices.
496
Chapter 12 Tock Systems Management
The first step in doing this is to obtain the binary kernel image to be
flashed on the device. To get it, we navigate to the device folder and run
the make command. In the case of the micro:bit, the generated image is
tock/target/thumbv7em-none-eabi/release/microbit_v2.bin, and for
the Raspberry Pi Pico, it is tock/target/thumbv6m-none-eabi/release/
raspberry_pi_pico. .bin.
Once we have the binary file, to flash it on the micro:bit, we run the
command in Listing 12-4.
497
Chapter 12 Tock Systems Management
Listing 12-5. Flash the Tock kernel on the Raspberry Pi Pico using
tockloader
$ tockloader flash raspberry_pi_pico.bin --board raspberry_pi_
pico --openocd --address 0x10000000
D
evice Console
Tockloader can also be used to listen for messages coming from the
devices over a serial connection. In this case, the command is tockloader
listen. If we run this command, and then reset the device, we can notice
some kernel messages displayed, as shown in Listing 12-6.
$ tockloader listen
...
[INFO ] Listening for serial output.
Press any key to start the process console...
Initialization complete. Entering main loop.
498
Chapter 12 Tock Systems Management
I nstall/Remove Applications
The next step, after flashing the kernel on the device, is to deploy
applications. The advantage of the Tock architecture is that each
application is a separate executable file and can be built and deployed
independently from the kernel.
To get started, we create two applications, one that makes the onboard
LEDs blink and one that prints a message in the console using printf
(Listing 12-7).
#include <stdio.h>
int main(void){
printf ("Hello Tock!\n");
}
We name the first application folder blink and the second hello, then
we build each of them by using the make command in each application’s
folder (Listing 12-8).
499
Chapter 12 Tock Systems Management
...
Application size report for target cortex-m7:
text data bss dec hex filename
3996 252 2400 6648 19f8 build/cortex-m7/
cortex-m7.elf
T ockloader Install
Once we have obtained the tab files, we install them using the tockloader
install command, as illustrated in Listing 12-9. By running tockloader
listen, we can view the second process’ output while the first
application’s LED is blinking.
When running tockloader install without any TAB file, tockloader will
search all the available tab files in the current folder and its subfolders and
install all of them.
500
Chapter 12 Tock Systems Management
T ockloader Uninstall
To remove an application from the system, we run tockloader uninstall
followed by its name. The example in Listing 12-10 removes the hello
application.
U
pdate an Application
When changing an application, we can redeploy it by using the tockloader
update command. To test it, we can change the blink timing for the blink
application and update it (Listing 12-11).
$ make
. . .
$ tockloader update build/blink.tab
[STATUS ] Updating application on the board...
[INFO ] Flashing app blink binary to board.
[INFO ] CRC check passed. Binaries successfully loaded.
[INFO ] Finished in 7.182 seconds
501
Chapter 12 Tock Systems Management
I nspect the Applications
So far, we have used tockloader to upload and delete the applications that
run on the device. Besides these capabilities, tockloader can also be used
to inspect the system and get details about its applications.
Among the operations supported are: list the installed applications or
inspect the tab file associated with an app.
L ist Applications
Using tockloader list (Listing 12-12), we can print all the applications
running on a device together with the following details about them:
$tockloader list
[INFO ] Using "/dev/cu.usbmodem1462402 - "BBC micro:bit
CMSIS-DAP" - mbed Serial Port".
┌──────────────────────────────────────────┐
│ App 0 |
└──────────────────────────────────────────┘
Name: blink
Enabled: True
Sticky: False
Total Size in Flash: 8192 bytes
502
Chapter 12 Tock Systems Management
┌──────────────────────────────────────────┐
│ App 1 |
└──────────────────────────────────────────┘
Name: hello
Enabled: True
Sticky: False
Total Size in Flash: 2048 bytes
503
Chapter 12 Tock Systems Management
[3] cortex-m7
[4] None
cortex-m7:
version : 2
header_size : 52 0x34
total_size : 8192 0x2000
checksum : 0x6e505e1f
flags : 1 0x1
enabled : Yes
sticky : No
TLV: Main (1)
init_fn_offset : 41 0x29
protected_size : 0 0x0
minimum_ram_size : 4660 0x1234
TLV: Package Name (3)
package_name : blink
TLV: Kernel Version (8)
kernel_major : 2
kernel_minor : 0
kernel version : ^2.0
Application Configurations
When deploying a new application, we can specify some of the
characteristics analyzed above. To be more specific, tockloader
enables us to set if a process should start at boot or if it can be easily
uninstalled or not.
504
Chapter 12 Tock Systems Management
E nable/Disable an Application
By enabling or disabling an application, we specify if it should be loaded
after the device boots or not. By default, the applications are deployed
with the enabled flag set to True. However, by using the command
tockloader disable-app [app_name], we can disable it. To enable back
the application, we can use the tockloader enable-app [app_name]
command.
Listing 12-14 illustrates how the two work using the blink application.
To outline the result, we use tockloader list to print the application
details. What is more, after disabling the app, we can reset the device and
notice that the LED stops blinking.
505
Chapter 12 Tock Systems Management
S
ticky Applications
Another important characteristic of the applications running on
Tock systems is stickiness. This refers to how easily we can remove an
application from the system. If the sticky flag is set to True, uninstalling
that application requires the --force flag. By default, applications are
not sticky.
Listing 12-15 illustrates the command required to set the sticky flag to
True and the result if we try to uninstall that application.
F ault Policies
The Tock architecture is designed to allow for multiple concurrent
processes run independently. The main advantage is that if one process
fails, the others continue to run. However, to ensure this behavior, we need
to pay attention to the system’s fault policy.
Tock defines several policies based on which the system’s behavior
changes when a process crashes:
506
Chapter 12 Tock Systems Management
• TresholdRestartFaultPolicy – It is a custom
ProcessFaultPolicy implementation that uses a
threshold based on which the process will be restarted
or not. If the process has been restarted more times
than the threshold, the system will stop trying to
restart it.
#include <stdio.h>
int main(void){
int *array;
printf ("%d", array[0]);
}
507
Chapter 12 Tock Systems Management
notice the error message (Listing 12-17). What is more, we can notice that
the panic LED is blinking. This is because the kernel is configured to fault if
any of the processes crashes.
Listing 12-17. Listen for serial output when a process crashes and
the kernel is using the PanicFaultPolicy
$ tockloader listen
[INFO ] Using "/dev/cu.usbmodem1462402 - "BBC micro:bit
CMSIS-DAP" - mbed Serial Port".
[INFO ] Listening for serial output.
Initialization complete. Entering main loop.
To set a more relaxed fault policy, which will make the system
continue running even when a process crashes, we have to change a line
in the board’s implementation main.rs file to set FAULT_RESPONSE to
StopFaultPolicy. For the micro:bit, the file is tock/boards/microbit_
v2/main.rs, and the line we need to change is 63 (Listing 12-18). For
the Raspberry Pi Pico, we need to change line 54 in the tock/boards/
raspberry_pi_pico/main.rs file.
508
Chapter 12 Tock Systems Management
509
Chapter 12 Tock Systems Management
use core::cell::Cell;
struct CountFaultPolicy {
count: Cell<u32>
}
impl CountFaultPolicy {
pub const fn new() -> CountFaultPolicy {
CountFaultPolicy {
count: Cell::new(0)
}
}
}
510
Chapter 12 Tock Systems Management
kernel::process::load_processes(
board_kernel,
/* ... */
fault_response,
&process_management_capability,
)
Now we can reflash the kernel and upload the same faulty process as in
the previous example. After we run tockloader listen, we can notice the
debug messages in the console.
511
Chapter 12 Tock Systems Management
use kernel::process
impl ProcessFault for MicroBit {
fn process_fault_hook(
&self, process: &dyn process::Process
) -> Result<(), ()> {
debug!(
512
Chapter 12 Tock Systems Management
513
Chapter 12 Tock Systems Management
S
ystem Information
When deploying an embedded system in production, monitoring it is an
important aspect. Tockloader can be used to print information about a
device. The printed details include applications’ information, but also
device attributes. Listing 12-23 outlines an example of the output obtained
when running tockloader info.
$ tockloader info
[STATUS ] Showing all properties of the board...
Apps:
┌──────────────────────────────────────────┐
│ App 0 |
└──────────────────────────────────────────┘
Name: example_app
Enabled: True
Sticky: True
Total Size in Flash: 2048 bytes
Address in Flash: 0x40000
version : 2
header_size : 48 0x30
total_size : 2048 0x800
checksum : 0x3243745e
flags : 3 0x3
514
Chapter 12 Tock Systems Management
enabled : Yes
sticky : Yes
TLV: Main (1)
init_fn_offset : 41 0x29
protected_size : 0 0x0
minimum_ram_size : 6144 0x1800
TLV: Package Name (3)
package_name : example_app
Attributes:
00: board = microbit_v2
01: arch = cortex-m4
02: appaddr = 0x40000
03: boothash = d07821e78b75d811de62f997de51808a50c38395
We can also print only the board attributes by running the tockloader
list-attributes command.
Inspecting Processes
So far, we have used tockloader to inspect the system and the application
executable TBF and TAB files. Finally, we have a way of inspecting the
processes while they are running on the system. For this, we use the
process console, which is a capsule designed to interact with the Tock
kernel. The capsule reads the keyboard input and executes the commands
that are typed.
515
Chapter 12 Tock Systems Management
$ tockloader listen
...
[INFO ] Listening for serial output.
Initialization complete. Entering main loop.
Press any key to start the process console...
Kernel version: 2.0 (build 686d8d3)
Welcome to the process console.
Valid commands are: help status list stop start fault
process kernel
S
ystem Status
By running the status command in the process console, we can view basic
information about the system: the total number of processes installed, the
total number of processes that are running on the system, and how many
times the processes have been preempted because the allocated timeslice
expired (Listing 12-25).
516
Chapter 12 Tock Systems Management
Total processes: 3
Active processes: 3
Timeslice expirations: 301
List Processes
The process console also enables us to view a detailed list of all the
processes running on the device. This is done by running the list
command, which prints a table containing all the processes with the
following characteristics:
517
Chapter 12 Tock Systems Management
list
Listing 12-27. Stop and start a process using the process console
stop blink
518
Chapter 12 Tock Systems Management
process blink
╔═══════╤══════════════════════════════════╗
║ Address │Region Name Used | Allocated (bytes) ║
╚0x2000A000╪══════════════════════════════════╝
│ ▼ Grant 1120 | 1120
0x20009BA0 ┼──────────────────────────────────
│ Unused
0x200089E4 ┼──────────────────────────────────
│ ▲ Heap 0 | 4540 S
0x200089E4 ┼───────────────────────────────── R
│ Data 484 | 484 A
0x20008800 ┼───────────────────────────────── M
│ ▼ Stack 2048 | 2048
0x20008000 ┼──────────────────────────────────
│ Unused
0x20008000 ┴──────────────────────────────────
.....
0x00041400 ┬───────────────────────────────── F
│ App Flash 972 L
0x00041034 ┼───────────────────────────────── A
│ Protected 52 S
0x00041000 ┴───────────────────────────────── H
519
Chapter 12 Tock Systems Management
K
ernel Memory
The last command the process console supports is meant to print the
details about the kernel’s memory, as shown in Listing 12-29.
kernel
Kernel version: 2.0 (build 686d8d3)
╔════════╤═════════════════════════════════╗
║ Address │ Region Name Used (bytes) ║
╚0x200039E0═╪═════════════════════════════════╝
│ Bss 10720
0x20001000 ┼─────────────────────────────── S
│ Relocate 0 R
0x20001000 ┼─────────────────────────────── A
│ ▼ Stack 4096 M
0x20000000 ┼───────────────────────────────
.....
0x00023000 ┼─────────────────────────────── F
│ RoData 30398 L
0x0001B942 ┼─────────────────────────────── A
│ Code 80194 S
0x00008000 ┼─────────────────────────────── H
S
ummary
In this chapter, we focused on the means to configure and prepare our
Tock-based systems for production release. By leveraging the tockloader
tool and the process console capsule, we can flash the kernel, install/
uninstall applications, list applications and system details, and configure
the running processes.
520
Chapter 12 Tock Systems Management
2
https://github.com/tock/tockloader
521
Index
A B
ADC library, 242 boards directory, 217
Advanced High-performance Bootloader software, 87
Bus (AHB), 15
Advanced Peripheral Bus (APB), 15
Advanced RISC Machine (ARM), 25 C
allocate_grant function, 336, 358 Capsule (Asynchronous)
allow system call, 336 allow system call, 337–340
allow_readonly function, 362, 363 buffer/share, 362–366
Apollo Guidance command system call, 336
Computer (AGC), 2–5 command system, 366–370
Application Binary DigitLetterDisplay, 334, 335
Interface (ABI), 50 hardware components, 333, 334
Application Programming subscribe system call, 336
Interface (API), 50 TextDisplay driver, 341–343
app_state library, 257 text displays
Asynchronous system calls character, 374
callback function, 72 hardware implementation,
callback function returns, 71 377, 378
environment sensor, 69 kernel structure, 375
GPIO ports, 72 LED matrix, 371, 372
process calls, 69 signal application, 378, 380
read command, 70 Tock drivers, 343–345
schedule, 71 writing capsule
schedule/subscribe, 70 alarm field, 348, 349
system call pattern, 72 capsule’s state, 360
yield system, 71 code, 345
524
INDEX
525
INDEX
E, F advantages, 126
constant, 133
Embedded computers/systems, 1
function implementation,
Apollo Guidance
124, 125
Computer (AGC), 2–5 structure definition, 132
computers, 22–25 inheritance, 114, 116, 117, 119
display/keyboard interface, 4 trait objects
generic platform (see Generic debugging, 130
embedded platform) device method, 120
hybrids, 25 free-standing function, 120
microcontrollers, 22 function
system on a Chip (SoC), 23 implementation, 127
enter function, 358, 375, 380 implementation, 121
pins trait, 121
Rust standard traits, 128–130
G setPinsZero, 120
Garbage collection, 39 set_pins_zero
Generic embedded platform implementation, 123
architecture, 7 structure definition, 132
central processing git clone command, 216
unit (CPU), 7–14 gpio library, 237
component, 5 GPIO library
debug interface, 20, 21 LED blink application, 238
Raspberry Pi, 239
input/output (I/O) devices, 16
interrupt controller, 18–20
memory, 15 H
security issue, 5 Hardware Interface Layer (HIL), 54
storage space, 17, 18 Hardware systems, 27
system bus, 14 /home/tock directory, 174
Generics/trait objects
advantages, 114
associated type, 134 I
composition, 115 Internet of Things (IoT), 4
diamond problem, 117, 119 Inter-process communication
generics (IPC), 54, 263, 479
526
INDEX
527
INDEX
528
INDEX
529
INDEX
530
INDEX
531
INDEX
532
INDEX
533