Devices

Uniform naming

Linux places all devices in the /dev directory

/dev/tty*       terminals
/dev/fd*        floppy fisk
/dev/mt*        magnetic tape
/dev/st*        streaming tape
/dev/mouse      mouse

Abstraction layers

To handle the variety of devices in an independent manner requires several layers. These are

Device independant software

Standard C library calls such as "printf", "getchar" etc are clearly part of the I/O system. However, most of what these function calls do is at a software level, divorced from the actual devices themselves. For example, the call

printf("the value of %s is %d",
                str, n)
goes through these steps: The last step is the only one that requires knowledge of the device, and this is passed down to the next layer.

Frequently this layer insulates itself from the device by using buffers in kernel space. The layer reads and writes to these buffers, and the device driver layer is responsible for filling or emptying these.

Device controller

The device itself will come with some controller card. This typically gives a set of registers or memory and allows a set of operations to be performed on it.

This set of operations depends on the device and cannot be generalised.

Interrupt handlers

When the device does something, it will often take quite some time over it (depending on device and operation). It will signal the end of this by generating an interrupt.

The process that is using the device can do one of two things: wait until the device completes (block) or continue asynchronously (non-blocking).

In either case, something must happen when the interrupt occurs. If the process has blocked, then it can be woken up. If the process continued then a signal should be sent.

> Note that a context switch may have occurred before the interrupt occurs. This means that the device driver cannot leave any information on the user stack or in user space because that may not be accessible. This makes writing device drivers a bit tricky.

Device driver

The responsibility of this layer is to convert device independent calls into device dependent operations for the device controller. Example device independent calls are Devices are often categorised further:

Block devices

A block device is one that stores information in fixed size blocks. Common block sizes are 128 bytes to 1k bytes. Each block may be read independently of the others, so the device allows random access to each block.

Typical block devices are disks.

Additional operations on block devices will include

Character devices

Character devices deliver or accept a stream of characters (bytes) without regard to any other structure.

Such devices include terminals, mice, etc.

Other

The above classification is not rigid. There may be devices that do not fit into either. For example, in Unix System V there are streams that are devices that implement queue-ing devices.

Device typing

If a device is represented by a file in the file system, then there must be some way of knowing that in fact it is not an ordinary file.

This information is maintained (in Unix) by information in the inode. A field labels the device type. See this by "ls -l" on files in /dev


crw-rw-rw- root 22,   0 fb
brw-rw-rw- root 16,   2 fd0
"fb" (frame buffer) is a character device, "fd0" is a block device. The numbers "22, 0" are the major and minor device numbers respectively. These identify which device driver it actually is.

Terminal drivers

Terminal drivers typically maintain two buffers in kernel space, one for input and one for output. These are maintained as queues so that the correct order is maintained.

When the device interrupts to say that data is ready to read, the data is placed on the incoming queue.

Buffering

The purpose of the input and output buffers is firstly to allow for different rates of process and physical I/O. Nevertheless, if they get too out of step data may be lost. For example, data coming in when the input buffer is full may be lost.

If buffering was not used, then the reading process would have to be ready to process it immediately and finish processing it before the next character arrived. Typically, the input device would be much slower than the process. What should the process do?

If it is blocked, then on each new arrival, it would have to be the active process. This would involve a context switch on every character read. If it is "busy waiting", then time would be wasted.

On a multi-tasking system either way wastes time. So the norm on multi-tasking systems is to buffer character I/O. MSDOS would not need to buffer though.

Canonical mode

when the backspace character is pressed, what should happen? The expectation is that it should erase the currently showing character (if on a screen) and move the cursor back.

Who should do this? If the process should do it, then it would have to be aware of each character as it was typed. This would require unbuffered input.

The device driver could do it, because it needs to handle characters as they arrive anyway. In this case the terminal driver does input mode processing.

If the device driver does this processing, then the process ultimately reading from the device would not want to see the original data but the processed data.

The terminal drivers would maintain two input queues: the one containing the raw data and the one containing the canonical data. There are three modes that the driver can be in:

The output side of this is that in cooked and rare modes the processing is done, but in raw mode no output processing is done.

General control

Unix a set of functions to change terminal driver behaviour. For example

termios

Terminal devices are described by the C structure termios


#include <termios.h>
#include <unistd.h>

struct termios {
    tcflag_t c_iflag;      /* input modes */
    tcflag_t c_oflag;      /* output modes */
    tcflag_t c_cflag;      /* control modes */
    tcflag_t c_lflag;      /* local modes */
    cc_t c_cc[NCCS];       /* control chars */
There is an (incomplete) set of functions to manipulate the contents of this structure

#include 
#include 

int tcgetattr(int fd, struct termios *termios_p);

int tcsetattr(int fd, int optional_actions, struct termios *termios_p);

int cfmakeraw(struct termios *termios_p);

speed_t cfgetospeed(struct termios *termios_p);

int cfsetospeed(struct termios *termios_p, speed_t speed);

speed_t cfgetispeed(struct termios *termios_p);

int cfsetispeed(struct termios *termios_p, speed_t speed);
A typical program will use tcgetattr() on an opened serial port to get the terminal attributes.

  struct termios term_info;
  int fd;

  fd = open("/dev/ttyS0", O_RDWR);
  tcgetattr(fd, &term_info);
(NB we have omitted error handling code here).

A typical program will then make changes to the termios structure and then set these changes in the serial port by tcsetattr(). The second parameter to this call controls when the action takes place. For example, TCSANOW means that the change takes place immediately, and applies to all bytes in the input and output buffers.

There are convenience functions to get and change serial line speeds, such as cfsetospeed() to set the output port speed. These functions act on the termios structure, and do not affect the device until tcsetattr() is called.


  cfsetispeed(&term_info, B2400);
  cfsetospeed(&term_info, B2400);
  tcsetattr(fd, TCSANOW, &term_info);
The cfmakeraw() function primarily affects the input processing mode, by setting it into raw mode.

At the end of processing, a well-behaved progam will reset the old values in the serial port.

There is also a set of functions to manipulate a terminal port once opened as a file


#include 
#include 

int tcsendbreak(int fd, int duration);

int tcdrain(int fd);

int tcflush(int fd, int queue_selector);

int tcflow(int fd, int action);

pid_t tcgetpgrp(int fd);

int tcsetpgrp(int fd, pid_t pgrpid);

To communicate between a computer using the serial port and another device at the other end of the serial cable, there has to be agreement about a number of factors

For example,

  term_info.c_cflag |= CRTSCTS | CS8;

Typical Unix I/O will block until something is ready for reading or writing. It does not have a timeout mechanism, and so can block for hours if I/O devices are not ready. The serial line driver can be set to

These are controlled by two special values in the c_cc[] array. For example, this sets a timeout of 15 seconds if no characters have been read

  term_info.c_cc[VTIME] = 150; // 15 seconds
  term_info.c_cc[VMIN] = 0;

Here is a C program that reads from the serial port


#include <termios.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>

#define LEN 512

int main(int argc, char **argv) {
  struct termios term_info;
  struct termios old_term_info;
  int fd;
  int old_out_baud_rate;
  char str[LEN];
  int nread;
  fd_set rfds;

  if ((fd = open("/dev/ttyS0", O_RDWR)) < 0) {
    perror("Can't open serial port");
    exit(1);
  }

  if (tcgetattr(fd, &term_info) == -1) {
    perror("Can't get serial port attributes");
    close(fd);
    exit(2);
  }
  old_term_info = term_info;

  // set h/w control
  term_info.c_cflag |= CRTSCTS | CS8;

  if (term_info.c_lflag & ICANON) {
    fprintf(stderr, "In canonical mode");
    term_info.c_lflag &= ~ICANON;
  }

  // set timeout
  term_info.c_cc[VTIME] = 150; // 15 seconds
  term_info.c_cc[VMIN] = 0; 

  cfsetispeed(&term_info, B2400);
  cfsetospeed(&term_info, B2400);
  if (tcsetattr(fd, TCSANOW, &term_info) == -1) {
    perror("Can't set serial port attributes");
    close(fd);
    exit(3);
  }

  while ((nread = read(fd, str, LEN)) > 0) {
    fprintf(stderr, "Read %d %d\n", nread, str[0]);
    /* write(1, str, nread); */
  }

  /* 
   * reset state
   */
  if (tcsetattr(fd, TCSANOW, &old_term_info) == -1) {
    perror("Can't reset serial port attributes");
    close(fd);
    exit(4);
  }

  close(fd);
  exit(0);
}