Getting started

To program an XMOS device you can used C, C++ or xC (C with multicore extensions). In your software project you can mix all three types of source file. To use the xC, just use the .xc file extension for your source file instead of .c. The xTIMEcomposer compiler will automatically detect this file extension and enable the C extensions for that file.

The xTIMEcomposer tools provide fully standards compliant C and C++ compilation (for example, files with a .c will be compiled as standard C). Applications can contain code written in a mixture of xC and C - you can call functions written in xC from standard C and vice-versa.

Hello World

Let’s start with the traditional “hello world” program:

#include <stdio.h>

int main() {
  printf("Hello World\n");
  return 0;
}

This program is exactly the same in C and xC. Since XMOS devices are embedded devices, you will only see print when the debug adapter is connected. In that case prints will be directed through the debug adapter and appear on the xTIMEcomposer console when running a program.

When you compile projects, the tools keep track of what resources you have used. For example, if you compile this program with the -report option you get the following output:

Constraint check for "tile[0]" (node "0", tile 0):
  Cores available:            8,   used:          1 .  OKAY
  Timers available:          10,   used:          1 .  OKAY
  Chanends available:        32,   used:          0 .  OKAY
  Memory available:       65536,   used:       1176 .  OKAY
    (Stack: 336, Code: 720, Data: 120)
Constraints checks PASSED.

You can see that the compiler tells you exactly how much memory is used (including stack usage). The xC language extensions have been designed so that you can always get this information even when running tasks in parallel that use the same memory space.

Going parallel

One of the major features of the extensions to C provided by xC is the ability to run code in parallel. The following program runs three threads of execution, all of which print out a “Hello World” message:

#include <stdio.h>

void hw(unsigned n) {
  printf("Hello world from task number %u\n", n);
}

int main() {
  par {
    hw(0);
    hw(1);
    hw(3);
  }
  return 0;
}

The key is the par construct which runs several tasks in parallel. This is described in more detail later.

Accessing the I/O hardware: A flashing LED

This code implements a simple flashing led:

#include <platform.h>
#include <xs1.h>
#include <timer.h>

port p = XS1_PORT_1A;

int main() {
  while (1) {
     p <: 0;
     delay_milliseconds(200);
     p <: 1;
     delay_milliseconds(200);
  }
  return 0;
}

This example uses the xC port type to declare a port p set to the port 1A (the details of ports are described later in I/O). The <: operator outputs a value to a port. The example also uses the delay_milliseconds function which is part of the libraries supplied with the tools (in the header timer.h).

Integration with C and C++

Within an XMOS project you can have C, C++ and xC files. They will all compile together into the same binary. The build system compiles the file based on its extension. For example, if a project had the following directory structure:

app_my_project/
  Makefile
  src/
    foo.xc
    bar.c

The foo.xc would be compiled with the multicore extensions as xC and bar.c would be compiled as normal C. The two object files would then be linked together into the same binary.

xC provides extensions to C, but does not support some C features. The currently unsupported features are:

  • goto statements
  • bitfields
  • function pointers
  • C99-style designated initializers

Functions written in C can be called from xC and vice versa. Since xC is an extension to C, functions from C can be prototyped in xC (apart from prototypes that use from the unsupported language features above).

For example, if a file called foo.c contains the following function:

int f() {
  printf("This is a C function\n");
}

and a file called bar.xc contains this function:

extern "c" {
  extern int f();  // This function is defined in C
}

void g() {
  par {
    f();
    f();
  }
}

Then compiling and linking the files will work with the xC function g calling the C function f.

When calling from C to xC, some new types in xC are not available in C. However you can use C types that are converted into xC types at link time.

For example, a chanend can be passed to a C function that takes an int or unsigned int parameter. The xccompat.h header included with the xTIMEcomposer suite contains useful macros and typedefs that expand to the correct type in each language.

The multicore programming model

Parallel tasks of execution

On xCORE devices, programs are composed of multiple tasks running in parallel. The concurrent tasks manage their own state and resources and interact by performing transactions with each other. A sample task decomposition of a system is shown in the figure below.

Example task communication diagram
images/gpio_task2-crop-wide.png

Tasks are defined in the same way as any C function e.g.:

void task1(int x, int a[20]) { ...  }

There are no special keywords and any function can be a task. Tasks can take any arguments but generally have a no return value. Usually tasks do not return at all and consist of a never ending loop:

void task1(...args...) {
  ... initialization ...
  while (1) {
    ... main loop ...
  }
}

Tasks are scheduled to run in the main function of the program using the par construct:

int main(void) {
  par {
   task1(...args...);
   task2(...args...);
   task3(...args...);
  }
}

Here, you can pass arguments into the tasks to configure them. Each task will run in parallel using the available hardware of the xCORE device.

The compiler automatically checks how many hardware resources are required and will error if too many are used. The compiler automatically allocates the required amount of stack and data memory to each task and reports back, telling you the total amount of memory used.

Explicit communication

Tasks always share information using explicit connections between the tasks. The connections allow synchronized transactions to occur between the tasks where data can be shared.

Each task runs independently on their own data. At some point one of the tasks will initiate a transaction between the tasks. This task will wait until the other task enters a state where it is ready to accept that transaction. At this point the two tasks will exchange some data and then both tasks will carry on. the figure below shows the progression of the transaction.

Two tasks performing a transaction
images/task_transaction-crop-wide.png

Shared memory access

Tasks never share data by accessing common data (e.g. global variables) so other synchronization methods (locks, mutexes, semaphores) are not used.

Two tasks can share common data by making a request to an intermediate task that owns the data. This keeps a clean separation of concerns. the figure below shows this method of sharing data.

Using an intermediate task to share memory
images/shared-crop-wide.png

This approach makes any possible race conditions explicit in the task communication. In terms of efficiency, the compiler implements this method as efficiently as directly sharing memory.

Asynchronous communication

One aspect of the communication described here is that it is synchronous - one task waits for the other to be ready before the transaction can continue.

Sometimes asynchronous communication is required - a task wants to send some data to another without blocking. The task can then continue with other work. The sent data is buffered until the destination task is ready to capture the data.

A notification between two tasks
images/notification-crop.png

There are two methods for asynchronous communication in xC. The first is to use a built-in communication method called notifications as shown in A notification between two tasks. These allow a task to raise a flag between two tasks indicating a wish to communicate. Once the notification is raised the task can continue until the other tasks initiates a communication back. This is analogous to an hardware interrupt line back to the bus master in a hardware communication bus.

The second method to enable asynchronous communication is to insert a third task between two communicating tasks to act as a buffer as shown in the figure below. The intermediate task is very responsive since it only handles buffering. One task can make a “push” transaction to put data in the buffer and then carry on. Asynchronously the other task can perform a “pull” transaction to extract the data from the buffer.

Two tasks with a shared memory FIFO task in between
images/fifo-crop.png

This is the same as using a shared memory fifo between two tasks.

Event based programming

Tasks can react to events using the select construct, which pauses the tasks and waits for an event to occur. A select can wait for several events and handles the event that occurs first.

The syntax of a select statement is similar to a C switch statement:

select {
  case event1 :
    // handle the event
    ...
    break;
  case event2 :
    // handle the event
    ...
    break;
}

This statement will pause until either of the events occur and then execute the code within the relevant case. Although the select waits on several events, only one of the events is handled by the statement when an event occurs.

Often when programming other microcontrollers your program will react to external events via interrupts. A function will be registered against an interrupt that occurs when a certain event happens (e.g. a timeout or an external I/O event). This function provides an interrupt service routine (ISR) that handles the event. Interrupts are not used to handle events in xC, the select construct provides all that is needed. The equivalent of an ISR is a separate task that executes a select. The advantages of the XMOS approach are:

  • Response time to events can be drastically improved (in conjunction with the multi-core xCORE architecture)
  • Reasoning about worst case execution time (WCET) is easier since code cannot be interrupted during its execution.

The full syntax for specifying cases depends on the type of event and is described later. Events can be caused by other tasks initiating a transaction (Communication), timer events (Timing) or external I/O events (I/O).

The underlying hardware model

To understand programming in xC, it is useful to understand the underlying hardware that is being targeted. This section provides a description of how the hardware is organized. The description gives a high level overview; for specific details on a particular XMOS device consult the device datasheet.

Underlying hardware system
images/underlying_hardware-crop-wide.png

Underlying hardware system shows the layout of the hardware that xC programs execute on. The system is composed of one or more tiles consisting of several cores.

Tiles

A system is split into tiles. A tile consists of a collection of hardware resources. Each tile has:

  • A number of cores that can execute code
  • A reference clock
  • Some memory
  • Access to an I/O sub-system

One of the key concepts of this model is that only code executing on a tile has direct access to the resources of that tile.

On XMOS XS1 devices:

  • Each tile has 64KB of memory
  • The memory on each tile has no cache.
  • The memory on each tile has no data bus contention (all peripherals are implemented via the I/O sub-system which does not use the memory bus; there is no DMA for peripherals).

The last two properties ensure that a load or store from memory always takes one or two instruction cycles on each core. This makes worst case execution time analysis very accurate for code that accesses memory.

Tasks on different tiles do not share memory but can communicate via inter-core communication.

In xC, the underlying hardware platform being targeted provides a set of names that can be used to refer to tiles in the system. These are declared in the platform.h header file. The standard is to provide an array named tile. So the tiles of the system can be referred to as tile[0], tile[1], etc.

Cores

Within a tile, a core (or logical core) provides an independent unit to execute code on. All the cores on a tile run in parallel.

Cores may run a different speed depending on the configuration of the device, but are guaranteed a minimum MIPS.

In xC, you can refer to a core via the core array associated with a particular tile. For example, tile[0].core[1] or tile[1].core[7].

Timers

Each tile has a reference clock running at a specified rate. On XMOS XS-1 devices the clock will run at 100MHz. Associated with this clock is a counter. This counter is 32-bits wide on XMOS XS-1 devices.

In xC, the timer type is used to reference this clock. You can declare variables of this type, and operations on these variables can read the current time and wait on a particular timeout to occur.

Communication fabric

Between the cores there is a communication fabric (the xCONNECT fabric on XMOS devices). This communication fabric allows any core on any tile to access any other core on any other tile. This means that any software task can perform a transaction with another task whether the other task is on the same core or not (or even on the same tile or not).

I/O

Each tile has its own I/O subsystem that consists of ports and clock blocks. Full details of these are given in I/O. In xC, ports and clock blocks are represented by the xC types port and clock respectively.

See Also