8º Projeto de MC104 - SISTEMAS OPERACIONAIS

Interfacing with an External Device

 

Introduction

One of the distinguishing factors of real-time systems is that they must interact with the real world. They must communicate with external devices. The information that is received from these external devices must then be processed, stored, displayed, analyses, etc... The interaction with external-devices generates a whole new set of problems within a real-time system. Some of these problems include, how to communicate with the external device, what format is the incoming data in, does the system need to "manage" the device, etc... Some examples of external devices that real-time system may have to interact with include, AIRNC 428 bus, 1553 data bus, 488 data bus, A/D cards, in addition to more traditional devices such as serial ports, hard disks, scsi port, etc... There are an endless number of external devices that a system must communicate with.

In this experiment, the serial device will be examined. A serial device is commonly found in modern PCs and is used extensively to interface with an "external-device" such as the mouse, modem, printer, etc... The serial device follows a specified format and the UNIX system provides a large variety of ways with which to interact with the serial device. Since the serial device is found on many computers and has a standardized interface between UNIX implementations, it will be used as the "external-device" in the last two experiments.

This Experiment will create two processes. These two processes will run on different machines and pass data across a serial line. A data source will be created to send data and a data sink will be created to receive data. The source will attempt to send the data at a period interval with as little deviation as possible. The sink will read the data and write it to disk. The next experiment will expand on this system, so be sure to learn the concepts well in this experiment.

 

Objectives

The following are the primary objectives of this experiment:

 

Theory Aspects

There are many "external-devices" that a system must communicate with. Some are listed in the introduction, but even this list is only a small sample. There are hundreds of devices that a system must communicate with. Image for a minute if each of these devices took a different set of function calls to access. You can see this would lead to thousands of different function calls that would be needed to communicate with devices. Therefore, the developers of UNIX came up with a standard way to access devices. First, they are treated like files. You can open(), close(), write(), and read() to and from UNIX devices.

You can try this simple test on a machine. While at the console on the Lynx or Linux machine (don't try this in X as it may lock the screen), type the following command:

  
  cat - < /dev/mouse (Linux)
  cat - < /dev/mouse (Lynx)
 

NOTE: be sure to use the actual device name of the mouse, it may vary from machine to machine. The above is correct for the ERAU real-time lab.

This command will read from the standard input and display it to the screen. The redirection states that the standard input will come from the specified device (/dev/mouse for Linux and Lynx). This is the first serial port on the PC. Now, move the mouse and see if anything is displayed to the screen. This is a simple way to read/write to/from devices under UNIX.

Obviously, there must be a different set of instructions that needs to be executed for a serial port than a keyboard, so how does UNIX know what to execute when you request a read of a device? A device driver handles the call to open()/read()/write() and performs device specific instructions. For example, in the above command, the redirection of the serial port caused the /dev/mouse device to be opened and read from. When the serial port was opened, the kernel performed the device specific open by calling the open call in the device driver. How did the kernel know which device specific open call to call? Each device in the /dev directory has a major and minor node number associated with it (ls -l can be used to view these node numbers on most systems). These are "indexes" into the kernel and let the kernel know which device driver the device is associated with and thus which device specific calls to use. This may seem a bit complicated, but after a while, it will become easier. Right now, you are using a multitude of device drivers to read this. The video device driver communicates with the screen, the hard disk device driver accesses the disk, the Ethernet driver talks to the network, mouse driver to handle mouse input, etc ...

For common devices, the external device driver is part of the UNIX kernel. For example, the hard disk device driver is typically part of the operating system. However, sometimes, the device driver is not part of the kernel. If you must read and write to a data acquisition card, chances are the OS does not come with a device driver. In these cases, a device driver must be written. This is a complicated task and beyond the scope of this text. For this reason, the serial port will be used as an external device. The serial device driver is part of the kernel and it is standardized by POSIX . Therefore, the same device will be available on multiple machines.

 

Working with the Serial Port

There are many options that can be turned on and off when using a serial port. UNIX treats the serial port as a terminal and the terminal manual pages are some of the largest within a UNIX system. Even with all of the options associated with the terminal/serial port, there are two basic modes of operations: processed and raw. Under processed mode, the device driver will examine the incoming input stream, interpret it, and perform different actions based upon the data that was inputed. For example, if the terminal driver receives the break character while in processed mode, a signal will be sent to the process associated with the terminal. Under raw mode, the device driver does nothing more than read the data from the external device and return it to the process.

Since, the primary purpose of this experiment is to examine the external device, but not specifically examine the serial port, only a general introduction into how to setup the serial port for raw data transfer will be given. Detailed specifics will be left to the interested reader. Again, the focus of this experiment is to interact with an external device, not to learn all of the intricacies of the serial port on a UNIX system.

To use the serial port, the following steps must be followed:

UNIX provides a standard set of calls for operating on the serial port. For more information on these calls, see the specific manual pages of the operating system you are examining, only a general list is given here. First, the standard UNIX calls for dealing with devices:

UNIX also provides a set of calls that deal with terminals. The serial port is treated as a terminal under UNIX. Here are the calls used when dealing with the terminal:

Collectively, the above system calls fall under the termios (terminal input/output) subject and the manual page on termios can be helpful in explaining some of the above commands.

 

The Serial Library - "SerialLib.h" "SerialLib.c"

Since the calls used to open, close, and setup the serial port are common between the source and sink programs for this experiment, they are grouped into a single source file and shared between the two programs. This is a common practice when code is shared by more than one program. Typically, the code can be compiled into a library and used that way. Since the serial library is relatively small and uncomplex, it will simply be linked into the appropriate program at compile time.

The serial library provides two calls to the outside world:

One of the important items that this serial library does is it stores the terminal settings prior to changing into raw mode so that they can be restored at a later time. In this way, the state of the machine is left the same after the serial port is closed.

The following calls are internal to the serial library:

Again, the above shows another good program practice. The internals of the the code are subdivided into several small functions each performing a specific task. By combining the above functions, the serial library is built.

First, looking at serialSave(...), this call is used to save the serial terminal settings prior to changing them to raw mode. The following code segment shows the code for serialSave(...).

  
  static int              r_saved_term_flag = 0;
  static struct termios   r_saved_term;

  int serialSave( int fp )
  {
    if( tcgetattr( fp, &r_saved_term ) != 0 )  {
      perror("tcgetattr failed, could not save terminal settings");
      return -1;
    }
    r_saved_term_flag = 1;
    return 0;
  }
 

The two regional variables r_saved_term_flag and r_saved_term are used to save the terminal settings. If r_saved_term_flag is 0, no terminal information has been saved, if r_saved_term_flag is 1, terminal information has been saved. r_saved_term_flag will not be set to 1 unless valid terminal information has been retrieved. This ensures that an invalid terminal setting won't accidently be restored. The call to get the r_saved_term data is a single call to tcgetattr(...). This is all that is needed to save off the data.

The serialRestore(...) call is similar, it simply checks the value of r_saved_term_flag. If it is 1, tcsetattr(...) is used to restore the terminal settings saved in r_saved_term. The following code segment shows serialRestore(...):

  
  int serialRestore( int fp )
  {
    if( r_saved_term_flag = 0 ) {
      fprintf(stderr,"No terminal has been saved!\n");
      return -1;
    }
    if( tcsetattr( fp, TCSANOW, &r_saved_term ) != 0 ) {
      perror("tcsetattr failed, could not restore terminal settings");
      return -1;
    }
    return 0;
  }
 

Now, the serialSetRaw() call is used to change the serial port to raw mode. The termios structure contains the necessary information for setting up the terminal. The first part of serialSetRaw() initializes this structure for raw mode.

  
  struct termios term;
  ... 
  term.c_iflag  = 0;
  term.c_oflag  = 0;
  term.c_cflag  = CS8 | CLOCAL | CREAD;
  term.c_lflag  = 0;
  
  term.c_cc[VMIN] = 1;
  term.c_cc[VTIME] = 0;
  ...
 

There are four sets of flags in the termios structure. c_iflag is for input flags, c_oflag is the output flags, c_cflag is for control flag, and c_lflag is for local flags. In raw mode, c_iflag, c_oflag, and c_lflag are all set to 0. This will turn of all processing options. c_cflag is set using three constants. CS8 transfers 8 bits of data, a necessity for binary data. CLOCAL states that we are using a hard wired connection and ignores the modem control lines typically on serial ports, and CREAD states that this port can be read from. The c_cc array contains integer specific data. The VMIN and VTIME control how data will be returned to the process during a read. VMIN states how many bytes must be read for a read to return (in this case, the read will return if at least 1 byte is present). VTIME states the amount of time to wait for the given number of bytes before returning in tenths of a second. 0 for VTIME will wait forever for the data to be received.

Next in serialSetRaw(...), the input and output speeds are set to the defined rate:

  
  ...
  if( cfsetospeed( &term, DEFAULT_BAUD ) == -1 )  {
    perror("csetispeed, could not set terminal speed");
    serialRestore( fp );
    return -1;
  }
  if( cfsetispeed( &term, DEFAULT_BAUD ) == -1 )  {
    perror("csetispeed, could not set terminal speed");
    serialRestore( fp );
    return -1;
  }
  ...
 

Finally, serialSetRaw(...) calls tcsetattr(...) to set the attributes. Since the tcsetattr(...) command is shown above it will not be shown again.

The remaining serial library calls serialOpen(...) and serialClose(...) simply call the functions described above as well as the operating system open(...) and close(...) calls. They will be not be examined in detail here. The student should examine the code to see exactly what these functions are doing.

 

Example Program: "Data Format"

Data is transfered between dataSource and dataSink. The following structure is used to transfer data between the two processes:

  
  #define         INTEGER_TYPE    100
  #define         FLOAT_TYPE      200

  typedef struct {
    int  seq_num;
    int  data_type;
    struct timeval  time_tag;
    union {
      int  int_value;
      float  float_value;
    } raw_data;
} data_t;
 

Where the members of the structure are defined as follows:

This data structure allows both integer and float values to be transfered.

 

Example Program: "Data Source"

The dataSource program will be examined first. The dataSource program writes data to the serial port at a specific interval. The following algorithm is used:

Drift is described in Project #1 and #2, however, no solution is given. The interval timer used here is one solution to drift. The interval timer will send a signal to the process at a specified interval. The signal sent to the process is SIGALRM, therefore, the process must establish a signal handler for this signal.

The signal handler for dataSource simply counts the number of alarm signals received:

  
  unsigned int r_intr_count = 0;
  void itimer_intr_handler( void )
  {
     r_intr_count++;
  }
 

The following code is used to establish the interval timer and signal handler. Signals are explained in Project #4. For more information on interval timers, see the manual pages for the specific OS you are using.

  
  struct sigaction    sigact;
  struct itimerval    itimer;
  ...
  sigact.sa_handler = (void *)itimer_intr_handler;
  sigact.sa_flags = 0;
  sigemptyset( &sigact.sa_mask );
  sigaction( SIGALRM, &sigact, NULL );

  itimer.it_interval.tv_sec = 0;
  itimer.it_interval.tv_usec = INTERVAL;
  itimer.it_value = itimer.it_interval;
  setitimer( ITIMER_REAL, &itimer, NULL );
  ...
 

After establishing the timer and signal handler, the serial port is opened using the serialOpen() call described above. Be sure to use the correct device for the OS you are running on. After opening the device, a for loop is executed which sends NO_OF_PACKETS packets. Based upon the packet number, either a float (odd packet number) or an integer (even packet number) is sent. To see how the packets are generated, see the actual source code.

The write() system call is used to write out the packet. The serial device was opened in non-blocking mode which means that the write call will never wait if it cannot write to the device, ie, the device is full. If this happens, an error will be returned and errno will be set to EAGAIN. In this program, this means that the data rate is faster than the serial port can handle or the reader is not reading data off of the port quick enough. In either case, the program aborts with an error.

  
  if( (rtn = write(fp, &data, sizeof(data_t))) == -1 )  {
    if( errno = EAGAIN )  {
      perror("sender buffer full, this shouldnt happen, aborting");
      serialClose(fp);
      exit(1);
    }
    else  {
      perror("aborting sender, write failed");
      serialClose(fp);
      exit(1);
    }
  }
 

Next, a simple calculation is done to see if the program is drifting. The source code explains the drift calculation in detail and it is not reviewed here.

As the last step in the for loop, usleep() is called. This sleep call will sleep for TIMEOUT microseconds as defined by the #define at the top of the program. As long as TIMEOUT is larger than the specified interval, all should be fine. The sleep call does not sleep for the entire TIMEOUT period, instead, it is woken by the timer signal after the specified INTERVAL. At this point, execution continues and another packet is sent.

Once the specified number of packets have been sent, some statistics are displayed. The only key item to watch in the dataSource program is the INTERVAL. Depending on the OS, there is a hard limit to how quickly it can send the timer signal. For Linux, a timer signal can be sent every 10 milliseconds. For higher rates a different method needs to be used (such as the real-time clock provided which can send signals at much higher rates, however, it is a non-portable method, ie, it is OS specific.)

 

Example Program: "Data Sink"

dataSink will read the data produced by dataSource on the serial port. dataSink will read the data and write it out to a file. The following algorithm is used by dataSink:

Again, as in serialSource, the serialOpen() call described above is used to open the serial port. A standard fopen() call is used to open the output file, named out.file, to which the data will be written.

The only "complicated" section of code in the dataSink program is section where the data is read from the serial port. Here is the code segment associated with reading from the serial port:

  
  ...
  bytes = 0;
  while( bytes < sizeof(data_t) )  {
    if( (size = read( fp, buffer + bytes, sizeof(data_t) - bytes)) == -1 )  {
      perror("read failed");
      serialClose(fp);
      exit(1);
    }
    bytes += size;
  }
  total_bytes += bytes;
  ...
 

It is important to remember that a data packet is what we want to read off of the serial port. Therefore, we always want to read the number bytes equal to the size of the data structure. However, the read call only guarantees that it will read at most the specified number of bytes, it can also return less than the specified number of bytes. So, simply stating that sizeof(data_t) bytes should be read will be insufficient. Instead, the above code segment is needed. Although complex, once understood, it is not hard to follow.

First, we set bytes equal to zero. As long as bytes is less than the size of the data structure, we will attempt to read data from the serial port. So, this while statement is simply saying that we want to read from the serial port until we have read enough bytes to complete a data packet. Next, the read call has two important arguments. The first is the pointer to were the read information will be stored, (buffer + bytes in this case), and the second is the number of bytes to read, (sizeof(data_t) - bytes). Since there is a possibility that we have already read data into the buffer, we want to be sure to write any new data after the data previously read. Therefore, we always write into the buffer at an offset equal to the number of bytes previously read, which is given by the bytes variable. The number of bytes to be read is simply the number of bytes needed to complete a data packet (i.e., the size of the data minus the number of bytes already read). This will ensure that we will always read only the number of bytes needed to complete the data packet. As a final step before completing the while loop, the bytes read is incremented by the number of bytes read previously, i.e. size. Be sure to review this code and understand how it works.

After reading in the data packet, the sequence number is checked. The sequence number should always equal the current counter. This ensures that no packets are lost during transmission.

  
  ...
  if( data.seq_num != i )  {
    fprintf( stderr,"Sequence number mismatch, current: %d -- received:
             %d\n", data.seq_num, i ); 
    fprintf(stderr,"Adjusting sequence number to received value!\n");
    i = data.seq_num;
  }
  ...
 

The final two steps in the while loop write the data out to the file and increment the counter i.

  
  ...
  tm_ptr = localtime( (time_t *)&data.time_tag.tv_sec );
  switch( data.data_type )  {
    case INTEGER_TYPE:
      fprintf(out_file,"%02d:%02d:%02d.%06d -- %d -- %d\n",
              tm_ptr->tm_hour, tm_ptr->tm_min, tm_ptr->tm_sec,
              data.time_tag.tv_usec, data.seq_num, data.raw_data.int_value);
      break;
    case FLOAT_TYPE:
      fprintf(out_file,"%02d:%02d:%02d.%06d -- %d -- %f\n",
              tm_ptr->tm_hour, tm_ptr->tm_min, tm_ptr->tm_sec,
              data.time_tag.tv_usec, data.seq_num, data.raw_data.float_value);
      break;
    default:
      fprintf(stderr,"Received invalid packet type!\n");
  }
  
  /* Increment the number of packets received and complete the loop */
  i++;
  ...
 

As a final step, dataSink will display some statistics to the screen.

 

Follow on Assignment

Each experiment has a follow on assignment that needs to be completed. These assignments are to be completed in a specified format and contain all of the information listed below.

The assignment for this Project is to work with the above program. Compile the dataSource and dataSink programs on the Lynx machine, Linux machine, and SunOS machine. Run the program as is, connecting two Lynx machines together and record the results. Try increasing the interval and see what happens, again record your results. Now, hook the Lynx machine to the Linux machine. Does the program work? Record the results. Finally, hook the SunOS machine to the Linux machine, does this work? Record the results. If either of the above connection do not work, attempt to explain why.

Assignment Summary:

 

Due Date

 


Luís Fernando Faina
Last modified: Mon Nov 11 12:42:22 2002