Serial Programming/Unix V7

Unix V7 Serial Programming

edit

Unix V7 - Introduction

edit

Unix V7 was one of the early Unix versions which gained some wider distribution also outside of universities. It was commercially released in 1978. As a consequence, the Unix V7 way of accessing and controlling terminals set the standard for terminal and serial communication in Unix for quite some time. The V7 API does not consist of any particular C function calls. Instead, the settings and configuration of the serial device is done via the standard ioctl(2) system call, and data transfer is done via the standard read(2) and write(2) system calls.

Although, the API originated in Unix V7 it of course didn't remain unchanged. E.g. XENIX used a different name for the tchars data structure (tc). Some systems use a 32 bit value for the sg_flags entry in the sgttyb structure, others a 16 bit value. BSD added a bunch of ioctls, BSD 4.3 added a bunch of option codes (together sometimes called the BSD I/O environment), partly incompatible with Unix SVR3 codes, ...

All in all, if a later terminal API like termio or termios is available, it makes much sense to use the later API instead of the Unix V7, or V7-lookalike API. If the Unix V7 API is really used, some extensive study of the particular system's documentation and include files is in order. Nevertheless, there is some chance to find that the API is used in some classic Unix source code, particular in free software from that age that survived until today.

Originally, the necessary definitions for the terminal (serial) specific ioctl(2) system calls could be found in the sgtty.h and sys/tty.h> headers. However, in later Unix versions the contents was moved to sys/ttold.h and parts of it also ended up in termios.h, or the API was removed completely.

Mentioning termios.h here sounds rather strange, since it is a later API. But termio and termios borrowed from the V7 API, and systems which still support the V7 API often do this by emulating the API on top of the normal termio or termios interface with a compatible device driver. This is done by pushing an additional STREAMS device driver module on top of the termio or termios device driver. That STREAMS module is typically called ttycompat. How a particular Unix system can be configured to automatically push the module onto a device, how it can be manually pushed, or how the STREAMS device framework works is out of the scope of this description.

Unix V7 - Mode of Operation

edit

As already mentioned, the Unix V7 way of controlling terminal devices is to use ioctl(2) system calls. ioctl(2) is not specific to serial devices. The system call is part of Unix's generic device driver interface. The more widely used system calls for this interface are read(2) and write(2). While read(2) and write(2) do transport the "payload" (the data), ioctl(2) on the other hand controls the I/O operation as such. That is, it enables a program to configure the device.

The generic signature of the system call is

#include <unistd.h>       /* Unix SVR4 family */
int ioctl(int fd, int command, ...);

or

#include <sys/ioctl.h>    /* Unix BSD family */
int ioctl(int fd, int command, ...);

or

ioctl(fd, command, data) /* Unix V7 - K&R C */
int fd, command;
char *data;

With the following arguments:

fd
is a file descriptor pointing to an opened device.
command
is an integer which tells the device what to do, and logically needs to be known by the device. The list of commands a serial device is supposed to understand is defined in sys/ttold.h (newer Unix systems) or sgtty.h plus sys/tt.h (old Unix systems like Unix V7) as macros. For the remainder of this section sys/ttold.h is used. Typically, sys/ttold.h is itself included by sys/ioctl.h. However, it doesn't hurt to include it explicitly for good measure.
The names of the core terminal I/O control command macros start with TIO.... In addition, a few file I/O control commands are also applicable. Their names start with FIO.... We come to the commands and their purpose later, however, device-specific commands (DIO..., MX...) are not discussed.
...
The actual parameter for the vararg ellipse ... arguments of ioctl() is typically only a single pointer to some data structure - at least for the terminal control commands. This matches the old ioctl() format, where there was one single char *data argument.
data
For terminal control the data pointer points to a data structure of type
  • struct sgttyb,
  • struct tchars or
  • struct ltchars.

These data structures are used to communicate different types of information between the device driver and the application program. They control which settings are supposed to be done, or allow to fetch which parameters are currently set for the device.

Typically struct sgttyb looks like:

 struct sgttyb {
     char sg_ispeed;  /* input line speed  */
     char sg_ospeed;  /* output line speed */
     char sg_erase;   /* erase char        */
     char sg_kill;    /* kill char         */
     int  sg_flags;   /* flags             */
 };

As one can see, it allows a program to communicate basic settings like the line speed and essential characters for the line discipline. Remember, the interface was made for communication with terminals, and therefore provides the support for a line discipline.

Typically struct tchars contains additional information about terminal control characters and looks like:

struct tchars {
    char t_intrc;  /* interrupt character       */
    char t_quitc;  /* quit character            */
    char t_startc; /* start output character    */
    char t_stopc;  /* stop output character     */
    char t_eofc;   /* end-of-file character     */
    char t_brkc;   /* input delimiter character */
};

In addition to these data structures BSD added more terminal control characters in:

struct ltchars {
    char t_suspc;  /* stop-process character         */
    char t_dsuspc; /* delayed stop-process character */
    char t_rprntc; /* reprint line character         */
    char t_flushc; /* flush output character         */
    char t_werasc; /* word erase character           */
    char t_lnextc; /* literal next character         */
};

To summarize, a configuration of a serial device generally follows the following template of statements (pseudo code, error handling, etc. excluded):

#include <sys/stream.h>     /* for ldterm */
#include <sys/termios.h>    /* for ldterm - could of course also be used directly */
#include <sys/stropts.h>    /* for handling the STREAMS module */
#include <sys/ioctl.h>      /* ioctl() signature */
#include <sys/ttold.h>      /* terminal control */
#include <sys/types.h>      /* for open() */
#include <sys/stat.h>       /* for open() */
#include <fcntl.h>          /* for open() */
  :
  .
struct sgttyb data;
int fd;
  :
  .
/* 
 * Open the device.
 * O_RDRW   - open for reading and writing
 * O_NDELAY - ignore the state of the DCD line. Otherwise
 *            the ioctl() blocks until DCD indicates the
 *            remote side is ready. Also affects behavior of
 *            the read(2) system call, which will now not block
 *            if there is no input data available.
 * O_NOCTTY - Do not become a controlling terminal. Has no effect for STREAMS
 */
fd = open("/dev/ttya", O_RDWR | O_NDELAY | O_NOCTTY);
/* * Assume we have a "modern" STREAMS device implementation * and need to push the STREAMS modules on the stream to * get the desired V7 compatibility emulation. * TODO: Add error handling. */ if(ioctl(fd, I_FIND, "ldterm") == 0) { ioctl(fd, I_PUSH, "ldterm"); /* termios STREAMS terminal line discipline module */ } if(ioctl(fd, I_FIND, "ttcompat") == 0) { ioctl(fd, I_PUSH, "ttcompat"); /* Unix V7 STREAMS compatibility module */ } /* done with setting up the device */  : . /* Probably set some values in data here */ ioctl(fd, TIO..., &data); /* configure serial line */

Note:
All the STREAMS stuff would not exist in original Unix V7 code. There a simple #include <gstty.h> would have done.

Unix V7 - Terminal I/O Control Commands

edit

Template:Stubpar

Overview

edit

As already mentioned, the possible terminal I/O control commands to be used for terminal devices via ioctl() are defined as macros. This section discusses their meaning and usage in ioctl() system calls.

Unfortunately, the set of commands is a mess. Almost every implementation of Unix decided it would be cool to add their own, incompatible commands. To add insult to injury, some popular Unix versions defined particular commands, but couldn't be bothered to implement them for decades, then dropped them. And to make even more programmers happy, half of the commands where never in any way seriously documented or described. The programmers apparently thought that only those with access to the Kernel source code are deemed worthy to program serial interfaces.

Original Unix V7 ioctls

edit

This section lists the original Unix V7 I/O control commands. They are presented as they are used as part of an ioctl(2) call. This has been done, so the passed data types can also be presented. The argument data types are underlined in the following list.

Line Discipline
edit
ioctl(fd, TIOCGETD, int *dscp)
Get line discipline. See below.
ioctl(fd, TIOCSETD, int *dscp
Set line discipline.

These commands did exist in V7, and were implemented. However, they were not documented. Particularly, the argument's meaning was not documented (and probably not used). Later the following argument interpretation was added:

OTTYDISC or 0
Unix V7 tty driver behavior
NETLDISC or 1
BSD Unix driver behavior
NTTYDISC or 2
New tty line discipline (whatever that is)

A typical usage looks as it follows. The #ifdef test is done to avoid executing the ioctl() on a system with undefined TIOCSETD argument semantics (like the original V7, where the arguments of the command were not documented). The following changes the line discipline of the device identified by the handle fd to the Unix V7 tty driver behavior.

#ifdef OTTYDISC
  int dscp = OTTYDISC;
  ioctl(fd, TIOCSETD, &dscp);
#endif
Hang-up on close
edit

When a DT (data terminal, the computer) is no longer ready to send and receive serial data on an interface, it is supposed to drop the DTR (Data Terminal Ready) RS-232 control line of that serial interface. The following command allows to automate this.

ioctl(fd, TIOCHPCL, NULL)
Set hang up on last close flag. Typically this is implemented as dropping the DTR (Data Terminal Ready) pin on the serial interface when the last close(2) is called on the device. Note, there is no direct inverse operation in V7. However, the flag can be cleared in some implementations via the sg_flags member of struct sgttyb.

A typical sequence of events would look like it follows:

/**********************************************************
 * Set hang up on last close flag
 *********************************************************/
/*
/* open device */
fd = open("/dev/ttya", O_RDWR | O_NDELAY | O_NOCTTY);

/* device configuration (omitted) */
/* Set hang up on last close flag to drop DTR on last close */ ioctl(fd, TIOCHPCL, NULL);
/* communicate via serial interface */
/* * Close device. If this is the last close (which it typically is), DTR * will be dropped/ */ close(fd);
/**********************************************************
 * Clear hang up on last close flag via TIOCSETP
 * Note the slightly different flag name HUPCL here
 * vs. HPCL in TIOCHPCL.
 *********************************************************/

struct sgttyb tparam; ... fd = open( ... ); ... ioctl(fd, TIOCGETP, &tparam); /* get current values */ tparam.sg_flags &= ~HUPCL; /* only clear H[U]PCL flag */ ioctl(fd, TIOCSETP, &tparam); /* apply changed parameters */
Modem State
edit

Unix V7 had undocumented and probably unsupported commands for getting and setting a modem state.

ioctl(fd, TIOCMODG, int *state)
Get modem state. Usually not implemented and not used. See TIOCMGET for getting the RS-232 control line status.
ioctl(fd, TIOCMODS, int *state)
Set modem state. Usually not implemented and not used. See TIOCMSET for setting the RS-232 control lines.

The original semantics of the state variable have been lost in the mist of time. On some implementations the later TIOCM_x macros for TIOCMGET/TIOCMSET are available and do also work with TIOCMODx. The TIOCM_x flags are:

TIOCM_LE
(not V7) Line enable signal
TIOCM_DTR
(not V7) Data terminal ready
TIOCM_RTS
(not V7) Request to send
TIOCM_ST
(not V7) Secondary transmit (?)
TIOCM_SR
(not V7) Secondary receive (?)
TIOCM_CTS
(not V7) Clear to send
TIOCM_CD, TIOCM_CAR
(not V7) Carrier detect
TIOCM_RI, TIOCM_RNG
(not V7) Ring indicator
TIOCM_DSR
(not V7) Data set ready
/**********************************************************
 * Set the RTS line if possible.
 * This mixes a TIOCM_x macro intended for TIOCMGET/TIOCMSET
 * with TIOCMODx and is not recommended.
 *********************************************************/

int state; ... #if defined(TIOCM_RTS) && defined(TIOCMODG) ioctl(fd, TIOCMODG, &state); state |= TIOCM_RTS; ioctl(fd, TIOCMODS, &state); #endif
Terminal Parameters
edit
ioctl(fd, TIOCGETP, struct sgttyb *data)
Get the terminal parameters. They are stored in data.
gtty(fd, struct sgttyb *data)
Short version for TIOCGETP ioctl on some systems derived from V7.
ioctl(fd, TIOCSETP, struct sgttyb *data)
Set the interface to the provided parameters. Waits until output has drained. Clears (throws away) any pending input data, and then applies the parameters.
stty(fd, struct sgttyb *data)
Short version for TIOCSETP ioctl on some systems derived from V7.
ioctl(fd, TIOCSETN, struct sgttyb *data)
Sets the interface to the provided parameters, but does not wait for drained output and does not clear input data. Depending on the hardware this used to generate some garbage input/output characters.
/**********************************************************
 * Print out current line configuration
 *********************************************************/

/* * Map speed constants (not available in all V7-style implementations) * to actual speed value. * Some platforms might offer more constants. Some platforms do allow * to use a simpler mapping, e.g. because of the numbering schema of their * Bx constants. */ long speed2long(int speed) { switch(speed) { case B0: return 0; case B50: return 50; case B75: return 75; case B110: return 110; case B134: return 134; case B150: return 150; case B200: return 200; case B300: return 300; case B600: return 600; case B1200: return 1200; case B1800: return 1800; case B2400: return 2400; case B4800: return 4800; case B9600: return 9600; case EXTA: return 19200; case EXTB: return 38400; /* * untypical, non V7 constants, better check if defined */ #ifdef B19200 case B19200: return 19200; #endif #ifdef B38400 case B38400: return 38400; #endif #ifdef B57600 case B57600: return 57600; #endif #ifdef B115200 case B115200: return 115200; #endif #ifdef B230400 case B230400: return 230400; #endif #ifdef B460800 case B460800: return 460800; #endif } return -1; /* unknown, better update the code */ }
/* * TODO: implement conversion function to decode flags * * flags are: * flag2str() * #ifdef HUPCL * if(flag & HUPCL) ...; // not original V7 * #endif * if(flag & TANDEM) ...; * if(flag & CBREAK) ...; * if(flag & LCASE) ...; * if(flag & ECHO) ...; * if(flag & CRMOD) ...; * if(flag & RAW) ...; * if(!(flag & ANYP)) { * ...; // no parity * } else if((flag & ANYP) == ANYP) { * ...; * } else * if(flag & ODDP) ...; * if(flag & EVENP) ...; * } * if((flag & ALLDELAY) { delay2str(flag); } * * delay2str(flag) * flag = flag & ALLDELAY; * * if((flag & BSDELAY) == BS0) ...; * if((flag & BSDELAY) == BS1) ...; * if((flag & VTDELAY) == FF0) ...; * if((flag & VTDELAY) == FF1) ...; * if((flag & CRDELAY) == CR0) ...; * if((flag & CRDELAY) == CR1) ...; * if((flag & CRDELAY) == CR2) ...; * if((flag & CRDELAY) == CR3) ...; * if((flag & TBDELAY) == TAB0) ...; * if((flag & TBDELAY) == TAB1) ...; * if((flag & TBDELAY) == TAB2) ...; * #ifdef TAB3 * if((flag & TBDELAY) == TAB3) ...; // not original V7, there it was called XTABS * #endif * #ifdef XTABS * if((flag & TBDELAY) == XTABS) ...; * #endif * if((flag & NLDELAY) == NL0) ...; * if((flag & NLDELAY) == NL1) ...; * if((flag & NLDELAY) == NL2) ...; * if((flag & NLDELAY) == NL3) ...; */
struct sgttyb tparam; ... fd = open( ... ); ... ioctl(fd, TIOCGETP, &tparam); printf("Input line speed: %ld\n", speed2long(tparam.sg_ispeed)); printf("Output line speed: %ld\n", speed2long(tparam.sg_ospeed)); printf("Erase char: %x\n", tparam.erase); printf("Kill char: %x\n", tparam.kill); printf("Flags: %x\n", tparam.flags); /* printf("Flags: %s\n", flag2str(tparam.flags));
/**********************************************************
 * Change line speed to 2400 baud
 * Keep all other interface parameters as-is.
 *********************************************************/

struct sgttyb tparam; ... fd = open( ... ); ... ioctl(fd, TIOCGETP, &tparam); /* get current values */ tparam.sg_ispeed = tparam.sg_ospeed = B2400; ioctl(fd, TIOCSETP, &tparam); /* apply changed parameters */

See section #Unix V7 - struct sgttyb below for more details regarding the use of the struct sgttyb structure components.

Exclusive Mode
edit
ioctl(fd, TIOCEXCL, NULL)
Turn on exclusive mode. No other open() is permitted on the device.
ioctl(fd, TIOCNXCL, NULL)
Turn off exclusive mode. The device might be opened multiple times.
Flush
edit
ioctl(fd, TIOCFLUSH, NULL) /* original */
Input and output data is flushed.
ioctl(fd, TIOCTSTP, NULL)
Other name for TIOCFLUSH
ioctl(fd, TIOCFLUSH, int *mode) /* later versions */
Flush input and/or output according to mode. Constants are defined in sys/file.h
0 or (FREAD | FWRITE)
Flush Input and output.
FREAD
Flush input.
FWRITE
Flush output.
ioctl(fd, TIOHMODE, data_p)
?
Special Character State
edit
ioctl(fd, TIOCGETC, struct tchars *data)
Get the terminal state special characters from the device.
ioctl(fd, TIOCSETC, struct tchars *data)
Set the terminal state special characters.
ioctl(fd, TIOCSETP, struct tchars *data)
?

More Original Unix V7 sgtty.h ioctls

edit

The following ioctls were all defined in the Unix V7 tty interface, but with a few exceptions not properly documented. The D ioctls were probably intended to control dialers (ACUs). The F ioctls are borrowed from file I/O. The M ioctls are actually intended for Unix/V7 special multiplexed (mxp) files - an Unix/V7 mechanism for inter-process communication. Why these ioctls are defined in sgtty.h is unclear, other mpx ioctls are defined in sys/mx.h instead.

DIOCLSTN
DIOCNTRL
DIOCMPX
DIOCNMPX
DIOCSCALL
DIOCRCALL
DIOCPGRP
DIOCGETP
Per line-discipline get data.
DIOCSETP
Per line-discipline set data.
DIOCLOSE
DIOCTIME
DIOCRESET
FIOCLEX
Set the close-on-exec flag for this file descriptor. If the process is spawned (forked/execed), the file descriptor will be closed by the exec() system call. That way the spawned process doesn't inherit the open terminal interface.
FIONCLEX
Clear the close-on-exec flag for this file descriptor. If the process is spawned (forked/execed), the file descriptor will not be closed by the exec() system call. That way the spawned process inherits the open terminal interface.
MXLSTN
Place the mpx file in listener mode.
MXNBLK
Use non-blocking mode for the mpx file.

Terminal ioctls acquired over time

edit

There are quite some additional I/O controls which have been acquired over time and which enhance the Unix V7 teletype API. The following are the ones which are rather common. Later interfaces like termio and termios also added I/O controls. These are not listed here.


Sending a break signal can be performed with the following two commands:

ioctl(fd, TIOCSBRK, NULL)
Set the break flag. Sends a break signal over the serial line. A break is an all-zero framing error.
ioctl(fd, TIOCCBRK, NULL)
Clear the break flag. Stops sending a break signal.

A simplified way to send a one-second break looks as it follows:

ioctl(fd, TIOCSBRK, NULL); /* start break signal */
/*
 * The following blocks the process for one second.
 * This is "suboptimal" in real applications.
 * Using asynchronous I/O and alarm(2)/setitimer(2)/signal(2)
 * or similar system calls, plus a callback which, when called,
 * turns the break of, is a better alternative.
 * Also, the granularity of sleep() is to rough, since
 * breaks of 250 ... 500 milliseconds are typical in
 * serial communication.
 */
sleep(1);                  /* wait one second */
ioctl(fd, TIOCCBRK, NULL); /* stop sending break signal */ 


The following commands can be used to control the RS-232 control lines:

ioctl(fd, TIOCSDTR, NULL)
Set DTR (Data Terminal Ready) signal.
ioctl(fd, TIOCCDTR, NULL)
Clear DTR (Data Terminal Ready) signal.
ioctl(fd, TIOCMODG, int *state)
Get modem state. See below.
ioctl(fd, TIOCMODS, int *state)
Set modem state. Allows to access all RS-232 control lines. The status of each line is indicated by a bit in the state argument. The bits have the following symbolic names:
TIOCM_CAR or TIOCM_CD
DCD . Data Carrier Detect
TIOCM_CTS
CTS - Clear to send
TIOCM_DTR
DTR - Data terminal ready
TIOCM_LE or TIOCM_DSR
DSR - Data set ready
TIOCM_RNG or TIOCM_RI
RNG - Ring indicator
TIOCM_RTS
RTS - Request to send
TIOCM_SR
Secondary RxD
TIOCM_ST
Secondary TxD

Depending on which of the above commands is available, DTR can, for example be set as it follows:

ioctl(fd, TIOCSDTR, NULL);

or

int state;
ioctl(fd, TIOCMGET, &state);
state |= TIOCM_DTR;
ioctl(fd, TIOCMSET, &state);


ioctl(fd, TIOCSTOP, NULL)
Stop output as if the stop character has been typed.
ioctl(fd, TIOCSTART, data_p)
Restart output as if the start character has been typed.

ioctl(fd, TIOCLGET, int *flags)
Get the terminal flags. See element sg_flags of struct sgttyp for the details.
ioctl(fd, TIOCLBIS, int *flags)
Set particular terminal flags. The flags are or-ed with the currently already set flags.
ioctl(fd, TIOCLBIC, int *flags)
Clear particular terminal flags. The flags are "not-and-ed" with the existing flags.
ioctl(fd, TIOCLSET, int *flags)
Set the terminal flags.

ioctl(fd, TIOCGLTC, struct ltchars *data)
Get ltchars data.
ioctl(fd, TIOCSLTC, struct ltchars *data)
Set ltchars data.

ioctl(fd, FIORDCHK, NULL)
Return the number of currently available input characters in the input buffer. The number is returned as the return value of the function call.
ioctl(fd, FIONREAD, int *nbr)
Return the number of currently available input characters in the input buffer. The number is written in to the *nbt argument.

Set/Get Parameters

edit

Set/Get Special Characters

edit

Set/Get More Special Characters

edit

Unix V7 - struct chars

edit

Unix V7 - struct tchars

edit

Unix V7 - struct sgttyb

edit