XC (programming language)


In computers,[] XC is a programming language for real-time embedded parallel processors, targeted at the XMOS XCore processor architecture.
XC is an imperative language, based on the features for parallelism and communication in occam, and the syntax and sequential features of C. It provides primitive features that correspond to the various architectural resources provided, namely: channel ends, locks, ports and timers.
In combination with XCore processors, XC is used to build embedded systems with levels of I/O, real-time performance and computational ability usually attributed to field-programmable gate arrays or application-specific integrated circuit devices.

Introduction

Architectural model

An XC program executes on a collection of XCore tiles. Each tile contains one or more processing cores and resources that can be shared between the cores, including I/O and memory. All tiles are connected by a communication network that allows any tile to communicate with any other tile. A given target system is specified during compilation and the compiler ensures that a sufficient number of tiles, cores and resources are available to execute the program being compiled.

Features of XC

The following sections outline the key features of XC.

Parallelism

Statements in XC are executed in sequence, so that in the execution of:
f; g;
the function is only executed once the execution of the function has completed. A set of statements can be made to execute in parallel using a statement, so that
par
causes and to be executed simultaneously. The execution of parallel statement only completes when each of the component statements have completed. The component statements are called tasks in XC.
Because the sharing of variables can lead to race conditions and non-deterministic behaviour, XC enforces parallel disjointness. Disjointness means that a variable that is changed in one component statement of a may not be used in any other statement.
Parallel statements can be written with a replicator, in a similar fashion to a loop, so that many similar instances of a task can be created without having to write each one separately, so that the statement:

par
f;

is equivalent to:

par

The tasks in a parallel statement are executed by creating threads on the processor executing the statement. Tasks can be placed on different tiles by using a prefix. In following example:

par

the task is placed on any available core of tile 0 and instances of the task placed on cores 0, 1, 2 and 3 of tile 1. Task placement is restricted to the function of an XC program. Conceptually, this is because when an XC program is compiled, it is divided up at its top level, into separately executable programs for each tile.

Communication

Parallel tasks are able to communicate with each other using interfaces or channels.

Interfaces

An interface specifies a set of transaction types, where each type is defined as a function with parameter and return types. When two tasks are connected via an interface, one operates as a server and the other as a client. The client is able to initiate a transaction with the corresponding server, with syntax similar to a conventional function call. This interaction can be seen as a remote procedure call. For example, in the parallel statement:

interface I ;
interface I i;
par

the client initiates the transaction, with the parameter value 42, from the interface. The server waits on the transaction and responds when the client initiates it by printing out a message with the received parameter value. Transaction functions can also be used for two-way communication by using reference parameters, allowing data to be transferred from a client to a server, and then back again.
Interfaces can only be used by two tasks; they do not allow multiple clients to be connected to one server. The types of either end of an interface connection of type T are server interface T and client interface T. Therefore, when interface types are passed as parameters, the type of connection must also be specified, for example:

interface T i;
void s
void c
par

Transaction functions in an interface restrict servers to reacting only in response to client requests, but in some circumstances it is useful for a server to be able to trigger a response from the client. This can be achieved by annotating a function in the interface with no parameters and a void return type, with. The client waits on the notification transaction in a [|select statement] for the server to initiate it. A corresponding function can be annotated with, which is called by the slave to clear the notification. In the following simple example:

interface I ;
interface I i1, i2;
par

when client 2 initiates the transaction function, the server notifies client 1 via the transaction function. Client 1 waits for the server notification, and then initiates when it is received.
So that it is easier to connect many clients to one server, interfaces can also be declared as arrays. A server can select over an interface array using an index variable.
Interfaces can also be extended, so that basic client interfaces can be augmented with new functionality. In particular, client interface extensions can invoke transaction functions in the base interface to provide a layer of additional complexity.

Channels

Communication channels provide a more primitive way of communicating between tasks than interfaces. A channel connects two tasks and allows them to send and receive data, using the in and out operators respectively. A communication only occurs when an input is matched with an output, and because either side waits for the other to be ready, this also causes the tasks to synchronise. In the following:

chan c;
int x;
par

the value 42 is sent over the channel and assigned to the variable.

Streaming channels

A streaming channel does not require each input and matching output to synchronise, so communication can occur asynchronously.

Event handling

The statement waits for events to occur. It is similar to the alternation process in occam. Each component of a select is an event, such as an interface transaction, channel input or port input, and an associated action. When a select is executed, it waits until the first event is enabled and then executes that event's action. In the following example:

select

the select statement merges data from and channels on to an channel.
A select case can be guarded, so that the case is only selected if the guard expression is true at the same time the event is enabled. For example, with a guard:

case enable => left :> v:
out <: v;
break;

the left-hand channel of the above example can only input data when the variable is true.
The selection of events is arbitrary, but event priority can be enforced with the attribute for selects. The effect is that higher-priority events occur earlier in the body of the statement.
To aid in creating reusable components and libraries, select functions can be used to abstract multiple cases of a select into a single unit. The following select function encapsulates the cases of the above select statement:

select merge

so that the select statement can be written:

select

Timing

Every tile has a reference clock that can be accessed via timer variables. Performing an output operation on a timer reads the current time in cycles. For example, to calculate the elapsed execution time of a function :

timer t;
uint32_t start, end;
t :> start;
f;
t :> end;
printf;

where CYCLES_PER_SEC is defined to be the number of cycles per second.
Timers can also be used in select statements to trigger events. For example, the select statement:

timer t;
uint32_t time;
...
select

waits for the timer to exceed the value of before reacting to it. The value of is discarded with the syntax, but it can be assigned to a variable with the syntax.

IO

Variables of the type port provide access to IO pins on an XCore device in XC. Ports can have power-of-two widths, allowing the same number of bits to be input or output every cycle. The same channel input and output operators and respectively are used for this.
The following program continuously reads the value on one port and outputs it on another:

  1. include
in port p = XS1_PORT_1A;
out port q = XS1_PORT_1B;
int main

The declaration of ports must have global scope and each port must specify whether it is inputting or outputting, and is assigned a fixed value to specify which pins it corresponds to. These values are defined as macros in a system header file.
By default, ports are driven at the tile's reference clock. However, clock block resources can be used to provide different clock signals, either by dividing the reference clock, or based on an external signal. Ports can be further configured to use buffering and to synchronise with other ports. This configuration is performed using library functions.

Port events

Ports can generate events, which can be handled in select statements. For example, the statement:

select

uses the predicate to wait for the value on the port to equal before triggering the response to print a notification.

Port timing

To be able to control when outputs on a port occur with respect to the port's clock, outputs can be timestamped or timed. The timestamped statement:

p <: v @ count;

causes the value to be output on the port and for to be set to the value of the port's counter. The timed output statement:

p @ count <: v;

causes the port to wait until its counter reaches the value of before the value is output.

Multiplexing tasks onto cores

By default, each task maps to one core on a tile. Because the number of cores is limited, XC provides two ways to map multiple tasks to cores and better exploit the available cores.
Server tasks that are composed of a never-ending loop containing a select statement can be marked as combinable with the attribute. This allows the compiler to combine two or more combinable tasks to run on the same core, by merging the cases into a single select.
Tasks of the same form as combinable ones, except that each case of the select handles a transaction function, can be marked with the attribute. This allows the compiler to convert the select cases into local function calls.

Memory access

XC has two models of memory access: safe and unsafe. Safe access is the default in which checks are made to ensure that:
These guarantees are achieved through a combination of a different kinds of pointers, static checking during compilation and run-time checks.
Unsafe pointers provide the same behaviour as pointers in C. An unsafe pointer must be declared with the keyword, and they can only be used within regions.

Additional features

Nullable types

Resource types such as interfaces, channel ends, ports and clocks must always have a valid value. The nullable qualifier allows these types to have no value, which is specified with the symbol. For example, a nullable channel is declared with:

chan ?c;

Nullable resource types can also be used to implement optional resource arguments for functions. The builtin function can be used to check if a resource is null.

Multiple returns

In XC, functions can return multiple values. For example, the following function implements the swap operation:

swap

The function swap is called with a multiple assignment:

= swap;

Example programs

Multicore Hello World


  1. include
  2. include
void hello
int main

Historical influences

The design of XC was heavily influenced by the occam programming language, which first introduced channel communication, alternation, ports and timers. Occam was developed by David May and built on the Communicating Sequential Processes formalism, a process algebra developed by Tony Hoare.