Overview

This section describes two Pulse Programmer (PP) features that were not discussed in the pulse programs section.  These are callable sub-programs and multiple thread pulse programs.

Callable sub-programs are state sequences that are preloaded into pulse program memory so that they can be "called" from an executing pulse program.  This is useful in situations where a particular pulse sequence, such as one that describes a shaped RF pulse or a gradient, is used repeatedly.  This technique is advantageous in situations where the number of states that must be generated, and the rate at which this must occur, would otherwise be impractical.

Multiple thread pulse programs make use of that the fact that the PP can have up to four controller cards installed (the existing PPs each have two controllers).  Each controller card can run an independent pulse program, and each of the output cards can be assigned to any of the controllers.  This features is intended for applications such as imaging, or elaborate decoupling sequences, where writing the experiment as a single thread would be impractical.


Callable Sub-Programs

The basic FIFO architecture of the PP that was described in pulse programs is supplemented by the ability to store sequences of states in memory, called sub-programs, that can be called from any state in a pulse program.  The memory in the PP used to hold sub-programs is referred to as RAM to distinguish it from the FIFO memory that holds the main program.  In the hardware the same memory is actually used for both applications, the PP simply allocates memory to the two applications as necessary and manages it accordingly.  This does mean that any state memory used as RAM is subtracted from the amount of state memory available for use as FIFO.  Each PP controller has a total of 65,556 words (states) of state memory.

The PP state word contains contains a Call Address Field that is used to specify the RAM address of the sub-program that is to be called.  The state word Control Field also contains a call/return bit;  if this bit is set in a state fetched from the FIFO then the next state will be fetched from the address specified by the Call Address Field.  If the bit is set in a state fetched from RAM, the next state is fetched from the FIFO (see PP Controller Card 3.2).

In a pulse program, sub-programs are written as "C" language functions.   The function cannot return a value and, since the symbol table values are usually all available as global variables, it may well not have any arguments either.  The functions generate states in the same manner as the main body of a pulse program.

Loading Sub-Programs

The sub-programs states are loaded into RAM by calling the sub-programs function via the LOAD_SUBPROGRAM macro.  For example, the statement:

sub_address = LOAD_SUBPROGRAM( gauss() );

calls the function gauss() in such a manner as to load the states generated by gauss() into RAM.  The value returned by the macro is the RAM address at which the sub-program was loaded.  This value will later be used to call the sub-program.  The details of PP memory allocation are transparent to the user.  The macro also adds a default length state containing a set call/return bit to the end of the sequence loaded into RAM.

Sub-programs should be loaded after the default state declaration, so that the sub-program states will contain the proper default field values.  However, all sub-programs must be loaded before the first FIFO state is generated.  Attempts to do so later will generate a fatal error.

Calling Sub-Programs

The main program can call a sub-program by generating an appropriate state with a macro as follows:

CALL_SUBPROGRAM( sub_address);

This macro generates the following state:

state( Control( CAll_RETURN),  Call( sub_address) );

This is a single default length state that makes a call to the RAM address specified by the variable "sub_address".  This value was returned by LOAD_SUBPROGRAM when the sub-program was loaded into RAM.  It is not strictly necessary to generate a separate state to call a sub-program, the call can be incorporated into the previous state.  However, if the extra state does not cause any timing concerns, it is frequently convenient.

Sub-Program Example

The following simple example is a single pulse experiment that uses a sub-program to generate a Gaussian shaped RF pulse.  The "C" function gauss() generates the states that describe the shaped pulse.  The CALL_SUBPROGRAM macro is used to invoke the shaped pulse at the point in the program where a more conventional example program would use a PULSE1 macro.

#include "pulse.h"

/* Declare variables that will be used as symbols */
double scans;				/* Number of scans per experiment */
double dwell;		 		/* Dwell time*/
double size;				/* Number of points in FID */
double relax;				/*Relaxation delay */
double rec_phase;			/* Receiver phase */
double xmit_phase;			/* Xmit phase */
double gauss_a;				/* Xmit Gaussian shape */
double gauss_step_time;			/* Xmit Gauss step time */
double gauss_steps;			/* # of steps in xmit Gauss pulse */
double t90;				/* xmit pulse width */
double ad_bits;				/* A-D converter type */
double sf1;				/* Resonant frequency */
char * fname;				/* Output file name */


void pulse_program()
{
	int j, phase_reverse;
	int sub_address;
	double lo_freq;

/* Get symbol values */
	
	scans		= symbol("scans",	16,	1,	1e6);
	dwell		= symbol("dwell",	5e-6,	100e-9,	1e6);
	relax		= symbol("relax",	2,	100e-9,	1e3);
	size		= symbol("size",	1024,	8,	1e6);
	rec_phase	= symbol("rec_phase",	0,	0,	360);
	xmit_phase	= symbol("xmit_phase",	0,	0,	10000);
	gauss_a		= symbol("gauss_a",	.35,	.01, 	10);
	gauss_step_time = symbol("gauss_step_time",		100e-9,	100e-9,	1);
	gauss_steps	= symbol("gauss_steps", 100, 1, 1000);
	ad_bits		= symbol("ad_bits",	16,	12,	16);
	sf1		= symbol("sf1", 	1e6,	100e3,	499999999);
	fname		= tsymbol("outfile",	"temp.dat");


/* Set the global symbols.
 *
 * The global symbols are written into the data file and are uniform for
 * all pulse programs.  Therefore, they must be derived from the local
 * symbol table in each program.
 */
	symbols.sw =  1.0/dwell;
	symbols.size = size;
	symbols.scans =  scans;

        initialize_data_system(fname);

	assign_name(CONTROLLER_1, ESR_1, "Scans");
	assign_name(CONTROLLER_1, ESR_2, "Experiments");

/*	Calculate the required local oscillator frequency. */
	lo_freq = calculate_lo_freq( sf1, FREQ1, &phase_reverse );

/*	Setup the default state */
	default_state( Freq1(lo_freq), Time(200e-9) );

/*	Load the sub-program into PP RAM.  The address where the
	the sub-program is loaded is returned by the macro */
	sub_address = LOAD_SUBPROGRAM( gauss() );

/*	Now start loading states into the FIFO */
	AD_TYPE(ad_bits);
	FID_SIZE(size);
	INIT_EXPERIMENT;

	for (j=0; j < scans; j++) {
	    LOAD_ESR(1, j+1); 
	    CALL_SUBPROGRAM( sub_address );
	    FID(rec_phase, dwell, 100e-6, size);
	    RELAX(relax, j, scans);

	}

	printf("\nProgram Done\n");
	return;
} 


/* Function to generate the sub-program states.  The Gaussian pulse
   is piece-wise approximated in "gauss_steps" number of steps */
gauss()
{
	int i;
	double x, y;

	for (i = 0; i < gauss_steps; i++) {
		x = 2.0 * ((double) i / (gauss_steps - 1)) - 1.0;
		y = 1.0 / exp((x * x) / (gauss_a * gauss_a));
	    	PULSE1(y, xmit_phase, gauss_step_time); 
	}
}


/*****************************************************************/
/*	At a minimum, the 'stubs' for pulse_program_2/3/4 must exist here
 */

void pulse_program_2()
{
	return;
}


/*****************************************************************/

void pulse_program_3()
{
	return;
}


/*****************************************************************/

void pulse_program_4()
{
	return;
}

Multiple Thread Pulse-Programs

A pulse programmer can contain up to four controller cards, each of which is capable of running a separate pulse program sequence or thread.  Each of the 16 possible output cards in the PP can be assigned to the control of any of the four possible controller cards.  Thus, for example, transmitter 1 might be assigned to controller card 1 while transmitter 2 might be assigned to controller card 2.

There are a small number of  state fields that are permanently assigned to controller card 1 and cannot be reallocated.  Specifically, these are:

This means that the receiver and digitizer are always controlled by the main thread of the program, which executes on controller card 1.  The other controller cards can control anything that is connected to an output card: transmitters, synthesizers, gradient drivers, etc.

Device Allocation

The function allocate_to_controller() is used to allocate one or more output cards to a particular controller card.  The first argument is the controller card number, the second is the number of cards to be assigned, and the remainder is a variable length list of the output cards to be assigned.  For example:

allocate_to_controller(2, 1, XMIT2_CARD);
allocate_to_controller(3, 2, XMIT3_CARD, XMIT4_CARD);

These calls assign the output card for transmitter 2 to controller card 2, and the output cards for transmitters 3 and 4 to controller card 3.

All unallocated output cards always default to controller card 1.

Writing Additional Threads

The main body of the pulse program always corresponds to the thread that executes on controller card 1.  Additional threads are written as functions with the names: pulse_program_2(), pulse_program_3(), and pulse_program_4().

In fact, these functions must be included in every pulse program, to avoid a linker error, but in single thread pulse programs they are empty functions.

The main thread always takes care of the usual pulse program tasks: reading the symbol table, writing values to the global symbols, initializing the data system, and assigning names to the Experiment State Registers.  Additionally, the main thread allocates the output cards.  Thus each additional thread contains only a default state declaration and the actual state generation code.

Forking Additional Threads

The function call_controller(n) performs the task of forking and additional thread.  In Unix, a fork operation copies a process (an executing program) so that there are now two copies in memory.  One copy continues to execute starting with the next state immediately following the fork.  The other copy immediately branches to some other part of the program.

The statement:

call_controller(2)

causes the program to fork, and the new copy of the process immediately begins executing the function pulse_program_2().

The main thread makes the call (or calls) to call_controller() immediately before it begins actual state generation.

Synchronization

All four controllers run off the same clock, so their timing is inherently synchronous.  However, it is essential that there be a way of making certain that controllers start executing a sequence at the same point in time.

To this end, the control field of each controller card contains three synchronization bits: cm sync 1,   cm sync 2 and, cm sync 3.  Each of these bits can be used to request synchronization with one of the three other controller cards.  The numbering is a little tricky, since it always relative to controller card on which the bit resides.  The following table shows which bit on each controller card corresponds to each neighboring card:

Controller Card # cm sync 1 cm sync 2 cm sync 3
1 2 3 4
2 1 3 4
3 1 2 4
4 1 2 3

Each cm sync n bit requests synchronization with the controller card specified by n.  If a controller card has cm sync n is set in the current state, then it will wait at the end of the current state until it achieves synchronization with the controller card specified by n.  Two actions occur during a state where cm sync n is set.  First, the controller card sends a signal to the controller card specified by n indicating a request for synchronization.  Second, the controller card waits at the end of the current state until it receives a corresponding signal from the controller card specified by n. 

For example, if controller card 1 expresses a state with the cm sync 1 bit set, then it will wait until controller 2 expresses a state with the cm sync 1 bit set.  Whichever processor arrives at the state with cm sync n set first will be forced to wait for the other processor.  When both controller cards have arrived at the synchronization state, and the state has expired on both controller cards, then both controller cards will proceed to the next state simultaneously.   Note that more than one cm sync n bit can be set simultaneously in a state, thereby forcing synchronization with more than one controller card.  Thus all four controller cards can be forced into synchronization. 

In the example below, the macro WAIT_FOR_CONTROLLER() is used to create a state that requests synchronization.

Halting Multiple Controllers

If any of the four controller cards make a transition from running to stopped, the other three controllers are automatically halted.  Thus, if any of the threads reaches the end of its program, all the controllers will halt at the same time.

In the example presented below, the thread on controller card 2 simply loops forever.  Thus the point at which the experiment terminates is determined by the main thread.

Issuing a stop or abort command from the command line or the Manger always halts all the controller cards.

Multiple Thread Example

The following is a simple example of pulse program that utilizes two threads.  The main thread controls the receiver and digitizer as well as transmitter 1, while controller 2 controls transmitter 2.

#include "pulse.h"

/* Declare variables that will be used as symbols */
double scans;				/* Number of scans per experiment */
double dwell;		 		/* Dwell time*/
double dead;				/* Dead time */
double size;				/* Number of points in FID */
double relax;				/*Relaxation delay */
double rec_phase;			/* Receiver phase */
double xmit1_phase;			/* Xmit phase */
double xmit1_amp;			/* Xmit amplitude */
double xmit1_t90;			/* xmit pulse width */
double xmit2_phase;			/* Xmit phase */
double xmit2_amp;			/* Xmit amplitude */
double ad_bits;				/* A-D converter type */
double sf1;				/* Resonant frequency */
double sf2;				/* Resonant frequency */
char * fname;				/* Output file name */


void pulse_program()
{
	int j, phase_reverse1, phase_reverse2;
	double lo_freq1, lo_freq2;
	
/* Get symbol values */
	
	scans		= symbol("scans",	16,	1,	1e6);
	dwell		= symbol("dwell",	5e-6,	100e-9,	1e6);
	dead		= symbol("dead",	5e-6,	100e-9,	1e6);
	relax		= symbol("relax",	2,	100e-9,	1e3);
	size		= symbol("size",	1024,	8,	1e6);
	rec_phase	= symbol("rec_phase",	0,	0,	360);
	xmit1_phase	= symbol("xmit1_phase",	0,	0,	360);
	xmit1_amp	= symbol("xmit1_amp",	1,	-1, 	1);
	xmit1_t90	= symbol("xmit1_t90",	10e-6,	200e-9,	10e-3);
	xmit2_phase	= symbol("xmit2_phase",	0,	0,	360);
	xmit2_amp	= symbol("xmit2_amp",	1,	-1, 	1);
	ad_bits		= symbol("ad_bits",	16,	12,	16);
	sf1		= symbol("sf1", 	500e6,	100e3,	800e6);
	sf2		= symbol("sf2", 	125e6,	100e3,	500e6);
	fname		= tsymbol("outfile",	"temp.dat");


/* Set the global symbols.
 *
 * The global symbols are written into the data file and are uniform for
 * all pulse programs.  Therefore, they must be derived from the local
 * symbol table in each program.
 */
	symbols.sw =  1.0/dwell;
	symbols.size = size;
	symbols.scans =  scans;

        initialize_data_system(fname);

	assign_name(CONTROLLER_1, ESR_1, "Scans");
	
/*	Calculate the required local oscillator frequencies. */
	lo_freq1 = calculate_lo_freq( sf1, FREQ1, &phase_reverse1 );
	lo_freq2 = calculate_lo_freq( sf2, FREQ2, &phase_reverse2 );
	
/*	Allocate the transmitter 2 to controller 2 */
	allocate_to_controller(2, 1, XMIT2_CARD);

/* If you wish to fork another pulse_program, this is the place to do it. */

 	call_controller(2);	       /* forks and calls pulse_program_2() */ 
/*	call_controller(3);		* forks and calls pulse_program_3() *
 *	call_controller(4);		* forks and calls pulse_program_4() *
*/ 
	default_state( Freq1(lo_freq1), Freq2(lo_freq2), Time(200e-9) );

	AD_TYPE(ad_bits);
	FID_SIZE(size);
	INIT_EXPERIMENT;


	for (j=0; j < scans; j++) {
	    LOAD_ESR(1, j+1); 
	    WAIT_FOR_CONTROLLER(1); /* Wait for controller 2 */
	    PULSE1(xmit1_amp, xmit2_phase, 0xf, xmit1_t90); 
	    FID(rec_phase, dwell, dead, size);
	    RELAX(relax, j, scans);

	}

	printf("\nProgram 1 Done\n");
	return;
} 


/*****************************************************************/
/*	At a minimum, the 'stubs' for pulse_program_2/3/4 must exist here
 */

void pulse_program_2()
{
	double pulse_length;

	pulse_length = xmit1_t90 + dead + dwell * size;

	default_state( Time(200e-9) ); 

	while (1){
	    WAIT_FOR_CONTROLLER(1); /* Wait for controller 1 */
	    PULSE2(xmit2_amp, xmit2_phase, 0xf, pulse_length);
	    state( Time(relax), Control(FIFO_SYNC) );
	} 
	printf("\nProgram 2 Done\n");
	return;
}


/*****************************************************************/

void pulse_program_3()
{
	return;
}


/*****************************************************************/

void pulse_program_4()
{
	return;
}