This document explains how application code works in Tock. This is not a guide to creating your own applications, but rather documentation of the design thoughts behind how applications function.
Applications in Tock are the user-level code meant to accomplish some type of task for the end user. Applications are distinguished from kernel code which handles device drivers, chip-specific details, and general operating system tasks. Unlike many existing embedded operating systems, in Tock applications are not compiled with the kernel. Instead they are entirely separate code that interact with the kernel and each other through system calls.
Since applications are not a part of the kernel, they may be written in any language that can be compiled into code capable of running on a microcontroller. Tock supports running multiple applications concurrently. Co-operatively multiprogramming is the default, but applications may also be time sliced. Applications may talk to each other via Inter-Process Communication (IPC) through system calls.
Applications do not have compile-time knowledge of the address at which they will be installed and loaded. In the current design of Tock, applications must be compiled as position independent code (PIC). This allows them to be run from any address they happen to be loaded into. The use of PIC for Tock apps is not a fundamental choice, future versions of the system may support run-time relocatable code.
Applications are unprivileged code. They may not access all portions of memory and will fault if they attempt to access memory outside of their boundaries (similarly to segmentation faults in Linux code). To interact with hardware, applications must make calls to the kernel.
System calls (aka syscalls) are used to send commands to the kernel. These could include commands to drivers, subscriptions to callbacks, granting of memory to the kernel so it can store data related to the application, communication with other application code, and many others. In practice, system calls are made through library code and the application need not deal with them directly.
For example, consider the following system call that sets a GPIO pin high:
int gpio_set(GPIO_Pin_t pin) { return command(GPIO_DRIVER_NUM, 2, pin); }
The command system call itself is implemented as the ARM assembly instruction svc
(service call):
int __attribute__((naked)) command(uint32_t driver, uint32_t command, int data) { asm volatile("svc 2\nbx lr" ::: "memory", "r0"); }
A more in-depth discussion can be found in the system call documentation.
Tock is designed to support embedded applications, which often handle asynchronous events through the use of callback functions. For example, in order to receive timer callbacks, you first call timer_subscribe
with a function pointer to your own function that you want called when the timer fires. Specific state that you want the callback to act upon can be passed as the pointer userdata
. After the application has started the timer, calls yield
, and the timer fires, the callback function will be called.
It is important to note that yield
must be called for events to be serviced in the current implementation of Tock. Callbacks to the application will be queued when they occur but the application will not receive them until it yields. This is not fundamental to Tock, and future version may service callbacks on any system call or when performing application time slicing. After receiving and running the callback, application code will continue after the yield
. Applications which are “finished” (i.e. have returned from main()
) should call yield
in a loop to avoid being scheduled by the kernel.
IPC allows for multiple applications to communicate directly through shared buffers. IPC in Tock is implemented with a service-client model. Each app can support one service and the service is identified by its package name which is included in the Tock Binary Format Header for the app. An app can communicate with multiple services and will get a unique handle for each discovered service. Clients and services communicate through shared buffers. Each client can share some of its own application memory with the service and then notify the service to instruct it to parse the shared buffer.
Services are named by the package name included in the app's TBF header. To register a service, an app can call ipc_register_svc()
to setup a callback. This callback will be called whenever a client calls notify on that service.
Clients must first discover services they wish to use with the function ipc_discover()
. They can then share a buffer with the service by calling ipc_share()
. To instruct the service to do something with the buffer, the client can call ipc_notify_svc()
. If the app wants to get notifications from the service, it must call ipc_register_client_cb()
to receive events from when the service when the service calls ipc_notify_client()
.
See ipc.h
in libtock-c
for more information on these functions.
An application specifies the first function the kernel should call by setting the variable init_fn_offset
in its TBF header. This function should have the following signature:
void _start(void* text_start, void* mem_start, void* memory_len, void* app_heap_break);
The Tock kernel tries to impart no restrictions on the stack and heap layout of application processes. As such, a process starts in a very minimal environment, with an initial stack sufficient to support a syscall, but not much more. Application startup routines should first move their program break to accomodate their desired layout, and then setup local stack and heap tracking in accordance with their runtime.
Applications can specify memory requirements by setting the minimum_ram_size
variable in their TBF headers. Note that the Tock kernel treats this as a minimum, depending on the underlying platform, the amount of memory may be larger than requested, but will never be smaller.
If there is insufficient memory to load your application, the kernel will fail during loading and print a message.
If an application exceeds its alloted memory during runtime, the application will crash (see the Debugging section for an example).
If an application crashes, Tock can provide a lot of useful information. By default, when an application crashes Tock prints a crash dump over the platform's default console interface.
Note that because an application is relocated when it is loaded, the binaries and debugging .lst files generated when the app was originally compiled will not match the actual executing application on the board. To generate matching files (and in particular a matching .lst file), you can use the make debug
target app directory to create an appropriate .lst file that matches how the application was actually executed. See the end of the debug print out for an example command invocation.
---| Fault Status |--- Data Access Violation: true Forced Hard Fault: true Faulting Memory Address: 0x00000000 Fault Status Register (CFSR): 0x00000082 Hard Fault Status Register (HFSR): 0x40000000 ---| App Status |--- App: crash_dummy - [Fault] Events Queued: 0 Syscall Count: 0 Dropped Callback Count: 0 Restart Count: 0 Last Syscall: None ╔═══════════╤══════════════════════════════════════════╗ ║ Address │ Region Name Used | Allocated (bytes) ║ ╚0x20006000═╪══════════════════════════════════════════╝ │ ▼ Grant 948 | 948 0x20005C4C ┼─────────────────────────────────────────── │ Unused 0x200049F0 ┼─────────────────────────────────────────── │ ▲ Heap 0 | 4700 S 0x200049F0 ┼─────────────────────────────────────────── R │ Data 496 | 496 A 0x20004800 ┼─────────────────────────────────────────── M │ ▼ Stack 72 | 2048 0x200047B8 ┼─────────────────────────────────────────── │ Unused 0x20004000 ┴─────────────────────────────────────────── ..... 0x00030400 ┬─────────────────────────────────────────── F │ App Flash 976 L 0x00030030 ┼─────────────────────────────────────────── A │ Protected 48 S 0x00030000 ┴─────────────────────────────────────────── H R0 : 0x00000000 R6 : 0x20004894 R1 : 0x00000001 R7 : 0x20004000 R2 : 0x00000000 R8 : 0x00000000 R3 : 0x00000000 R10: 0x00000000 R4 : 0x00000000 R11: 0x00000000 R5 : 0x20004800 R12: 0x12E36C82 R9 : 0x20004800 (Static Base Register) SP : 0x200047B8 (Process Stack Pointer) LR : 0x000301B7 PC : 0x000300AA YPC : 0x000301B6 APSR: N 0 Z 1 C 1 V 0 Q 0 GE 0 0 0 0 EPSR: ICI.IT 0x00 ThumbBit true Cortex-M MPU Region 0: base: 0x20004000, length: 8192 bytes; ReadWrite (0x3) Region 1: base: 0x30000, length: 1024 bytes; ReadOnly (0x6) Region 2: Unused Region 3: Unused Region 4: Unused Region 5: Unused Region 6: Unused Region 7: Unused To debug, run `make debug RAM_START=0x20004000 FLASH_INIT=0x30059` in the app's folder and open the .lst file.
For example applications, see the language specific userland repos: