Keywords: Linux Serial Communication | C Programming | termios Configuration | FTDI USB | Real-Time Systems
Abstract: This article provides a comprehensive guide to implementing serial port communication in C on Linux systems. Through analysis of a common FTDI USB serial communication issue, it explains the use of POSIX terminal interfaces, including serial port configuration, read/write operations, and error handling. Key topics include differences between blocking and non-blocking modes, critical parameter settings in the termios structure, and proper handling of ASCII character transmission and reception. Verified code examples are provided, along with explanations of why the original code failed to communicate with devices, concluding with optimized solutions suitable for real-time environments.
Fundamentals of Serial Communication and Linux Implementation
In Linux systems, serial communication is implemented through POSIX terminal interfaces, primarily involving the termios.h header and related system calls. Serial port devices typically exist as files in the /dev directory, such as /dev/ttyUSB0 or /dev/ttyS0, allowing standard file I/O functions for read and write operations.
Problem Analysis and Common Errors
The original code contained two critical issues: First, it used O_NONBLOCK and O_NDELAY flags when opening the serial port, causing read/write operations to return immediately without waiting for data availability. Second, the VMIN parameter was set to 0, meaning read() calls would return 0 even when no data arrived. This combination prevented proper waiting for device responses.
Another significant issue was the use of cout for output without including corresponding C++ headers, which would cause compilation errors. The correct approach is to use C standard library functions or ensure <iostream> is included.
Correct Serial Port Configuration Method
Proper serial port configuration begins with opening the device:
int USB = open("/dev/ttyUSB0", O_RDWR | O_NOCTTY);
Here, O_NOCTTY is used instead of O_NONBLOCK to ensure the serial port doesn't become the controlling terminal while maintaining blocking mode.
Configuring the termios structure is crucial:
struct termios tty;
struct termios tty_old;
memset(&tty, 0, sizeof tty);
if (tcgetattr(USB, &tty) != 0) {
// Error handling
}
tty_old = tty;
cfsetospeed(&tty, B9600);
cfsetispeed(&tty, B9600);
tty.c_cflag &= ~PARENB; // No parity
tty.c_cflag &= ~CSTOPB; // 1 stop bit
tty.c_cflag &= ~CSIZE; // Clear data bit settings
tty.c_cflag |= CS8; // 8 data bits
tty.c_cflag &= ~CRTSCTS; // No hardware flow control
tty.c_cc[VMIN] = 1; // Read at least 1 character
tty.c_cc[VTIME] = 5; // 0.5 second timeout
tty.c_cflag |= CREAD | CLOCAL; // Enable receiver, ignore modem control lines
cfmakeraw(&tty); // Set to raw mode
tcflush(USB, TCIFLUSH);
if (tcsetattr(USB, TCSANOW, &tty) != 0) {
// Error handling
}
Data Writing Strategy
For sending ASCII commands, ensure proper command string formatting. According to device requirements, the format is typically <command><SPACE><CR>:
unsigned char cmd[] = "INIT \r";
int n_written = write(USB, cmd, strlen(cmd));
Note that strlen(cmd) calculates actual character count, excluding the string terminator. For scenarios requiring guaranteed complete transmission, loop-based sending can be used:
int n_written = 0, spot = 0;
do {
n_written = write(USB, &cmd[spot], 1);
spot += n_written;
} while (cmd[spot-1] != '\r' && n_written > 0);
Data Reading and Response Processing
When reading device responses, proper timeout handling and buffer management are essential:
int n = 0, spot = 0;
char buf = '\0';
char response[1024];
memset(response, '\0', sizeof response);
do {
n = read(USB, &buf, 1);
if (n > 0) {
response[spot] = buf;
spot++;
}
} while (buf != '\r' && n > 0);
response[spot] = '\0'; // Ensure string termination
if (n < 0) {
printf("Error reading: %s\n", strerror(errno));
} else if (n == 0) {
printf("Read nothing!\n");
} else {
printf("Response: %s\n", response);
}
Optimization Considerations for Real-Time Environments
In real-time systems like OROCOS, avoid sleep()-type functions. By properly setting VMIN and VTIME parameters, non-blocking polling or precise timeout control can be achieved. For scenarios requiring simultaneous handling of multiple I/O operations, consider using select() or poll() system calls.
Error Handling and Debugging Recommendations
Comprehensive error handling is crucial for serial communication programs:
if (USB < 0) {
fprintf(stderr, "Error %d opening %s: %s\n",
errno, "/dev/ttyUSB0", strerror(errno));
return -1;
}
For debugging, use the strace tool to trace system calls, or verify serial data flow with cat /dev/ttyUSB0. Ensure proper device permissions (users typically need to belong to the dialout group).
Conclusion
Serial communication on Linux requires careful configuration of termios parameters, particularly VMIN and VTIME settings that directly affect read/write behavior. Raw mode (cfmakeraw()) is generally optimal as it disables all line processing and special character interpretation. Proper error handling and resource management (timely closing of file descriptors) are essential for stable operation. The solutions provided in this article are practically verified, suitable for most serial communication scenarios, and consider the special requirements of real-time systems.