Before attempting to build this tool, you must install the Linux_GPIB package first. To build this tool, create a directory to work in and unpack the tgz file into the working directory. As a normal user in the working directory, type make and the code should build with no errors or warnings. Then copy the executable file HP3634_ctl into a convenient directory like ~/bin.

The E3634 power supply defaults to address 8 and the tool is configured to default to that address. If you have changed the address for your E3634, you can supply the new address on the command line via the -D option:
> HP3634_ctl -D new_address.
If you want to change the default address, edit the default address value  DEFAULT_HP3634A_ADDR value in HP3634A.h.

All of the commands supported by the tool can be viewed by entering the tool with no command line parameters and the tool will respond with the list:
> ./HP3634_ctl
Usage: HP3634_ctl [-d dev_addr] CMD1 [arg1] [CMD2]...
 where CMDn [argn] is one of:

You can also see the parameters for a single command with the -H option:
> HP3634_ctl -H SELECT_CAL_VOLTAGE
and the tool will respond with the command line options for that command:
  SELECT_CAL_VOLTAGE MIN/MID/MAX

Instrument commands may be typed on the command line. Multiple instrument commands can be on the command line and they are executed in the order of appearance. Commands are not case sensitive, so get_cur_limit works as well as GET_CUR_LIMIT.
> HP3634_ctl SET_CUR_LIMIT 1.5 SET_OUTPUT_VOLTAGE 3.3 SET_OUTPUT ON
will set the current limit value to 1.5 Amps, the output voltage to 3.3 Volts, and turn the output on.

A text file with commands in it can also be used with the READ_CMDFILE command. If you created a text file cfg_3p3 containing:
SET_VOLTAGE_RANGE LOW
SET_CUR_LIMIT 1.5
SET_OUTPUT_VOLTAGE 3.3
and then issued the following command:
> HP3634_ctl READ_CMDFILE cfg_3p3 SET_OUTPUT ON
the supply will go to the low output voltage range, set the current limit to 1.5 Amps, set the output voltage to 3.3V, and turn the output on.

Syntax for the command file is the same as the syntax for the command line with the exception that you cannot change the GPIB address in the command file.

Some error checking is done in the tool but it is possible to send a command that the E3634 will not accept and it will generate an error. Errors can be retrieved from the E3634 with the GET_ERRORS command. This will also clear any errors on the E3634.

It may be necessary to insert delays between some commands. The SLEEP_MS command will insert the specified number of milliseconds of delay:
> HP3634_ctl SET_OUTPUT_VOLTAGE 10.0 SLEEP_MS 100 SET_OUTPUT_VOLTAGE 5.0
will set the output voltage to 10 Volts, wait 100mS, then set the output voltage to 5 Volts. Inserting short delays between instrument commands will often fix strange behavior in talking to instruments. The HP E3634 power supply is running an old 6809 processor at 1MHz, so giving it time to get through it's tasks is good.

Beyond the HELP, READ_CMDFILE, and SLEEP_MS commands, the rest are just access to individual commands on the E3634 power supply. Most of the E3634 commands are implemented. The calibration commands are at the bottom of the list, and are the least tested of this tool. I went through one calibration sequence on my supply with them and did not note any problems.

The Code

This program is written in 3 layers: User Interface, Low Level Command handling, and I/O. I did this so that it would be easy to replace the command line User Interface with a GUI if I decided to later. It would also allow me to use the Low Level Command handling code to build other automated tools using the E3634 power supply.

User Interface code lives in the files: HP3634_ctl.c parse_and_exec.c and parse_and_exec.h.
HP3634_ctl.c contains the main () function that parses the - options and calls the function to open the GPIB interface and calls the parse_and_exec function to handle the instrument commands.

Parse_and_exec contains the parser to examine the input text and call the user interface functions to handle each command. The user interface functions are also in parse_and_exec.c. Structure definitions used by the parser and user interface functions are in parse_and_exec.h. The prototypes for the few user interface functions needed in the top level main file are also in parse_and_exec.h. The same parser code is used to process the instrument commands from the command line or from command files. Text_utils.c and .h are my standard text handling library and are mostly used in the user interface stuff but are used in a couple of places in the Low Level Command handling code.

Low Level Command handling is done in HP3634A.c and .h. These functions convert the parameters to the text used in the SCPI command set used by the E3634. They also convert the responses to query commands back into parameters. Most of the limit values used in the User Interface code input checking live in the top of the HP3634A.h file. Prototypes for the Low Level Command handling functions also live in the .h file. The sleep_ms.c and .h files don't interact with the E3634, but are low level commands to the operating system. A lot of the interaction with the Linux_GPIB library is done in the Low Level Command handling code.

Initializing, opening and closing the GPIB interface is done in the files gpib_stuff.c and .h. The gpib_query_IDN command is one of the common GPIB commands that most modern instruments support, so it is in here too. The hex dump function is useful for debugging communications problems.

The E3634 power supply also supports control via a serial port. I chose to use the GPIB bus because a number of other instruments on my bench are using it and it seems consistent this way.

Hewlett Packard/Agilent made another power supply, the E3633. It supports the same command set, but with different voltage and current capabilities. This tool could be modified to work with the E3633 by changing the limit values set in the HP3634A.h file.

5/31/2023 Added an ifdef to the HP3634A.h file to support the I3633 power supply.

Command Parsers

Through the years, I have found that simple command line tools suit my work style well. They are easy to write and easy to modify. As a result, I have written quite a few of them.

All of the tools accept the C command line argument setup coming into main(int argc, char *argv[ ] ). The argc variable is the number of separate arguments on the command line, including the name of the command. Argv is an array of pointers to each word on the command line, where a word is a group of characters separated from others by a space character. Input data for command foo would look like:

  foo  word1 word2 word3
  argc = 4
  argv[0] -> foo
  argv[1] -> word1
  argv[2] -> word2
  argv[3] -> word3

This is a pretty convenient format to work with throughout the program. It is flexible to deal with different length commands and does not require many variables to pass around. It is easy to create a function that will read an input file of space separated words and break it up into this format as well.

Basic configuration options like hardware addresses and communications setup are handled with -char  (char being a single character) options. This information is needed to open ports and such before you can talk to your target systems. Normally, these values don't need to be changed later in the program. I normally deal with configuration options separately from things like the instrument commands used in this system. Because there are not generally a lot of the configuration options, the nested IF/ELSE structure is adequate for this. Handling the instrument commands is more complex.

I have tried several designs for parsing the commands, and I think that the one used for this tool is the best that I have tried. Before going into the design of the parser used for this tool, I will talk about a couple of other designs that I have tried and the issues with them.

Nested IF/ELSE Parser
This implementation is the simplest conceptually.

int current_argument;
int return_value;

current_argument = 1;
return_value = OK;

while ((current_argument < argc) && (return_value == OK))
{
  if (str_match (argv[current_argument], "CMD_A", IGNORE_CASE)
  {
    // do whatever CMD_A needs. Assume CMD_A does not need any arguments
    return_value = do_cmd_a ();
    current_argument += 1;
  }
  else
  if (str_match (argv[current_argument], "CMD_B", IGNORE_CASE)
  {
    // do whatever CMD_B needs. Assume CMD_B needs 1 argument
    return_value = do_cmd_b (argv[current_argument+1]);

    current_argument += 2;
  }
  else
  {
    printf ("Unknown command: %s \n", argv[current_argument]);
    return_value = ERROR;
  }
}

The function str_match (char *s1, char *s2, int case_sensitive) is one of my library functions in text_utils.c. It returns true if s1 matches s2 including length. Case sensitivity is controlled by the case_sensitive argument.

The biggest problem with the IF/ELSE parser is scalability. If you need to do much unique stuff per command, or you have a lot of choices of commands, the parser function gets huge. Back in college programing courses, we were told that functions should be shorter than 100 lines. I don't completely subscribe to that, but 2500 line functions are too big, even for me.

When using a program like this, the help option is really nice to remind you of the spelling of commands and the order of arguments. The IF/ELSE structure does not have all of the command strings in a form that is easy to print for the help function.

Indentation gets very messy if the individual commands get complex.

Parallel List and ifdefs Parser

This implementation uses an initialized array of the command words and a parallel set of #defines to link the command word to the command work code. The #defines could be replaced by an enumerated type, but that is not really any improvement.

#define NUM_CMD_WORDS (3)

const char *cmd_word_list[NUM_CMD_WORDS] =
{
  "CMD_A",     // 0
  "CMD_B",     // 1
  "CMD_C"      // 2
};

#define PSR_CMD_A  (0)
#define PSR_CMD_B  (1)
#define PSR_CMD_C  (2)


int return_value = OK;
int cmd_index = 0;
int current_argument = 1;

while ((current_argument < argc) && (return_value = OK))
{
  while ((cmd_index < NUM_CMD_WORDS) &&
      !str_match (argv[current_argument], cmd_word_list[cmd_index],IGNORE_CASE))
    cmd_index += 1;

  if (cmd_index >= NUM_CMD_WORDS)
  {
    printf ("Unknown command: %s \n", argv[current_argument])
    return_value = ERROR;
  }
  else
  {
    switch (cmd_index)
    {
      case PSR_CMD_A:
      {
         return_value = do_cmd_a ();
         current_argument += 1;
         break;
      }

      case PSR_CMD_B:
      {
         return_value = do_cmd_b (argv[current_argument+1]);
         current_argument += 2;
         break;
      }

      case PSR_CMD_C:
      {
         return_value = do_cmd_c (argv[current_argument+1]);
         current_argument += 2;
         break;
      }

      default:
      {
        printf ("Internal Error, cmd_index: %1d\n", cmd_index);
        return_value = ERROR;
      }
    }
  }
}

 This implementation only improves the scalability a little bit. The switch statement has the same problem of growing rapidly as complexity or number of commands grows.

It is easy to print a list of the commands for a help option with this structure.

Indentation problems are similar to the IF/ELSE parser.

Keeping the cmd_word_list array aligned with the PSR_ ifdefs is a new problem with the parser.

Initialized Structure Parser

This implementation breaks up the huge if/else or switch statement into a series of individual functions, reducing the scalability problems a lot. It provides a single place to read all the command names for a help function, and even offers a place to attach extra information on the help function. It requires all of the individual command functions to have the same arguments.

Using the argc, argv arrangement for passing input information to the individual command functions minimizes the issue of all command functions having the same arguments. An additional structure is added to allow passing information between the command functions if needed. This type of tool mostly outputs information to the screen for the user so passing information between command functions is not as useful as you might expect.

The current_argument variable is passed into the command function as a pointer so that each command function can increment the value according to the number of parameters needed for that command.

Line numbers are passed into the command functions for error message display. In previous tools like this, when the command files get long, isolating the instance that caused an error is painful. Having the line number of the file helps a lot in isolating problems. The only reason that they are passed as a pointer to an int is so that the read_command_file() function can be re-entrant and support multiple levels of command file.

A typedef defines the command function arguments list. An initialized array containing the command name, the help string, and a pointer to the command function itself makes up the main parser data structure. The initialized array is searched for command names and the functions are called through the corresponding pointers in the array.

======  From parse_and_exec.h ==================================
// This is the inter-function message block definition.
typedef struct
{
  int placeholder1;
} IFMB_t;


//  Command executer function typedef.
typedef int (*CMD_EXEC_FUNC_t) (int dev_desc, int n_args, char *argvec[], 
                                int cur_arg, int *lnum, IFMB_t *if_msg_block);


// element from parse tree.
typedef struct cmd_element
{
   char *cmd_name;
   CMD_EXEC_FUNC_t *cef;
   const char *helpstring;
}CMD_ELEMENT_t;


======  From parse_and_exec.c ==================================
// prototypes for the command executer functions
int cef_cmd_a (int dev_desc, int n_args, char *argvec[], int *cur_arg, 
               int *lnum, IFMB_t *if_msg_block);

int cef_cmd_b (int dev_desc, int n_args, char *argvec[], int *cur_arg, 
               int *lnum, IFMB_t *if_msg_block);

int cef_cmd_c (int dev_desc, int n_args, char *argvec[], int *cur_arg, 
               int *lnum, IFMB_t *if_msg_block);

/ This is the initialized array of CMD_ELEMENTS that has everything for each
// command.
#define NUM_CMD_ELEMENTS (3)          // Number of commands in Parse List
CMD_ELEMENT_t parse_list [NUM_CMD_ELEMENTS] =
{
  {"CMD_A", (CMD_EXEC_FUNC_t*)cef_cmd_a, " no parameters"},
  {"CMD_B", (CMD_EXEC_FUNC_t*)cef_cmd_b, " param1"},
  {"CMD_C", (CMD_EXEC_FUNC_t*)cef_cmd_c, " param1, param2"}
}

// Here is the parse_and_exec function:
int parse_and_exec (int dev_desc, int n_args, char *argvec[], int *cur_arg, 
                    int *lnum, IFMB_t *if_msg_block)
{
  int retval = OK;
  int cmd_index;
  int (*func) (int dev_desc, int n_args, char *argvec[], int *cur_arg, 
               int *lnum, IFMB_t *if_msg_block);

  while ((retval == OK) && (*cur_arg < n_args))
  {
    // find the command name
    cmd_index = 0;

    while ((cmd_index < NUM_CMD_ELEMENTS) &&
           !str_match (argvec[*cur_arg], parse_list[cmd_index].cmd_name,
                       IGNORE_CASE)) cmd_index += 1;
    if (cmd_index >= NUM_CMD_ELEMENTS)
    {
      // didn't find a match for command
      fprintf (stderr,"ERROR, line: %1d unrecognized command: %s\n",
               *lnum, argvec[*cur_arg]);
      retval = TROUBLE;
    }
    else
    {
      // found a match for the command, go execute it
      func = (void*)parse_list[cmd_index].cef;
      retval = func(dev_desc, n_args, argvec, cur_arg, lnum, if_msg_block);
    }
  }
  return (retval);
}

// the command executer functions
// This command does not use any arguments from the argc/argv[] input.
int cef_cmd_a (int dev_desc, int n_args, char *argvec[], int *cur_arg, 
               int *lnum, IFMB_t *if_msg_block)
{
  int retval = OK;
  long period;

  // do cmd_a that does not need any parameters
  retval = do_cmd_a ();

  *cur_arg += 1;
    }
  }
  return (retval);
}

// This command uses one parameter from the argc/argv[] input
int cef_cmd_b (int dev_desc, int n_args, char *argvec[], int *cur_arg, 
               int *lnum, IFMB_t *if_msg_block)
{
  int retval = OK;

  // is there enough arguments on callers list?
  if (n_args < *cur_arg+2)
  {
    fprintf (stderr,"ERROR, not enough parameters for cmd_b command\n");
    retval = TROUBLE;
  }
  else
  {
    // there are enough parameters
    retval = do_cmd_b (argvec[*cur_arg+1]);

    *cur_arg += 2;
    }
  }
  return (retval);
}

// This command uses two parameters from the argc/argv[] input
int cef_cmd_c (int dev_desc, int n_args, char *argvec[], int *cur_arg, 
               int *lnum, IFMB_t *if_msg_block)
{
  int retval = OK;

  // is there enough arguments on callers list?
  if (n_args < *cur_arg+3)
  {
    fprintf (stderr,"ERROR, not enough parameters for cmd_c command\n");
    retval = TROUBLE;
  }
  else
  {
    // there are enough parameters
    retval = do_cmd_c (argvec[*cur_arg+1], argvec[*cur_arg+2]);

    *cur_arg += 3;
    }
  }
  return (retval);
}

In reality, the command executer functions (cef_) convert all the incoming string data to the destination formats like int, double or whatever. I did not show this to be consistent with the other examples and not clutter up the example code shown here.

The argc and argv variables in conjunction with the current_argument variable make it possible to parse multiple commands from the command line or from each input line in the optional command files. They do limit the amount of error checking that can be done though. If there is just one command in the incoming set, insuring that there are enough arguments to parse it will prevent a crash. Most of the command execution functions do some error checking on each argument, but it is not perfect. Requiring some command delimiter like a semicolon or something would allow better argument checking, but it has not been worth it to me.

Low Level Command Handling

The E3634 uses SCPI (Standard Commands for Programmable Instruments) to talk with the controller. SCPI has the advantage of being human readable. It is more verbose than the old school "R2D2" command sets, but the human readability makes debug a lot easier. Under SCPI, commands can be abbreviated to 4 character strings if desired. For example "MEASURE:CURRENT" can be abbreviated to "MEAS:CURR". I chose not to use the abbreviations in this program to maintain readability. A power supply is not likely to need a lot of high speed traffic, so an extra few microseconds per command will not make much difference.

Two commonly used functions in the low level command handling functions are ibwrt() and ibrd() to write to an instrument or read from it. A command to set the output voltage on the power supply looks like this:

int HP3634_set_output_voltage(int dev_desc, double voltage)
{
  int retval;
  int tx_len;
  char tx_buffer[SHORT_BUFFER_LEN];

  retval = OK;
  snprintf (tx_buffer, SHORT_BUFFER_LEN, "VOLTAGE %5.3f", voltage);

  tx_len = strlen (tx_buffer);

  ibwrt (dev_desc, tx_buffer, tx_len);
  if (ThreadIbsta() & ERR)
  {
    gpib_showError ("Send set output voltage cmd failed");
    retval = TROUBLE;
  }

  return (retval);
}

 Snprintf is used to build the SCPI command string with the command VOLTAGE and the value written to the string via the %5.3f parameter. The length of the output string is determined with the strlen() call and the string is written to the instrument with the ibwrt() call. Results of the command are determined by looking at the ERR bit in the global ThreadIbsta variable. Note that this only checks that the bus transaction completed or not. It does not indicate that the E3634 accepted the value! Things like the E3634 not being turned on, or the GPIB cable not being connected will cause detectable errors. Detecting whether the E3634 accepted the value requires issuing a hp3634_get_sys_err () call which is done as a separate instrument command (get_errors) after the set_output_voltage command.

Fetching the output voltage setting from the power supply is done through the get_output_voltage () call. Code for get_output_voltage looks like this:

static char *HP3634_QUERY_OUTPUT_VOLTAGE_CMD = "VOLTAGE?";

int HP3634_get_output_voltage  (int dev_desc, double *value)
{
  int retval;
  int tx_len;

  retval = OK;

  tx_len = strlen (HP3634_QUERY_OUTPUT_VOLTAGE_CMD);

  ibwrt (dev_desc, HP3634_QUERY_OUTPUT_VOLTAGE_CMD, tx_len);
  if (ThreadIbsta() & ERR)
  {
    gpib_showError ("Send get_output_voltage cmd failed");
    retval = TROUBLE;
  }
  else
  {
    ibrd (dev_desc, hp3634_med_buffer, MED_BUFFER_LEN);
    if (ThreadIbsta() & ERR)
    {
      gpib_showError ("Read get_output_voltage cmd failed");
      retval = TROUBLE;
    }
    else
    {
      // OK read, convert string to double.
      *value = strtod (hp3634_med_buffer, NULL);
    }
  }

  return (retval);
}

 A fixed string with the SCPI command is used to request the voltage value. That command is sent to the E3634 with the ibrwt() call and if that write succeeds, a call to ibrd () gets the value from the E3634. The value is returned as an ASCII string that gets converted to a double value for return to the caller.

All of the low level commands are implemented with minor variations on these two command functions. There is enough variation between the different commands that I opted to use a single function for each command. This simplifies the code above these functions at a cost of a larger executable program. If this were going into an embedded system, I would make more effort to share functions across multiple commands.

GPIB Stuff

The function gpib_init_bus_config_struct() sets the default values for the GPIB bus. Those default values are set at the top of the file gpib_stuff.h. The only value that gets modified from these setting is the target device primary bus address (pad). The actual address is set in the top level main () function. The value for the address may be the hard coded default value or a value from the -D option on the command line.

Linux_GPIB has a call to return a string corresponding the the value of the global variable ThreadIberror. That call is used in the function gpib_showError() after it displays the error message supplied by the caller. Gpib_showERROR() helps make the error responses appear consistent across the code. The gpib_error_string () function is missing in the version of the National Instruments GPIB library for Microsoft operating systems. This is one of the few differences between the Linux_GPIB library and the National Instruments one.

Opening and closing the GPIB bus are simple functions that are in gpib_stuff.c

When ANSI standardized the original HPIB (Hewlett Packard Instrumentation Bus) into the GPIB (General Purpose Instrumentation Bus), they standardized the commands for a dozen or more functions that most instruments support. The IDN query requests that the instrument return an identification string. Most instruments return their manufacturer and model number and some return additional information like the internal software revisions or serial number. The gpib_query_IDN () function issues the IDN query and reads the response into a character array. Note that the string is not terminated with the C customary EOS character. The number of characters read is returned along with the string, so that the code can supply that EOS termination if needed.

Examining the ASCII and hexadecimal values is often needed in debugging the output from query commands. The show_hex_dump() function is included here for that task as needed.