UNIX Serial Communications
Ross Goldman
Networks and their high-speed data communications have become an everyday part of computing life. However, not every device comes with a built-in Ethernet port. Serial communication remains an important technique to talk to many devices. Modems, bar code printers, intelligent controllers and many other devices still primarily rely on serial communications. This article will show how to perform serial communications from a C program using a paging program as an example. My own environment is HP-UX 10.20, but the techniques and procedures described here should easily port to other System V and POSIX UNIX environments. BSD uses five different data structures in place of termio. You should be able to map the termio references into these structures by reading the man pages.
Communication Ports Because UNIX treats most system resources as files, serial communications looks just like regular file I/O. That is, of course, after the communication port has been configured. The actual communication port interface is through a special device file. The names of these files are usually of the form /dev/ttyxxx. For example, on my HP9000/C110, the two built-in serial ports are accessed through the files /dev/tty0p0 and /dev/tty1p0. This can vary from system to system depending on what special device files have been created. See your system documentation for details. These file names come from the old days when terminals were Teletype machines (tty) connected via a serial cable to the host.
It is important that the file permissions on these special device files be set for both read and write access. I usually set them at 666.
System Library Calls Serial communications are accomplished through two types of system library calls. These are the file I/O calls (open, read, write, etc.) and the ioctl call. Closely associated with ioctl is the general terminal interface, termio. A program that needs to perform serial communications will use ioctl and termio to configure the communications port and characteristics, and will use the file I/O calls to send and/or receive data.
The POSIX standard has made some changes to termio and ioctl. termio has been replaced with termios and the generic ioctl call has been replaced with several more specific system calls. The good news is that termio and termios are very similar to each other. In fact, for the purposes of this article, we will not even need to worry about the differences. The change from ioctl to the new calls is a little more radical. This article will discuss ioctl as well as two of the new calls that are used to get and set the port parameters. These calls are tcgetattr and tcsetattr. The sample code has been written to support both the POSIX and System V environments.
termio
termio (and the POSIX termios) is the general terminal interface. It is used to set the communication parameters and the behavior of the communication port. A program creates a termio structure for use with the routines that set port settings:
#ifdef _POSIX
struct termios settings; /* POSIX termios structure */
#else
struct termio settings; /* System V termio structure */
#endif
There are many options associated with termio, and I will discuss only a few of the basic ones in this article. I highly recommend reading the man page for termio for more information. When you do read about termio, remember that it was developed with terminals in mind. For serial device communications, many of the features of termio will not be used.
The first use we will make of termio will be to set the communication parameters. The termio structure variable for setting the parameters is c_cflag. The parameters are ORed together. The settings that are needed for the pager program are 1200 baud, even parity, and 7-bit character size. The paging service requires these settings. It is also necessary to explicitly tell termio to "enable receiver" in order to be able to read data. These settings are set in the termio structure by this line of code:
settings.c_cflag = B1200 | CS7 | PARENB | CREAD; /* 1200 E71 */
Note that there are four system calls in the POSIX environment that can be used to get and set baud rates. These are cfgetispeed, cfgetospeed, cfsetispeed, and cfsetospeed. To write truly portable POSIX code, these routines should be used instead of manually setting the baud rate as shown above.
The next use of the termio structure is to set the behavior of the port. By "behavior" I mean how the input processing (a read posted on the port) is satisfied. When dealing with non-terminal serial devices, the input data is usually processed one character at a time. This mode of input processing is known as "non-canonical mode." There are four ways that input characters may be processed. These different processing techniques are determined by the MIN and TIME (referenced as VMIN and VTIME) values of the c_cc array of termio. All four ways are thoroughly described in the man pages for termio. To determine which case to use, you must know what to expect from your serial device. In the case of the pager program, we know that a complete string of data will be received in reply to our issuance of commands. For example, we know that when a command is issued directly to the modem, it should send an "OK" reply. Also, when a string of data is sent to the paging service, we know that a prompt for the next piece of data should be received as a reply. We want to know whether some data has come in as a reply to the write. Reading through the different cases on the man pages shows that this requirement is case C, MIN = 0 and TIME > 0. The value of TIME will be used as a read timer. The read call posted on the port will return either when some actual data has been read or when the timer times out as specified by these lines of code:
settings.c_cc[VMIN] = 0;
settings.c_cc[VTIME] = 10; /* 10 X .1 seconds = a 1 second
timeout clock */
Note that the timer resolution is .1 seconds. The above code sets the time to time out after 1 second. I like to use a 1 second timer because I can then multiply it by any value to get a larger whole seconds timeout value. I use this technique in the pager program because I know that certain events take longer to complete than others. For example, it may take up to 60 seconds for the pager service to pick up the phone call and send a carrier signal. Other events, such as an "OK" reply from the modem should only take a second or two at the very most.
ioctl and the POSIX calls Before any communication parameters can be set, it is necessary to read the current settings for the port. This ensures that only the parameters that we want to change will be modified. The calls for reading and writing port settings are very different for the POSIX and System V standards. I will cover both cases.
The System V method for reading and writing port settings is use of the ioctl function. The parameters passed to the function are the file handle of the special device file for the port (/dev/tty0p0 in our example), a request reference, and a pointer to the program's termio structure. To read the current port settings for our pager program:
if (ioctl (fpPort, TCGETA, &settings) == -1) {
printf ("ioctl get failure - %s\n", strerror (errno));
exit (1);
}
After setting the port settings to the desired values, it is necessary to write the new settings:
if (ioctl (fpPort, TCSETA, &settings) == -1) {
printf ("ioctl set failure - %s\n", strerror (errno));
exit (1);
}
The only difference between the read and write function is the request reference, TCGETA for read (get) and TCSETA for write (set).
The new (POSIX) method uses separate functions for reading and writing port settings. The read function, tcgetattr, takes as parameters the file handle for the special device file and a pointer to the termios structure:
if (tcgetattr (fpPort, &settings) == -1) {
printf ("tcgetattr get failure - %s\n", strerror (errno));
exit (1);
}
The write function, tcsetattr, takes an additional parameter, which is for optional actions. This parameter is used to determine exactly when the new settings are to take effect:
Optional action When settings take effect
TCSANOW Immediately.
TCSADRAIN After all pending data has been written.
TCSAFLUSH After all pending data has been written and all input data has been flushed.
For the pager program, the call to write the port settings is:
if (tcsetattr (fpPort, TCSANOW, &settings) == -1) {
printf ("tcsetattr set failure - %s\n", strerror (errno));
exit (1);
}
Input/Output operations As previously mentioned, serial communications uses file I/O for reading and writing data. Before any operations can be performed on the serial port, including getting and setting of port parameters, the special device file associated with that port must be opened. Note that non-streamed file I/O is used. The command to open the device file that will be used in the pager program is:
if ( (fpPort = open (COMM_PORT, O_RDWR)) == -1 ) {
printf ("port open failure - %s\n", strerror (errno));
exit (1);
}
COMM_PORT is defined as /dev/tty0p0 in the code. Note that setting the port for reading is really a two-step process. First the file must be opened with read and write access with the O_RDWR flag. Then the program must also enable the receiver function by setting CREAD in the c_cflag variable of the termio structure (see above).
Once the file or port has been opened, the program can set the necessary port parameters. At this point everything should be ready for reading and writing data through the port. Writing data is a fairly straightforward operation. Just like normal file I/O, the program calls the write function with the proper parameters. Other than error checking, there is nothing further for the program to do. A sample write from the pager program looks like this:
if (write (fpPort, buffer, strlen (buffer)) == -1) {
printf ("send failure on '%s' - %s\n", message, strerror
(errno));
/* quit if the port write failed */
close (fpPort);
exit (1);
}
Remember that in non-streamed file I/O, you must specify the number of bytes to be written. Reading data from the port is a more complicated matter. Recall that in the above text, the port behavior was set for completing a read after the data was received or a timeout of 1 second had occurred. The routine that processes reads is passed a timeout value in seconds. The 1-second timer is used with this value to construct longer variable timeout periods. Listing 1 shows how this method is implemented. The characters are read from the port one at a time. If a 1-second port timeout occurs, then the number of tries is incremented. Once this number of tries equals the timeout value, then a read timeout is considered to have occurred, and a TIMEOUT value is returned.
This basic functionality is the core of the data reception portion of the program. As the characters are received one at a time and placed in the buffer, the buffer is compared to the expected reply. Once the expected reply comparison is satisfied, the routine returns. If the reply is longer than expected, the extra characters are simply read and placed in the buffer the next time the routine is called. Since the routine dynamically compares the read data to the expected reply, these extra characters are effectively ignored. These techniques allow the expected replies to be defined less exactly. For example, if the expected reply is defined as "Enter the Message," and the actual reply is "Enter the Message, then RETURN", the routine will return after "Enter the Message" has been received and ", then RETURN" will be read and ignored the next time the routine is called.
Compiling and Running the Code The sample code was written for use with the PageNet text paging service. Other services may have different prompts. For example the AirTouch service uses "Send another page?" instead of "Thank You" as PageNet uses. Also note that the code will do case-insensitive compares, so the prompts may actually be specified case-independent.
Using the HP Ansi C compiler, the command to compile the code as POSIX is:
cc -Ae -D_POSIX textpage.c -o textpage
and to compile the code as System V:
cc -Ae textpage.c -o textpage
To keep things simple, I wrote the sample program to read its parameters from the runstring. The program expects a pager PIN, the message to send, and an optional debug flag. For example:
textpage 00001234 "The 9:00am staff meeting has moved to
10:00am." debug
The debug mode is useful in getting the prompts right and also in uncovering problems such as having to enter PINs as 8-digit numbers. Listing 2 shows this case with actual sample output from the program with debug mode on.
Conclusion In this article, I showed how to perform basic serial device communications. I have used these techniques to write communication drivers for modems, bar code printers, programmable controller interfaces, and many other serial devices.
The sample program (Listing 3) was taken from a larger program that I wrote for our in-house paging needs. (All Listings can be found at: ftp.mfi.com in /pub/sysadmin.) This larger "paging engine" runs as a daemon and processes pages via files received from many different applications. I hope you find the program useful.
About the Author
Ross Goldman is a UNIX sys admin, NT systems administrator, and C programmer. He works for CDI Computer Services and is currently on assignment at Ford Motor Company's Utica Trim Plant. He may be reached via email at: rgoldma1@mailhost.ptpd.ford.com.
|