RSS YouTube LinkedIn Twitter XCore IRC

Search

Projects Tutorials Forum

Personal tools

Using Channels in XC

From XCore Exchange

Jump to: navigation, search

Contents

Overview of channels

Today we are going to learn the basics about channels in the XC programming language.

When dealing with concurrency common problems arrive with sharing data, how does one guarantee that two concurrent processes don't try to make changes to the same data at the same time. Thus if this kind of data sharing can be bypassed one of the major pitfalls of concurrent programming can be avoided. Channels represent a way for different concurrent processes to send messages to each other on a controlled fashion. Because messages are passed by value the data sharing problem does not exist. We know by simply using channels we are communicating data safely between processes.

Background

The method of passing messages by channels in XC is based on the formal language Communicating Sequential Processes (CSP). CSP was highly influential in the design of the occam programming language, which was the programming language for the INMOS Transputer.

Basic Channel Usage

Processes in XC running on Xmos are represented by threads either on the same core or on different cores, they are a universal mechanism to safely exchange data between threads. What is more the XC language has event based language constructs to enable efficient use of channels within program flow. Just like XS1 cores allow program flow based on input pin states and events, XC also enables you to alter the flow of your program based on arrival of messages or data over channels. If this were not so each thread would have to block its flow permanently until channel messages arrived. The key to the is feature of XC is the select construct when combined with an infinite loop such as While(1):

while(1){
select
  {
  case wait for x to happen:
    process(x);
    break;
  case wait for y to happen :
    process(y);
    break;
  default :
    if neither X or Y happened do this by default
    do_default();
    break;
  }
}

When this select runs, 3 things can happen :

  1. X happens and process(x) is called
  2. Y happens and process(y) is called
  3. Neither X or Y happens and thus do_default() is called

So select means select the program flow from these possible cases depending on external events, select waits concurrently (because of the controlling while loop) for one of them to happen on a first come first served basis. Such events may be results of timers reaching values, port inputs or even channel inputs and these can be combined to create the event flow required for your solution.

But there are some basic rules channels, ports and timers may only appear in any one case, if your case depends on the state of input from a channel, timer or port and has multiple possibilities or states, these have to be catered for in the flow of that case. This can often be achieved by adding a switch statement or if/else conditions in the flow for example here is the select statement married with a switch in order to create a light switch controller:

#include <platform.h>
#include <stdio.h>
#define DELAY 1000000000
#define ON 1
#define OFF 0

port lamp = XS1_PORT_1K;

void lamp_control_mod(chanend c, port lamp){
unsigned int time = 0;
timer t;
while(1){
select
  {
  case c :> msg :
    switch (msg) 
      {
       case ON:
        lamp <: 1;
        t :> time ;
        time += DELAY;
        break;
       case OFF :
        lamp <: 0;
        break;
      }
  case when timeafter(time) : // lets save energy, someone left the light on
    lamp <: 0;
    break;
  }
}

main(void){
  chan c;
  par {
    test_lc_mod(c);
    lamp_control_mod(c,lamp);
  }
}

The control of the lamp's state is initially determined by messages send over channel c from a remote concurrent processes (thread) which could be on the same core or another core. The function listens to that channel for its instructions and activates or deactivates the lamp accordingly. In addition because we are environmentally friendly bunch at XCore we decided to add this neat energy saving light timer to switch the lamp off after a predetermined delay. If the light control had other responsibilities to tend to when light switching or energy saving was being actioned, we could add a default section to do such housekeeping. here an exercise to explore selects further :

1) Our lamp controller moonlights as a an energy meter, as do all of our appliance controllers, thus when it is not busy doing its job it estimates power used. In this case the lamp is a small low powered 10 Watt LED type. add the code required so that when the controller is queried over channel c about its energy used since it started it prints out the accumulated value using a printf() statement.

Channels Enable Modularisation

We are assuming the test_lc_mod(c) understands the operation of lamp_control_mod and sends the relevant ON's and OFF's. By this we are asserting a contract between the 2 modules test_lc_mod and lamp_control_mod. This is an important feature of contract programming (or contracting to an interface or documented command set). It may see trivial to design a lamp controller in this way but it actually has advantages. lets replace test_lc_mod with toggle_mod and close the channel loop:

void toggle_switch_mod(chanend c,port button){
  int state = OFF;
  while(1){
    select
     {
     case button :> void :
       state = !state;
       c <: state;
       break;
     }
  }
}

main(void){
  chan c;
  par {
    toggle_switch__mod(c,button);
    lamp_control_mod(c,lamp);
  }
}

An because we are programming by contract and using channels the toggle switch could be on any core or thread. What's more we can interchange other standard switch modules that speak "lamp_control", perhaps we want a factory like control switch with an on and off button, well that's simple and we don't have to touch our lamp_controller_mod:

void factory_switch_mod(chanend c,port onButton, port offButton){
  while(1){
    select
     {
     case onButton :> void :
       c <: ON;
       break;
     case offButton :> void :
       c <: OFF;
       break;
     }
  }
}


main(void){
  chan c;
  par {
    factory_switch__mod(c,onButton,offButton);
    lamp_control(c,lamp);
  }
}

Thus we can separate concerns of our design in a modular fashion not just to distribute work across teams but also to allow reuse of modules. The channels enable these modules to operate not only on different threads but also different cores or even chips connected via Xmos Links thus.

main(void){
  chan c;
  par {
    on stdcore[0] : factory_switch__mod(c,onButton,offButton);
    on stdcore[1] : lamp_control(c,lamp);
  }
}

2) We have been asked to create a new kind of automated switch module, that uses a buffered active low phototransistor and emitter pair to detect a beam of light across the doorway so that the lamp can be turned on automatically when someone walks into the room. Develop a module that speaks "lamp" to the following prototype:

auto_switch_mod(chanend c, port phtoDetector, port photoEmitter){..}

3) After adding the automated light switch we have been asked to improve our lamp controller by adding some intelligence to further save energy, in testing we found the lamp being switched on even in clear daylight, which of course wastes energy. Adapt the lamp controller to receive a second port in its prototype to include the output of a buffered photo transistor. When the photo transistor detects light its output is taken low, when no light is detected it is taken high. remember that the photo transistor is sensitive enough to detect the light from the lamp.

Dynamic Channel Usage

Ok if you have got this far and completed the exercises you will have a good understanding of channel basics well done, you are now ready to get some more value from these essential features of XC. So having worked so hard to get here I think you have earned enough brownie points to play a game. But as usual we are going to play a game with channels thrown in for good measure, so lets play table tennis.

#include <platform.h>
#include <stdio.h>
#define END -1
#define PING 0
#define PONG 1
#define PINGS 5

void ping(chanend c){
  int reply;
  c <: PING;
  for(int i = 0; i < PINGS; i++){
  select
    {
    case c :> reply :
      switch(reply)
	{
	case PONG :
	  printf("Ping received Pong\n");
	  c <: PING;
	  break;
	default :
	  printf("Ping received unknown reply\n");
	  break;
	}
      break;
    }
  }
  c :> reply;
  printf("Goodbye cruel Pong\n");
  c <: END;
}

void pong(chanend c){
  int msg;
  while(1){
  select 
    {
    case c :> msg :
      switch(msg)
	{
	case PING :
	  printf("Pong received Ping\n");
	  c <: PONG;
	  break;
	case END :
	  return;
	default :
	  printf("Pong received unknown message\n");
	  break;
      } 
      break;
    }
  }
}

main(void){
  chan c;
  par {
    pong(c);
    ping(c);
  }
  printf("End of all Pings\n");
  
  return 0;
}

In this game of table tennis we have two players Ping and Pong, Ping starts by pinging Pong and Pong responds by ponging Ping is that clear ;-)

Both ping and pong use printc() to acknowledge that they have received ball (in the form of a message) from the other player. When you run this you should get the following:

Pong received Ping
Ping received Pong
Pong received Ping
Ping received Pong
Pong received Ping
Ping received Pong
Pong received Ping
Ping received Pong
Pong received Ping
Ping received Pong
Pong received Ping
Goodbye cruel Pong
End of all Pings

What this shows is the use of channels in a dynamic fashion. In our previous example channels were used in the same direction; the switch always sent over the channel, the lamp always received over the channel. Well it doesn't have to work in this manner channels can be used bi-directionally, but we must take care about using them in both directions, we must keep track of which thread is sending and which thread is receiving, otherwise things go very wrong! Channels require symmetric input and output, that is for every input there must be an input, if this isn't observed it will crash the program. You can see the effects by removing the "c :> reply;" line recompiling and re-running, notice the resource errors you get when trying to run the program with this channel fault. There are a number of other more subtle gotchas that can happen with this dynamic channel usage try moving the first "c <PING" to after the for statement and see what happens. See if you can guess what is going on here.

4) Because our xmos chip will be used in a very battery powered situation, power consumption is paramount and other devices may have a greater priority for power than lighting. We have therefore been instructed that the switches must be power conscious and may be allocated quotas, if those quotas are reached the lamp switching should be disabled until the system is restarted to allow power to be routed to more critical devices. Thus Armed with our new dynamic channel techniques rewrite the lamp_control_module to return the power usage (over the same channel) after receiving a channel message READ (an integer value different from ON or OFF say 3). When the lamp_control module receives this it should respond to the switch_mod with the reading of power consumptionsince last READ query, rather than using printf(). Also modify the switch modules to disable the lamp usage when the consumption reaches a passed in quota. The READ requested should be generated by a timer running in the switch modules and should be polled at least once every second. Once quota is met and lamp disabled the switch module needs to printf() its status and then complete its thread along with the lamp control thread. Remember in this case the switch is in charge, it is the supervisor so things must be orchestrated with that in mind.