Wednesday, July 1, 2009

Linux serial port driver madness

Serial ports are almost extinct on desktop PCs because everyone uses USB or ethernet. But for many automation tasks, RS-232 connections are still state of the art. That's mostly because they are well standardized since the 60s and UART chips are cheap.

I wanted to write a program, which uses a serial port for controlling 2 stepper motors. I wanted to write it such that users have the least possible trouble. Especially during the startup phase, a dialog box should show all available serial ports and let the user select the right one.

And while trying to get all available serial ports, I stumbled across 3 nasty linux bugs:

Bug 1: Always 4 serial port devices
One of the intentions of udev was that only the devices, which are physically present on the system, appear as nodes in the /dev directory. A big step forward compared to the situation before. Unfortunately, the serial driver always creates /dev/ttyS[0-3]. The reason (forgot the link) is that the ports of some exotic multi I/O board aren't detected properly. So the fix was to get 1000s of users into trouble instead of just making one special module parameter for that board.

Bug 2: open() succeeds for nonexistant ports
The manual page of open(2) says, that if one tries to open a device node with no physical device is behind it, errno is set to ENODEV or ENXIO. In the case of a non existing serial port, a valid filedescriptor is returned

Bug 3: tcgetattr returns EIO
According to the glibc manual EIO is usually used for physical read/write errors. For regular files this error means, that you should backup your data because the disk is about to die. A physical read/write error can never happen on a device which is physically nonexistant.

The second best thing would be to return ENXIO (no such device or address). The best thing would be to return EBADF (bad file descriptor) because the open() call before would have returned -1 already.

At least tcgetattr() doesn't succeed like open() so it can be used for the detection routine below.

The solution
The good thing is that once there is a workaround, the problem is quickly forgotten. Here's mine (returns 1 if the port is a physically existant serial device, 0 else):
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <termios.h>
#include <unistd.h>

int is_serial_port(const char * device)
{
int fd, ret = 1;
struct termios t;
fd = open(device, O_RDWR);

if(fd < 0)
return 0;

if(tcgetattr(fd, &t))
ret = 0;
close(fd);
return ret;
}

Of course this won't find ports, which are currently opened by another application.

No comments: