Tock OS Book

This book introduces you to Tock, a secure embedded operating system for sensor networks and the Internet of Things. Tock is the first operating system to allow multiple untrusted applications to run concurrently on a microcontroller-based computer. The Tock kernel is written in Rust, a memory-safe systems language that does not rely on a garbage collector. Userspace applications are run in single-threaded processes that can be written in any language.

Getting Started

The book includes a quick start guide.

Tock Workshop Courses

For a more in-depth walkthough-style less, look here.

Development Guides

The book also has walkthoughs on how to implement different features in Tock OS.

Hands-on Guides

This portion of the book includes workshops and tutorials to teach how to use and develop with Tock, and is divided into two sections: the course and a series of mini tutorials. The course is a good place to start, and provides a structured introduction to Tock that should take a few hours to complete (it was designed for a half day workshop). The tutorials are smaller examples that highlight specific features.

Tock Course

In this hands-on guide, we will look at some of the high-level services provided by Tock. We will start with an understanding of the OS and its programming environment. Then we'll look at how a process management application can help afford remote debugging, diagnosing and fixing a resource-intensive app over the network. The last part of the tutorial is a bit more free-form, inviting attendees to further explore the networking and application features of Tock or to dig into the kernel a bit and explore how to enhance and extend the kernel.

This course assumes some experience programming embedded devices and fluency in C. It assumes no knowledge of Rust, although knowing Rust will allow you to be more creative during the kernel exploration at the end.

Tock Mini Tutorials

These tutorials feature specific examples of Tock applications. They can be completed after the course to learn about different capabilities of Tock apps.

Getting Started

This getting started guide covers how to get started using Tock.

Hardware

To really be able to use Tock and get a feel for the operating system, you will need a hardware platform that tock supports. The TockOs Hardware includes a list of supported hardware boards. You can also view the boards folder to see what platforms are supported.

Note, not all boards are equally supported, and some may have some "quirks" around what is implemented (or not), and exactly how to load code and test that it is working. This guides tries to be general, and Tock generally tries to follow a certain convention, but the project is under active development and new boards are added rapidly. You should definitely consult the board-specific README to see if there are any board-specific details you should be aware of.

Software

You can either download a virtual machine with the development environment pre-installed, or, if you have a Linux or OS X workstation, you may install the development environment natively. Using a virtual machine is quicker and easier to set up, while installing natively will yield the most comfortable development environment and is better for long term use.

Virtual Machine

If you're comfortable working inside a Debian virtual machine, you can download an image with all of the dependencies already installed here or here. Using curl to download the image is recommended, but your browser should be able to download it as well:

$ curl -O <url>

With the virtual machine image downloaded, you can run it with VirtualBox or VMWare:

The VM account is "tock" with password "tock". Feel free to customize it with whichever editors, window managers, etc. you like before the training starts.

If the Host OS is Linux, you may need to add your user to the vboxusers group on your machine in order to connect the hardware boards to the virtual machine.

Native Installation

If you choose to install the development environment natively on an existing operating system install, you will need the following software:

  1. Command line utilities: curl, make, git, python (version 3) and pip3.

  2. Clone the Tock kernel repository.

     $ git clone https://github.com/tock/tock
    
  3. rustup. This tool helps manage installations of the Rust compiler and related tools.

     $ curl https://sh.rustup.rs -sSf | sh
    
  4. arm-none-eabi toolchain (version >= 5.2)

     # mac
     $ brew tap ARMmbed/homebrew-formulae && brew update && brew install arm-none-eabi-gcc
    
     # linux
     $ sudo apt install gcc-arm-none-eabi
    

    OS-specific installation instructions can be found here

  5. riscv64-unknown-elf toolchain (version >= v2019.08.0). Scroll down to the "Prebuilt RISC‑V GCC Toolchain" section.

     # mac
     $ brew install riscv-gnu-toolchain --with-multilib
    
  6. tockloader

     $ pip3 install -U --user tockloader
    

    Note: On MacOS, you may need to add tockloader to your path. If you cannot run it after installation, run the following:

     $ export PATH=$HOME/Library/Python/3.6/bin/:$PATH
    

    Similarly, on Linux distributions, this will typically install to $HOME/.local/bin, and you may need to add that to your $PATH if not already present:

     $ PATH=$HOME/.local/bin:$PATH
    

Testing

To test if your environment is working, go to the tock/boards/imix directory and type make program (even if you don't have an imix board). This should compile the kernel for the imix board, and try to program it over a USB serial connection. It may need to compile several supporting libraries first (so may take 30 seconds or so the first time). You should see output like this:

$ make program
   Compiling tock-cells v0.1.0 (/Users/bradjc/git/tock/libraries/tock-cells)
   Compiling tock-registers v0.5.0 (/Users/bradjc/git/tock/libraries/tock-register-interface)
   Compiling enum_primitive v0.1.0 (/Users/bradjc/git/tock/libraries/enum_primitive)
   Compiling tock-rt0 v0.1.0 (/Users/bradjc/git/tock/libraries/tock-rt0)
   Compiling imix v0.1.0 (/Users/bradjc/git/tock/boards/imix)
   Compiling kernel v0.1.0 (/Users/bradjc/git/tock/kernel)
   Compiling cortexm v0.1.0 (/Users/bradjc/git/tock/arch/cortex-m)
   Compiling capsules v0.1.0 (/Users/bradjc/git/tock/capsules)
   Compiling cortexm4 v0.1.0 (/Users/bradjc/git/tock/arch/cortex-m4)
   Compiling sam4l v0.1.0 (/Users/bradjc/git/tock/chips/sam4l)
   Compiling components v0.1.0 (/Users/bradjc/git/tock/boards/components)
    Finished release [optimized + debuginfo] target(s) in 28.67s
   text    data     bss     dec     hex filename
 165376    3272   54072  222720   36600 /Users/bradjc/git/tock/target/thumbv7em-none-eabi/release/imix
   Compiling typenum v1.11.2
   Compiling byteorder v1.3.4
   Compiling byte-tools v0.3.1
   Compiling fake-simd v0.1.2
   Compiling opaque-debug v0.2.3
   Compiling block-padding v0.1.5
   Compiling generic-array v0.12.3
   Compiling block-buffer v0.7.3
   Compiling digest v0.8.1
   Compiling sha2 v0.8.1
   Compiling sha256sum v0.1.0 (/Users/bradjc/git/tock/tools/sha256sum)
6fa1b0d8e224e775d08e8b58c6c521c7b51fb0332b0ab5031fdec2bd612c907f  /Users/bradjc/git/tock/target/thumbv7em-none-eabi/release/imix.bin
tockloader  flash --address 0x10000 /Users/bradjc/git/tock/target/thumbv7em-none-eabi/release/imix.bin
[INFO   ] No device name specified. Using default name "tock".
[ERROR  ] No serial ports found. Is the board connected?

make: *** [program] Error 1

Note, that this failed because it couldn't find the board, which is fine. It still demonstrates that the kernel compiled and that tockloader was successfully invoked.

Getting the Hardware Connected

Plug your hardware board into your computer. Generally this requires a micro USB cable, but your board may be different.

Note! Some boards have multiple USB ports.

Some boards have two USB ports, where one is generally for debugging, and the other allows the board to act as any USB peripheral. You will want to connect using the "debug" port.

Some example boards:

  • imix: Use the port labeled DEBUG.
  • nRF52 development boards: Use the port of the left, on the skinny side of the board.

The board should appear as a regular serial device (e.g. /dev/tty.usbserial-c098e5130006 on my Mac or /dev/ttyUSB0 on my Linux box). This may require some setup, see the "one-time fixups" box.

One-Time Fixups

  • On Linux, you might need to give your user access to the serial port used by the board. If you get permission errors or you cannot access the serial port, this is likely the issue.

    You can fix this by setting up a udev rule to set the permissions correctly for the serial device when it is attached. You only need to run the command below for your specific board, but if you don't know which one to use, running both is totally fine, and will set things up in case you get a different hardware board!

    $ sudo bash -c "echo 'ATTRS{idVendor}==\"0403\", ATTRS{idProduct}==\"6015\", MODE=\"0666\"' > /etc/udev/rules.d/99-ftdi.rules"
    $ sudo bash -c "echo 'ATTRS{idVendor}==\"2341\", ATTRS{idProduct}==\"005a\", MODE=\"0666\"' > /etc/udev/rules.d/98-arduino.rules"
    

    Afterwards, detach and re-attach the board to reload the rule.

  • With a virtual machine, you might need to attach the USB device to the VM. To do so, after plugging in the board, select in the VirtualBox/VMWare menu bar:

    Devices -> USB Devices -> [The name of your board]
    

    If you aren't sure which board to select, it is often easiest to unplug and re-plug the board and see which entry is removed and then added.

    If this generates an error, often unplugging/replugging fixes it. You can also create a rule in the VM USB settings which will auto-attach the board to the VM.

  • With Windows Subsystem for Linux (WSL), the serial device parameters stored in the FTDI chip do not seem to get passed to Ubuntu. Plus, WSL enumerates every possible serial device. Therefore, tockloader cannot automatically guess which serial port is the correct one, and there are a lot to choose from.

    You will need to open Device Manager on Windows, and find which COM port the tock board is using. It will likely be called "USB Serial Port" and be listed as an FTDI device. The COM number will match what is used in WSL. For example, COM9 is /dev/ttyS9 on WSL.

    To use tockloader you should be able to specify the port manually. For example: tockloader --port /dev/ttyS9 list.

With the board connected, you should be able to use tockloader to interact with the board. For example, to retrieve serial UART data from the board, run tockloader listen, and you should see something like:

$ tockloader listen
No device name specified. Using default "tock"
Using "/dev/ttyUSB0 - Imix - TockOS"

Listening for serial output.
Initialization complete. Entering main loop
Hello World!

You may also need to reset (by pressing the reset button on the board) the board to see the message.

Flash the kernel

Now that the board is connected and you have verified that the kernel compiles (from the steps above), we can flash the board with the latest Tock kernel:

$ cd boards/<your board>
$ make

Generally boards are programmed with either make program or make flash. Try make program first:

$ make program

You can also look at the board's README for more details.

Why both make program and make flash?

While these commands do the same thing, the way they go about it is very different.

The make program version communicates with the board via a serial connection and a bootloader running on the board. You may need to manually enter the bootloader when using make program.

The make flash version uses a JTAG debugger to communicate with the chip and flash the kernel binary directly to the chip.

Install Some Applications

We have the kernel flashed, but the kernel doesn't actually do anything. Applications do! To load applications, we are going to use tockloader.

Note about Identifying Boards

Tockloader tries to automatically identify which board is attached to make this process simple. However, tockloader often does not have a good way to identify which board is attached, and requires that you manually specify which board you are trying to program. This can be done with the --board argument. For example, if you have an nrf52dk or nrf52840dk, you would run Tockloader like:

$ tockloader <command> --board nrf52dk --jlink

The --jlink flag tells tockloader to use the JLink JTAG tool to communicate with the board (this mirrors using make flash above). Some boards support OpenOCD, in which case you would pass --openocd instead.

To see a list of boards that tockloader supports, you can run tockloader list-boards. If you have an imix or Hail board, you should not need to specify the board.

Note, a board listed in tockloader list-boards means there are default settings hardcoded into tockloader's source on how to support those boards. However, all of those settings can be passed in via command-line parameters for boards that tockloader does not know about. See tockloader --help for more information.

Loading Pre-built Applications

We're going to install some pre-built applications, but first, let's make sure we're in a clean state, in case your board already has some applications installed. This command removes any processes that may have already been installed.

$ tockloader erase-apps [--board <your board> --jlink|--openocd]

Now, let's install two pre-compiled example apps. Remember, you may need to specify which board you are using and how to communicate with it for all of these commands. If you are using Hail or imix you will not have to.

$ tockloader install https://www.tockos.org/assets/tabs/blink.tab

The install subcommand takes a path or URL to an TAB (Tock Application Binary) file to install.

The board should restart and the user LED should start blinking. Let's also install a simple "Hello World" application:

$ tockloader install https://www.tockos.org/assets/tabs/c_hello.tab

If you now run tockloader listen you should be able to see the output of the Hello World! application. You may need to manually reset the board for this to happen. Also, since tockloader listen is only receiving from a serial port, you will not need to pass in --board or --jlink or --openocd.

$ tockloader listen
[INFO   ] No device name specified. Using default name "tock".
[INFO   ] Using "/dev/cu.usbserial-c098e513000a - Hail IoT Module - TockOS".

[INFO   ] Listening for serial output.
Initialization complete. Entering main loop.
Hello World!
␀

Uninstalling and Installing More Apps

Lets check what's on the board right now:

$ tockloader list
...
[App 0]
  Name:                  blink
  Enabled:               True
  Sticky:                False
  Total Size in Flash:   2048 bytes

[App 1]
  Name:                  c_hello
  Enabled:               True
  Sticky:                False
  Total Size in Flash:   1024 bytes

As you can see, the apps are still installed on the board. We can remove apps with the following command:

$ tockloader uninstall

Following the prompt, if you remove the blink app, the LED will stop blinking, however the console will still print Hello World.

Now let's try adding a more interesting app:

$ tockloader install https://www.tockos.org/assets/tabs/sensors.tab

The sensors app will automatically discover all available sensors, sample them once a second, and print the results.

$ tockloader listen
[INFO   ] No device name specified. Using default name "tock".
[INFO   ] Using "/dev/cu.usbserial-c098e513000a - Hail IoT Module - TockOS".

[INFO   ] Listening for serial output.
Initialization complete. Entering main loop.
[Sensors] Starting Sensors App.
Hello World!
␀[Sensors] All available sensors on the platform will be sampled.
ISL29035:   Light Intensity: 218
Temperature:                 28 deg C
Humidity:                    42%
FXOS8700CQ: X:               -112
FXOS8700CQ: Y:               23
FXOS8700CQ: Z:               987

Familiarize Yourself with tockloader Commands

The tockloader tool is a useful and versatile tool for managing and installing applications on Tock. It supports a number of commands, and a more complete list can be found in the tockloader repository, located at github.com/tock/tockloader. Below is a list of the more useful and important commands for programming and querying a board.

tockloader install

This is the main tockloader command, used to load Tock applications onto a board. By default, tockloader install adds the new application, but does not erase any others, replacing any already existing application with the same name. Use the --no-replace flag to install multiple copies of the same app. To install an app, either specify the tab file as an argument, or navigate to the app's source directory, build it (probably using make), then issue the install command:

$ tockloader install

Tip: You can add the --make flag to have tockloader automatically run make before installing, i.e. tockloader install --make

Tip: You can add the --erase flag to have tockloader automatically remove other applications when installing a new one.

tockloader uninstall [application name(s)]

Removes one or more applications from the board by name.

tockloader erase-apps

Removes all applications from the board.

tockloader list

Prints basic information about the apps currently loaded onto the board.

tockloader info

Shows all properties of the board, including information about currently loaded applications, their sizes and versions, and any set attributes.

tockloader listen

This command prints output from Tock apps to the terminal. It listens via UART, and will print out anything written to stdout/stderr from a board.

Tip: As a long-running command, listen interacts with other tockloader sessions. You can leave a terminal window open and listening. If another tockloader process needs access to the board (e.g. to install an app update), tockloader will automatically pause and resume listening.

tockloader flash

Loads binaries onto hardware platforms that are running a compatible bootloader. This is used by the Tock Make system when kernel binaries are programmed to the board with make program.

Explore Other Example Applications

There are many example applications in the libtock-c repository. To use them, first clone the repository:

$ git clone https://github.com/tock/libtock-c
$ cd libtock-c

Then you can build an example app. For example, if you want to use inter-process communication (IPC) with Tock:

# First remove the existing applications:
$ tockloader erase-apps

# Now build the IPC client app, and install it:
$ cd examples/rot13_client
$ make
$ tockloader install

# Now build the IPC service app, and install it:
$ cd ../rot13_server
$ make
$ tockloader install

With those two applications installed, you should be able to see the following output from the board:

$ tockloader listen
[INFO   ] No device name specified. Using default name "tock".
[INFO   ] Using "/dev/cu.usbserial-c098e513000a - Hail IoT Module - TockOS".

[INFO   ] Listening for serial output.
Initialization complete. Entering main loop.
12: Uryyb Jbeyq!
12: Hello World!
12: Uryyb Jbeyq!
12: Hello World!

Note: Tock platforms are limited in the number of apps they can load and run. However, it is possible to install more apps than this limit, since tockloader is (currently) unaware of this limitation and will allow to you to load additional apps. However the kernel will only load the first apps until the limit is reached.

Tock Course

The Tock course includes several different modules that guide you through various aspects of Tock and Tock applications. Each module is designed to be standalone such that a full course can be composed of different modules depending on the interests and backgrounds of those doing the course.

Prerequisites

You should follow the getting started guide to get your development setup and ensure you can communicate with the hardware.

Hardware

The Tock course is currently written for an imix hardware platform. To follow the directions directly, you will need an imix hardware platform (pictured above). Tock is a general operating system, however, and other boards should work, but they might not provide the exact same hardware sensors or peripherals.

To complete the 6LoWPAN networking portion of this guide, you'll need an additional imix to act as a hub.

Setup to Compile and Program the Kernel

All of the hands-on exercises will be done within the source code for this book. So pop open a terminal, and navigate to the repository. If you're using the VM, that'll be:

$ cd ~/book

Make sure your Tock repository is up to date

$ git pull

Build the kernel

To build the kernel, just type make in the imix/ subdirectory.

$ cd imix/
$ make

If this is the first time you are trying to make the kernel, the build system will use cargo and rustup to install various Tock dependencies.

If this is your first time building a Tock kernel for this particular architecture, you may get an error complaining that you don't have the proper the cargo target installed. We can use rustup to fix that:

$ rustup target add thumbv7em-none-eabi

imix is based around an ARM Cortex-M4 microcontroller, which uses the thumbv7em instruction set. The rustup command above just downloads Rust core libraries for this architecture.

Modules

The various modules are independent lessons that guide you through certain aspects of Tock. You should be able to do the lessons that are of interest to you.

Each module begins with a description of the lesson, and then includes steps to follow. The modules cover both programming in the kernel as well as applications.

Write an environment sensing application

Process overview, relocation model and system call API

In this section, we're going to learn about processes (a.k.a applications) in Tock, and build our own applications in C.

Get a C application running on your board

You'll find the outline of a C application in the directory exercises/app.

Take a look at the code in main.c. So far, this application merely prints "Hello, World!".

The code uses the standard C library routine printf to compose a message using a format string and print it to the console. Let's break down what the code layers are here:

  1. printf is provided by the C standard library (implemented by newlib). It takes the format string and arguments, and generates an output string from them. To actually write the string to standard out, printf calls _write.

  2. _write (in libtock-c's sys.c) is a wrapper for actually writing to output streams (in this case, standard out a.k.a. the console). It calls the Tock-specific console writing function putnstr.

  3. putnstr(in libtock-c's console.c) is a buffers data to be written, calls putnstr_async, and acts as a synchronous wrapper, yielding until the operation is complete.

  4. Finally, putnstr_async (in libtock-c's console.c) performs the actual system calls, calling to allow, subscribe, and command to enable the kernel to access the buffer, request a callback when the write is complete, and begin the write operation respectively.

The application could accomplish all of this by invoking Tock system calls directly, but using libraries makes for a much cleaner interface and allows users to not need to know the inner workings of the OS.

Loading an application

Okay, let's build and load this simple program.

  1. Erase all other applications from the development board:

     $ tockloader erase-apps
    
  2. Build the application and load it (Note: tockloader install automatically searches the current working directory and its subdirectories for Tock binaries.)

     $ tockloader install --make
    
  3. Check that it worked:

     $ tockloader listen
    

The output should look something like:

$ tockloader listen
No device name specified. Using default "tock"
Using "/dev/cu.usbserial-c098e5130012 - Hail IoT Module - TockOS"

Listening for serial output.
Hello, World!

Creating your own application

Now that you've got a basic app working, modify it so that it continuously prints out Hello World twice per second. You'll want to use the user library's timer facilities to manage this:

Timer

You'll find the interface for timers in libtock/timer.h. The function you'll find useful today is:

#include <timer.h>
void delay_ms(uint32_t ms);

This function sleeps until the specified number of milliseconds have passed, and then returns. So we call this function "synchronous": no further code will run until the delay is complete.

Write an app that periodically samples the on-board sensors

Now that we have the ability to write applications, let's do something a little more complex. The development board you are using has several sensors on it. These sensors include a light sensor, a humidity sensor, and a temperature sensor. Each sensing medium can be accessed separately via the Tock user library. We'll just be using the light and temperature for this exercise.

Light

The interface in libtock/ambient_light.h is used to measure ambient light conditions in lux. imix uses the ISL29035 sensor, but the userland library is abstracted from the details of particular sensors. It contains the function:

#include <ambient_light.h>
int ambient_light_read_intensity_sync(int* lux);

Note that the light reading is written to the location passed as an argument, and the function returns non-zero in the case of an error.

Temperature

The interface in libtock/temperature.h is used to measure ambient temperature in degrees Celsius, times 100. imix uses the SI7021 sensor. It contains the function:

#include <temperature.h>
int temperature_read_sync(int* temperature);

Again, this function returns non-zero in the case of an error.

Read sensors in a Tock application

Using the example program you're working on, write an application that reads all of the sensors on your development board and reports their readings over the serial port.

As a bonus, experiment with toggling an LED when readings are above or below a certain threshold:

LED

The interface in libtock/led.h is used to control lights on Tock boards. On the Hail board, there are three LEDs which can be controlled: Red, Blue, and Green. The functions in the LED module are:

#include <led.h>
int led_count(void);

Which returns the number of LEDs available on the board.

int led_on(int led_num);

Which turns an LED on, accessed by its number.

int led_off(int led_num);

Which turns an LED off, accessed by its number.

int led_toggle(int led_num);

Which toggles the state of an LED, accessed by its number.

Keep the client happy

You, an engineer newly added to a top-secret project in your organization, have been directed to commission a new imix node for your most important client. The directions you receive are terse, but helpful:

On Sunday, Nov 4, 2018, Director Hines wrote:

Welcome to the team, need you to get started right away. The client needs an
imix setup with their two apps -- ASAP. Make sure it is working, we need to keep
this client happy.

- DH

Hmm, ok, not a lot to go on, but luckily in orientation you learned how to flash a kernel and apps on to the imix board, so you are all set for your first assignment.

Poking around, you notice a folder called "important-client". While that is a good start, you also notice that it has two apps inside of it! "Alright!" you are thinking, "My first day is shaping up to go pretty smoothly."

After installing those two apps, which are a little mysterious still, you decide that it would also be a good idea to install an app you are more familiar with: the "blink" app. After doing all of that, you run tockloader list and see the following:

$ tockloader list

No device name specified. Using default "tock"
Using "/dev/ttyUSB1 - imix IoT Module - TockOS"

[App 0]
  Name:                  app2
  Enabled:               True
  Sticky:                False
  Total Size in Flash:   16384 bytes


[App 1]
  Name:                  app1
  Enabled:               True
  Sticky:                False
  Total Size in Flash:   8192 bytes


[App 2]
  Name:                  blink
  Enabled:               True
  Sticky:                False
  Total Size in Flash:   2048 bytes


Finished in 1.959 seconds

Checkpoint

Make sure you have these apps installed correctly and tockloader list produces similar output as shown here.


Great! Now you check that the LED is blinking, and sure enough, no problems there. The blink app was just for testing, so you tockloader uninstall blink to remove that. So far, so good, Tock! But, before you prepare to head home after a successful day, you start to wonder if maybe this was a little too easy. Also, if you get this wrong, it's not going to look good as the new person on the team.

Looking in the folders for the two applications, you notice a brief description of the apps, and a URL. Ok, maybe you can check if everything is working. After trying things for a little bit, everything seems to be in order. You tell the director the board is ready and head home a little early—you did just successfully complete your first project for a major client after all.

Back at Work the Next Day

Expecting a more challenging project after how well things went yesterday, you are instead greeted by this email:

On Monday, Nov 5, 2018, Director Hines wrote:

I know you are new, but what did you do?? I've been getting calls all morning
from the client, the imix board you gave them ran out battery already!! Are you
sure you set up the board correctly? Fix it, and get it back to me later today.

- DH

Well, that's not good. You already removed the blink app, so it can't be that. What you need is some way to inspect the board and see if something looks like it is going awry. You first try:

$ tockloader listen

to see if any debugging information is being printed. A little, but nothing helpful. Before trying to look around the code, you decided to try sending the board a plea for help:

help

and, surprisingly, it responded!

Welcome to the process console.
Valid commands are: help status list stop start

Ok! Maybe the process console can help. Try the status command:

Total processes: 2
Active processes: 2
Timeslice expirations: 4277

It seems this tool is actually able to inspect the current system and the active processes! But hmmm, it seems there are a lot of "timeslice expirations". From orientation, you remember that processes are allocated only a certain quantum of time to execute, and if they exceed that the kernel forces a context switch back to the kernel. If that is happening a lot, then the board is likely unable to go to sleep! That could explain why the battery is draining so fast!

But which process is at fault? Perhaps we should try another command. Maybe list:

 PID    Name                Quanta  Syscalls  Dropped Callbacks    State
  00	app2                     0       336                  0  Yielded
  01	app1                  8556   1439951                  0  Running

Ok! Now we have the status of individual applications. And aha! We can clearly see the faulty application. From our testing we know that one app detects button presses and one app is transmitting sensor data. Let's see if we can disable the faulty app somehow and see which data packets we are still getting. Going back to the help command, the stop command seems promising:

stop <app name>

Time to Fix the App

After debugging, we now know a couple things about the issue:

  • The name of the faulty app.
  • That it is functionally correct but is for some reason consuming excess CPU cycles.

Using this information, dig into the the faulty app.

A Quick Fix

To get the director off your back, you should be able to introduce a simple fix that will reduce wakeups by waiting a bit between samples.

A Better Way

While the quick fix will slow the number of wakeups, you know that you can do better than polling for something like a button press! Tock supports asynchronous operations allowing user processes to subscribe to interrupts.

Looking at the button interface (in button.h), it looks like we'll first have to enable interrupts and then sign up to listen to them.

Once this energy-optimal patch is in place, it'll be time to kick off a triumphant e-mail to the director, and then off to celebrate!

Capsule

The goal of this part of the course is to make you comfortable with the Tock kernel and writing code for it. By the end of this part, you'll have written a new capsule that reads a humidity sensor and outputs its readings over the serial port.

During this you will:

  1. Learn how Tock uses Rust's memory safety to provide isolation for free
  2. Read the Tock boot sequence, seeing how Tock uses static allocation
  3. Learn about Tock's event-driven programming
  4. Write a new capsule that reads a humidity sensor and prints it over serial

Read the Tock boot sequence (20m)

Open imix/src/main.rs in your favorite editor. This file defines the imix platform: how it boots, what capsules it uses, and what system calls it supports for userland applications.

How is everything organized?

Find the declaration of struct Imix (it's pretty early in the file). This declares the structure representing the platform. It has many fields, all of which are capsules. These are the capsules that make up the imix platform. For the most part, these map directly to hardware peripherals, but there are exceptions such as IPC (inter-process communication).

Recall the discussion about how everything in the kernel is statically allocated? We can see that here. Every field in struct Imix is a reference to an object with a static lifetime.

The capsules themselves take a lifetime as a parameter, which is currently always `static. The implementations of these capsules, however, do not rely on this assumption.

The boot process is primarily the construction of this Imix structure. Once everything is set up, the board will pass the constructed imix to kernel::kernel_loop and we're off to the races.

How do things get started?

The method reset_handler is invoked when the chip resets (i.e., boots). It's pretty long because imix has a lot of drivers that need to be created and initialized, and many of them depend on other, lower layer abstractions that need to be created and initialized as well.

Take a look at the first few lines of the reset_handler. The boot sequence initializes memory (copies initialized variables into RAM, clears the BSS), sets up the system clocks, and configures the GPIO pins.

How do capsules get created?

The next lines of reset_handler create and initialize the system console, which is what turns calls to println into bytes sent to the USB serial port:


# #![allow(unused_variables)]
#fn main() {
let uart_mux = static_init!(
    MuxUart<'static>,
    MuxUart::new(
        &sam4l::usart::USART3,
        &mut capsules::virtual_uart::RX_BUF,
        115200
    )
);
uart_mux.initialize();

hil::uart::Transmit::set_transmit_client(&sam4l::usart::USART3, uart_mux);
hil::uart::Receive::set_receive_client(&sam4l::usart::USART3, uart_mux);

let console = ConsoleComponent::new(board_kernel, uart_mux).finalize();
#}

Eventually, once all of the capsules have been created, we will populate a imix structure with them:


# #![allow(unused_variables)]
#fn main() {
let imix = Imix {
    console: console,
    gpio: gpio,
    ...
#}

The static_init! macro is simply an easy way to allocate a static variable with a call to new. The first parameter is the type, the second is the expression to produce an instance of the type. This call creates a Console that uses serial port 3 (USART3) at 115200 bits per second.

A brief aside on buffers:

Notice that you have to pass a write buffer to the console for it to use: this buffer has to have a `static lifetime. This is because low-level hardware drivers, especially those that use DMA, require `static buffers. Since Tock doesn't promise when a DMA operation will complete, and you need to be able to promise that the buffer outlives the operation, the one lifetime that is assured to be alive at the end of an operation is `static. So that other code which has buffers without a `static lifetime, such as userspace processes, can use the Console, it copies them into its own internal `static buffer before passing it to the serial port. So the buffer passing architecture looks like this:

Console/UART buffer lifetimes

It's a little weird that Console's new method takes in a reference to itself. This is an ergonomics tradeoff. The Console needs a mutable static buffer to use internally, which the Console capsule declares. However writing global statics is unsafe. To avoid the unsafe operation in the Console capsule itself, we make it the responsibility of the instantiator to give the Console a buffer to use, without burdening the instantiator with sizing the buffer.

Let's make an imix!

The code continues on, creating all of the other capsules that are needed by the imix platform. By the time we get down to around line 360, we've created all of the capsules we need, and it's time to create the actual imix platform structure (let imix = Imix { ...).

Capsule initialization

Up to this point we have been creating numerous structures and setting some static configuration options and mappings, but nothing dynamic has occurred (said another way, all methods invoked by static_init! must be const fn, however Tock's static_init! macro predates stabilization of const fn's. A future iteration could possibly leverage these and obviate the need for the macro).

Some capsules require initialization, some code that must be executed before they can be used. For example, a few lines after creating the imix struct, we initialize the console:


# #![allow(unused_variables)]
#fn main() {
imix.nrf51822.initialize();
#}

This method is responsible for actually writing the hardware registers that configure the associated UART peripheral for use as a text console (8 data bits, 1 stop bit, no parity bit, no hardware flow control).

Inter-capsule dependencies

Just after initializing the console capsule, we find this line:


# #![allow(unused_variables)]
#fn main() {
kernel::debug::assign_console_driver(Some(imix.console), kc);
#}

This configures the kernel's debug! macro to print messages to this console we've just created. The debug! mechanism can be very helpful during development and testing. Today we're going to use it to print output from the capsule you create.

Let's try it out really quick:

--- a/boards/imix/src/main.rs
+++ b/boards/imix/src/main.rs
@@ -10,7 +10,7 @@
 extern crate capsules;
 extern crate cortexm4;
 extern crate compiler_builtins;
-#[macro_use(static_init)]
+#[macro_use(debug, static_init)]
 extern crate kernel;
 extern crate sam4l;

@@ -388,6 +388,8 @@ pub unsafe fn reset_handler() {
         capsules::console::App::default());
     kernel::debug::assign_console_driver(Some(imix.console), kc);

+    debug!("Testing 1, 2, 3...");
+
     imix.nrf51822.initialize();

Compile and flash the kernel (make program) then look at the output (tockloader listen).

  • What happens if you put the debug! before assign_console_driver?
  • What happens if you put imix.console.initialize() after assign_console_driver?

As you can see, sometimes there are dependencies between capsules, and board authors must take care during initialization to ensure correctness.

Note: The debug! implementation is asynchronous. It copies messages into a buffer and the console prints them via DMA as the UART peripheral is available, interleaved with other console users (i.e. processes). You shouldn't need to worry about the mechanics of this for now.

Loading processes

Once the platform is all set up, the board is responsible for loading processes into memory:


# #![allow(unused_variables)]
#fn main() {
kernel::process::load_processes(&_sapps as *const u8,
                                &mut APP_MEMORY,
                                &mut PROCESSES,
                                FAULT_RESPONSE);
#}

A Tock process is represented by a kernel::Process struct. In principle, a platform could load processes by any means. In practice, all existing platforms write an array of Tock Binary Format (TBF) entries to flash. The kernel provides the load_processes helper function that takes in a flash address and begins iteratively parsing TBF entries and making Processes.

Starting the kernel

Finally, the board passes a reference to the current platform, the chip the platform is built on (used for interrupt and power handling), the processes to run, and an IPC server instance to the main loop of the kernel:


# #![allow(unused_variables)]
#fn main() {
kernel::main(&imix, &mut chip, &mut PROCESSES, &imix.ipc);
#}

From here, Tock is initialized, the kernel event loop takes over, and the system enters steady state operation.

Create a "Hello World" capsule

Now that you've seen how Tock initializes and uses capsules, you're going to write a new one. At the end of this section, your capsule will sample the humidity sensor once a second and print the results as serial output. But you'll start with something simpler: printing "Hello World" to the debug console once on boot.

The imix board configuration you've looked through has a capsule for the this tutorial already set up. The capsule is a separate Rust crate located in exercises/capsule. You'll complete this exercise by filling it in.

In addition to a constructor, Our capsule has start function defined that is currently empty. The board configuration calls this function once it has initialized the capsule.

Eventually, the start method will kick off a state machine for periodic humidity readings, but for now, let's just print something to the debug console and return:


# #![allow(unused_variables)]
#fn main() {
debug!("Hello from the kernel!");
#}
$ cd [PATH_TO_BOOK]/imix
$ make program
$ tockloader listen
No device name specified. Using default "tock"                                                                         Using "/dev/ttyUSB0 - Imix IoT Module - TockOS"
Listening for serial output.
Hello from the kernel!

Extend your capsule to print "Hello World" every second

In order for your capsule to keep track of time, it will need to depend on another capsule that implements the Alarm interface. We'll have to do something similar for reading the accelerometer, so this is good practice.

The Alarm HIL includes several traits, Alarm, Client, and Frequency, all in the kernel::hil::time module. You'll use the set_alarm and now methods from the Alarm trait to set an alarm for a particular value of the clock. Note that both methods accept arguments in the alarm's native clock frequency, which is available using the Alarm trait's associated Frequency type:


# #![allow(unused_variables)]
#fn main() {
// native clock frequency in Herz
let frequency = <A::Frequency>::frequency();
#}

Your capsule already implements the alarm::Client trait so it can receive alarm events. The alarm::Client trait has a single method:


# #![allow(unused_variables)]
#fn main() {
fn fired(&self)
#}

Your capsule should now set an alarm in the start method, print the debug message and set an alarm again when the alarm fires.

Compile and program your new kernel:

$ make program
$ tockloader listen
No device name specified. Using default "tock"                                                                         Using "/dev/ttyUSB0 - Imix IoT Module - TockOS"
Listening for serial output.
TOCK_DEBUG(0): /home/alevy/hack/helena/rustconf/tock/boards/imix/src/accelerate.rs:31: Hello World
TOCK_DEBUG(0): /home/alevy/hack/helena/rustconf/tock/boards/imix/src/accelerate.rs:31: Hello World
TOCK_DEBUG(0): /home/alevy/hack/helena/rustconf/tock/boards/imix/src/accelerate.rs:31: Hello World
TOCK_DEBUG(0): /home/alevy/hack/helena/rustconf/tock/boards/imix/src/accelerate.rs:31: Hello World

Sample Solution

Extend your capsule to sample the humidity once a second

The steps for reading an accelerometer from your capsule are similar to using the alarm. You'll use a capsule that implements the humidity HIL, which includes the HumidityDriver and HumidityClient traits, both in kernel::hil::sensors.

The HumidityDriver trait includes the method read_accelerometer which initiates an accelerometer reading. The HumidityClient trait has a single method for receiving readings:


# #![allow(unused_variables)]
#fn main() {
fn callback(&self, humidity: usize);
#}

Implement logic to initiate a accelerometer reading every second and report the results.

Structure of rustconf capsule

Compile and program your kernel:

$ make program
$ tockloader listen
No device name specified. Using default "tock"                                                                         Using "/dev/ttyUSB0 - Imix IoT Module - TockOS"
Listening for serial output.
Humidity 2731
Humidity 2732

Sample solution

Some further questions and directions to explore

Your capsule used the si7021 and virtual alarm. Take a look at the code behind each of these services:

  1. Is the humidity sensor on-chip or a separate chip connected over a bus?

  2. What happens if you request two humidity sensors back-to-back?

  3. Is there a limit on how many virtual alarms can be created?

  4. How many virtual alarms does the imix boot sequence create?

Extra credit: Write a virtualization capsule for humidity sensor (∞)

If you have extra time, try writing a virtualization capsule for the Humidity HIL that will allow multiple clients to use it. This is a fairly open ended task, but you might find inspiration in the virtua_alarm and virtual_i2c capsules.

Graduation

Now that you have the basics of Tock down, we encourage you to continue to explore and develop with Tock! This book includes a "slimmed down" version of Tock to make it easy to get started, but you will likely want to get a more complete development environment setup to continue. Luckily, this shouldn't be too difficult since you have the tools installed already.

Using the latest kernel

The Tock kernel is actively developed, and you likely want to build upon the latest features. To do this, you should get the Tock source from the repository:

$ git clone https://github.com/tock/tock

While the master branch tends to be relatively stable, you may want to use the latest release instead. Tock is thoroughly tested before a release, so this should be a reliable place to start. To select a release, you should checkout the correct tag. For example, for the 1.4 release this looks like:

$ cd tock
$ git checkout release-1.4

You should use the latest release. Check the releases page for the name of the latest release.

Now, you can compile the board-specific kernel in the Tock repository. For example, to compile the kernel for imix:

$ cd boards/imix
$ make

All of the operations described in the course should work the same way on the main repository.

Using the full selection of apps

The book includes some very minimal apps, and many more can be found in the libtock-c repository. To use this, you should start by cloning the repository:

$ git clone https://github.com/tock/libtock-c

Now you can compile and run apps inside of the examples folder. For instance, you can install the basic "Hello World!" app:

$ cd libtock-c/examples/c_hello
$ make
$ tockloader install

With the libtock-c repository you have access to the full suite of Tock apps, and additional libraries include BLE and Lua support.

Tock Mini Tutorials

These tutorials walk through how to use some various features of Tock. They are narrower in scope than the course, but try to explain in detail how various Tock apps work.

You will need the libtock-c repository to run these tutorials. You should check out a copy of libtock-c by running:

$ git clone https://github.com/tock/libtock-c

libtock-c contains many example Tock applications as well as the library support code for running C and C++ apps on Tock. If you are looking to develop Tock applications you will likely want to start with an existing app in libtock-c and modify it.

Setup

You need to be able to compile and load the Tock kernel and Tock applications. See the prerequisites guide on how to get setup.

You also need hardware that supports Tock.

The tutorials assume you have a Tock kernel loaded on your hardware board. To get a kernel installed, follow these steps.

  1. Obtain the Tock Source. You can clone a copy of the Tock repository to get the kernel source:

    $ git clone https://github.com/tock/tock
    $ cd tock
    
  2. Compile Tock. In the root of the Tock directory, compile the kernel for your hardware platform. You can find a list of boards by running make list. For example if your board is imix then:

    $ make list
    $ cd boards/imix
    $ make
    

    If you have another board just replace "imix" with <your-board>

    This will create binaries of the Tock kernel. Tock is compiled with Cargo, a package manager for Rust applications. The first time Tock is built all of the crates must be compiled. On subsequent builds, crates that haven't changed will not have to be rebuilt and the compilation will be faster.

  3. Load the Tock Kernel. The next step is to program the Tock kernel onto your hardware. Generally, two options are supported for loading the kernel: make program and make flash. You should likely try make program first. Alternatively, the README file for the board should show which options are available. To load the kernel, run:

    $ make program  # Load code via bootloader
      -- or --      # Check the README in your board folder
    $ make flash    # Load code via jtag
    

    in the board directory. Now you have the kernel loaded onto the hardware. The kernel configures the hardware and provides drivers for many hardware resources, but does not actually include any application logic. For that, we need to load an application.

    Note, you only need to program the kernel once. Loading applications does not alter the kernel, and applications can be re-programed without re-programming the kernel.

With the kernel setup, you are ready to try the mini tutorials.

Tutorials

  1. Blink an LED: Get your first Tock app running.
  2. Button to Printf(): Print to terminal in response to button presses.
  3. BLE Advertisement Scanning: Sense nearby BLE packets.
  4. Sample Sensors and Use Drivers: Use syscalls to interact with kernel drivers.
  5. Inter-process Communication: Tock's IPC mechanism.

Board compatiblity matrix

Tutorial #Supported boards
1All
2All Cortex-M based boards
3Hail and imix
4Hail and imix
5All that support IPC

Blink: Running Your First App

This guide will help you get the blink app running on top of Tock kernel.

Instructions

  1. Erase any existing applications. First, we need to remove any applications already on the board. Note that Tockloader by default will install any application in addition to whatever is already installed on the board.

    $ tockloader erase-apps
    
  2. Install Blink. Tock supports an "app store" of sorts. That is, tockloader can install apps from a remote repository, including Blink. To do this:

    $ tockloader install blink
    

    You will have to tell Tockloader that you are OK with fetching the app from the Internet.

    Your specific board may require additional arguments, please see the readme in the boards/ folder for more details.

  3. Compile and Install Blink. We can also compile the blink app and load our compiled version. The basic C version of blink is located in the libtock-c repository.

    1. Clone that repository:

      $ cd tock-book
      $ git clone https://github.com/tock/libtock-c
      
    2. Then navigate to examples/blink:

      $ cd libtock-c/examples/blink
      
    3. From there, you should be able to compile it and install it by:

      $ make
      $ tockloader install
      

    When the blink app is installed you should see the LEDs on the board blinking. Congratulations! You have just programmed your first Tock application.

Say "Hello!" On Every Button Press

This tutorial will walk you through calling printf() in response to a button press.

  1. Start a new application. A Tock application in C looks like a typical C application. Lets start with the basics:

    #include <stdio.h>
    
    int main(void) {
      return 0;
    }
    

    You also need a makefile. Copying a makefile from an existing app is the easiest way to get started.

  2. Setup a button callback handler. A button press in Tock is treated as an interrupt, and in an application this translates to a function being called, much like in any other event-driven system. To listen for button presses, we first need to define a callback function, then tell the kernel that the callback exists.

    #include <stdio.h>
    #include <button.h>
    
    // Callback for button presses.
    //   btn_num: The index of the button associated with the callback
    //   val:     1 if pressed, 0 if depressed
    static void button_callback(int btn_num,
                                int val,
                                int arg2 __attribute__ ((unused)),
                                void *user_data __attribute__ ((unused)) ) {
    }
    
    int main(void) {
      button_subscribe(button_callback, NULL);
    
      return 0;
    }
    

    All callbacks from the kernel are passed four arguments, and the meaning of the four arguments depends on the driver. The first three are integers, and can be used to represent buffer lengths, pin numbers, button numbers, and other simple data. The fourth argument is a pointer to user defined object. This pointer is set in the subscribe call (in this example it is set to NULL), and returned when the callback fires.

  3. Enable the button interrupts. By default, the interrupts for the buttons are not enabled. To enable them, we make a syscall. Buttons, like other drivers in Tock, follow the convention that applications can ask the kernel how many there are. This is done by calling button_count().

    #include <stdio.h>
    #include <button.h>
    
    // Callback for button presses.
    //   btn_num: The index of the button associated with the callback
    //   val:     1 if pressed, 0 if depressed
    static void button_callback(int btn_num,
                                int val,
                                int arg2 __attribute__ ((unused)),
                                void *user_data __attribute__ ((unused)) ) {
    }
    
    int main(void) {
      button_subscribe(button_callback, NULL);
    
      // Ensure there is a button to use.
      int count = button_count();
      if (count < 1) {
        // There are no buttons on this platform.
        printf("Error! No buttons on this platform.");
      } else {
        // Enable an interrupt on the first button.
        button_enable_interrupt(0);
      }
    
      // Can just return here. The application will continue to execute.
      return 0;
    }
    

    The button count is checked, and the app only continues if there exists at least one button. To enable the button interrupt, button_enable_interrupt() is called with the index of the button to use. In this example we just use the first button.

  4. Call printf() on button press. To print a message, we call printf() in the callback.

    #include <stdio.h>
    #include <button.h>
    
    // Callback for button presses.
    //   btn_num: The index of the button associated with the callback
    //   val:     1 if pressed, 0 if depressed
    static void button_callback(int btn_num,
                                int val,
                                int arg2 __attribute__ ((unused)),
                                void *user_data __attribute__ ((unused)) ) {
      // Only print on the down press.
      if (val == 1) {
        printf("Hello!\n");
      }
    }
    
    int main(void) {
      button_subscribe(button_callback, NULL);
    
      // Ensure there is a button to use.
      int count = button_count();
      if (count < 1) {
        // There are no buttons on this platform.
        printf("Error! No buttons on this platform.\n");
      } else {
        // Enable an interrupt on the first button.
        button_enable_interrupt(0);
      }
    
      // Can just return here. The application will continue to execute.
      return 0;
    }
    
  5. Run the application. To try this tutorial application, you can find it in the tutorials app folder. See the first tutorial for details on how to compile and install a C application.

    Once installed, when you press the button, you should see "Hello!" printed to the terminal!

Look! A Wild BLE Packet Appeared!

Note! This tutorial will only work on Hail and imix boards.

This tutorial will walk you through getting an app running that scans for BLE advertisements. Most BLE devices typically broadcast advertisements periodically (usually once a second) to allow smartphones and other devices to discover them. The advertisements typically contain the BLE device's ID and name, as well as as which services the device provides, and sometimes raw data as well.

To provide BLE connectivity, several Tock boards use the Nordic nRF51822 as a BLE co-processor. In this configuration, the nRF51822 runs all of the BLE operations and exposes a command interface over a UART bus. Luckily for us, Nordic has defined and implemented the entire interface. Better yet, they made it interoperable with their nRF51 SDK. What this means is any BLE app that would run on the nRF51822 directly can be compiled to run on a different microcontroller, and any function calls that would have interacted with the BLE hardware are instead packaged and sent to the nRF51822 co-processor. Nordic calls this tool "BLE Serialization", and Tock has a port of the serialization libraries that Tock applications can use.

So, with that introduction, lets get going.

  1. Initialize the BLE co-processor. The first step a BLE serialization app must do is initialize the BLE stack on the co-processor. This can be done with Nordic's SDK, but to simplify things Tock supports the Simple BLE library. The goal of simple_ble.c is to wrap the details of the nRF5 SDK and the intricacies of BLE in an easy-to-use library so you can get going with creating BLE devices and not learning the entire spec.

    #include <simple_ble.h>
    
    // Intervals for advertising and connections.
    // These are some basic settings for BLE devices. However, since we are
    // only interesting in scanning, these are not particularly relevant.
    simple_ble_config_t ble_config = {
      .platform_id       = 0x00, // used as 4th octet in device BLE address
      .device_id         = DEVICE_ID_DEFAULT,
      .adv_name          = "Tock",
      .adv_interval      = MSEC_TO_UNITS(500, UNIT_0_625_MS),
      .min_conn_interval = MSEC_TO_UNITS(1000, UNIT_1_25_MS),
      .max_conn_interval = MSEC_TO_UNITS(1250, UNIT_1_25_MS)
    };
    
    int main () {
        printf("[Tutorial] BLE Scanning\n");
    
        // Setup BLE.
        simple_ble_init(&ble_config);
    }
    
  2. Scan for advertisements. With simple_ble this is pretty straightforward.

    int main () {
        printf("[Tutorial] BLE Scanning\n");
    
        // Setup BLE.
        simple_ble_init(&ble_config);
    
        // Scan for advertisements.
        simple_ble_scan_start();
    }
    
  3. Handle the advertisement received event. Just as the main Tock microcontroller can send commands to the nRF co-processor, the co-processor can send events back. When these occur, a variety of callbacks are generated in simple_ble and then passed to users of the library. In this case, we only care about ble_evt_adv_report() which is called on each advertisement reception.

    // Called when each advertisement is received.
    void ble_evt_adv_report (ble_evt_t* p_ble_evt) {
      ble_gap_evt_adv_report_t* adv = (ble_gap_evt_adv_report_t*) &p_ble_evt->evt.gap_evt.params.adv_report;
    }
    

    The ble_evt_adv_report() function is passed a pointer to a ble_evt_t struct. This is a type from the Nordic nRF51 SDK, and more information can be found in the SDK documentation.

  4. Display a message for each advertisement. Once we have the advertisement callback, we can use printf() like normal.

    #include <stdio.h>
    
    #include <led.h>
    
    // Called when each advertisement is received.
    void ble_evt_adv_report (ble_evt_t* p_ble_evt) {
      ble_gap_evt_adv_report_t* adv = (ble_gap_evt_adv_report_t*) &p_ble_evt->evt.gap_evt.params.adv_report;
    
      // Print some details about the discovered advertisement.
      printf("Recv Advertisement: [%02x:%02x:%02x:%02x:%02x:%02x] RSSI: %d, Len: %d\n",
        adv->peer_addr.addr[5], adv->peer_addr.addr[4], adv->peer_addr.addr[3],
        adv->peer_addr.addr[2], adv->peer_addr.addr[1], adv->peer_addr.addr[0],
        adv->rssi, adv->dlen);
    
      // Also toggle the first LED.
      led_toggle(0);
    }
    
  5. Handle some BLE annoyances. The last step to getting a working app is to handle some annoyances using BLE serialization with the simple_ble library. Typically errors generated by the nRF51 SDK are severe and mean there is a significant bug in the code. With serialization, however, messages between the two processors can be corrupted or misframed, causing parsing errors. We can ignore these errors safely and just drop the corrupted packet.

    Additionally, the simple_ble library makes it easy to set the address of a BLE device. However, this functionality only works when running on an actual nRF51822. To disable this, we override the weakly defined ble_address_set() function with an empty function.

    void app_error_fault_handler(uint32_t error_code, uint32_t line_num, uint32_t info) { }
    void ble_address_set () { }
    
  6. Run the app and see the packets! To try this tutorial application, you can find it in the tutorials app folder.

    For any new applications, ensure that the following is in the makefile so that the BLE serialization library is included.

     include $(TOCK_USERLAND_BASE_DIR)/libnrfserialization/Makefile.app
    

Details

This section contains a few notes about the specific versions of BLE serialization used.

Tock currently supports the S130 softdevice version 2.0.0 and SDK 11.0.0.

Reading Sensors From Scratch

Note! This tutorial will only work on Hail and imix boards.

In this tutorial we will cover how to use the syscall interface from applications to kernel drivers, and guide things based on reading the ISL29035 digital light sensor and printing the readings over UART.

OK, lets get started.

  1. Setup a generic app for handling asynchronous events. As with most sensors, the ISL29035 is read asynchronously, and a callback is generated from the kernel to userspace when the reading is ready. Therefore, to use this sensor, our application needs to do two things: 1) setup a callback the kernel driver can call when the reading is ready, and 2) instruct the kernel driver to start the measurement. Lets first sketch this out:

    #include <tock.h>
    
    #define DRIVER_NUM 0x60002
    
    // Callback when the ISL29035 has a light intensity measurement ready.
    static void isl29035_callback(int intensity, int unused1, int unused2, void* ud) {
    
    }
    
    int main() {
        // Tell the kernel about the callback.
    
        // Instruct the ISL29035 driver to begin a reading.
    
        // Wait until the reading is complete.
    
        // Print the resulting value.
    
        return 0;
    }
    
  2. Fill in the application with syscalls. The standard Tock syscalls can be used to actually implement the sketch we made above. We first use the subscribe syscall to inform the kernel about the callback in our application. We then use the command syscall to start the measurement. To wait, we use the yield call to wait for the callback to actually fire. We do not need to use allow for this application, and typically it is not required for reading sensors.

    For all syscalls that interact with drivers, the major number is set by the platform. In the case of the ISL29035, it is 0x60002. The minor numbers are set by the driver and are specific to the particular driver.

    To save the value from the callback to use in the print statement, we will store it in a global variable.

    #include <stdio.h>
    
    #include <tock.h>
    
    #define DRIVER_NUM 0x60002
    
    static int isl29035_reading;
    
    // Callback when the ISL29035 has a light intensity measurement ready.
    static void isl29035_callback(int intensity, int unused1, int unused2, void* ud) {
        // Save the reading when the callback fires.
        isl29035_reading = intensity;
    }
    
    int main() {
        // Tell the kernel about the callback.
        subscribe(DRIVER_NUM, 0, isl29035_callback, NULL);
    
        // Instruct the ISL29035 driver to begin a reading.
        command(DRIVER_NUM, 1, 0);
    
        // Wait until the reading is complete.
        yield();
    
        // Print the resulting value.
        printf("Light intensity reading: %d\n", isl29035_reading);
    
        return 0;
    }
    
  3. Be smarter about waiting for the callback. While the above application works, it's really relying on the fact that we are only sampling a single sensor. In the current setup, if instead we had two sensors with outstanding commands, the first callback that fired would trigger the yield() call to return and then the printf() would execute. If, for example, sampling the ISL29035 takes 100 ms, and the new sensor only needs 10 ms, the new sensor's callback would fire first and the printf() would execute with an incorrect value.

    To handle this, we can instead use the yield_for() call, which takes a flag and only returns when that flag has been set. We can then set this flag in the callback to make sure that our printf() only occurs when the light reading has completed.

    #include <stdio.h>
    #include <stdbool.h>
    
    #include <tock.h>
    
    #define DRIVER_NUM 0x60002
    
    static int isl29035_reading;
    static bool isl29035_done = false;
    
    // Callback when the ISL29035 has a light intensity measurement ready.
    static void isl29035_callback(int intensity, int unused1, int unused2, void* ud) {
        // Save the reading when the callback fires.
        isl29035_reading = intensity;
    
        // Mark our flag true so that the `yield_for()` returns.
        isl29035_done = true;
    }
    
    int main() {
        // Tell the kernel about the callback.
        subscribe(DRIVER_NUM, 0, isl29035_callback, NULL);
    
        // Instruct the ISL29035 driver to begin a reading.
        command(DRIVER_NUM, 1, 0);
    
        // Wait until the reading is complete.
        yield_for(&isl29035_done);
    
        // Print the resulting value.
        printf("Light intensity reading: %d\n", isl29035_reading);
    
        return 0;
    }
    
  4. Use the libtock library functions. Normally, applications don't use the bare command and subscribe syscalls. Typically, these are wrapped together into helpful commands inside of libtock and come with a function that hides the yield_for() to a make a synchronous function which is useful for developing applications quickly. Lets port the ISL29035 sensing app to use the Tock Standard Library:

    #include <stdio.h>
    
    #include <isl29035.h>
    
    int main() {
        // Take the ISL29035 measurement synchronously.
        int isl29035_reading = isl29035_read_light_intensity();
    
        // Print the resulting value.
        printf("Light intensity reading: %d\n", isl29035_reading);
    
        return 0;
    }
    
  5. Explore more sensors. This tutorial highlights only one sensor. See the sensors app for a more complete sensing application.

Friendly Apps Share Data

This tutorial covers how to use Tock's IPC mechanism to allow applications to communicate and share memory.

Tock IPC Basics

IPC in Tock uses a client-server model. Applications can provide a service by telling the Tock kernel that they provide a service. Each application can only provide a single service, and that service's name is set to the name of the application. Other applications can then discover that service and explicitly share a buffer with the server. Once a client shares a buffer, it can then notify the server to instruct the server to somehow interact with the shared buffer. The protocol for what the server should do with the buffer is service specific and not specified by Tock. Servers can also notify clients, but when and why servers notify clients is service specific.

Example Application

To provide an overview of IPC, we will build an example system consisting of three apps: a random number service, a LED control service, and a main application that uses the two services. While simple, this example both demonstrates the core aspects of the IPC mechanism and should run on any hardware platform.

LED Service

Lets start with the LED service. The goal of this service is to allow other applications to use the shared buffer as a command message to instruct the LED service on how to turn on or off the system's LEDs.

  1. We must tell the kernel that our app wishes to provide a service. All that we have to do is call ipc_register_svc().

    #include "ipc.h"
    
    int main(void) {
      ipc_register_svc(ipc_callback, NULL);
      return 0;
    }
    
  2. We also need that callback (ipc_callback) to handle IPC requests from other applications. This callback will be called when the client app notifies the service.

    static void ipc_callback(int pid, int len, int buf, void* ud) {
      // pid: An identifier for the app that notified us.
      // len: How long the buffer is that the client shared with us.
      // buf: Pointer to the shared buffer.
    }
    
  3. Now lets fill in the callback for the LED application. This is a simplified version for illustration. The full example can be found in the examples/tutorials folder.

    #include "led.h"
    
    static void ipc_callback(int pid, int len, int buf, void* ud) {
      uint8_t* buffer = (uint8_t*) buf;
    
      // First byte is the command, second byte is the LED index to set,
      // and the third byte is whether the LED should be on or off.
      uint8_t command = buffer[0];
      if (command == 1) {
          uint8_t led_id = buffer[1];
          uint8_t led_state = buffer[2] > 0;
    
          if (led_state == 0) {
            led_off(led_id);
          } else {
            led_on(led_id);
          }
    
          // Tell the client that we have finished setting the specified LED.
          ipc_notify_client(pid);
          break;
      }
    }
    

RNG Service

The RNG service returns the requested number of random bytes in the shared folder.

  1. Again, register that this service exists.

    int main(void) {
      ipc_register_svc(ipc_callback, NULL);
      return 0;
    }
    
  2. Also need a callback function for when the client signals the service. The client specifies how many random bytes it wants by setting the first byte of the shared buffer before calling notify.

    #include <rng.h>
    
    static void ipc_callback(int pid, int len, int buf, void* ud) {
      uint8_t* buffer = (uint8_t*) buf;
      uint8_t rng[len];
    
      uint8_t number_of_bytes = buffer[0];
    
      // Fill the buffer with random bytes.
      int number_of_bytes_received = rng_sync(rng, len, number_of_bytes);
      memcpy(buffer, rng, number_of_bytes_received);
    
      // Signal the client that we have the number of random bytes requested.
      ipc_notify_client(pid);
    }
    

    This is again not a complete example but illustrates the key aspects.

Main Logic Client Application

The third application uses the two services to randomly control the LEDs on the board. This application is not a server but instead is a client of the two service applications.

  1. When using an IPC service, the first step is to discover the service and record its identifier. This will allow the application to share memory with it and notify it. Services are discovered by the name of the application that provides them. Currently these are set in the application Makefile or by default based on the folder name of the application. The examples in Tock commonly use a Java style naming format.

    int main(void) {
      int led_service = ipc_discover("org.tockos.tutorials.ipc.led");
      int rng_service = ipc_discover("org.tockos.tutorials.ipc.rng");
    
      return 0;
    }
    

    If the services requested are valid and exist the return value from ipc_discover is the identifier of the found service. If the service cannot be found an error is returned.

  2. Next we must share a buffer with each service (the buffer is the only way to share between processes), and setup a callback that is called when the server notifies us as a client. Once shared, the kernel will permit both applications to read/modify that memory.

    char led_buf[64] __attribute__((aligned(64)));
    char rng_buf[64] __attribute__((aligned(64)));
    
    int main(void) {
      int led_service = ipc_discover("org.tockos.tutorials.ipc.led");
      int rng_service = ipc_discover("org.tockos.tutorials.ipc.rng");
    
      // Setup IPC for LED service
      ipc_register_client_cb(led_service, ipc_callback, NULL);
      ipc_share(led_service, led_buf, 64);
    
      // Setup IPC for RNG service
      ipc_register_client_cb(rng_service, ipc_callback, NULL);
      ipc_share(rng_service, rng_buf, 64);
    
      return 0;
    }
    
  3. We of course need the callback too. For this app we use the yield_for function to implement the logical synchronously, so all the callback needs to do is set a flag to signal the end of the yield_for.

    bool done = false;
    
    static void ipc_callback(int pid, int len, int arg2, void* ud) {
      done = true;
    }
    
  4. Now we use the two services to implement our application.

    #include <timer.h>
    
    void app() {
      while (1) {
        // Get two random bytes from the RNG service
        done = false;
        rng_buf[0] = 2; // Request two bytes.
        ipc_notify_svc(rng_service);
        yield_for(&done);
    
        // Control the LEDs based on those two bytes.
        done = false;
        led_buf[0] = 1;                     // Control LED command.
        led_buf[1] = rng_buf[0] % NUM_LEDS; // Choose the LED index.
        led_buf[2] = rng_buf[1] & 0x01;     // On or off.
        ipc_notify_svc(led_service);        // Notify to signal LED service.
        yield_for(&done);
    
        delay_ms(500);
      }
    }
    

Try It Out

To test this out, see the complete apps in the IPC tutorial example folder.

To install all of the apps on a board:

$ cd examples/tutorials/05_ipc
$ tockloader erase-apps
$ pushd led && make && tockloader install && popd
$ pushd rng && make && tockloader install && popd
$ pushd logic && make && tockloader install && popd

You should see the LEDs randomly turning on and off!

Kernel Development Guides

These guides provide walkthroughs for specific kernel development tasks. For example, there is a guide on how to add a new syscall interface for userspace applications. The guides are intended to be general and provide high-level instructions which will have to be adapted for the specific functionality to be added.

Overtime, these guides will inevitably become out-of-date in that the specific code examples will fail to compile. However, the general design aspects and considerations should still be relevant even if the specific code details have changed. You are encourage to use these guides as just that, a general guide, and to copy from up-to-date examples contained in the Tock repository.

Implementing a Chip Peripheral Driver

This guide covers how to implement a peripheral driver for a particular microcontroller (MCU). For example, if you wanted to add an analog to digital converter (ADC) driver for the Nordic nRF52840 MCU, you would follow the general steps described in this guide.

Overview

The general steps you will follow are:

  1. Determine the HIL you will implement.
  2. Create a register mapping for the peripheral.
  3. Create a struct for the peripheral.
  4. Implement the HIL interface for the peripheral.
  5. Create the peripheral driver object and cast the registers to the correct memory location.

The guide will walk through how to do each of these steps.

Background

Implementing a chip peripheral driver increases Tock's support for a particular microcontroller and allows capsules and userspace apps to take more advantage of the hardware provided by the MCU. Peripheral drivers for an MCU are generally implemented on an as-needed basis to support a particular use case, and as such the chips in Tock generally do not have all of the peripheral drivers implemented already.

Peripheral drivers are included in Tock as "trusted code" in the kernel. This means that they can use the unsafe keyword (in fact, they must). However, it also means more care must be taken to ensure they are correct. The use of unsafe should be kept to an absolute minimum and only used where absolutely necessary. This guide explains the one use of unsafe that is required. All other uses of unsafe in a peripheral driver will likely be very scrutinized during the pull request review period.

Step-by-Step Guide

The steps from the overview are elaborated on here.

  1. Determine the HIL you will implement.

    The HILs in Tock are the contract between the MCU-specific hardware and the more generic capsules which use the hardware resources. They provide a common interface that is consistent between different microcontrollers, enabling code higher in the stack to use the interfaces without needing to know any details about the underlying hardware. This common interface also allows the same higher-level code to be portable across different microcontrollers. HILs are implemented as traits in Rust.

    All HILs are defined in the kernel/src/hil directory. You should find a HIL that exposes the interface the peripheral you are writing a driver for can provide. There should only be one HIL that matches your peripheral.

    Note: As of Dec 2019, the hil directory also contains interfaces that are only provided by capsules for other capsules. For example, the ambient light HIL interface is likely not something an MCU would implement.

    It is possible Tock does not currently include a HIL that matches the peripheral you are implementing a driver for. In that case you will also need to create a HIL, which is explained in a different development guide.

    Checkpoint: You have identified the HIL your driver will implement.

  2. Create a register mapping for the peripheral.

    To start implementing the peripheral driver, you must create a new source file within the MCU-specific directory inside of chips/src directory. The name of this file generally should match the name of the peripheral in the the MCU's datasheet.

    Include the name of this file inside of the lib.rs (or potentially mod.rs) file inside the same directory. This should look like:

    
    # #![allow(unused_variables)]
    #fn main() {
    pub mod ast;
    #}

    Inside of the new file, you will first need to define the memory-mapped input/output (MMIO) registers that correspond to the peripheral. Different embedded code ecosystems have devised different methods for doing this, and Tock is no different. Tock has a special library and set of Rust macros to make defining the register map straightforward and using the registers intuitive.

    The full register library is here, but to get started, you will first create a structure like this:

    
    # #![allow(unused_variables)]
    #fn main() {
    use tock_registers::registers::{ReadOnly, ReadWrite, WriteOnly};
    
    register_structs! {
        XyzPeripheralRegisters {
            /// Control register.
            /// The 'Control' parameter constrains this register to only use
            /// fields from a certain group (defined below in the bitfields
            /// section).
            (0x000 => cr: ReadWrite<u32, Control::Register>),
            // Status register.
            (0x004 => s: ReadOnly<u8, Status::Register>),
            /// spacing between registers in memory
            (0x008 => _reserved),
            /// Another register with no meaningful fields.
            (0x014 => word: ReadWrite<u32>),
    
            // Etc.
    
            // The end of the struct is marked as follows.
            (0x100 => @END),
        }
    }
    #}

    You should replace XyzPeripheral with the name of the peripheral you are writing a driver for. Then, for each register defined in the datasheet, you must specify an entry in the macro. For example, a register is defined like:

    
    # #![allow(unused_variables)]
    #fn main() {
    (0x000 => cr: ReadWrite<u32, Control::Register>),
    #}

    where:

    • 0x000 is the offset (in bytes) of the register from the beginning of the register map.
    • cr is the name of the register in the datasheet.
    • ReadWrite is the access control of the register as defined in the datasheet.
    • u32 is the size of the register.
    • Control::Register maps to the actual bitfields used in the register. You will create this type for this particular peripheral, so you can name this whatever makes sense at this point. Note that it will always end with ::Register due to how Rust macros work. If it doesn't make sense to define the specific bitfields in this register, you can omit this field. For example, an esoteric field in the register map that the implementation does not use likely does not need its bitfields mapped.

    Once the register map is defined, you must specify the bitfields for any registers that you gave a specific type to. This looks like the following:

    
    # #![allow(unused_variables)]
    #fn main() {
    register_bitfields! [
        // First parameter is the register width for the bitfields. Can be u8,
        // u16, u32, or u64.
        u32,
    
        // Each subsequent parameter is a register abbreviation, its descriptive
        // name, and its associated bitfields. The descriptive name defines this
        // 'group' of bitfields. Only registers defined as
        // ReadWrite<_, Control::Register> can use these bitfields.
        Control [
            // Bitfields are defined as:
            // name OFFSET(shift) NUMBITS(num) [ /* optional values */ ]
    
            // This is a two-bit field which includes bits 4 and 5
            RANGE OFFSET(4) NUMBITS(3) [
                // Each of these defines a name for a value that the bitfield
                // can be written with or matched against. Note that this set is
                // not exclusive--the field can still be written with arbitrary
                // constants.
                VeryHigh = 0,
                High = 1,
                Low = 2
            ],
    
            // A common case is single-bit bitfields, which usually just mean
            // 'enable' or 'disable' something.
            EN  OFFSET(3) NUMBITS(1) [],
            INT OFFSET(2) NUMBITS(1) []
        ],
    
        // Another example:
        // Status register
        Status [
            TXCOMPLETE  OFFSET(0) NUMBITS(1) [],
            TXINTERRUPT OFFSET(1) NUMBITS(1) [],
            RXCOMPLETE  OFFSET(2) NUMBITS(1) [],
            RXINTERRUPT OFFSET(3) NUMBITS(1) [],
            MODE        OFFSET(4) NUMBITS(3) [
                FullDuplex = 0,
                HalfDuplex = 1,
                Loopback = 2,
                Disabled = 3
            ],
            ERRORCOUNT OFFSET(6) NUMBITS(3) []
        ],
    ]
    #}

    The name in each entry of the register_bitfields! [] list must match the register type provided in the register map declaration. Each register that is used in the driver implementation should have its bitfields declared.

    Checkpoint: The register map is correctly described in the driver source file.

  3. Create a struct for the peripheral.

    Each peripheral driver is implemented with a struct which is later used to create an object that can be passed to code that will use this peripheral driver. The actual fields of the struct are very peripheral specific, but should contain any state that the driver needs to correctly function.

    An example struct looks for a timer peripheral called the AST by the MCU datasheet looks like:

    
    # #![allow(unused_variables)]
    #fn main() {
    pub struct Ast<'a> {
        registers: StaticRef<AstRegisters>,
        callback: OptionalCell<&'a dyn hil::time::AlarmClient>,
    }
    #}

    The struct should contain a reference to the registers defined above (we will explain the StaticRef later). Typically, many drivers respond to certain events (like in this case a timer firing) and therefore need a reference to a client to notify when that event occurs. Notice that the type of the callback handler is specified in the HIL interface.

    Peripheral structs typically need a lifetime for references like the callback client reference. By convention Tock peripheral structs use 'a for this lifetime, and you likely want to copy that as well.

    Think of what state your driver might need to keep around. This could include a direct memory access (DMA) reference, some configuration flags like the baud rate, or buffer indices. See other Tock peripheral drivers for more examples.

    Note: you will most likely need to update this struct as you implement the driver, so to start with this just has to be a best guess.

    Hint: you should avoid keeping any state in the peripheral driver struct that is already stored by the hardware itself. For example, if there is an "enabled" bit in a register, then you do not need an "enabled" flag in the struct. Replicating this state leads to bugs when those values get out of sync, and makes it difficult to update the driver in the future.

    Peripheral driver structs make extensive use of different "cell" types to hold references to various shared state. The general wisdom is that if the value will ever need to be updated, then it needs to be contained in a cell. See the Tock cell documentation for more details on the cell types and when to use which one. In this example, the callback is stored in an OptionalCell, which can contain a value or not (if the callback is not set), and can be updated if the callback needs to change.

    With the struct defined, you should next create a new() function for that struct. This will look like:

    
    # #![allow(unused_variables)]
    #fn main() {
    impl Ast {
        const fn new(registers: StaticRef<AstRegisters>) -> Ast {
            Ast {
                registers: registers,
                callback: OptionalCell::empty(),
            }
        }
    }
    #}

    Checkpoint: There is a struct for the peripheral that can be created.

  4. Implement the HIL interface for the peripheral.

    With the peripheral driver struct created, now the main work begins. You can now write the actual logic for the peripheral driver that implements the HIL interface you identified earlier. Implementing the HIL interface is done just like implementing a trait in Rust. For example, to implement the Time HIL for the AST:

    
    # #![allow(unused_variables)]
    #fn main() {
    impl hil::time::Time for Ast<'a> {
        type Frequency = Freq16KHz;
    
        fn now(&self) -> u32 {
            self.get_counter()
        }
    
        fn max_tics(&self) -> u32 {
            core::u32::MAX
        }
    }
    #}

    You should include all of the functions from the HIL and decide how to implement them.

    Some operations will be shared among multiple HIL functions. These should be implemented as functions for the original struct. For example, in the Ast example the HIL function now() uses the get_counter() function. This should be implemented on the main Ast struct:

    
    # #![allow(unused_variables)]
    #fn main() {
    impl Ast {
        const fn new(registers: StaticRef<AstRegisters>) -> Ast {
            Ast {
                registers: registers,
                callback: OptionalCell::empty(),
            }
        }
    
        fn get_counter(&self) -> u32 {
            let regs = &*self.registers;
            while self.busy() {}
            regs.cv.read(Value::VALUE)
        }
    }
    #}

    Note the get_counter() function also illustrates how to use the register reference and the Tock register library. The register library includes much more detail on the various register operations enabled by the library.

    Checkpoint: All of the functions in the HIL interface have MCU peripheral-specific implementations.

  5. Create the peripheral driver object and cast the registers to the correct memory location.

    The last step is to actually create the object so that the peripheral driver can be used by other code. Start by casting the register map to the correct memory address where the registers are actually mapped to. For example:

    
    # #![allow(unused_variables)]
    #fn main() {
    use kernel::common::StaticRef;
    
    const AST_BASE: StaticRef<AstRegisters> =
        unsafe { StaticRef::new(0x400F0800 as *const AstRegisters) };
    #}

    StaticRef is a type in Tock designed explicitly for this operation of casting register maps to the correct location in memory. The 0x400F0800 is the address in memory of the start of the registers and this location will be specified by the datasheet.

    Note that creating the StaticRef requires using the unsafe keyword. This is because doing this cast is a fundamentally memory-unsafe operation: this allows whatever is at that address in memory to be accessed through the register interface (which is exposed as a safe interface). In the normal case where the correct memory address is provided there is no concern for system safety as the register interface faithfully represents the underlying hardware. However, suppose an incorrect address was used, and that address actually points to live memory used by the Tock kernel. Now kernel data structures could be altered through the register interface, and this would violate memory safety.

    With the address reference created, we can now create the actual driver object:

    
    # #![allow(unused_variables)]
    #fn main() {
    pub static mut AST: Ast = Ast::new(AST_BASE);
    #}

    This object will be used by a board's main.rs file to pass, in this case, the driver for the timer hardware to various capsules and other code that needs the underlying timer hardware.

Wrap-Up

Congratulations! You have implemented a peripheral driver for a microcontroller in Tock! We encourage you to submit a pull request to upstream this to the Tock repository.

Implementing a Sensor Driver

This guide describes the steps necessary to implement a capsule in Tock that interfaces with an external IC, like a sensor, memory chip, or display. These are devices which are not part of the same chip as the main microcontroller (MCU), but are on the same board and connected via some physical connection.

Note: to attempt to be generic, this guide will use the term "IC" to refer to the device the driver is for.

Note: "driver" is a bit of an overloaded term in Tock. In this guide, "driver" is used in the generic sense to mean code that interfaces with the external IC.

To illustrate the steps, this guide will use a generic light sensor as the running example. You will need to adapt the generic steps for your particular use case.

Often the goal of an IC driver is to expose an interface to that sensor or other IC to userspace applications. This guide does not cover creating that userspace interface as that is covered in a different guide.

Background

As mentioned, this guide describes creating a capsule. Capsules in Tock are units of Rust code that extend the kernel to add interesting features, like interfacing with new sensors. Capsules are "untrusted", meaning they cannot call unsafe code in Rust and cannot use the unsafe keyword.

Overview

The high-level steps required are:

  1. Create a struct for the IC driver.
  2. Implement the logic to interface with the IC.

Optional:

  1. Provide a HIL interface for the IC driver.
  2. Provide a userspace interface for the IC driver.

Step-by-Step Guide

The steps from the overview are elaborated on here.

  1. Create a struct for the IC driver.

    The driver will be implemented as a capsule, so the first step is to create a new file in the capsules/src directory. The name of this file should be [chipname].rs where [chipname] is the part number of the IC you are writing the driver for. There are several other examples in the capsules folder.

    For our example we will assume the part number is ls1234.

    You then need to add the filename to capsules/src/lib.rs like:

    
    # #![allow(unused_variables)]
    #fn main() {
    pub mod ls1234;
    #}

    Now inside of the new file you should create a struct with the fields necessary to implement the driver for the IC. In our example we will assume the IC is connected to the MCU with an I2C bus. Your IC might use SPI, UART, or some other standard interface. You will need to adjust how you create the struct based on the interface. You should be able to find examples in the capsules directory to copy from.

    The struct will look something like:

    
    # #![allow(unused_variables)]
    #fn main() {
    pub struct Ls1234 {
        i2c: &'a dyn I2CDevice,
        state: Cell<State>,
        buffer: TakeCell<'static, [u8]>,
        client: OptionalCell<&'a dyn Ls1234Client>,
    }
    #}

    You can see the resources this driver requires to successfully interface with the light sensor:

    • i2c: This is a reference to the I2C bus that the driver will use to communicate with the IC. Notice in Tock the type is I2CDevice, and no address is provided. This is because the I2CDevice type wraps the address in internally, so that the driver code can only communicate with the correct address.

    • alarm:

    • state: Often drivers will iterate through various states as they communicate with the IC, and it is common for drivers to keep some state variable to manage this. Our State is defined as an enum, like so:

      
      # #![allow(unused_variables)]
      #fn main() {
      #[derive(Copy, Clone, PartialEq)]
      enum State {
          Disabled,
          Enabling,
          ReadingLight,
      }
      #}

      Also note that the state variable uses a Cell. This is so that the driver can update the state.

    • buffer: This holds a reference to a buffer of memory the driver will use to send messages over the I2C bus. By convention, these buffers are defined statically in the same file as the driver, but then passed to the driver when the board boots. This provides the board flexibility on the buffer to use, while still allowing the driver to hint at the size required for successful operation. In our case the static buffer is defined as:

      
      # #![allow(unused_variables)]
      #fn main() {
      pub static mut BUF: [u8; 3] = [0; 3];
      #}

      Note the buffer is wrapped in a TakeCell such that it can be passed to the I2C hardware when necessary, and re-stored in the driver struct when the I2C code returns the buffer.

    • client: This is the callback that will be called after the driver has received a reading from the sensor. All execution is event-based in Tock, so the caller will not block waiting for a sample, but instead will expect a callback via the client when the same is ready. The driver has to define the type of the callback by defining the Ls1234Client trait in this case:

      
      # #![allow(unused_variables)]
      #fn main() {
      pub trait Ls1234Client {
      	 fn callback(light_reading: usize);
      }
      #}

      Note that the client is stored in an OptionalCell. This allows the callback to not be set initially, and configured at bootup.

    Your driver may require other state to be stored as well. You can update this struct as needed to for state required to successfully implement the driver. Note that if the state needs to be updated at runtime it will need to be stored in a cell type. See the cell documentation for more information on the various cell types in Tock.

    Note: your driver should not keep any state in the struct that is also stored by the hardware. This easily leads to bugs when that state becomes out of sync, and makes further development on the driver difficult.

    The last step is to write a function that enables creating an instance of your driver. By convention, the function is called new() and looks something like:

    
    # #![allow(unused_variables)]
    #fn main() {
    impl Ls1234<'a> {
        pub fn new(i2c: &'a dyn I2CDevice, buffer: &'static mut [u8]) -> Ls1234<'a> {
            Ls1234 {
                i2c: i2c,
                alarm: alarm,
                state: Cell::new(State::Disabled),
                client: OptionalCell::empty(),
            }
        }
    }
    #}

    This function will get called by the board's main.rs file when the driver is instantiated. All of the static objects or configuration that the driver requires must be passed in here. In this example, a reference to the I2C device and the static buffer for passing messages must be provided.

    Checkpoint: You have defined the struct which will become the driver for the IC.

  2. Implement the logic to interface with the IC.

    Now, you will actually write the code that interfaces with the IC. This requires extending the impl of the driver struct with additional functions appropriate for your particular IC.

    With our light sensor example, we likely want to write a sample function for reading a light sensor value:

    
    # #![allow(unused_variables)]
    #fn main() {
    impl Ls1234<'a> {
        pub fn new(...) -> Ls1234<'a> {...}
    
        pub fn start_light_reading(&self) {...}
    }
    #}

    Note that the function name is "start light reading", which is appropriate because of the event-driven, non-blocking nature of the Tock kernel. Actually communicating with the sensor will take some time, and likely requires multiple messages to be sent to and received from the sensor. Therefore, our sample function will not be able to return the result directly. Instead, the reading will be provided in the callback function described earlier.

    The start reading function will likely prepare the message buffer in a way that is IC-specific, then send the command to the IC. A rough example of that operation looks like:

    
    # #![allow(unused_variables)]
    #fn main() {
    impl Ls1234<'a> {
        pub fn new(...) -> Ls1234<'a> {...}
    
        pub fn start_light_reading(&self) {
            if self.state.get() == State::Disabled {
                self.buffer.take().map(|buf| {
                    self.i2c.enable();
    
                    // Set the first byte of the buffer to the "on" command.
                    // This is IC-specific and will be described in the IC
                    // datasheet.
                    buf[0] = 0b10100000;
    
                    // Send the command to the chip and update our state
                    // variable.
                    self.i2c.write(buf, 1);
                    self.state.set(State::Enabling);
                });
            }
        }
    }
    #}

    The start_light_reading() function kicks off reading the light value from the IC and updates our internal state machine state to mark that we are waiting for the IC to turn on. Now the Ls1234 code is finished for the time being and we now wait for the I2C message to finish being sent. We will know when this has completed based on a callback from the I2C hardware.

    
    # #![allow(unused_variables)]
    #fn main() {
    impl I2CClient for Ls1234<'a> {
        fn command_complete(&self, buffer: &'static mut [u8], error: Error) {
            // Handle what happens with the I2C send is complete here.
        }
    }
    #}

    In our example, we have to send a new command after turning on the light sensor to actually read a sampled value. We use our state machine here to organize the code as in this example:

    
    # #![allow(unused_variables)]
    #fn main() {
    impl I2CClient for Ls1234<'a> {
        fn command_complete(&self, buffer: &'static mut [u8], _error: Error) {
            match self.state.get() {
                State::Enabling => {
                    // Put the read command in the buffer and send it back to
                    // the sensor.
                    buffer[0] = 0b10100001;
                    self.i2c.write_read(buf, 1, 2);
                    // Update our state machine state.
                    self.state.set(State::ReadingLight);
                }
                _ => {}
            }
        }
    }
    #}

    This will send another command to the sensor to read the actual light measurement. We also update our self.state variable because when this I2C transaction finishes the exact same command_complete callback will be called, and we must be able to remember where we are in the process of communicating with the sensor.

    When the read finishes, the command_complete() callback will fire again, and we must handle the result. Since we now have the reading we can call our client's callback after updating out state machine.

    
    # #![allow(unused_variables)]
    #fn main() {
    impl I2CClient for Ls1234<'a> {
        fn command_complete(&self, buffer: &'static mut [u8], _error: Error) {
            match self.state.get() {
                State::Enabling => {
                    // Put the read command in the buffer and send it back to
                    // the sensor.
                    buffer[0] = 0b10100001;
                    self.i2c.write_read(buf, 1, 2);
                    // Update our state machine state.
                    self.state.set(State::ReadingLight);
                }
                State::ReadingLight => {
                    // Extract the light reading value.
                    let mut reading: u16 = buffer[0] as 16;
                    reading |= (buffer[1] as u16) << 8;
    
                    // Update our state machine state.
                    self.state.set(State::Disabled);
    
                    // Trigger our callback with the result.
                    self.client.map(|client| client.callback(reading));
                }
                _ => {}
            }
        }
    }
    #}

    Note: likely the sensor would need to be disabled and returned to a low power state.

    At this point your driver can read the IC and return the information from the IC. For your IC you will likely need to expand on this general template. You can add additional functions to the main struct implementation, and then expand the state machine to implement those functions. You may also need additional resources, like GPIO pins or timer alarms to implement the state machine for the IC. There are examples in the capsules/src folder with drivers that need different resources.

Optional Steps

  1. Provide a HIL interface for the IC driver.

    The driver so far has a very IC-specific interface. That is, any code that uses the driver must be written specifically with that IC in mind. In some cases that may be reasonable, for example if the IC is very unusual or has a very unique set of features. However, many ICs provide similar functionality, and higher-level code can be written without knowing what specific IC is being used on a particular hardware platform.

    To enable this, some IC types have HILs in the kernel/src/hil folder in the sensors.rs file. Drivers can implement one of these HILs and then higher-level code can use the HIL interface rather than a specific IC.

    To implement the HIL, you must implement the HIL trait functions:

    
    # #![allow(unused_variables)]
    #fn main() {
    impl AmbientLight for Ls1234<'a> {
        fn set_client(&self, client: &'static dyn AmbientLightClient) {
    
        }
    
        fn read_light_intensity(&self) -> ReturnCode {
    
        }
    }
    #}

    The user of the AmbientLight HIL will implement the AmbientLightClient and provide the client through the set_client() function.

  2. Provide a userspace interface for the IC driver.

    Sometimes the IC is needed by userspace, and therefore needs a syscall interface so that userspace applications can use the IC. Please refer to a separate guide on how to implement a userspace interface for a capsule.

Wrap-Up

Congratulations! You have implemented an IC driver as a capsule in Tock! We encourage you to submit a pull request to upstream this to the Tock repository. Tock is happy to accept capsule drivers even if no boards in the Tock repository currently use the driver.

Implementing a Syscall Interface for Userspace

This guide provides an overview and walkthrough on how to add a syscall interface for userspace applications in Tock. This syscall interface exposes some kernel functionality to applications. For example, this could be the ability to sample a new sensor, or use some service like doing AES encryption.

In this guide we will use a running example of providing a userspace interface for a hypothetical water level sensor (the "WS00123" water level sensor). This interface will allow applications to query the current water level, as well as get notified when the water level exceeds a certain threshold.

Setup

This guide assumes you already have existing kernel code that needs a userspace interface. Likely that means there is already a capsule implemented. Please see the other guides if you also need to implement the capsule.

We will assume there is a struct WS00123 {...} object already implemented that includes all of the logic needed to interface with this particular water sensor.

Overview

The high-level steps required are:

  1. Decide on the interface to expose to userspace.
  2. Map the interface to the existing syscalls in Tock.
  3. Create grant space for the application.
  4. Implement the Driver trait.
  5. Document the interface.
  6. Expose the interface to userspace.
  7. Implement the syscall library in userspace.

Step-by-Step Guide

The steps from the overview are elaborated on here.

  1. Decide on the interface to expose to userspace.

    Creating the interface for userspace means making design decisions on how applications should be able to interface with the kernel capsule. This can have a lasting impact, and is worth spending some time on up-front to avoid implementing an interface that is difficult to use or does not match the needs of applications.

    While there is not a fixed algorithm on how to create such an interface, there are a couple tips that can help with creating the interface:

    • Consider the interface for the same or similar functionality in other systems (e.g. Linux, Contiki, TinyOS, RIOT, etc.). These may have iterated on the design and include useful features.
    • Ignore the specific details of the capsule that exists or how the particular sensor the syscall interface is for works, and instead consider what a user of that capsule might want. That is, if you were writing an application, how would you expect to use the interface? This might be different from how the sensor or other hardware exposes features.
    • Consider other chips that provide similar functionality to the specific one you have. For example, imagine there is a competing water level sensor the "OWlS789". What features do both provide? How would a single interface be usable if a hardware board swapped one out for the other?

    The interface should include both actions (called "commands" in Tock) that the application can take (for example, "sample this sensor now"), as well as events (called subscribe callbacks in Tock) that the kernel can trigger inside of an application (for example, when the sensed value is ready).

    The interface can also include memory sharing between the application and the kernel. For example, if the application wants to receive a number of samples at once, or if the kernel needs to operate on many bytes (say for example encrypting a buffer), then the interface should allow the application to share some of its memory with the kernel to enable that functionality.

  2. Map the interface to the existing syscalls in Tock.

    With a sketch of the interface created, the next step is to map that interface to the specific syscalls that the Tock kernel supports. Tock has three main relevant syscall operations that applications can use when interfacing with the kernel:

    1. allow: This lets an application share some of its memory with the kernel.

    2. subscribe: This provides a callback function pointer that the kernel can use to trigger a callback in the application.

    3. command: This enables the application to direct the kernel to do some action.

    All three also include a couple other parameters to differentiate different commands, subscriptions, or allows. Refer to the more detailed documentation on the Tock syscalls for more information.

    As the Tock kernel only supports these syscalls, each feature in the design you created in the first step must be mapped to one or more of these syscalls. To help, consider these hypothetical interfaces that an application might have for our water sensor:

    • What is the maximum water level? This can be a simple command, where the return value of the command is the maximum water level.
    • What is the current water level? This will require two steps. First, there needs to be a subscribe call where the application can setup a callback function. The kernel will call this when the water level value has been acquired. Second, there will need to be a command to instruct the kernel to take the water level reading.
    • Take ten water level samples. This will require three steps. First, the application must use an allow syscall to share a buffer with the kernel large enough to hold 10 water level readings. Then it must setup a subscribe callback that the kernel will call when the 10 readings are ready (note this callback function can be the same as in the single sample case). Finally it will use a command to tell the kernel to start sampling.
    • Notify me when the water level exceeds a threshold. A likely way to implement this would be to first require a subscribe syscall for the application to set the function that will get called when the high water level event occurs. Then the application will need to use a command to enable the high water level detection and to optionally set the threshold.

    Checkpoint: You have defined how many allow, subscribe, and command syscalls you need, and what each will do.

  3. Create grant space for the application.

    Grants are regions in a process's memory space that are shared with the kernel. The kernel uses these to store state on behalf of the process. To provide our syscall interface for the water level sensor, we need to setup a grant so that we can store state for all of the requests we may get from processes that want to use the sensor. In particular, we will need to be able to store the callback that a process registers with us.

    The first step to do this is to create a struct that contains fields for all of the state we want to store for each process that uses our syscall interface. By convention in Tock, this struct is named App, but it could have a different name. We will need to keep two values, the callback and the high water alert threshold:

    
    # #![allow(unused_variables)]
    #fn main() {
    pub struct App {
        callback: Option<Callback>,
        threshold: usize,
    }
    #}

    Now that we have the type we want to store in the grant region we can create the grant type for it by extending our WS00123 struct:

    
    # #![allow(unused_variables)]
    #fn main() {
    pub struct WS00123 {
    	...
        apps: Grant<App>,
    }
    #}

    We will also need the grant region to be created by the board and passed in to us by adding it to the capsules new() function:

    
    # #![allow(unused_variables)]
    #fn main() {
    impl WS00123 {
        pub fn new(
            ...
            grant: Grant<App>,
        ) -> WS00123 {
            WS00123 {
                ...,
                apps: grant,
            }
        }
    }
    #}

    Now we have somewhere to store values on a per-process basis.

  4. Implement the Driver trait.

    The Driver trait is how a capsule provides implementations for the various syscalls an application might call. The basic framework looks like:

    
    # #![allow(unused_variables)]
    #fn main() {
    impl Driver for WS00123 {
    	fn allow(
    	    &self,
    	    appid: AppId,
    	    allow_num: usize,
    	    slice: Option<AppSlice<Shared, u8>>,
    	) -> ReturnCode { }
    
        fn subscribe(
            &self,
            subscribe_num: usize,
            callback: Option<Callback>,
            _app_id: AppId,
        ) -> ReturnCode { }
    
        fn command(&self,
        	       command_num: usize,
        	       data: usize,
        	       data2: usize,
        	       app: AppId,
        ) -> ReturnCode { }
    }
    #}

    Note: there are default implementations for each of these, so in our water level sensor case we can simply omit the allow call.

    By Tock convention, every syscall interface must at least support the command call with command_num == 0. This allows applications to check if the syscall interface is supported on the current platform. If the command returns <0 then the syscall interface is not present. Many implementations return SUCCESS (i.e. 0), however, some return other positive numbers, like for example the number of LEDs present on the board. For our example, we use the simple case:

    
    # #![allow(unused_variables)]
    #fn main() {
    impl Driver for WS00123 {
    	fn command(&self,
        	       command_num: usize,
        	       data: usize,
        	       data2: usize,
        	       app: AppId,
        ) -> ReturnCode {
    		match command_num {
    			0 => ReturnCode::SUCCESS,
    			_ => ReturnCode::ENOSUPPORT,
    		}
        }
    }
    #}

    Now let's handle the subscribe call where the app can setup the callback we should use. For this capsule, we will use a single callback for both when a measurement is ready and for when a high water alert is triggered, but with different arguments passed into the callback.

    
    # #![allow(unused_variables)]
    #fn main() {
    impl Driver for WS00123 {
        /// Setup callbacks.
        ///
        /// ### `subscribe_num`
        ///
        /// - `0`: Setup the main callback to be used when samples are ready
        ///        and when any alerts are triggered.
        fn subscribe(
            &self,
            subscribe_num: usize,
            callback: Option<Callback>,
            app_id: AppId,
        ) -> ReturnCode {
            self.apps
                .enter(app_id, |app, _| {
                    match subscribe_num {
                        0 => app.callback = callback,
                        _ => return ReturnCode::ENOSUPPORT,
                    }
                    ReturnCode::SUCCESS
                })
                .unwrap_or_else(|err| err.into())
        }
    }
    #}

    As you can see, we use the enter() function to "enter" the grant region of the specific requesting app app_id. This performs checks like ensuring the grant region exists and that the application is valid. If enter() succeeds then we can update the App state like normal. Here we only need to save the callback.

    Next we can implement more commands so that the application can direct our capsule as to what the application wants us to do. We need two commands, one to sample and one to enable the alert. In both cases the commands must return a ReturnCode, and call functions that likely already exist in the original implementation of the WS00123 sensor. If the functions don't quite exist, then they will need to be added as well.

    
    # #![allow(unused_variables)]
    #fn main() {
    impl Driver for WS00123 {
    	/// Command interface.
    	///
    	/// ### `command_num`
    	///
    	/// - `0`: Return SUCCESS if this driver is included on the platform.
    	/// - `1`: Start a water level measurement.
    	/// - `2`: Enable the water level detection alert. `data` is used as the
    	///        height to set as the the threshold for detection.
    	fn command(&self,
        	       command_num: usize,
        	       data: usize,
        	       data2: usize,
        	       app: AppId,
        ) -> ReturnCode {
    		match command_num {
    			0 => ReturnCode::SUCCESS,
    
    			1 => self.start_measurement(app),
    
    			2 => {
    				// Save the threshold for this app.
    				self.apps
    				    .enter(app_id, |app, _| {
    				        app.threshold = data;
    				        ReturnCode::SUCCESS
    				    })
    				    .map_or_else(
    				    	|err| err.into(),
    				    	|ok| self.set_high_level_detection()
    				    )
    			}
    
    			_ => ReturnCode::ENOSUPPORT,
    		}
        }
    }
    #}

    The last item that needs to be added is to actually use the callback when the sensor has been sampled or the alert has been triggered. Actually issuing the callback will need to be added to the existing implementation of the capsule. As an example, if our water sensor was attached to the board over I2C, then we might trigger the callback in response to a finished I2C command:

    
    # #![allow(unused_variables)]
    #fn main() {
    impl i2c::I2CClient for WS00123 {
        fn command_complete(&self, buffer: &'static mut [u8], _error: i2c::Error) {
        	...
        	let app_id = <get saved appid for the app that issued the command>;
        	let measurement = <calculate water level based on returned I2C data>;
    
        	self.apps.enter(app_id, |app, _| {
        	    app.callback.map(|mut cb| cb.schedule(0, measurement, 0));
        	});
        }
    }
    #}

    There may be other cleanup code required to reset state or prepare the sensor for another sample by a different application, but these are the essential elements for implementing the syscall interface.

    Finally, we need to assign our new Driver implementation a number so that the kernel (and userspace apps) can differentiate this syscall interface from all others that a board supports. By convention this is specified by a global value at the top of the capsule file:

    
    # #![allow(unused_variables)]
    #fn main() {
    pub const DRIVER_NUM: usize = 0x80000A;
    #}

    The value cannot conflict with other capsules in use, but can be set arbitrarily, particularly for testing. Tock has a procedure for assigning numbers, and you may need to change this number if the capsule is to merged into the main Tock repository.

    Checkpoint: You have the syscall interface translated from a design to code that can run inside the Tock kernel.

  5. Document the interface.

    A syscall interface is a contract between the kernel and any number of userspace processes, and processes should be able to be developed independently of the kernel. Therefore, it is helpful to document the new syscall interface you made so applications know how to use the various command, subscribe, and allow calls.

    An example markdown file documenting our water level syscall interface is as follows:

    ---
    driver number: 0x80000A
    ---
    
    # Water Level Sensor WS00123
    
    ## Overview
    
    The WS00123 water level sensor can sample the depth of water as well as
    trigger an event if the water level gets too high.
    
    ## Command
    
      * ### Command number: `0`
    
        **Description**: Does the driver exist?
    
        **Argument 1**: unused
    
        **Argument 2**: unused
    
        **Returns**: SUCCESS if it exists, otherwise ENODEVICE
    
      * ### Command number: `1`
    
        **Description**: Initiate a sensor reading.  When a reading is ready, a
        callback will be delivered if the process has `subscribed`.
    
        **Argument 1**: unused
    
        **Argument 2**: unused
    
        **Returns**: `EBUSY` if a reading is already pending, `ENOMEM` if there
        isn't sufficient grant memory available, or `SUCCESS` if the sensor
        reading was initiated successfully.
    
      * ### Command number: `2`
    
        **Description**: Enable the high water detection. THe callback will the
        alert will be delivered if the process has `subscribed`.
    
        **Argument 1**: The water depth to alert for.
    
        **Argument 2**: unused
    
        **Returns**: `EBUSY` if a reading is already pending, `ENOMEM` if there
        isn't sufficient grant memory available, or `SUCCESS` if the sensor
        reading was initiated successfully.
    
    ## Subscribe
    
      * ### Subscribe number: `0`
    
        **Description**: Subscribe a callback for sensor readings and alerts.
    
        **Callback signature**: The callback's first argument is `0` if this is
        a measurement, and `1` if the callback is an alert. If it is a
        measurement the second value will be the water level.
    
        **Returns**: SUCCESS if the subscribe was successful or ENOMEM if the
        driver failed to allocate memory to store the callback.
    

    This file should be named <driver_num>_<sensor>.md, or in this case: 80000A_ws00123.md.

  6. Expose the interface to userspace.

    The last kernel implementation step is to let the main kernel know about this new syscall interface so that if an application tries to use it the kernel knows which implementation of Driver to call. In each board's main.rs file (e.g. boards/hail/src/main.rs) there is a implementation of the Platform trait where the board can setup which syscall interfaces it supports. To enable our water sensor interface we add a new entry to the match statement there:

    
    # #![allow(unused_variables)]
    #fn main() {
    impl Platform for Hail {
        fn with_driver<F, R>(&self, driver_num: usize, f: F) -> R
        where
            F: FnOnce(Option<&dyn kernel::Driver>) -> R,
        {
            match driver_num {
            	...
                capsules::ws00123::DRIVER_NUM => f(Some(self.ws00123)),
                ...
                _ => f(None),
            }
        }
    }
    #}
  7. Implement the syscall library in userspace.

    At this point userspace applications can use our new syscall interface and interact with the water sensor. However, applications would have to call all of the syscalls directly, and that is fairly difficult to get right and not user friendly. Therefore, we typically implement a small library layer in userspace to make using the interface easier.

    In this guide we will be setting up a C library, and to do so we will create libtock-c/libtock/ws00123.h and libtock-c/libtock/ws00123.c, both of which will be added to the libtock-c repository. The .h file defines the public interface and constants:

    #pragma once
    
    #include "tock.h"
    
    #ifdef __cplusplus
    extern "C" {
    #endif
    
    #define DRIVER_NUM_WS00123 0x80000A
    
    int ws00123_set_callback(subscribe_cb callback, void* callback_args);
    int ws00123_read_water_level();
    int ws00123_enable_alerts(uint32_t threshold);
    
    #ifdef __cplusplus
    }
    #endif
    

    While the .c file provides the implementations:

    #include "ws00123.h"
    #include "tock.h"
    
    int ws00123_set_callback(subscribe_cb callback, void* callback_args) {
      return subscribe(DRIVER_NUM_WS00123, 0, callback, callback_args);
    }
    
    int ws00123_read_water_level() {
      return command(DRIVER_NUM_WS00123, 1, 0, 0);
    }
    
    int ws00123_enable_alerts(uint32_t threshold) {
      return command(DRIVER_NUM_WS00123, 2, threshold, 0);
    }
    

    This is a very basic implementation of the interface, but it provides some more readable names to the numbers that make up the syscall interface. See other examples in libtock for how to make synchronous versions of asynchronous operations (like reading the sensor).

Wrap-Up

Congratulations! You have added a new API for userspace applications using the Tock syscall interface! We encourage you to submit a pull request to upstream this to the Tock repository.

Implementing a HIL Interface

This guide describes the process of creating a new HIL interface in Tock. "HIL"s are one or more Rust traits that provide a standard and shared interface between pieces of the Tock kernel.

Background

The most canonical use for a HIL is to provide an interface to hardware peripherals to capsules. For example, a HIL for SPI provides an interface between the SPI hardware peripheral in a microcontroller and a capsule that needs a SPI bus for its operation. The HIL is a generic interface, so that same capsule can work on different microcontrollers, as long as each microcontroller implements the SPI HIL.

HILs are also used for other generic kernel interfaces that are relevant to capsules. For example, Tock defines a HIL for a "temperature sensor". While a temperature sensor is not generally a hardware peripheral, a capsule may want to use a generic temperature sensor interface and not be restricted to using a particular temperature sensor driver. Having a HIL allows the capsule to use a generic interface. For consistency, these HILs are also specified in the kernel crate.

Note: In the future Tock will likely split these interface types into separate groups.

HIL development often significantly differs from other development in Tock. In particular, HILs can often be written quickly, but tend to take numerous iterations over relatively long periods of time to refine. This happens for three general reasons:

  1. HILs are intended to be generic, and therefore implementable by a range of different hardware platforms. Designing an interface that works for a range of different hardware takes time and experience with various MCUs, and often incompatibilities aren't discovered until an implementation proves to be difficult (or impossible).
  2. HILs are Rust traits, and Rust traits are reasonably complex and offer a fair bit of flexibility. Balancing both leveraging the flexibility Rust provides and avoiding undue complexity takes time. Again, often trial-and-error is required to settle on how traits should be composed to best capture the interface.
  3. HILs are intended to be generic, and therefore will be used in a variety of different use cases. Ensuring that the HIL is expressive enough for a diverse set of uses takes time. Again, often the set of uses is not known initially, and HILs often have to be revised as new use cases are discovered.

Therefore, we consider HILs to be evolving interfaces.

Tips on HIL Development

As getting a HIL interface "correct" is difficult, Tock tends to prefer starting with simple HIL interfaces that are typically inspired by the hardware used when the HIL is initially created. Trying to generalize a HIL too early can lead to complexity that is never actually warranted, or complexity that didn't actually address a problem.

Also, Tock prefers to only include code (or in this case HIL interface functions) that are actually in use by the Tock code base. This ensures that there is at least some method of using or testing various components of Tock. This also suggests that initial HIL development should only focus on an interface that is needed by the initial use case.

Overview

The high-level steps required are:

  1. Determine that a new HIL interface is needed.
  2. Create the new HIL in the kernel crate.
  3. Ensure the HIL file includes sufficient documentation.

Step-by-Step Guide

The steps from the overview are elaborated on here.

  1. Determine that a new HIL interface is needed.

    Tock includes a number of existing HIL interfaces, and modifying an existing HIL is preferred to creating a new HIL that is similar to an existing interface. Therefore, you should start by verifying an existing HIL does not already meet your need or could be modified to meet your need.

    This may seem to be a straightforward step, but it can be complicated by microcontrollers calling similar functionality by different names, and the existing HIL using a standard name or a different name from another microcontroller.

    Also, you can reach out via the email list or slack if you have questions about whether a new HIL is needed or an existing one should suffice.

  2. Create the new HIL in the kernel crate.

    Once you have determined a new HIL is required, you should create the appropriate file in kernel/src/hil. Often the best way to start is to copy an existing HIL that is similar in nature to the interface you are trying to create.

    As noted above, HILs evolve over time, and HILs will be periodically updated as issues are discovered or best practices for HIL design are learned. Unfortunately, this means that copying an existing HIL might lead to "mistakes" that must be remedied before the new HIL can be merged.

    Likely, it is helpful to open a pull request relatively early in the HIL creation process so that any substantial issues can be detected and corrected quickly.

    While Tock does not have a full guide to writing a HIL (although perhaps that could be created eventually), there are some guidelines that HILs generally follow:

    • Avoid initialize() or start() or on() functions in HIL interfaces. While these types of methods are intuitive when implementing a HIL, they are often not intuitive for users of that HIL. For example, when should initialize() be called? Once when the board boots? Or before every use? What if there are multiple users of the peripheral? In general, it is better to require that the implementation track the state of the underlying hardware and configure it as needed.
    • Avoid using 'static lifetimes in HILs. This limits flexibility, and Tock prefers to use a generic 'a lifetime instead. Also, unless there is a clear reason not to, only a single lifetime should be used.
    • Include the set_client() function in the HIL if needed. Not all HILs require split-phase operation and need a client interface, but for those that do the set_client() function should be included in the HIL. This makes writing generic components for boards possible.

    Tock only uses non-blocking interfaces in the kernel, and HILs should reflect that as well. Therefore, for any operation that will take more than a couple cycles to complete, or would require waiting on a hardware flag, a split interface design should be used with a Client trait that receives a callback when the operation has completed.

  3. Ensure the HIL file includes sufficient documentation.

    HIL files should be well commented with Rustdoc style (i.e. ///) comments. These comments are the main source of documentation for HILs.

    As HILs grow in complexity or stability, they will be documented separately to fully explain their design and intended use cases.

Wrap-Up

Congratulations! You have implemented a new HIL in Tock! We encourage you to submit a pull request to upstream this to the Tock repository.

Implementing a Kernel Test

This guide covers how to write in-kernel tests of hardware functionality. For example, if you have implemented a chip peripheral, you may want to write in-kernel tests of that peripheral to test peripheral-specific functionality that will not be exposed via the HIL for that peripheral. This guide outlines the general steps for implementing kernel tests.

Setup

This guide assumes you have existing chip, board, or architecture specific code that you wish to test from within the kernel.

Note: If you wish to test kernel code with no hardware dependencies at all, such as a ring buffer implementation, you can use cargo's test framework instead. These tests can be run by simply calling cargo test within the crate that the test is located, and will be executed during CI for all tests merged into upstream Tock. An example of this approach can be found in kernel/src/common/ring_buffer.rs.

Overview

The general steps you will follow are:

  1. Determine the board(s) you want to run your tests on
  2. Add a test file in boards/{board}/src/
  3. Determine where to write actual tests -- in the test file or a capsule test
  4. Write your tests
  5. Call the test from main.rs
  6. Document the expected output from the test at the top of the test file

This guide will walk through how to do each of these steps.

Background

Kernel tests allow for testing of hardware-specific functionality that is not exposed to userspace, and allows for fail-fast tests at boot that otherwise would not be exposed until apps are loaded. Kernel tests can be useful to test chip peripherals prior to exposing these peripherals outside the Kernel. Kernel tests can also be included as required tests run prior to releases, to ensure there have been no regressions for a particular component. Additionally, kernel tests can be useful for testing capsule functionality from within the kernel, such as when unsafe is required to verify the results of tests, or for testing virtualization capsules in a controlled environment.

Kernel tests are generally implemented on an as-needed basis, and are not required for all chip peripherals in Tock. In general, they are not expected to be run in the default case, though they should always be included from main.rs so they are compiled. These tests are allowed to use unsafe as needed, and are permitted to conflict with normal operation, by stealing callbacks from drivers or modifying global state.

Notably, your specific use case may differ some from the one outline here. It is always recommended to attempt to copy from existing Tock code when developing your own solutions. A good collection of kernel tests can be found in boards/imix/src/tests/ for that purpose.

Step-by-Step Guide

The steps from the overview are elaborated on here.

  1. Determine the board(s) you want to run your test on.

    If you are testing chip or architecture specific functionality, you simply need to choose a board that uses that chip or architecture. For board specific functionality you of course need to choose that board. If you are testing a virtualization capsule, then any board that implements the underlying resource being virtualized is acceptable. Currently, most kernel tests are implemented for the Imix platform, and can be found in boards/imix/src/tests/

    Checkpoint: You have identified the board you will implement your test for.

  2. Add a test file in boards/{board}/src/

    To start implementing the test, you should create a new source file inside the boards/{board}/src directory. For boards with lots of tests, like the Imix board, there may be a tests subdirectory -- if so, the test should go in tests instead, and be added to the tests/mod.rs file. The name of this test file generally should indicate the functionality being tested.

    Note: If the board you select is one of the nrf52dk variants (nrf52840_dongle, nrf52840dk, or nrf52dk), tests should be moved into the nrf52dk_base/src/ folder, and called from lib.rs.

    Checkpoint: You have chosen a board for your test and created a test file.

  3. Determine where to write actual tests -- in the test file or a capsule test.

    Depending on what you are testing, it may be best practice to write a capsule test that you call from the test file you created in the previous step.

    Writing a capsule test is best practice if your test meets the following criteria:

    1. Test does not require unsafe
    2. The test is for a peripheral available on multiple boards
    3. A HIL or capsule exists for that peripheral, so it is accessible from the capsules crate
    4. The test relies only on functionality exposed via the HIL or a capsule
    5. You care about being able to call this test from multiple boards

    Examples:

    • UART Virtualization (all boards support UART, there is a HIL for UART devices and a capsule for the virtual_uart)
    • Alarm test (all boards will have some form of hardware alarm, there is an Alarm HIL)
    • Other examples: see capsules/src/test

    If your test meets the criteria for writing a capsule test, follow these steps:

    Add a file in capsules/src/test/, and then add the filename to capsules/src/mod.rs like this:

    
    # #![allow(unused_variables)]
    #fn main() {
    pub mod virtual_uart;
    #}

    Next, create a test struct in this file that can be instantiated by any board using this test capsule. This struct should implement a new() function so it can be instantiated from the test file in boards, and a run() function that will run the actual tests. An example for UART follows:

    
    # #![allow(unused_variables)]
    #fn main() {
    //! capsules/src/test/virtual_uart.rs
    
    pub struct TestVirtualUartReceive {
        device: &'static UartDevice<'static>,
        buffer: TakeCell<'static, [u8]>,
    }
    
    impl TestVirtualUartReceive {
        pub fn new(device: &'static UartDevice<'static>, buffer: &'static mut [u8]) -> Self {
            TestVirtualUartReceive {
                device: device,
                buffer: TakeCell::new(buffer),
            }
        }
    
        pub fn run(&self) {
            // TODO: See Next Step
        }
    }
    #}

    If your test does not meet the above requirements, you can simply implement your tests in the file that you created in step 2. This can involve creating a test structure with test methods. The UDP test file takes this approach, by definining a number of self-contained tests. One such example follows:

    
    # #![allow(unused_variables)]
    #fn main() {
    //! boards/imix/src/test/udp_lowpan_test.rs
    
    pub struct LowpanTest {
        port_table: &'static UdpPortManager,
        // ...
    }
    
    impl LowpanTest {
    
        // This test ensures that an app and capsule cant bind to the same port
        // but can bind to different ports
        fn bind_test(&self) {
            let create_cap = create_capability!(NetworkCapabilityCreationCapability);
            let net_cap = unsafe {
                static_init!(
                    NetworkCapability,
                    NetworkCapability::new(AddrRange::Any, PortRange::Any, PortRange::Any, &create_cap)
                )
            };
            let mut socket1 = self.port_table.create_socket().unwrap();
            // Attempt to bind to a port that has already been bound by an app.
            let result = self.port_table.bind(socket1, 1000, net_cap);
            assert!(result.is_err());
            socket1 = result.unwrap_err(); // Get the socket back
    
            //now bind to an open port
            let (_send_bind, _recv_bind) = self
                .port_table
                .bind(socket1, 1001, net_cap)
                .expect("UDP Bind fail");
    
            debug!("bind_test passed");
        }
        // ...
    }
    #}

    Checkpoint: There is a test capsule with new() and run() implementations.

  4. Write your tests

    The first part of this step takes place in the test file you just created -- writing the actual tests. This part is highly dependent on the funcitonality being verified. If you are writing your tests in test capsule, this should all be triggered from the run() function.

    Depending on the specifics of your test, you may need to implement additional functions or traits in this file to make your test functional. One example is implementing a client trait on the test struct so that the test can receive the results of asynchronous operations. Our UART example requires implementing the uart::RecieveClient on the test struct.

    
    # #![allow(unused_variables)]
    #fn main() {
    //! boards/imix/src/test/virtual_uart_rx_test.rs
    
    impl TestVirtualUartReceive {
        // ...
    
        pub fn run(&self) {
            let buf = self.buffer.take().unwrap();
            let len = buf.len();
            debug!("Starting receive of length {}", len);
            let (err, _opt) = self.device.receive_buffer(buf, len);
            if err != ReturnCode::SUCCESS {
                panic!(
                    "Calling receive_buffer() in virtual_uart test failed: {:?}",
                    err
                );
            }
        }
    }
    
    impl uart::ReceiveClient for TestVirtualUartReceive {
        fn received_buffer(
            &self,
            rx_buffer: &'static mut [u8],
            rx_len: usize,
            rcode: ReturnCode,
            _error: uart::Error,
        ) {
            debug!("Virtual uart read complete: {:?}: ", rcode);
            for i in 0..rx_len {
                debug!("{:02x} ", rx_buffer[i]);
            }
            debug!("Starting receive of length {}", rx_len);
            let (err, _opt) = self.device.receive_buffer(rx_buffer, rx_len);
            if err != ReturnCode::SUCCESS {
                panic!(
                    "Calling receive_buffer() in virtual_uart test failed: {:?}",
                    err
                );
            }
        }
    }
    #}

    Note that the above test calls panic!() in the case of failure. This pattern, or the similar use of assert!() statements, is the preferred way to communicate test failures. If communicating errors in this way is not possible, tests can indicate success/failure by printing different results to the console in each case and asking users to verify the actual output matches the expected output.

    The next step in this process is determining all of the parameters that need to be passed to the test. It is preferred that all logically related tests be called from a single pub unsafe fn run(/* optional args */) to maintain convention. This ensures that all tests can be run by adding a single line to main.rs. Many tests require a reference to an alarm in order to seperate tests in time, or a reference to a virtualization capsule that is being tested. Notably, the run() function should initialize any components itself that would not have already been created in main.rs. As an example, the below function is a starting point for the virtual_uart_receive test for Imix:

    
    # #![allow(unused_variables)]
    #fn main() {
    pub unsafe fn run_virtual_uart_receive(mux: &'static MuxUart<'static>) {
        debug!("Starting virtual reads.");
    }
    #}

    Next, a test function should initialize any objects required to run tests. This is best split out into subfunctions, like the following:

    
    # #![allow(unused_variables)]
    #fn main() {
    unsafe fn static_init_test_receive_small(
        mux: &'static MuxUart<'static>,
    ) -> &'static TestVirtualUartReceive {
        static mut SMALL: [u8; 3] = [0; 3];
        let device = static_init!(UartDevice<'static>, UartDevice::new(mux, true));
        device.setup();
        let test = static_init!(
            TestVirtualUartReceive,
            TestVirtualUartReceive::new(device, &mut SMALL)
        );
        device.set_receive_client(test);
        test
    }
    #}

    This initializes an instance of the test capsule we constructed earlier. Simpler tests (such as those not relying on capsule tests) might simply use static_init!() to initialize normal capsules directly and test them. The log test does this, for example:

    
    # #![allow(unused_variables)]
    #fn main() {
    //! boards/imix/src/test/log_test.rs
    
    pub unsafe fn run(
        mux_alarm: &'static MuxAlarm<'static, Ast>,
        deferred_caller: &'static DynamicDeferredCall,
    ) {
        // Set up flash controller.
        flashcalw::FLASH_CONTROLLER.configure();
        static mut PAGEBUFFER: flashcalw::Sam4lPage = flashcalw::Sam4lPage::new();
    
        // Create actual log storage abstraction on top of flash.
        let log = static_init!(
            Log,
            log::Log::new(
                &TEST_LOG,
                &mut flashcalw::FLASH_CONTROLLER,
                &mut PAGEBUFFER,
                deferred_caller,
                true
            )
        );
        flash::HasClient::set_client(&flashcalw::FLASH_CONTROLLER, log);
        log.initialize_callback_handle(
            deferred_caller
                .register(log)
                .expect("no deferred call slot available for log storage"),
        );
    
        // ...
    }
    #}

    Finally, your run() function should call the actual tests. This may involve simply calling a run() function on a capsule test, or may involve calling test functions written in the board specific test file. The virtual UART test run() looks like this:

    
    # #![allow(unused_variables)]
    #fn main() {
    pub unsafe fn run_virtual_uart_receive(mux: &'static MuxUart<'static>) {
        debug!("Starting virtual reads.");
        let small = static_init_test_receive_small(mux);
        let large = static_init_test_receive_large(mux);
        small.run();
        large.run();
    }
    #}

    As you develop your kernel tests, you may not immediately know what functions are required in your test capsule -- this is okay! It is often easiest to start with a basic test and expand this file to test additional functionality once basic tests are working.

    Checkpoint: Your tests are written, and can be called from a single run() function.

  5. Call the test from main.rs, and iterate on it until it works

    Next, you should run your test by calling it from the reset_handler() in main.rs. In order to do so, you will also need it import it into the file by adding a line like this:

    
    # #![allow(unused_variables)]
    #fn main() {
    #[allow(dead_code)]
    mod virtual_uart_test;
    #}

    However, if your test is located inside a test module this is not needed -- your test will already be included.

    Typically, tests are called after completing setup of the board, immediately before the call to load_processes():

    
    # #![allow(unused_variables)]
    #fn main() {
    virtual_uart_rx_test::run_virtual_uart_receive(uart_mux);
    debug!("Initialization complete. Entering main loop");
    
    extern "C" {
        /// Beginning of the ROM region containing app images.
        static _sapps: u8;
    
        /// End of the ROM region containing app images.
        ///
        /// This symbol is defined in the linker script.
        static _eapps: u8;
    }
    kernel::procs::load_processes(
      // ...
    #}

    Observe your results, and tune or add tests as needed.

    Before you submit a PR including any kernel tests, however, please remove or comment out any lines of code that call these tests.

    Checkpoint: You have a functional test that can be called in a single line from main.rs

  6. Document the expected output from the test at the top of the test file

    For tests that will be merged to upstream, it is good practice to document how to run a test and what the expected output of a test is. This is best done using
    document level coments (//!) at the top of the test file. The documentation for the virtual UART test follows:

    
    # #![allow(unused_variables)]
    #fn main() {
    //! Test reception on the virtualized UART by creating two readers that
    //! read in parallel. To add this test, include the line
    //! ```
    //!    virtual_uart_rx_test::run_virtual_uart_receive(uart_mux);
    //! ```
    //! to the imix boot sequence, where `uart_mux` is a
    //! `capsules::virtual_uart::MuxUart`.  There is a 3-byte and a 7-byte
    //! read running in parallel. Test that they are both working by typing
    //! and seeing that they both get all characters. If you repeatedly
    //! type 'a', for example (0x61), you should see something like:
    //! ```
    //! Starting receive of length 3
    //! Virtual uart read complete: CommandComplete:
    //! 61
    //! 61
    //! 61
    //! 61
    //! 61
    //! 61
    //! 61
    //! Starting receive of length 7
    //! Virtual uart read complete: CommandComplete:
    //! 61
    //! 61
    //! 61
    //! ```
    #}

    Checkpoint: You have documented your tests

Wrap-Up

Congratulations! You have written a kernel test for Tock! We encourage you to submit a pull request to upstream this to the Tock repository.