Complete Implementation and Problem Solving for Serial Port Communication in C on Linux

Dec 01, 2025 · Programming · 9 views · 7.8

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.

Copyright Notice: All rights in this article are reserved by the operators of DevGex. Reasonable sharing and citation are welcome; any reproduction, excerpting, or re-publication without prior permission is prohibited.