The experiment is described as a sequence of states. A state describes the condition of all electrical outputs for a specified period of time. The description of each state contains many fields. For example, the timing field specifies the duration of the state and each group of outputs controlled by the PP has a corresponding output field. Furthermore, there are special control fields that specify special actions to be taken during or at the end of the state.
The pulse programmer is based on a FIFO ( "First In First Out") state memory which is 65536 words in length. Items written into the FIFO are stored until such time as they are read out. Writing an item increases the number of items stored in the FIFO by one. Likewise, reading an item decrements the number of items stored in the FIFO. Items always read out in the same order as they are written in, this is what is meant by "First In First Out". The FIFO memory is 65536 words in length and each word in the FIFO memory holds one complete state.
The pulse program is an executable that runs on the host computer (the SGI workstation) and generates all the states that will occur in the experiment, in the order which they will occur, and writes them into the FIFO. If the FIFO becomes full, the program simply waits until space is available before continuing. The pulse programmer removes state words from the FIFO sequentially and expresses each state for the duration specified by that states timing field.
The pulse programmer also has several features that extend this basic model of operation. These include strobe outputs, repeatable states, and callable subprograms. See Advanced Pulse Programs and NMR Console Design Report 4.2.2 and 4.2.3 for a detailed explanation.
A pulse program is a specially written "C" program that utilizes a library of macros and functions to specify the exact sequence of events in an experiment. Such a program performs all the tasks associated with running an experiment and collecting the resulting data. Although each different kind of experiment requires a unique pulse program, all such programs share common structural elements.
The pulse program consists of a user written "C" language function, always named "pulse_progam()", which is linked to a library of support routines. A variety of "C" preprocessor macros are provided to greatly simplify writing the pulse program. The purpose of the pulse program is to generate a sequential list of states that describe an experiment. When the pulse program is executed it loads the list of states into the memory of the pulse programmer and commences execution of the experimentIn a pulse program, the actual work of generating the states that describe the experiment is done by calls to the function state(). Each call to this function generates one complete state to be loaded into the pulse programmer. A pulse program makes as many call to state() as is necessary to generate all the states in the experiment. The pulse program makes the calls to state() in the same sequence that the states will be expressed at the outputs of the PP.
The state() function accepts a variable length argument list; as many arguments may be included as are required to fully define the state. The arguments are actually specified by using predefined macros. Each possible field in a PP state has an associated macro that generates the arguments for state() that are required to specify that field. These macros are referred to as primitive macros or just primitives. For example, the primitive macro Time(secs) generates arguments that will cause the timing field to be loaded with a value corresponding to the period of time "secs". Various higher level macros implement one or more calls to state() to perform common functions. The are referred to as program macros or just macros.
An important aspect of pulse programs is their use of the symbol table. Since it is frequently necessary to change controlling parameters in a pulse program while setting up an experiment (pulse lengths for example) it would be very inconvenient to re-compile and re-link the pulse program each such time a change is made. Instead, the pulse program reads parameter values from an external symbol table each time it is executed. The values in the external symbol table can be changed by the user by using either the define command or the symbol editor graphical user interface.
The user written pulse program is actually in the form of a function named pulse_program(). When the user's pulse program is compiled and linked, it is linked to a main() function that is supplied from the system library. This allows the system to take of housekeeping tasks both before and after the user written code is executed.
This is a simple example of a functional pulse program. Each section of the program contains a comment with a hyperlink to a an explanation.
/* * npulsepc.c * * March 1996 * James Gladden * * A simple pulse program to perform a one pulse phase cycling experiment. * Data is accumulated until the specified number of scans is completed. * */ /* Include the pulse program header file */ #include "pulse.h" /* Declare the global variables that will be used to hold values extracted from the symbol table */ 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 xmit_amp; /* Xmit amplitude */ double t90; /* xmit pulse width */ double ad_bits; /* A-D converter type */ double sf1; /* Resonant frequency */ double o1; /* Offset frequency */ double filter; /* Filter Frequency */ double gain; /* Receiver gain */ char * fname; /* Output file name */ /* Start of the pulse_program()function */ void pulse_program() { /* Declare the local variables. */ int j, phase_reversal_flag; double lo_freq, phase_cycle; /* Get/Set symbol table 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); xmit_amp = symbol("xmit_amp", 1, -1, 1); t90 = symbol("t90", 10e-6, 200e-9, 100); ad_bits = symbol("ad_bits", 16, 12, 16); sf1 = symbol("sf1", 500e6, 10e6, 1100e6); o1 = symbol("o1", 0, -10e6, 10e6); filter = symbol("filter", 4e3, 400, 1.5e6); gain = symbol("gain", 50, 0, 90); fname = tsymbol("outfile", "temp.dat"); /* Set global symbol structure values. */ symbols.sw = 1.0/dwell; symbols.size = size; symbols.scans = scans; /* Assign a display name to the ESR used to keep track of the scan count.*/ assign_name(CONTROLLER_1, ESR_1, "Scans"); /* Initialize the data system */ initialize_data_system(fname); /* Calculate the local oscillator frequency */
lo_freq1 = calculate_lo_freq( sf1 + o1, FREQ1, &phase_reversal_flag ); /* Define the default state. */
/* The default state defines the condition of all outputs that are not explicitly set in a given state. */ default_state( Freq1(lo_freq), Rec_filter(filter), Rec_gain(gain), Time(200e-9) ); /* Start of the pulse program state generation. */
/* Insert an initial delay to allow "slow" responding controls, such as receiver gain to settle to the value specified by the default state. */ state( Time(10e-3) ); /* Send messages to the DAP specifying which AD converters to use and the number of points in the FID. */ AD_TYPE(ad_bits); FID_SIZE(size); PHASE_ROTATION_DIRECTION(phase_reversal_flag); /* Clear the FID buffer */ CLEAR_FID_BUFFER; /* Enter the scan loop */ for (j=0; j < scans; j++) { /* Calculate the phase shift for this scan */ phase_cycle = (j % 4) * 90.0; /* Load the Experiment State Register with the scan number */ LOAD_ESR(1, j+1); /* Pulse the transmitter */ PULSE1(xmit_amp, xmit_phase + phase_cycle, 0xf, t90);
/* Collect the FID */ FID(rec_phase + phase_cycle, dwell, 100e-6, size); /* Start the relaxation delay, update display, and transfer the data */
RELAX(relax, j, scans); } printf("\nProgram Done\n"); return; } At a minimum, the 'stubs' for pulse_program_2/3/4 must exist in this file. void pulse_program_2() { return; } void pulse_program_3() { return; } void pulse_program_4() { return; }
This section contains explanations of each part of the simple example pulse program in the preceding section. Each of the explanations in this section can be reached from the following table of contents, or from the links embedded in the example pulse program.
#include "pulse.h"
This file contains the definitions for the primitive macros and program macros that are used in constructing pulse programs. It also includes function prototypes for support functions that are called by pulse programs as well as declarations of global variables and structures. Additionally, this file includes the standard system header files "stdio.h" and "math.h" as a convenience.
In the "C" language all variables declared outside the body of a function are global.
The entry point of a user written pulse program is always a function named pulse_program(). When the user's pulse program is compiled and linked, it is linked to a main() function that is supplied from the system library. This allows the system to take care of housekeeping tasks both before and after the user written code is executed.
Any variables that are local to the pulse_program() function (that is, they are only use within that function) are usually declared immediately after the function header. Be aware that in the "C" language such local variables are not automatically initialized to zero, so if the value of such a variable is used before the first assignment to it the results are unpredictable.
t90 = symbol("t90", 5e-6, 1e-6, 1e-3);
The character string argument is the name of the symbol as it appears in the external symbol table and the three numeric arguments are the default value for the symbol, the minimum allowable value, and the maximum allowable value, in that order.
A similar function, tsymbol(), is used for symbols which hold a character string value rather than a numeric value.
These functions operate in two different ways, depending on how the pulse program is executed. In the normal case, the function looks up the value of the symbol with the name specified by the character string argument, and returns that value. If the symbol is not found in the table a fatal error results. However, if the pulse program is executed with the "-d" command line argument, the argument requests that the program build a default symbol table, rather than actually run the experiment. In this case, each call to symbol() or tsymbol() creates an entry in the new symbol table; the entry is created using the default, minimum, and maximum values specified as function arguments.
The structure "symbols" is used to record parameters in the output data file. This structure is always written to the output data file. A complete list of the structure elements can be found in Global symbol structure. The pulse program should explicitly assign values to structure elements as appropriate to the needs of the program. Examples of such assignments are:
symbols.size = size; /* FID size */
symbols.sw = 1/dwell; /* Spectral width */
assign_ESR_name(CONTROLLER_1, ESR_1, "Scans");
assign_ESR_name(CONTROLLER_1, ESR_2, "Experiments");
In this case the names "Scans" and "Experiments" are assigned to ESR registers 1 and 2 respectively in controller 1. Thus the names and contents of these two registers will be displayed.
initialize_data_system(fname)
The argument "fname" is a pointer to a character string which contains the data file name.
If the pulse program is executed with the "-d" command line flag, indicating that it should build a new symbol file rather than actually load states into the pulse programmer, this function will cause a program exit rather than performing its normal function. It is thus important that this call appear in the pulse_program() function after all calls to symbol() and tsymbol() have been made, but before any calls to state() are made.
The function calculate_lo_freq() returns the local oscillator frequency (LO) necessary to produce the desired transmitter output and/or receiver observe frequency.
lo_freq1 = calculate_lo_freq( sf1 + o1, FREQ1, &phase_reversal_flag );
The first argument specifies the desired operating frequency. The second argument is a predefined constant that specifies which of the four possible transmitter channels is being used (FREQ1, FREQ2, FREQ3, or FREQ4).
The third argument is passed as a pointer so that the function can return a value to this argument. This returned value is only relevant if the frequency being calculated will be used as the receiver LO. If the function selects an LO frequency which is above the requested operating frequency, then the receiver first mixer will subtract the input RF from the LO to generate the IF. This will result in a reversal of the apparent sign of frequencies at the receiver's detector output, which will ultimately result in a left-to-right swap of peaks in the Fourier transform result. In this case the returned value will be one. If no reversal occurs the returned value will be zero. This value can later be passed to the PHASE_ROTATION_DIRECTION() macro to send a message to the DAP requesting that the frequency inversion be corrected if necessary. See DAP Messages for an example.
default_state( Freq1(f1_lo), Rec_filter(filter), Time(200e-9) );
This call specifies that, in all states where no other value is specified, synthesizer 1 shall be set to a frequency of "f1_lo", the receiver anti-aliasing filter shall be set to a frequency of "filter", and the duration shall be 200 nanoseconds. Outputs that are not assigned a default value are zero doing all states that do not explicitly assign a value. Note that if a value is explicitly assigned in a state the assigned value overrides the default value for the duration of the state.
Here is a an example:
state( Xmit1_amp(amp), Xmit1_phase(phase), Xmit1_gate(gates), Time(period) );
This state produces a pulse on transmitter 1 by specifying duration, amplitude, phase, and the condition of the transmitter gates. Four different primitives are used to specify the fields in the state.
Pulse program writing can sometimes be simplified by the use of program macros (referred to simply as "macros") which define certain frequently used states or sequences of states. For example, the previous state can be generated by using the PULSE1 macro.
Repetitive sequences of states are usually generated by placing state() functions call inside a loop such as a "for" loop. See The Scan Loop for an illustration of this technique.
The first state in a program is usually quite simple:
state( Time(10e-3) );
This state specifies only a duration; 10 milliseconds. All other fields assume their default values or zero if they were not defined in the default state. The purpose of this state is to allow "slow" spectrometer controls (such as the relays that control receiver gain) to settle to there default values before the actual experiment begins.
After generating the initial delay a pulse program typically generates one or more states whose sole function is to send messages to the Data Acquisition Processor (DAP). The example program generates four such states, each of which sends a different message to the DAP. Since these are all commonly used messages, there are predefined macros for generating the states:
AD_TYPE(ad_bits);
This macro generates a state which tells the DAP which A-D converters should be used (12 bit or 16 bit);
FID_SIZE(size);
This macro generates a state which tells
the DAP how many points will be collected in the FID. The DAP needs this
information so that it will know how many data points should ultimately be
transferred to the work station.
PHASE_ROTATION_DIRECTION(phase_reversal_flag);
This message tells the DAP to
reverse the rotating frame of the data if the receiver is being operated in a
mode that results in frequency inversion.
CLEAR_FID_BUFFER;
This message tells the DAP to clear the data buffer by writing zeros to all locations.
Most pulse programs contain a loop that generates most of the states in the experiment. In the case of this example, each iteration of the loop generates the states in one "scan" of the experiment. More complex programs often have nested loops.
This program uses the "C" language "for" statement to control the loop:
for (j=0; j < scans; j++) {
}
This statement starts the loop with the variable "j" equal to zero, and continues to loop as long as "j" is less that the value of "scans". The variable "j" is incremented at the end of each loop repetition. The loop executes all the statements that are between the braces {} that delimit the body of the loop.
This pulse program has already assigned the name "Scans" to Experiment State Register 1 (see Assign Names to ESRs). This statement:
LOAD_ESR(1, j+1);
assigns the value of the scan loop counter (variable "j") to ESR 1 at the beginning of each repetition of the loop. This will automatically update the status displays produced by the Manager and FID Display programs.
It is also makes the value available to the conditional stop/pause mechanism so that the user can request that the experiment halt or pause at the completion of a particular scan.
This example uses a macro to generate a state during which transmitter 1 is turned on with a specified phase and amplitude for a specified duration.
PULSE1(xmit_amp, xmit_phase + phase_cycle, 0xf, t90);
This macro actually generates a call to state() that looks looks like this:
state( Xmit1_amp(xmit_amp), Xmit1_phase(xmit_phase + phase_cycle), Xmit1_gate(1 | ((int) 0xf << 1 )), Time(t90) );
The argument "0xf" is a hexadecimal constant specifying the state of the gate outputs on the back of the transmitter. In this case it specifies that all four gate outputs should be turned on for the duration of the state.
This program uses a macro to generate the states that perform the actions necessary to digitize the FID and store results in the DAP memory. The macro invocation looks like this:
FID(rec_phase + phase_cycle, dwell, 100e-6, size);
The arguments specify the receiver phase, the dwell time between points, the dead time, and the number of points to be digitized, in that order.
Most pulse programs contain a relaxation delay after the FID has been digitized. This is also the point in the experiment where the PP should send a message to the DAP requesting that the host workstation be notified that new FID data is available for display. And, if this is the last scan in the experiment, a message should also be sent requesting that the host write the FID data to the data file. All of these functions are performed by this macro:
RELAX(relax, j, scans);
The first argument is the length of the delay, the second is the current scan number, and the third is the intended total number of scans. The second and third arguments are necessary so that the macro can determine which invocation is the last relaxation delay in an experiment.
The pulse programmer can have up to four controller modules installed, and
each module can run a separate pulse program. The various output fields
can be allocated to different controllers so that each pulse program can control
a different set of outputs. For more information about such features see Advanced
Pulse Programs.
Primitives are pre-processor macros that are used to specify arguments for the state() function. Here is an example:
state( Xmit1_phase(90.0), Xmit1_amp(1.0), Xmit1_gates(15), Time(5e-6) );
This example generates a state in which transmitter one is turned on with a phase of 90 degrees, an amplitude of 1.0, and four gate control bits asserted. The state lasts five microseconds. Each primitive is a macro that generates the arguments to state() necessary to specify the contents of one field in the pulse programmer memory word. There is no restriction on the order in which the primitives appear in the state() argument list.
By convention the names of primitives start with a capital letter and the remaining letters are all lower case. Some primitives, such as Control, require pre-defined constants as arguments. The DAP Software and PP Controller Card documents specify values for these constants and also define symbolic names for them. These names are also available as macros which can be used instead of the constants. By convention, the corresponding macro names are all upper case with any spaces or punctuation replaced by "_" characters. If an argument is bit encoded, and more than one bit needs to be asserted, the values should be "ored" together and the result used as the argument.
The current list of primitives is described below:
Ad_phase(phase)
Each macro contains one or more calls to state() with one or more primitives passed in each call. Pulse programs will typically use macros frequently and the user is free to define any additional macros required for new functionality.
A representative macro from "macros.h" is:
#define PULSE1(amp, phase, gates, period) \ state( Xmit1_amp(amp), Xmit1_phase(phase),\ Xmit1_gate(1 | ((int) gates << 1 )), Time(period) )
Each time a line such as
PULSE1(amp, phase, gates, 100e-6);
is encountered in the user's code, state() is called with the primitives defined by the macro.
An example of a multi-state macro is FID:
#define FID(phase, dwell, dead, size) \ state(Rec_gate(1), Time(dead)); \ state(Rec_gate(1), Ad_disposition(SUM_SAMPLE), Ad_pointer(PRE_RESET), \ Ad_phase(phase), Control(AD_STROBE), Time(dwell)); \ state(Rec_gate(1), Ad_disposition(SUM_SAMPLE), Ad_pointer(PRE_INCR), \ Ad_phase(phase), Control(AD_STROBE | REPEAT), Time(dwell), \ Repeat(size-1)); \ if (ad_type == 16) { \ state(Rec_gate(1), Ad_disposition(DISCARD), Ad_pointer(NOOP), \ Control(AD_STROBE), Time(dwell)); \ } else if (ad_type == 12) { \ state(Rec_gate(1), Ad_disposition(DISCARD), Ad_pointer(NOOP), \ Control(AD_STROBE | REPEAT), Time(dwell), Repeat(4)); \ }
Note that each call to "state()" is a complete statement and ends with a semicolon. However, the last call to state in a macro definition does not end with a semicolon so that the macro can be invoked in the pulse program code as if it were a typical C statement. That is, the line that the macro name appears in ends with a semicolon so that, when the contents of the macro are substituted for the macro name, the proper number of semicolons exist.
The current list of program macros is described below:
Additionally, if "scan_num" is equal to "num_scans" minus 1 (i.e., this is the last scan, assuming that for the first scan "scan_num" equaled zero), the state contains a field that sends a TRANSMIT_BUFFER message to the DAP (tells the DAP it is time to write the data to the output file). It also has the CONDITIONAL_ACTION_2 bit set in the control field (indicates that this state is the end of an experiment).
The symbol table is accessed from the pulse program by using the symbol() and tsymbol() functions:
t90 = symbol("t90", 3e-6, 1e-6, 100e-6); amp = symbol("amplitude", 1.0, -1.0, 1.0); phase = symbol("phase", 0.0, 0.0, 360.0); fname = tsymbol("outfile", "temp.dat");
When the pulse program is called without any command line options, the symbol() function searches the symbol table file for a symbol of the name specified by the character string argument("t90", for example) and returns the value as a double float. The tsymbol() routine is similar except it returns a pointer to a character string.
If the pulse program is invoked with the -d option the calls to symbol() and tsymbol() do not read the symbol table file; instead they write a new file. In this case the three numeric arguments to symbol() are the default value for the symbol, the minimum acceptable value and the maximum acceptable value. The -d argument is used to build a new symbol table using the defaults specified in the symbol() and tsymbol() functions. The "symbols.sym" file is an ASCII file that can be altered with a text editor, although this is not the intended mode of operation. The file created by the above example would be as follows:
*n t90 3.0e-06 1.0e-6 100.0e-6 *n amplitude 1.0 -1.0 1.0 *n phase 0.0 0.0 360.0 *t outfile temp.dat
The tokens *n,*t designate the type of record, "*n" for a numeric record used by symbol() and "*t" for a text record used by tsymbol(). The order of the records in the file is not significant.
symbols.sw = sw; symbols.sf1 = sf1 + o1; symbols.size = size; symbols.scans = scans;The entire global symbol structure is initialized to zero and thus those elements that are not assigned values by the pulse program will have a value of zero.
The global symbols are found in a structure defined in "file_utils.h":
typedef struct global_symbols_tag { double sw; double sf1; double sf2; double sf3; double size; double scans; double experiment; } GLOBAL_SYMBOLS;
Each SCSI command issued to the Pulse Programmer generates a line of output except "load_FIFO()" and "load_RAM()" which print out the contents of the data buffer sent to the PP by these two commands. The data buffer is printed out one data packet at a time as in the following example generated with the -g option:
On LUN: 0 (load_FIFO) with 40 bytes of data. (below this line) <--- address ---> <--- data ---> Packet Load Cat Card Offset (hex) (decimal) 0 3 1 1 1000 = 4096 1 3 2 0 0150 = 336 2 1 0 0 0 0000 = 0 End of state 3 2 1 0 0001 = 1 4 1 0 0 0 0000 = 0 End of state 5 2 1 0 0002 = 2 6 1 0 0 0 0000 = 0 End of state 7 2 1 0 0000 = 0 8 2 1 6 0042 = 66 9 1 0 0 0 0000 = 0 End of stateWhen the -g3 option is used, the same information is generated but each data packet is prefaced by the name of the macro which created that data packet. -g3
For further information on the contents of the data packet please see the document PP Software 1.1.1.11.
Jonathan Callahan and James Gladden