Developing #

Device Applications #

Consider this C program:

#include <types.h>
#include <tk1_mem.h>

#define SLEEPTIME 100000
#define LED_RED   (1 << TK1_MMIO_TK1_LED_R_BIT)
#define LED_GREEN (1 << TK1_MMIO_TK1_LED_G_BIT)
#define LED_BLUE  (1 << TK1_MMIO_TK1_LED_B_BIT)

static volatile uint32_t *led = (volatile uint32_t *)TK1_MMIO_TK1_LED;

void sleep(uint32_t n)
	for (volatile int i = 0; i < n; i++);

int main(void)
	for (;;) {
		*led = LED_RED;
		*led = LED_GREEN;
		*led = LED_BLUE;

To get this to work you will need our header files and to link with at least the libcrt0 C runtime, otherwise your program won’t even reach main(). Header files, libcrt0, and other libraries are available in

as mentioned in Tools & libraries.

We also provide a linker script there in which shows the linker the memory layout.

Minimal compilation of the above program would look something like this if tkey-libs is cloned in the directory next to this one:

$ clang -g -target riscv32-unknown-none-elf -march=rv32iczmmul -mabi=ilp32 \
  -mcmodel=medany -static -std=gnu99 -O2 -ffast-math -fno-common \
  -fno-builtin-printf -fno-builtin-putchar -nostdlib -mno-relax -flto \
  -Wall -Werror=implicit-function-declaration \
  -I ../tkey-libs/include \
  -I ../tkey-libs -c -o rgb.o rgb.c

$ clang -g -target riscv32-unknown-none-elf -march=rv32iczmmul -mabi=ilp32 \
  -mcmodel=medany -static -ffast-math -fno-common -nostdlib \
  -T ../tkey-libs/ \
  -L ../tkey-libs/libcrt0/ -lcrt0 \
  -I ../tkey-libs -o rgb.elf rgb.o

$ llvm-objcopy --input-target=elf32-littleriscv --output-target=binary rgb.elf rgb.bin

Now you have rgb.bin which you can load into a TKey with tkey-runapp (see Running TKey apps below).

To make development easier a sample Makefile is provided in tkey-libs/example-app.

Memory #

RAM starts at 0x4000_0000 and ends at 0x4002_0000 (128 kiB). The device app will be loaded by firmware at RAM start. The stack for the app is set up by the C runtime libcrt0 to start just below the end of RAM.

There are no heap allocation functions, no malloc() and friends. You can access memory directly yourself. APP_ADDR and APP_SIZE are provided so the loaded device app knows where it’s loaded and how large it is.

Special memory areas for memory mapped hardware functions are available at base 0xc000_0000 and an offset. See the memory map and the header file tk1_mem.h.

Running TKey apps #

To run the TKey, plug it into a USB port on a computer. If the TKey status indicator LED is white, it has been programmed with the standard FPGA bitstream (including the firmware). If the status indicator LED is not white it is unprovisioned. For instructions on how to do the initial programming of an unprovisioned TKey, see: (in the tillitis-key1 repository).

The examples below refer to files in the tillitis-key1-apps repository.

Users on Linux #

Running lsusb should list the USB stick as 1207:8887 Tillitis MTA1-USB-V1. On Linux, the TKey’s serial port device path is typically /dev/ttyACM0 (but it may end with another digit, if you have other devices plugged in already). The client applications try to auto-detect TKeys, but if more than one TKey is found you need to choose one using the --port flag.

Your current Linux user must have read and write access to the serial port. One way to get access is by installing the provided system/60-tkey.rules in /etc/udev/rules.d/ and running udevadm control --reload. When a TKey is plugged in, its device path (like /dev/ttyACM0) should be accessible by anyone logged in on the console (see loginctl).

Another way to get access is by becoming a member of the group that owns the serial port. On Ubuntu that group is dialout, and you can do it like this:

$ id -un
$ ls -l /dev/ttyACM0
crw-rw---- 1 root dialout 166, 0 Sep 16 08:20 /dev/ttyACM0
$ sudo usermod -a -G dialout exampleuser

For the change to take effect, you need to log out from your system and then log back in again, or run the command newgrp dialout in the terminal that you are working in.

Users on MacOS #

The client apps tries to auto-detect the serial port of the TKey. If more than one serial port is found, use the --port flag to select the appropriate one.

To find the serial ports device path manually you can do ls -l /dev/cu.*. There should be a device named /dev/cu.usbmodemN (where N is a number, for example 101). This is the device path that might need to be passed as --port when running the client app.

You can verify that the OS has found and enumerated the TKey by running:

ioreg -p IOUSB -w0 -l

There should be an entry with "USB Vendor Name" = "Tillitis".

Running a TKey Device Application #

You can use tkey-runapp from the tillitis-key1-apps repository to load a device application onto the TKey.

$ tkey-runapp apps/blink/app.bin

This should auto-detect any attached TKeys, upload, and start a tiny device app that blinks the LED in many different colors.

Many TKey client applications embed the device app they want to use in their own binary. They auto-detect the TKey and automatically loads the device app onto it. See, for instance, tkey-ssh-agent which embeds the device app signer.

Debugging #

If you run a TKey device app in the QEMU emulator there is a debug port on 0xfe00_1000 (TK1_MMIO_QEMU_DEBUG). Anything written there is printed as a character by QEMU on stdout.

qemu_putchar(), qemu_puts(), qemu_putinthex(), qemu_hexdump() and friends (see libcommon/lib.[ch] in tkey-libs) use this debug port to print out things.

libcommon is compiled with no debug output by default. Rebuild libcommon without -DNODEBUG to get the debug output.

The emulator can output some memory access (and other) logs. You can add -d guest_errors to the qemu command line to make QEMU send these to stderr.

You can also use the QEMU monitor for debugging, for example, info registers, or run QEMU with -d in_asm or -d trace:riscv_trap for tracing.


If you run QEMU with -s it provides the GDB protocol on localhost:1234 (default).

If you run with -S QEMU doesn’t start the firmware automatically. If you also specified -s and attach a GDB you can control the start entirely from GDB.

Your OS package system might include a GDB with RV32IMC support, perhaps under a name like riscv32-elf-gdb. Ubuntu, however, does not. Instead you can use GDB from

To attach GDB to the process running in QEMU do something like:

riscv32-elf-gdb firmware.elf \
-ex "set architecture riscv:rv32" \
-ex "target remote :1234" \
-ex "load"

This works with both firmware and apps. Remember to compile your programs with -g in CFLAGS to include debug symbols.

Client applications #

Typically you start a client application by connecting to the TKey:

package main

import (


func main() {
	tk := tkeyclient.New()
	err := tk.Connect("/dev/ttyACM0")
	if err != nil {
		panic("Couldn't connect to TKey!")

Then you can start using it by asking it to identify itself and make sure it’s in firmware mode:

	nameVer, err := tk.GetNameVersion()
	if err != nil {

	fmt.Printf("Firmware name0:'%s' name1:'%s' version:%d\n",
		nameVer.Name0, nameVer.Name1, nameVer.Version)

Then you can load and start an app on the TKey:

	err = tk.LoadAppFromFile("blink.bin", []byte{})
	if err != nil {

After this, the client program and the device program talk their own protocol. You are encouraged to use the same framing protocol used for the firmware while still replying negatively to a frame meant for the firmware.

See the tkeyclient Go doc for details on how to call the functions.