I/O

As well as allowing users to program real-time, parallel applications; XMOS microcontrollers allow complex I/O protocols to be implemented. This is done via the Hardware-Response ports within the microcontroller which can be programmed via the multicore extensions to C.

On XMOS devices, pins are used to interface with external components. Current XMOS devices have 64 digital I/O pins each of which can serve as either an input or output pin. The pins have an operating voltage of 3.3v. Not all products have all 64 pins accessible externally on the package - check the product datasheet to determine how many pins are available.

When referring to pins the following naming convention is use: XnDpq where n is the tile number within the device and pq is the number of the pin (e.g. X0D05).

Ports

The pins on the device are accessed via the hardware-response ports. The port is responsible for driving output on the pins or sampling input data.

Different ports have different widths: there are 1-bit, 4-bit, 8-bit, 16-bit and 32-bit ports. An n-bit port will simultaneously drive or sample n bits at once.

Ports of different widths
images/ports-crop.png

In current devices, each tile has 29 ports.

Port width

Number of ports

Port names

1

16

1A, 1B, 1C, 1D, 1E, 1F, 1G, 1H, 1I, 1J, 1K, 1L, 1M, 1N, 1O ,1P

4

6

4A, 4B, 4C, 4D, 4E ,4F

8

4

8A, 8B, 8C ,8D

16

2

16A, 16B

32

1

32A

In your code, you can access ports by declaring variables of port type. The header file xs1.h defines macros to initialize a port variable to access a specific port. For example, the following declaration creates a port variable p to access port 4A:

#include <xs1.h>

port p = XS1_PORT_4A;

Since the ports input and output all the bits at once, they should be used for inputs and outputs that work logically together. For example, a 4-bit port is not designed to drive 4 independent signals (e.g. the clock and data lines of a serial bus) - it is better to use independent 1-bit ports. However, a 4-bit port is very efficient when used to input or output from for a 4-bit wide data bus.

There is a fixed mapping between the external pins and the ports. Some pins map to multiple ports and, in general, overlapping ports should not be used together. The mapping between ports and pins can be found in the relevant devices datasheet.

Clock blocks

All ports are clocked - they are attached to a clock block in the device to control reading and writing from the port. A clock block provides a regular clock signal to the port.

Each port has a register called the shift register within it that holds either data to output or data just input depending on whether the port is in input or output mode. At each clock tick, the port samples the external pins into the shift register or drives the external pins based on the contents of the shift register. When a program “inputs” or “outputs” to a port it is actually reads or writes the shift register.

There are six clock blocks per tile. Any port can be connected to any of these six clock blocks. Each port can be set to one of two modes:

Mode

Description

Divide

The clock runs at a rate which is an integer divide of the core clock rate of the chip (e.g. a divide of 500MHz for a 500MHz part).

Externally driven

The clock runs at a rate governed by an port input.

The second mode is used to synchronize I/O to an external clock. For example, if the device was connected to an Ethernet PHY using the MII protocol, a clock block could be attached to a port connected to the RXCLK signal, which could then be used to drive the port which samples the data of the RXD signal.

By default, all ports are connected to clock block 0 which is designated the reference clock block and always runs at 100MHz.

You can access other clocks by declaring a variable of type clock. This type and initializers representing the clock on the device are declared in the xs1.h header file. For example, the following code declares a variable that allows you to access clock block 2:

#include <xs1.h>

clock clk = XS1_CLKBLK_2;

You can connect ports and clock blocks together using configuration library functions defined in xs1.h. Details of these functions are shown in the following sections.

Outputting data

A simple program that toggles a pin high and low is shown below:

#include <xs1.h>

out port p = XS1_PORT_1A;

int main(void) {
  p <: 1;
  p <: 0;
}

The declaration

out port p = XS1_PORT_1A;

declares an output port named p, which refers to the 1-bit port identifier 1A.

The statement

p <: 1;

outputs the value 1 to the port p, causing the port to drive its corresponding pin high. The port continues to drive its pin high until execution of the next statement

p <: 0;

which outputs the value 0 to the port, causing the port to drive its pin low. the figure below shows the output generated by this program.

Output waveform diagram
images/xc-output.png

The pin is initially not driven; after the first output is executed it is driven high; and after the second output is executed it is driven low. In general, when outputting to an n-bit port, the least significant n bits of the output value are driven on the pins and the rest are ignored.

All ports must be declared as global variables, and no two ports may be initialized with the same port identifier. After initialization, a port may not be assigned to. Passing a port to a function is allowed as long as the port does not appear in more than one of a function’s arguments, which would create an illegal alias.

Inputting data

The program below continuously samples the 4 pins of an input port, driving an output port high whenever the sampled value exceeds 9:

#include <xs1.h>

in  port p_in = XS1_PORT_4A;
out port p_out = XS1_PORT_1A;

int main(void) {
  int x;
  while (1) {
    p_in :> x;
    if (x > 9)
      p_out <: 1;
    else
      p_out <: 0;
  }
}

The declaration

in port p_in = XS1_PORT_4A;

declares an input port named p_in, which refers to the 4-bit port identifier 4A.

The statement

p_in :> x;

inputs the value sampled by the port p_in into the variable x. the figure below shows example input stimuli and expected output for this program.

Input waveform diagram
images/xc-input.png

The program continuously inputs from the port p_in: when 0x8 is sampled the output is driven low, when 0xA is sampled the output is driven high and when 0x2 is sampled the output is again driven low. Each input value may be sampled many times.

Waiting for a condition on an input pin

A port can trigger an event on one of two conditions on a pin: equal to or not equal to some value. The program below uses a select to count the transitions on a pin up to a certain value:

#include <xs1.h>

void wait_for_transitions(in port p, unsigned n) {
  unsigned i = 0;

  p :> x;
  while (i < n) {
    select {
      case p when pinsneq(x) :> x:
        i++;
        break;
    }
  }
}

The statement

p when pinsneq(x) :> x;

instructs the port p to wait until the value on its pins is not equal to x before sampling and providing an event to the task to react to. When the case is taken the current value is stored back in x.

As another example, a task could wait for an Ethernet preamble on a 4-bit port with the following condition:

p_eth_data when pinseq(0xD) :> void:

Here, void is used after the :> to show that the input value is not stored anywhere.

Selecting on conditional inputs is more power efficient than polling the port in software, because it allows the processor to idle, consuming less power, while the port remains active monitoring its pins.

Generating a clock signal

The program below configures a port to be clocked at a rate of 12.5MHz, outputting the corresponding clock signal with its output data:

#include <xs1.h>

out port p_out       = XS1_PORT_8A;
out port p_clock_out = XS1_PORT_1A;
clock    clk         = XS1_CLKBLK_1;

int main(void) {
  configure_clock_rate(clk, 100, 8);
  configure_out_port(p_out, clk, 0);
  configure_port_clock_output(p_clock_out, clk);
  start_clock(clk);

  for (int i=0; i<5; i++)
    p_out <: i;
}

The program configures the ports p_out and p_clock_out as illustrated in the figure below.

Port configuration diagram
images/clocks-port-counters-input.png

The declaration

clock clk = XS1_CLKBLK_1;

declares a clock named clk, which refers to the clock block identifier XS1_CLKBLK_1. Clocks are declared as global variables, with each declaration initialized with a unique resource identifier.

The statement:

configure_clock_rate(clk, 100, 8);

configures the clock clk to have a rate of 12.5MHz. The rate is specified as a fraction (100/8) because xC only supports integer arithmetic types.

The statement:

configure_out_port(p_out, clk, 0);

configures the output port p_out to be clocked by the clock clk, with an initial value of 0 driven on its pins.

The statement:

configure_port_clock_output(p_clock_out, clk)

causes the clock signal clk to be driven on the pin connected to the port p_clock_out, which a receiver can use to sample the data driven by the port p_out.

The statement:

start_clock(clk);

causes the clock block to start producing edges.

A port has an internal 16-bit counter, which is incremented on each falling edge of its clock. the figure below shows the port counter, clock signal and data driven by the port.

Waveform diagram
images/internal-clock.png

An output by the processor causes the port to drive output data on the next falling edge of its clock; the data is held by the port until another output is performed.

Using an external clock

The following program configures a port to synchronize the sampling of data to an external clock:

#include <xs1.h>

in port p_in       = XS1_PORT_8A;
in port p_clock_in = XS1_PORT_1A;
clock   clk        = XS1_CLKBLK_1;

int main(void) {
  configure_clock_src(clk, p_clock_in);
  configure_in_port(p_in, clk);

  start_clock(clk);
  for (int i=0; i<5; i++)
    p_in :> int x;
}

The program configures the ports p_in and p_clock_in as illustrated in the figure below.

Port configuration diagram
images/clocks-port-counters-wide.png

The statement:

configure_clock_src(clk, p_clock_in);

configures the 1-bit input port p_clock_in to provide edges for the clock clk. An edge occurs every time the value sampled by the port changes.

The statement :

configure_in_port(p_in, clk);

configures the input port p_in to be clocked by the clock clk.

The figure below shows the port counter, clock signal, and example input stimuli.

Waveform diagram
images/external-clock.png

An input by the processor causes the port to sample data on the next rising edge of its clock. The values input are 0x7, 0x5, 0x3, 0x1 and 0x0.

Performing I/O on specific clock edges

It is often necessary to perform an I/O operation on a port at a specific time with respect to its clock. The program below drives a pin high on the third clock period and low on the fifth:

void do_toggle(out port p) {
  int count;
  p <: 0 @ count;    // timestamped output
  while (1) {
    count += 3;
    p @ count <: 1;  // timed output
    count += 2;
    p @ count <: 0;  // timed output
  }
}

The statement

p <: 0 @ count;

performs a timestamped output, outputting the value 0 to the port p and reading into the variable count the value of the port counter when the output data is driven on the pins. The program then increments count by a value of 3 and performs a timed output statement

p @ count <: 1;

This statement causes the port to wait until its counter equals the value count+3 (advancing three clock periods) and to then drive its pin high. The last two statements delay the next output by two clock periods. the figure below shows the port counter, clock signal and data driven by the port.

Waveform diagram
images/timestamp-wide.png

The port counter is incremented on the falling edge of the clock. On intermediate edges for which no value is provided, the port continues to drive its pins with the data previously output.

Using a buffered port

The XS1 architecture provides buffers that can improve the performance of programs that perform I/O on clocked ports. A buffer can hold data output by the processor until the next falling edge of the port’s clock, allowing the processor to execute other instructions during this time. It can also store data sampled by a port until the processor is ready to input it. Using buffers, a single thread can perform I/O on multiple ports in parallel.

The following program uses a buffered port to decouple the sampling and driving of data on ports from a computation:

#include <xs1.h>

in  buffered port:8 p_in       = XS1_PORT_8A;
out buffered port:8 p_out      = XS1_PORT_8B;
in port             p_clock_in = XS1_PORT_1A;
clock               clk        = XS1_CLKBLK_1;

int main(void) {
  configure_clock_src(clk, p_clock_in);
  configure_in_port(p_in, clk);
  configure_out_port(p_out, clk, 0);
  start_clock(clk);
  while (1) {
    int x;
    p_in  :> x;
    p_out <: x + 1;
    f();
  }
}

The program configures the ports p_in, p_out and p_clock_in as illustrated in the figure below.

Port configuration diagram
images/buffer-io.png

The declaration

in buffered port:8 p_in = XS1_PORT_8A;

declares a buffered input port named p_in, which refers to the 8-bit port identifier 8A.

The statement:

configure_clock_src(clk, p_clock_in);

configures the 1-bit input port p_clock_in to provide edges for the clock clk.

The statement:

configure_in_port(p_in, clk);

configures the input port p_in to be clocked by the clock clk.

The statement:

configure_out_port(p_out, clk, 0);

configures the output port p_out to be clocked by the clock clk, with an initial value of 0 driven on its pins.

The figure below shows example input stimuli and expected output for this program. It also shows the relative waveform of the statements executed in the while loop by the processor.

Waveform diagram relative to processor execution
images/port-buffering.png

The first three values input are 0x1, 0x2 and 0x4, and in response the values output are 0x2, 0x3 and 0x5.

The figure below illustrates the buffering operation in the hardware. It shows the processor executing the while loop that outputs data to the port. The port buffers this data so that the processor can continue executing subsequent instructions while the port drives the data previously output for a complete period. On each falling edge of the clock, the port takes the next byte of data from its buffer and drives it on its pins. As long as the instructions in the loop execute in less time than the port’s clock period, a new value is driven on the pins on every clock period.

Port hardware logic
images/port-buffer-wide.png

The fact that the first input statement is executed before a rising edge means that the input buffer is not used. The processor is always ready to input the next data before it is sampled, which causes the processor to block, effectively slowing itself down to the rate of the port. If the first input occurs after the first value is sampled, however, the input buffer holds the data until the processor is ready to accept it and each output blocks until the previously output value is driven.

Timed operations represent time in the future. The waveform and comparator logic allows timed outputs to be buffered, but for timed and conditional inputs the buffer is emptied before the input is performed.

Synchronising clocked I/O on multiple ports

By configuring more than one buffered port to be clocked from the same source, a single thread can cause data to be sampled and driven in parallel on these ports. The program below first synchronizes itself to the start of a clock period, ensuring the maximum amount of time before the next falling edge, and then outputs a sequence of 8-bit character values to two 4-bit ports that are driven in parallel.

#include <xs1.h>

out buffered port p:4     = XS1_PORT_4A;
out buffered port q:4     = XS1_PORT_4B;
in           port p_clock_in = XS1_PORT_1A;
clock        clk          = XS1_CLKBLK_1;

int main(void) {

  configure_clock_src(clk, p_clock_in);
  configure_out_port(p, clk, 0);
  configure_out_port(q, clk, 0);
  start_clock(clk);

  p <: 0;  // start an output
  sync(p); // synchronize to falling edge

  for (char c='A'; c<='Z'; c++) {
    p <: (c & 0xF0) >> 4;
    q <: (c & 0x0F);
  }
}

The statement

sync(p);

causes the processor to wait until the next falling edge on which the last data in the buffer has been driven for a full period, ensuring that the next instruction is executed just after a falling edge. This ensures that the subsequent two output statements in the loop are both executed in the same clock period. the figure below shows the data output by the processor and driven by the two ports.

Processor synchronizing data on two output ports
images/synch-io.png

The recommended way to synchronize to a rising edge is to clear the buffer using the standard library function clearbuf and then make an input.

Serializing output data using a port

The XS1 architecture provides hardware support for operations that frequently arise in communication protocols. A port can be configured to perform serialization, useful if data must be communicated over ports that are only a few bits wide, and strobing, useful if data is accompanied by a separate data valid signal. Offloading these tasks to the ports frees up more processor time for executing computations.

A clocked port can serialize data, reducing the number of instructions required to perform an output. The program below outputs a 32-bit value onto 8 pins, using a clock to determine for how long each 8-bit value is driven.

#include <xs1.h>

out buffered port:32 p_out = XS1_PORT_8A;
in port p_clock_in         = XS1_PORT_1A;
clock clk                  = XS1_CLKBLK_1;

int main(void) {
  int x = 0xAA00FFFF;
  configure_clock_src(clk, p_clock_in);
  configure_out_port(p_out, clk, 0);
  start_clock(clk);

  while (1) {
    p_out <: x;
    x = f(x);
  }
}

The declaration

out buffered port:32 p_out = XS1_PORT_8A;

declares the port p_out to drive 8 pins from a 32-bit shift register. The type port:32 specifies the number of bits that are transferred in each output operation (the transfer width). The initialization XS1_PORT_8A specifies the number of physical pins connected to the port (the port width). the figure below shows the data driven by this program.

Serialized output waveform diagram
images/xc-port-serial.png

By offloading the serialization to the port, the processor has only to output once every 4 clock periods. On each falling edge of the clock, the least significant 8 bits of the shift register are driven on the pins; the shift register is then right-shifted by 8 bits.

On XS1 devices, ports used for serialization must be qualified with the keyword buffered; see Support for Ports for further explanation.

Deserializing input data using a port

A port can deserialize data, reducing the number of instructions required to input data. The program below performs a 4-to-8 bit conversion on an input port, controlled by a 25MHz clock.

#include <xs1.h>
in  buffered port:8 p_in = XS1_PORT_4A;
out port p_clock_out     = XS1_PORT_1A;
clock clk                = XS1_CLKBLK_1;

int main(void) {
  configure_clock_rate(clk, 100, 4);
  configure_in_port(p_in, clk);
  configure_port_clock_output(p_clock_out, clk);
  start_clock(clk);
  while (1) {
    int x;
    p_in :> x;
    f(x);
} }

The program declares p_in to be a 4-bit wide port with an 8-bit transfer width, meaning that two 4-bit values can be sampled by the port before they must be input by the processor. As with output, the deserializer reduces the number of instructions required to obtain the data. the figure below shows example input stimuli and the period during which the data is available in the port’s buffer for input.

Deserialized input waveform diagram
images/xc-port-deserial.png

Data is sampled on the rising edges of the clock and, when shifting, the least significant nibble is read first. The sampled data is available in the port’s buffer for input for two clock periods. The first two values input are 0x28 and 0x7A.

Inputting data accompanied by a data valid signal

A clocked port can interpret a ready-in strobe signal that determines the validity of the accompanying data. The program below inputs data from a clocked port only when a ready-in signal is high.

#include <xs1.h>

in buffered port:8 p_in = XS1_PORT_4A;
in port p_ready_in      = XS1_PORT_1A;
in port p_clock_in      = XS1_PORT_1B;
clock clk               = XS1_CLKBLK_1;

int main(void) {
  configure_clock_src(clk, p_clock_in);
  configure_in_port_strobed_slave(p_in, p_ready_in, clk);
  start_clock(clk);

  p_in :> void;
}

The statement

configure_in_port_strobed_slave(p_in, p_ready_in, clk);

configures the input port p_in to be sampled only when the value sampled on the port p_ready_in equals 1. The ready-in port must be 1-bit wide. the figure below shows example input stimuli and the data input by this program.

Input data with data valid signal
images/xc-port-strobe.png

Data is sampled on the rising edge of the clock whenever the ready-in signal is high. The port samples two 4-bit values and combines them to produce a single 8-bit value for input by the processor; the data input is 0x28. XS1 devices have a single-entry buffer, which means that data is available for input until the ready-in signal is high for the next two rising edges of the clock. Note that the port counter is incremented on every clock period, regardless of whether the strobe signal is high.

Outputting data and a data valid signal

A clocked port can generate a ready-out strobe signal whenever data is output. The program below causes an output port to drive a data valid signal whenever data is driven on a 4-bit port.

#include <xs1.h>

out buffered port:8 p_out = XS1_PORT_4B;
out port p_ready_out      = XS1_PORT_1A;
in port p_clock_in        = XS1_PORT_1B;
clock clk                 = XS1_CLKBLK_1;

int main(void) {
  configure_clock_src(clk, p_clock_in);
  configure_out_port_strobed_master(p_out, p_ready_out, clk, 0);
  start_clock(clk);

  p_out <: 0x85;
}

The statement

configure_out_port_strobed_master(p_out, p_ready_out, clk, 0);

configures the output port p_out to drive the port p_ready_out high whenever data is output. The ready-out port must be 1-bit wide. the figure below shows the data and strobe signals driven by this program.

Output data with data valid signal
images/ready-out.png

The port drives two 4-bit values over two clock periods, raising the ready-out signal during this time.

It is also possible to implement control flow algorithms that output data using a ready-in strobe signal and that input data using a ready-out strobe signal; when both signals are configured, the port implements a symmetric strobe protocol that uses a clock to handshake the communication of the data (see Port Configuration Functions).

On XS1 devices, ports used for strobing must be qualified with the keyword buffered; see Support for Ports for further explanation.

See Also