Source

LIME / doc / Patterns

Full commit
			    Preliminaries:

1. receiving data entails 3 operations: acquire_read, read and release_read
2. sending data entails 3 operations: acquire_write, write and release_write

  Some of these operations can be empty, depending on whether there
is double-buffering or in-place processing involved or how the
read/write logic is implementation. E.g., if acquire_read() returns
in-place pointer to the data, there is no need in read(). Then,
acquire()==lock() and release()==unlock()

  Also the order in which these operations are performed is
not globally fixed. I.e., if double-buffering is involved, then
release_read() can happen immediately after read(). The order in
which data is acquired for input/output and release for input/output
is also not fixed.

  What and the way how these operations are called (names, arguments,
return values, error handling) also varies wildly per platform.

3. code that is going to run in a multi-core/multi-threded concurrent
   environment can not use global variables and/or data allocated from
   static storage

  In other words, all dependencies on such variables must be either
removed (by moving them to stack) or must be encapsulated by a
LIME module and references to them must made explicit as a in/out
port dependency.

  LIME does not make any syntactic distinction between data, state
and control.

			     Basic LIME:

  A typical process F0 that has input (In) and output (Out) port with
static rates (compile-time constants) and does a calculation (C)
on In to produce Out (In and Out are distinct buffers) hence it is usually
structured like this:

void F0() {
    while (1) {
	InType *In;
	OutType *Out;
	In=acquire_read(InPort, InRate);
	read(In, InPort, InRate);
	Out=acquire_write(OutPort, OutRate);
	C(In,Out);
	write(Out, OutPort, OutRate);
	release_write(0, OutPort, OutRate);
	release_read(In, InPort, InRate);
    }
}

In LIME, F0 becomes F0.c containing a function:

#include LIME
actor C(In,Out)
	InPort InType In [Rate(InRate)];
	OutPort OutType Out [Rate(OutRate)];
{
	...
}

  This "lime" can be compiled by the slimer tool from the LIME
toolset, provided that all its compilation dependencies are also
specified. For all actors specified in given C files such as F0.c,
it ultimately produces streaming framework shell code such as F0()
depicted above and optionally compiles it for a given streaming
framework using a platform-specific compiler. At the very end, you
simply get the executable/firmware binary file that can be directly
deployed.

  Obviously, you can also have multi-dimensional ports, either
specified by the type (e.g., typedef int InType[SIZE]), or directly
in the signature:

actor C(In,Out)
	InPort InType In [Rate(InRate)][SIZE1];
	OutPort OutType Out [Rate(OutRate)][SIZE2];
{}

  Although you could specify SIZE constants directly in F0.c,
you can also put them in a separate file called "diversity":

F0.SIZE=1
F0.SIZE1=SIZE+1
F0.SIZE2=2*SIZE1

  The slimer tool will automatically apply these definitions when
generating code for F0.

  A typical situation in data-flow graphs is that you have several
actors in a pipeline. If you use application-defined data-structures
for linking the pipeline you need to share definitions across
limes.  Such data-structures have to be placed in a separate
header file and be mentioned in the Makefile as a header=<file>
dependency:

#ifndef struct_data_defined
#define struct_data_defined
struct data {
	float samples[16];
	short metric[2];
};
#endif//struct_data_defined

  Then you can simply refer to these structures from all limes by C
structure forward declaration. You can put multiple structures in
one file or every structure in a separate file. You are supposed
to also include setters/getters macros if the data-structure is
sufficiently complex, but please don't include macros/functions
that do complex stuff.

F0_0.c:
#include LIME
struct data;
actor C0(OutPort struct data out[Rate(1)]) {}

F0_1.c:
#include LIME
struct data;
actor C1(InPort struct data in[Rate(1)]) {}

  Note that all new keywords here (as well as the use of K&R syntax)
are just for your convenience, you could have specified it like
this as well:

#include LIME
typedef void actor;
actor C(const InType In [static restrict InRate],
              OutType Out [static restrict OutRate])
{
}
static int local_calc1();
int int exported_calc2();

  The "actor" keyword has to remain, because you could have other
miscellaneous functions in F0.c which are not actors but still are
exported to other modules (e.g., exported_calc2), and for which no
streaming framework shell code needs to be generated by the slimer.
It is otherwise just an annotation of "void".

  The #include LIME (which also is a valid C pre-processor statement)
which includes a back-end specific LIME.h , which contains all
the macros, functions and types defined for the back-end, as well
as automatically collected dependent application header files and
macro-definitions.

  Note that since a LIME is only using ANSI C99 with minor synactical
extensions you can always compile a lime direcly, circumventing the
use of slimer by simply issuing a command such as:

$ cc -DLIME="myinclude.h" -o F0.o -c F0.c

  where myinclude.h specifes all aforementioned dependencies
(data-structures, diversity parameters, system headers, used
syntactic extensions such as Rate or InPort).  You can also put
this in your lime instead of just #include LIME

#ifdef LIME_FRONTEND
#include LIME
#else
#include "myinclude.h"
#endif

  This way, you wouldn't even need to issue $cc -DLIME but just
$cc. You can develop your own shell code without usage of the LIME
tooling at all, but still keeping compatibility to LIME.

			     Connections

  To connect limes into a stream, the GXF/XML file needs to be
specified. It is usually called <application name>.graph.xml and
is given to the slimer tool on the command-line.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE stream PUBLIC '-//LIME/DTD stream 1.0 Transitional//EN' 'stream.dtd'>
<stream>
<edge type="fifo"><from-node id='source' port-id="buf"/>
                  <to-node id='copy' port-id="in"/></edge>
<edge type="fifo"><from-node id='copy' port-id="out"/>
                  <to-node id='sink' port-id="buf"/></edge>
</stream>

  This example assumes that source.c specifies an actor that has
output port called "buf", copy.c specifies an actor that has ports
"in" and "out", and that sink.c specifies an actor with "buf"
input port.

			   Internal State

  Stateful components retain values of local variables across
activations. Furthermore, different instances of stateful components
must have a separate version of each used local variable. Because
static storage is ruled out by the second requirement, and because they
can not be alloced on stack inside a lime (first requirement), the
application startup code has to take care of such allocation, which is
very much system/platform specific. Similarly to regular data connections,
the dependency of a component on such stateful behaviour is explicit in 
LIME:

void F1() {
	type state;
	//or: type *state = malloc();
	init (state);
	while (1) {
		C(state);
	}
	deinit(state);
	//free(state);
}

  becomes

actor C(InPort type in_state[State],
	OutPort type out_state[State])
{
	assert(in_state==out_state);
}

  or, if you prefer

actor C(const type in_state[volatile static 1],
	      type out_state[volatile static 1])
{
	assert(in_state==out_state);
}

  Because you can have several state variables (that you don't want to
pack into a struct) or you may want to have a staged initialization,
LIME forces you specify a state dependency edge, expressed in C like
this:

struct edge;
struct edge edges[] = {
    {.type="state",
     .from={.node=(actor_t)C, .port="out_state"},
     .to={.node=(actor_t)C, .port="in_state"},
    },
};

  This is essentially equivalent to GXF specification:

<stream>
  <edge type="state">
    <from-node id='C' port-id="out_state"/>
    <to-node id='C' port-id="in_state"/></edge>
</stream>
  
  Ofcourse, if you need more than 1 state variable of the same type
you can say e.g.,

actor C(InPort type state[StateRate(N)]) {}

  Note that restrict can not be used here, since in_state ==
out_state. In fact, in LIME this is what makes state (using volatile)
syntactically different from regular data (using restrict).

  Obviously, you need to provide a constructor and a destructor as well 
as connect them to the C actor:

actor init(OutPort type state[State]) {}
actor deinit(InPort type state[State]) {}

struct edge;
struct edge edges[] = {
    {.type="init_state",
     .from={.node=(actor_t)init, .port="state"},
     .to={.node=(actor_t)C, .port="in_state"},
    },
    {.type="init_state",
     .from={.node=(actor_t)C, .port="out_state"},
     .to={.node=(actor_t)deinit, .port="state"},
    },
};

  This lime specification of 3 actors in a component will result
in a guarded CSDF behaviour: the processing will include calls
to all 3 actors, with the constructors/destructors guarded by the
state-expression (do only once per instance).


		External state

  It is possible to share constructors for several different
components. In such a case, the init actor can be placed in a 
separate component, and then connected using GXF expressions.
Here, the processing of each component that is initialized this
way shall not include calls to the cosntructors/destructors, but
the framework shall ensure that for each component instance an
appropriate constructor/destructor is called before/after the
regular processing.

  The syntax of this is the same as for internal state. The
connections, however have to be specified as GXF edges, since
no single component can refer to actors as C functions.