Serial Programming/Unix V7

= Unix V7 Serial Programming =

Unix V7 - Introduction
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  data structure. Some systems use a 32 bit value for the  entry in the   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  or   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
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

int ioctl(int fd, int command, ...); or int ioctl(int fd, int command, ...); or ioctl(fd, command, data) /* Unix V7 - K&R C */ int fd, command; char *data;
 * 1) include       /* Unix SVR4 family */
 * 1) include    /* Unix BSD family */

With the following arguments:


 * : is a file descriptor pointing to an opened device.


 * :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 <tt>sgtty.h</tt> plus <tt>sys/tt.h</tt> (old Unix systems like Unix V7) as macros. For the remainder of this section <tt>sys/ttold.h</tt> is used. Typically, <tt>sys/ttold.h</tt> is itself included by <tt>sys/ioctl.h</tt>. 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 . In addition, a few file I/O control commands are also applicable. Their names start with  . We come to the commands and their purpose later, however, device-specific commands  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   argument.


 * :For terminal control the  pointer points to a data structure of type
 * or
 * or

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  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  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):

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 */
 * 1) include <sys/stream.h>    /* for ldterm */
 * 2) include <sys/termios.h>   /* for ldterm - could of course also be used directly */
 * 3) include <sys/stropts.h>   /* for handling the STREAMS module */
 * 4) include <sys/ioctl.h>     /* ioctl signature */
 * 5) include <sys/ttold.h>     /* terminal control */
 * 6) include <sys/types.h>     /* for open */
 * 7) include <sys/stat.h>      /* for open */
 * 8) include <fcntl.h>         /* for open */

Overview
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
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
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:
 * ioctl(fd, TIOCGETD, int * dscp):Get line discipline. See below.
 * ioctl(fd, TIOCSETD, int * dscp:Set line discipline.
 * 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  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.

int dscp = OTTYDISC; ioctl(fd, TIOCSETD, &dscp);
 * 1) ifdef OTTYDISC
 * 1) endif

Hang-up on close
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   member of.

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
Unix V7 had undocumented and probably unsupported commands for getting and setting a modem state. The original semantics of the  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:
 * 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.
 * 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; ...   ioctl(fd, TIOCMODG, &state); state |= TIOCM_RTS; ioctl(fd, TIOCMODS, &state);
 * 1) if defined(TIOCM_RTS) && defined(TIOCMODG)
 * 1) endif

Terminal Parameters

 * ioctl(fd, TIOCGETP, struct sgttyb * data):Get the terminal parameters. They are stored in.
 * 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 */      case B19200: return 19200; case B38400: return 38400; case B57600: return 57600; case B115200: return 115200; case B230400: return 230400; case B460800: return 460800; }   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));
 * 1) ifdef B19200
 * 1) endif
 * 2) ifdef B38400
 * 1) endif
 * 2) ifdef B57600
 * 1) endif
 * 2) ifdef B115200
 * 1) endif
 * 2) ifdef B230400
 * 1) endif
 * 2) ifdef B460800
 * 1) endif

/********************************************************** * 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 below for more details regarding the use of the   structure components.

Exclusive Mode

 * 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

 * ioctl(fd, TIOCFLUSH, NULL) /* original */:Input and output data is flushed.
 * ioctl(fd, TIOCTSTP, NULL):Other name for
 * ioctl(fd, TIOCFLUSH, int * mode) /* later versions */:Flush input and/or output according to mode. Constants are defined in <tt>sys/file.h</tt>
 * 0 or (FREAD | FWRITE):Flush Input and output.
 * FREAD:Flush input.
 * FWRITE:Flush output.

?

 * ioctl(fd, TIOHMODE, data_p):?

Special Character State

 * 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
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
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  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  of   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 <tt>*nbt</tt> argument.