In the first part of this column (LJ, August 2002) we covered the basics of the tty layer and how to create a minimal tty driver. Now we move on and cover more of the tty layer, trying to explain some of the advanced portions.
Remember from Part I the big struct tty_driver structure that all tty drivers need to implement? Let's focus on a few of its functions that were not fully covered last time.
The ioctl function callback in the struct tty_driver is called by the tty layer when ioctl(2) is called on the device node. If your driver does not know how to handle the ioctl value passed to it, it should return -ENOIOCTLCMD to try to let the tty layer implement a generic version of the call, if possible. But what ioctl values will your driver be expected to handle?
The 2.4.19 kernel defines around 60 different possible tty ioctls. Your tty driver doesn't have to implement all of them, but it is a good idea to try to handle the following common ones.
TIOCMGET: called when the user wants to find the status of control lines of the serial port, such as the DTR or RTS lines. If you can directly read the MSR or MCR registers of your serial port, or if you keep a copy of them locally (like some USB to serial type devices need to do), here is how this ioctl can be implemented:
int tiny_ioctl (struct tty_struct *tty, struct file *file, unsigned int cmd, unsigned long arg) { struct tiny_private *tp = tty->private; if (cmd == TIOCMGET) { unsigned int result = 0; unsigned int msr = tp->msr; unsigned int mcr = tp->mcr; result = ((mcr & MCR_DTR) ? TIOCM_DTR: 0) /* DTR is set */ | ((mcr & MCR_RTS) ? TIOCM_RTS: 0) /* RTS is set */ | ((msr & MSR_CTS) ? TIOCM_CTS: 0) /* CTS is set */ | ((msr & MSR_CD) ? TIOCM_CAR: 0) /* Carrier detect is set*/ | ((msr & MSR_RI) ? TIOCM_RI: 0) /* Ring Indicator is set */ | ((msr & MSR_DSR) ? TIOCM_DSR: 0); /* DSR is set */ if (copy_to_user((unsigned int *)arg, &result, sizeof(unsigned int))) return -EFAULT; return 0; } return -ENOIOCTLCMD; }
TIOCMBIS, TIOCMBIC and TIOCMSET: used to set different modem control registers on your tty device. The TIOCMBIS call can turn on the RTS, DTR or loopback registers, while the TIOCMBIC call can turn them off. The TIOCMSET call turns all three values off, and then it sets only the specific values it wants. Here's an example of how this can be handled:
int tiny_ioctl (struct tty_struct *tty, struct file *file, unsigned int cmd, unsigned long arg) { struct tiny_private *tp = tty->private; if ((cmd == TIOCMBIS) || (cmd == TIOCMBIC) || (cmd == TIOCMSET)) { unsigned int value; unsigned int mcr = tp->mcr; if (copy_from_user(&value, (unsigned int *)arg, sizeof(unsigned int))) return -EFAULT; switch (cmd) { case TIOCMBIS: if (value & TIOCM_RTS) mcr |= MCR_RTS; if (value & TIOCM_DTR) mcr |= MCR_RTS; if (value & TIOCM_LOOP) mcr |= MCR_LOOPBACK; break; case TIOCMBIC: if (value & TIOCM_RTS) mcr &= ~MCR_RTS; if (value & TIOCM_DTR) mcr &= ~MCR_RTS; if (value & TIOCM_LOOP) mcr &= ~MCR_LOOPBACK; break; case TIOCMSET: /* turn off the RTS and DTR and * LOOPBACK, and then only turn on * what was asked for */ mcr &= ~(MCR_RTS | MCR_DTR | MCR_LOOPBACK); mcr |= ((value & TIOCM_RTS) ? MCR_RTS : 0); mcr |= ((value & TIOCM_DTR) ? MCR_DTR : 0); mcr |= ((value & TIOCM_LOOP) ? MCR_LOOPBACK : 0); break; } /* set the new MCR value in the device */ tp->mcr = mcr; return 0; } return -ENOIOCTLCMD; }Note, the loopback request (TIOCM_LOOP) is not present in the 2.2 kernel, but it is in the 2.4 and newer kernels.
If your tty driver can handle these four ioctls, it will work with the majority of existing user-space programs. However, there is always an odd program that asks for one of the other ioctls. So you may want to consider handling some of these other common ioctl functions as well.
TIOCSERGETLSR: called to retrieve the line status register (LSR) value of your tty device.
TIOCGSERIAL: called to get a bunch of serial line information from your device all at once. A pointer to a struct serial_struct is passed to this call, which your driver should fill up with the proper values. Some programs (like setserial and dip) call this function to make sure that the baud rate was set properly and to get general information on what type of device your tty is. Here's an example of how this can be implemented:
int tiny_ioctl (struct tty_struct *tty, struct file *file, unsigned int cmd, unsigned long arg) { struct tiny_private *tp = tty->private; if (cmd == TIOCGSERIAL) { struct serial_struct tmp; if (!arg) return -EFAULT; memset(&tmp, 0, sizeof(tmp)); tmp.type = tp->type; tmp.line = tp->line; tmp.port = tp->port; tmp.irq = tp->irq; tmp.flags = ASYNC_SKIP_TEST | ASYNC_AUTO_IRQ; tmp.xmit_fifo_size = tp->xmit_fifo_size; tmp.baud_base = tp->baud_base; tmp.close_delay = 5*HZ; tmp.closing_wait = 30*HZ; tmp.custom_divisor = tp->custom_divisor; tmp.hub6 = tp->hub6; tmp.io_type = tp->io_type; if (copy_to_user(arg, &tmp, sizeof(struct serial_struct))) return -EFAULT; return 0; } return -ENOIOCTLCMD; }
TIOCSSERIAL: the opposite of TIOCGSERIAL; with this one the user can set the serial line status of your device all at once. A pointer to a struct serial_struct is passed to this call, full of the data to which your device should now be set. If your device does not implement this call, almost all programs still will work properly.
TIOCMIWAIT: an interesting call. If the user makes this ioctl call, they want to sleep within the kernel until something happens to the MSR register of the tty device. The “arg” parameter will contain the type of event for which the user is waiting. This ioctl is commonly used to wait for status line changes, signaling that more data is ready to be sent to the device.
Be careful in implementing the TIOCMIWAIT ioctl, however. Almost all of the existing kernel drivers that use it also use the interruptible_sleep_on() call, which is unsafe. (A lot of nasty race conditions are involved.) Instead, a wait_queue should be used in order to avoid these problems. Here's an example of a correct way to implement TIOCMIWAIT:
int tiny_ioctl (struct tty_struct *tty, struct file *file, unsigned int cmd, unsigned long arg) { struct tiny_private *tp = tty->private; if (cmd == TIOCMIWAIT) { DECLARE_WAITQUEUE(wait, current); struct async_icount cnow; struct async_icount cprev; cprev = tp->icount; while (1) { add_wait_queue(&tp->wait, &wait); set_current_state(TASK_INTERRUPTIBLE); schedule(); remove_wait_queue(&tp->wait, &wait); /* see if a signal woke us up */ if (signal_pending(current)) return -ERESTARTSYS; cnow = edge_port->icount; if (cnow.rng == cprev.rng && cnow.dsr == cprev.dsr && cnow.dcd == cprev.dcd && cnow.cts == cprev.cts) return -EIO; /* no change => error */ if (((arg & TIOCM_RNG) && (cnow.rng != cprev.rng)) || ((arg & TIOCM_DSR) && (cnow.dsr != cprev.dsr)) || ((arg & TIOCM_CD) && (cnow.dcd != cprev.dcd)) || ((arg & TIOCM_CTS) && (cnow.cts != cprev.cts)) ) { return 0; } cprev = cnow; } } return -ENOIOCTLCMD; }
Somewhere in the portion of the code that recognizes that the MSR register changes, the line:
wake_up_interruptible(&tp->wait);must be called for this code to work properly.
TIOCGICOUNT: called when the user wants to know the number of serial line interrupts that have occurred. The kernel is passed a pointer to a struct serial_icounter_struct, which needs to be filled up. This call is often made in conjunction with the previous TIOCMIWAIT ioctl call. If you keep track of all of these interrupts while your driver is operating, the code to implement this call can be quite simple. See drivers/usb/serial/io_edgeport.c for an example.
The write() callback in your tty_struct can be called from both interrupt context and user context. This is important to know, as you should not call any function that might sleep when you are in interrupt context. This includes any function that possibly might call schedule(), such as the common functions like copy_from_user(), kmalloc() and printk(). If you really want to sleep, then please check your status by calling in_interrupt().
The write() callback can be called when the tty subsystem itself needs to send some data through the tty device. This can happen if you do not implement the put_char() function in the tty_struct. (Remember, if there is no put_char() function, the tty layer will use the write() function.) This commonly happens when the tty layer wants to convert a newline character to a linefeed plus a newline character. The main point to remember here is your write() function must not return 0 for this kind of call. This means that you must write that byte of data to the device, as the caller (the tty layer) will NOT buffer the data and try again at a later time. As the write() call cannot determine if it is being called in place of put_char()--even if only one byte of data is being sent—please try to implement your write() call always to be able to accept at least one byte of data. A number of the current USB-to-serial tty drivers do not follow this rule, and as a result some terminal types do not work properly when connected to them.
In order to properly implement the set_termios() callback, your driver must be able to decode all of the different settings in the termios structure. This is a complicated task, because all of the line settings are packed into the termios structure in a wide variety of ways.
Listing 1 shows a simple implementation of the set_termios() call that will print, to the kernel debug log, all of the different line settings that have been requested by the user.
Listing 1. A Simple Implementation of the set_termios Call
First off, save a copy of the cflags variable from the tty structure, as we are going to be accessing it a lot:
unsigned int cflag; cflag = tty->termios->c_cflag;
Next, test to see if there is anything we need to do. For example, see if the user is trying to use the same settings we currently have; we don't want to do any extra work if it's not necessary.
/* check that they really want us to change * something */ if (old_termios) { if ((cflag == old_termios->c_cflag) && (RELEVANT_IFLAG(tty->termios->c_iflag) == RELEVANT_IFLAG(old_termios->c_iflag))) { printk (KERN_DEBUG " - nothing to change...\n"); return; } }The RELEVANT_IFLAG() macro is defined as:
#define RELEVANT_IFLAG(iflag) (iflag & (IGNBRK|BRKINT|IGNPAR|PARMRK|INPCK))and is used to mask off the important bits of the cflags variable. Compare this value to the old value, and see if they differ. If they don't, nothing needs to be changed, so we return. Note that we first check that the old_termios variable actually points to something, before we try to access a field off of it. This check is required, because sometimes this variable will be NULL. Trying to access a field off of a NULL pointer will cause a nasty oops in the kernel.
Now that we know we need to change the terminal settings, let's look at the requested byte size:
/* get the byte size */ switch (cflag & CSIZE) { case CS5: printk (KERN_DEBUG " - data bits = 5\n"); break; case CS6: printk (KERN_DEBUG " - data bits = 6\n"); break; case CS7: printk (KERN_DEBUG " - data bits = 7\n"); break; default: case CS8: printk (KERN_DEBUG " - data bits = 8\n"); break; }
We mask the cflag with the CSIZE bit field and test the result. If we can't figure out what bits were set, we can use the default of 8 data bits. Then we check for the requested parity value:
/* determine the parity */ if (cflag & PARENB) if (cflag & PARODD) printk (KERN_DEBUG " - parity odd\n"); else printk (KERN_DEBUG " - parity even\n"); else printk (KERN_DEBUG " - parity none\n");We first test to see if the user actually wants a type of parity set in the first place. If so, then we need to test which kind of parity (odd or even) they want.
The stop bits requested are also simple to test for:
/* figure out the stop bits requested */ if (cflag & CSTOPB) printk (KERN_DEBUG " - stop bits = 2\n"); else printk (KERN_DEBUG " - stop bits = 1\n");
Now, we're on to determining the proper flow control settings. It's a simple process to determine if we should use RTS/CTS:
/* figure out the flow control settings */ if (cflag & CRTSCTS) printk (KERN_DEBUG " - RTS/CTS is enabled\n"); else printk (KERN_DEBUG " - RTS/CTS is disabled\n");Determining the different modes of software flow control and the different stop and start characters, however, is a bit more difficult:
/* determine software flow control */ /* if we are implementing XON/XOFF, set the start * and stop character in the device */ if (I_IXOFF(tty) || I_IXON(tty)) { unsigned char stop_char = STOP_CHAR(tty); unsigned char start_char = START_CHAR(tty); /* if we are implementing INBOUND XON/XOFF */ if (I_IXOFF(tty)) printk (KERN_DEBUG " - INBOUND XON/XOFF is enabled, " "XON = %2x, XOFF = %2x", start_char, stop_char); else printk (KERN_DEBUG " - INBOUND XON/XOFF " "is disabled"); /* if we are implementing OUTBOUND XON/XOFF */ if (I_IXON(tty)) printk (KERN_DEBUG " - OUTBOUND XON/XOFF is enabled, " "XON = %2x, XOFF = %2x", start_char, stop_char); else printk (KERN_DEBUG " - OUTBOUND XON/XOFF " "is disabled"); }Finally we want to determine the baud rate. Luckily, the tty_get_baud_rate() function can extract the specific baud rate out of the termios settings and return it as an integer:
/* get the baud rate wanted */ printk (KERN_DEBUG " - baud rate = %d", tty_get_baud_rate(tty));Now that all of the different line settings have been determined, it is up to you to use the information to set up the device properly.
Vern Hoxie has an excellent set of documentation and example programs on how to access serial ports from user space available at scicom.alphacdc.com/pub/linux. Most of this information will not be too useful for a kernel programmer, but some of the descriptions of the different ioctl(2) commands and the history behind the wide variety of different ways to get and set tty information are quite good. I highly recommend that anyone implementing a tty kernel driver read over these, if for no other reason than to determine how users will be trying to use your driver.
Hopefully these two articles have helped to demystify the tty layer. If you are stuck on how to implement a specific callback, quite a few drivers in the kernel interact with the tty layer as complete examples. Search for “tty_register_driver” in the drivers/char and drivers/usb directories for these files.
I would like to thank Al Borchers, who helped to determine how the write() callback really works and all of the nuances involved in it. Together with Peter Berger, they wrote drivers/usb/serial/digi_acceleport.c, a USB to serial driver for the Digi AccelePort devices. It is an excellent example of a well-working tty driver.