Interfacing Laboratory Instruments

Brian presents a simple, instrument-independent system for making individual measurements under user control, or a timed series of measurements under program control--all using the PC's RS-232 port.


November 01, 1994
URL:http://www.drdobbs.com/architecture-and-design/interfacing-laboratory-instruments/184409346

NOV94: Interfacing Laboratory Instruments

Moving data from lab instruments to PCs via RS-232

Brian is an instructor at the British Columbia Institute of Technology in Burnaby, British Columbia. He can be contacted through the DDJ offices.


When most computer users think of RS-232 serial communications, what usually springs to mind are modems, BBSs, and the information highway. However, most modern laboratory instruments include a serial port for transferring data readings made by the instruments to a PC.

Lab-instrument user manuals frequently include sample programs for initiating a data transfer. While these programs usually function correctly, they invariably provide limited functionality, and each is different (requiring different actions by the users and producing output in a different format). Since most laboratories use a variety of instruments, the sample programs for each instrument need to be entered, tested, and debugged, and the operation of each must be learned by laboratory personnel. It is no surprise, then, that most scientists, engineers, and technicians still prefer to record readings from their instruments using pencil and paper!

A local research facility (for which I do consulting work) asked me to propose a solution to this sort of problem. The client frequently ran a battery of tests on both raw material and the manufactured product. The results of these tests were entered into spreadsheets and/or database programs for analysis. The researchers were wasting far too much time collecting data, and even more time entering it into the computer. Besides slowing down the collection and analysis cycle, the manual steps were prone to errors.

My solution was a program that runs on a laptop computer (the client had a number of under-utilized laptops) with interfaces to the instruments for generating data files in formats that can be imported into other programs (such as Lotus 1-2-3). To make my program (which I dubbed "LabMate") more flexible, I didn't hardcode "knowledge" of the individual instruments, opting instead to use a configuration file to describe the particular requirements of individual instruments. Each entry in the configuration file results in a new menu item. Regardless of the instrument, a simple and consistent user interface is maintained. The program provides for a series of individual measurements (under user control) or a timed series of measurements (under program control). Readings, including rudimentary analysis (minimum, maximum, average, and standard deviation) are saved to a file.

On the downside, my proposal eventually lost out to a competing one that used a central mainframe computer wired to each station (laboratory instrument). Each station was outfitted with a small keypad to initiate the transfer of data back to the central computer. On the upside, however, I'm able to present LabMate here. LabMate and the UI toolkit are available for both Microsoft C 6.0 and Bor-land Turbo C. Only the UI toolkit has compiler-specific code. All source code (including both versions of the UI toolkit), the LabMate executable, a sample configuration file, the online help file, and an ASCII version of the user's manual are available electronically; see "Availability," page 3. Contact me directly for information on hardcopies of the documentation and revisions to LabMate.

Some of the code I used for this project was originally developed as sample code for students in programming classes I teach. This recycled code includes a module for direct access to the RS-232 communications ports (written in C, but using many assembly-language techniques) and the port module is interrupt driven (unlike the DOS BIOS port functions). The other recycled code is the user-interface toolkit--similar to Al Stevens' D-Flat, but much cruder and simpler (and predating Al's code by a couple of years). The UI code is actually several modules, each with its own header file, but with all the compiled code collected into a single library.

The remainder of the program is new, and divided into two modules, each containing several functions: The main module (labmate.h and labmate.c) includes code to tie into the UI toolkit and code for accessing and processing the configuration file. The run.c module contains code relating to step-by-step readings under user control and continuous readings under program control.

Serial Communications

The port module--port.h and port.c (Listings One and Two)--interfaces directly to the UART chip in the computer. This module is interrupt driven--as soon as the UART receives a character, it generates a hardware interrupt. An interrupt service routine (ISR)in the port module saves the character into a buffer. The program can then get the character "at its leisure" with no danger of lost characters. DOS-based PCs commonly have four communications ports, COM1 through COM4, with COM1 and COM3 sharing interrupt vector 0Ch, and with COM2 and COM4 sharing interrupt vector 0Bh. LabMate's port module uses a single ISRto handle all four ports. (Despite this sharing, a program can use all four ports, if need be, although LabMate uses only one port at a time.)

The port module begins with a group of #define statements used to derive the addresses that access the various registers of the UART (each communications-port UART has its own unique address space). The next group of defines includes constants (mostly masks used to pick out particular bits) needed to access the UART and PIC. Also included is a constant related to the crystal frequency used to generate the communications pulses. Following the constants are a number of arrays used to access the UART, PIC, and data buffers. In some cases, the arrays have four elements (one for each port); in other cases, they have only two (one for each of the shared interrupts). These arrays are initialized with the proper address, interrupt vector, and so on for each port.

The receive interrupt service routine, rxisr(), is the heart of the port module. Because it is shared among up to four ports, when an interrupt does occur, we must check to see which port caused the interrupt (a procedure usually referred to as "poll on interrupt")--a For loop checks the interrupt-pending bit within the interrupt-identification register of each UART: ((INTPEND & inpw (adr + INTID)) == 0). If the test is True, the waiting character is copied into the circular buffer reserved for that port.

The initcom() function writes data directly to the UART control registers to set the bit rate, number of data bits, number of stop bits, and parity. The bit rate is set by turning on the divisor-latch access bit (DLAB) and then depositing a number derived from the clock (crystal) frequency and the bit rate. The other parameters are set via various bit fields with the line-control register of the UART.

The start() function installs the ISR. While usually straightforward, this is complicated somewhat by shared interrupt vectors. For instance, if a program is using both COM1 and COM3, the interrupt must be installed only once. This is ensured by an array of two flags. Each port has its own UART, and various bit fields in the UART must be set to activate it. The UART must also be told to generate interrupts upon receipt of a character. The stop() function removes interrupt vectors and deactivates the UART. Again, this is complicated because of sharing--you may want to deactivate COM3 without removing the interrupt (because that interrupt is also used by COM1); and again, flags help sort this out.

The other functions in the port module are simpler: rxstat() checks the buffer set up by the interrupt routine and returns True if there is a character in the buffer; rx() returns the next character in the buffer (yet relies on the previous function for correct operation!); txstat() and tx() directly access the UART registers, first to check if the UART is busy, and then to send the UART a character; and ctsstat(), ristat(), dcdstat(), and dsrstat() query the UART to determine the status of the various RS-232 input-handshake lines.

Configuration

LabMate uses a configuration file to hold information about the instrument(s) that it interfaces with. This file includes the menu text (what the user sees in one of the program's menus); information about how the instrument uses the RS-232 port; and the strings that must be sent to the instrument before, during, and after data transfer. Figure 1 is a sample configuration-file entry for a Fluke/Philips multimeter.

The first line appears in LabMate's Instrument menu--the tilde (~) indicates that the next letter (R, in this case) will become a hot key for this menu selection. The second line specifies the RS-232 port settings, including which input-handshake line to monitor (one of the output-handshake lines from the instrument must be wired to this line). The third line is an initialization string for the instrument. The backslashes (\) are used to indicate a noncharacter code (in decimal)--for example, you may recognize \27 as an ASCII <Esc> code. The contents of this line (and the next two lines) is determined solely by the requirements of the instrument: Consult your instrument user's manual for this information. The fourth line is a deinitialization string sent after all readings have been completed. It is needed by many instruments to switch them out of remote-transfer mode back into manual mode. The last line of the configuration entry for this instrument is the string that is sent to the instrument to cause a single datum to be transferred to the computer. Note that lines three, four, and five all end with \10, the ASCII <lf> (line feed) character. For some instruments this may be omitted; for others it may be <cr> or <cr><lf>. See your instrument manual for details.

Theory of Operation

The two source modules, labmate and run, are available electronically. The main() function of the text-based UI performs the following operations:

To process the configuration file, readcfg(), translate(), and freecfg() are used. Most of the work is done by readcfg(); translate() is responsible for interpreting the backslash sequences (such as: \127); and freecfg() is called when the program terminates to deallocate the dynamic memory used to save the configuration information. The readcfg() function processes each line of a configuration-entry sequence and will process up to MAXINST instruments. The first line of a configuration entry is saved into an array of pointers to strings (InstName). In order that the strings all be the same length (which looks better in the menu), this information is first put into a temporary array, then padded with a number of spaces, depending upon the length of the longest instrument name. The next step is to process the communications-port information: strtok() is used to parse the semicolon-separated fields. As each field (which are strings) is recognized, a field in the COMPORT struct for that instrument is filled with the correct numeric information. The final three lines of configuration information are passed through translate() to convert backslash codes into ASCII and saved for later use. There are five parallel arrays used to store the configuration information, one for each line in a configuration entry.

The pointers to functions in the menu struct refer to functions responsible for processing particular pull-down menus. Each pull-down menu causes other functions to be executed, some of which may also be executed via the pointers to functions in struct hotkey. The program terminates when one of these functions returns Quit, which eventually gets back to ServiceMenu() in the main() message loop. All of these functions--doopen(), dosaveas(), and the like--are involved with collecting information from the user: filenames, sample identifiers, and indications of which instrument to use. Eventually, the user must make a selection from the Collect menu, at which point functions from the run.c module are called to perform the interaction with the instrument to collect and save the data.

The run.c module interfaces to LabMate via three functions: dosrun() performs single-step measurement runs; docrun() performs continuous measurement runs; and checkmodem() ensures that the proper RS-232 handshake signals are present before any measurement is attempted. In addition, there are 17 "helper" functions defined as "static" to keep them local to this module.

Most of the code in this module is related to making measurements easy for the user, who can access any of the measurement-screen features via either keyboard or mouse and get help via the F1 key.

The dosrun() function begins by initializing the serial-port module and sending the initialization string to the instrument. Then the screen is "painted"--that is, a number of user-input fields are placed on the screen. Next, an event loop is entered, where you wait for the user to initiate an action: Within the event loop is a Switch/Case statement for recognizing keystrokes and a nested If/Else for checking to see if the mouse has been clicked over one of the button commands on the screen. When the user chooses an action, one of six functions is called to process the request.

For instance, if the user selects "Measure," dom() is called. This results in the Trigger string being sent to the instrument via putinststr(), at which point the program monitors for a reply from the instrument via getinststr(). If a reply is found, it is added to the end of a dynamically allocated array using realloc(), and a count is updated both internally and on the screen. The getinststr() and putinststr() functions are similar to the standard library functions gets() and puts(), except that the instrument input/output functions provide for time-out if data cannot be received from, or sent to the serial-communications hardware.

At any time during the reading of a group of measurements, the user may replace or change a reading; this is useful if a bad reading is taken. On the measurement screen is a small scrolling window that shows readings as they are taken and edit events as they occur. The user may choose either Edit or Delete; in either case, the scrolling window is replaced with a list box used to select the measurement to be edited (replaced, actually) or deleted. The list box always reflects the order in which the data is stored, whereas the scrolling window simply shows the events as they occur. Deletes are shown as a line of red dashes, replacements are shown in red.

When the user finally selects OK to finish a group of measurements, the event loop is terminated after calling doo() (do OK). The doo() function first uses scanf() to ensure that all of the data in the dynamic array is numeric, find the minimum and maximum, calculate the average, and sample standard deviation. scanf() writes all of the information it has collected and/or calculated to the file, including an error message if any nonnumeric data was encountered.

The docrun() function is similar in many ways to dosrun() but is somewhat simpler. First the user is prompted for the number of measurements to take and the interval between them. Both of these figures must be given "up front" before the program will continue. At that point, a measurement screen is presented to the user with the measurements "paused." The user has only three choices: Stop, Go, or Pause. Readings are shown on a scrolling window as they occur. When readings are paused, a bright red message appears on screen.

Conclusion

The process presented here shows how to significantly automate the task of making and analyzing many measurements via instruments that include an RS-232 interface. Some instruments may have an IEEE-488 parallel interface port instead of an RS-232 serial port, but these ideas can be adapted to handle that type of instrument as well.

Figure 1: Sample configuration-file entry.

PM2525 (~Resistance)
COM1;1200;8;N;2;DSR
\27 2, \27 5, \27 4, FNC RTW, OUT N, TRG B, EMO A, X 20 \10
EMO 0, \27 1 \10
X 1 \10

Listing One


/* Physical Layer Module -- IBM-PC Serial Interface
 * Employs Hardware Interrupts and direct UART access.
 * Programmer: Brian R. Anderson * Date: Sept. 29, 1990
 * Modified: December 17, 1990 -- (Support for COM3 & COM4)
 * Modified: December 10, 1993 -- (Monitor handshake)
 */

#define COM1 0
#define COM2 1
#define COM3 2
#define COM4 3

#define TRUE 1
#define FALSE 0

enum { NONE, ODD, filler, EVEN };

/* initialize baud rate, data bits, parity, and stop bits */
void initcom (int port, int baud, int data, int parity, int stop);
/* start receiving -- install interrupt service routine */
void start (int port);
/* stop receiving -- remove ISR, disable interrupts */
void stop (int port);
/* determine if UART is able to accept another character */
int txstat (int port);
/* send one character to the UART */
void tx (int port, char c);
/* determine if there is a character waiting in the receive buffer */
int rxstat (int port);
/* get the next character from the receive buffer */
char rx (int port);
/* determine status of Clear To Send */
int ctsstat (int port);
/* determine status of Ring Indicator */
int ristat (int port);
/* determine status of Data Carrier Detect */
int dcdstat (int port);
/* determine status of Data Set Ready */
int dsrstat (int port);



Listing Two


/* Physical Layer Module -- IBM-PC Serial Interface
 * Employs Hardware Interrupts and direct UART access.
 * Programmer: Brian R. Anderson * Date: Sept. 29, 1990
 * Modified: December 17, 1990: Added Support for COM3 & COM4
 *    NOTE: COM3 shares an interrupt vector with COM1; COM4 shares an interrupt
 *    vector with COM2. In this implementation, all ports use a common ISR.
 * Modified: December 10, 1993: Provide monitoring for input handshake lines...
 *                 DSR, DCD, CTS, RI
 *    N.B.: both output handshake lines (DTR and RTS) activated when the port 
 *    receiving is started.
 */
 
#include <dos.h>
#include "port.h"

/* 8250 UART port address offsets */
#define DATA 0   /* Send or Receive Data */
#define BAUDDIV 0  /* Baud Rate Divisor (DLAB set to 1) */
#define ENINT 1    /* Enable Interrupts */
#define INTID 2    /* Interrupt Identification */
#define LNCTRL 3   /* Line Control */
#define MDMCTRL 4  /* MODEM Control */
#define LNSTAT 5   /* Line Status */
#define MDMSTAT 6  /* MODEM Status */

#define DLAB 0x80   /* Divisor Latch Access Bit (in Line Control Register) */
#define DTR 0x01   /* Data Terminal Ready (in MODEM Control Register) */
#define RTS 0x02   /* Request to Send (in MODEM Control Register) */
#define OUT2 0x08   /* UART OUT2 enables Interrupts on IBM-PC */
#define RXINT 0x01   /* Enable Receive Interrupts for UART */
#define RXREADY 0x01   /* Receive Ready Bit (in Line Status Register) */
#define TXREADY 0x60   /* Transmit Holding & Shift Register Empty Bits */
#define INTPEND 0x01   /* This bit will be Zero if an interrupt is pending */
#define DCTS 0x01   /* Delta (change in) Clear to Send */
#define DDSR 0x02   /* Delta (change in) Data Set Ready */
#define DRI 0x04   /* Delta (change in) Ring Indicator */
#define DDCD 0x08   /* Delta (change in) Data Carrier Detect */
#define CTS 0x10   /* Clear to Send */
#define DSR 0x20   /* Data Set Ready */
#define RI 0x40   /* Ring Indicator */
#define DCD 0x80   /* Data Carrier Detect */

#define CLOCK 0x1C200   /* Baud Rate Clock (1,843,200 / 16) */

#define PICMR 0x21   /* Priority Interrupt Controller Mask Address */
#define PICCR 0x20   /* Priority Interrupt Controller Control Reg. Address */
#define EOI 0x20     /* End Of Interrupt signal to PIC */
 
#define BUFSIZE 1024   /* size of circular receive buffer */

static int PortAdr[4] = {0x03F8, 0x02F8, 0x03E8, 0x02E8};
static char Buffer[4][BUFSIZE];   /* circular buffer for COM1 - COM4 */
static int PtrB[4] = {0, 0, 0, 0};   /* pointer (index) to start of buffer */
static int PtrE[4] = {0, 0, 0, 0};   /* pointer (index) to end of buffer */
static int InUse[4] = {FALSE, FALSE, FALSE, FALSE};   /* port still in use? */
static void (_interrupt _far *OldFunc[2])(void);   /* original ISR vectors */
static int Vect[2] = {0x0C, 0x0B};   /* serial port vector numbers */
static int PICMask[2] = {0x10, 0x08};  /* priority interrupt controller mask */
static int Installed[2] = {FALSE, FALSE};   /* ISR installed yet? */

/* local function for handling COM1 - COM4 serial port interrupts */
static void interrupt rxisr (unsigned bp, unsigned di, unsigned si,
                             unsigned ds, unsigned es, unsigned dx,
                             unsigned cx, unsigned bx, unsigned ax)
{
   char c;   /* character to read from port */
   int adr;   /* address of port */
   int port;   /* port number: COM1 - COM4 */
   int pos;   /* position in buffer */
   
   for (port = COM1; port <= COM4; port++) {   /* poll each port */
      adr = PortAdr[port];   /* get port address */
      if ((INTPEND & inpw (adr + INTID)) == 0) {   /* Interrupt Pending? */
         c = inp (adr + DATA);   /* get character */
         pos = PtrE[port];   /* get position of buffer end */
         Buffer[port][pos++] = c;   /* add character to end of buffer */
         PtrE[port] = (pos == BUFSIZE) ? 0 : pos;  /* update buffer position */
      }
   }
   /* tell priority interrupt controller that interrupt is over */
   outp (PICCR, EOI); 
}
/* initialize baud rate, data bits, parity, and stop bits */
void initcom (int port, int baud, int data, int parity, int stop)
{
   int line;
   int adr = PortAdr[port];
   /* Access Divisor Latch; set baud rate */
   outp (adr + LNCTRL, DLAB);
   outpw (adr + BAUDDIV, CLOCK / baud);
   /* combine data, parity, and stop bits; set port accordingly */  
   line = (data - 5) | (parity << 3) | ((stop - 1) << 2);
   outp (adr + LNCTRL, line);
}
/* start receiving -- install interrupt service routine */
void start (int port)
{
   char pic;
   int adr = PortAdr[port];
   int ip = port & 0x0001;   /* COM3/4 uses ISR for COM1/2 */
   PtrB[port] = PtrE[port] = 0;   /* initialize receive buffer */
   if (!Installed[ip]) {
      /* save original value from vector table */
      OldFunc[ip] = _dos_getvect (Vect[ip]);
      /* set vector table with address of our serial port ISR */
      _dos_setvect (Vect[ip], rxisr);
      /* unmask the priority interrupt controller */
      pic = inp (PICMR);
      pic &= ~PICMask[ip];
      outp (PICMR, pic);
      /* mark isr for this "group" as installed */
      Installed[ip] = TRUE;
   }
   if (rxstat (port))
      rx (port);   /* remove any character "stuck" in UART */
   /* enable Data Terminal Ready, Request to Send; allow interrupts through */
   outp (adr + MDMCTRL, DTR | RTS | OUT2);
   /* enable receive interrupts in UART */
   outp (adr + ENINT, RXINT);
   /* mark this port as in use */
   InUse[port] = TRUE;
}
/* stop receiving -- remove ISR, disable interrupts */
void stop (int port)
{
   char pic;   /* PIC Mask */
   int adr = PortAdr[port];   /* port address */
   int ip = port & 0x0001;   /* COM3/4 uses ISR for COM1/2 */
   /* mark this port as no longer in use */
   InUse[port] = FALSE;
   /* disable all interrupts in UART */
   outp (adr + ENINT, 0);
   /* disable Data Terminal Ready, Request to Send; block interrupts */
   outp (adr + MDMCTRL, 0);
   /* disable interrupt only if other port not using it */
   if (!InUse[ip] && !InUse[ip | 0x0002]) {   /* neither port using ISR */
      /* mask the priority interrupt controller */
      pic = inp (PICMR);
      pic |= PICMask[ip];
      outp (PICMR, pic);
      /* restore original contents of vector table */
      _dos_setvect (Vect[ip], OldFunc[ip]);
      /* mark ISR as no longer installed */
      Installed[ip] = FALSE;
   }
}
/* determine if UART is able to accept another character */
int txstat (int port)
{
   unsigned char stat;   /* UART status */
   stat = inp (PortAdr[port] + LNSTAT);
   return (stat & TXREADY) == TXREADY;   /* Holding and Shift Empty */
}
/* send one character to the UART */
void tx (int port, char c)
{
   outp (PortAdr[port] + DATA, c);
}
/* determine if there is a character waiting in the receive buffer */
int rxstat (int port)
{
   return PtrB[port] != PtrE[port];
}
/* get the next character from the receive buffer */
char rx (int port)
{
   char c;   /* character to get from buffer */
   int pos = PtrB[port];   /* position within buffer */
   c = Buffer[port][pos++];   /* get the character from the buffer */
   PtrB[port] = (pos == BUFSIZE) ? 0 : pos;   /* update buffer position */
   return c;
}
/* determine status of Clear To Send */
int ctsstat (int port)
{
   unsigned char stat;   /* MODEM status */
   stat = inp (PortAdr[port] + MDMSTAT);
   return (stat & CTS) == CTS;
}
/* determine status of Ring Indicator */
int ristat (int port)
{
   unsigned char stat;   /* MODEM status */
   stat = inp (PortAdr[port] + MDMSTAT);
   return (stat & RI) == RI;
}
/* determine status of Data Carrier Detect */
int dcdstat (int port)
{
   unsigned char stat;   /* MODEM status */
   stat = inp (PortAdr[port] + MDMSTAT);
   return (stat & DCD) == DCD;
}
/* determine status of Data Set Ready */
int dsrstat (int port)
{
   unsigned char stat;   /* MODEM status */
   stat = inp (PortAdr[port] + MDMSTAT);
   return (stat & DSR) == DSR;
}

Copyright © 1994, Dr. Dobb's Journal

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.