在Linux下使用C/C++编写基于异步机制的串口通信程序,可以通过信号驱动I/O或I/O多路复用(如epoll
)实现。以下是使用信号驱动I/O的详细步骤和示例代码:
步骤说明
-
打开串口设备
使用open
函数以非阻塞模式打开串口设备文件(如/dev/ttyS0
),设置标志O_RDWR
(读写)、O_NOCTTY
(不分配控制终端)和O_NONBLOCK
(非阻塞)。 -
配置串口参数
通过termios
结构体设置波特率、数据位、停止位、校验位等参数,使用tcsetattr
应用配置。 -
设置信号处理函数
注册SIGIO
信号的处理函数,用于异步接收数据通知。在信号处理函数中读取数据。 -
启用异步I/O模式
使用fcntl
设置文件描述符的属主(F_SETOWN
)并启用异步模式(O_ASYNC
),确保数据到达时触发信号。 -
主循环等待信号
主程序进入循环等待信号,信号触发后处理数据。
示例代码
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <termios.h>
#include <signal.h>
#include <errno.h>
#include <string.h>
int fd; // 串口文件描述符
// SIGIO信号处理函数
void sigio_handler(int sig) {
(void)sig; // 避免未使用参数警告
char buffer[256];
ssize_t bytes_read;
// 非阻塞读取所有可用数据
while ((bytes_read = read(fd, buffer, sizeof(buffer))) {
if (bytes_read > 0) {
// 输出接收到的数据(示例:直接写入标准输出)
write(STDOUT_FILENO, "Received: ", 10);
write(STDOUT_FILENO, buffer, bytes_read);
} else if (bytes_read == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// 无更多数据可读
break;
} else {
perror("read error");
exit(EXIT_FAILURE);
}
} else {
// bytes_read为0表示EOF(通常串口不会触发)
break;
}
}
}
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "Usage: %s <serial_port>\n", argv[0]);
exit(EXIT_FAILURE);
}
// 1. 打开串口(非阻塞模式)
fd = open(argv[1], O_RDWR | O_NOCTTY | O_NONBLOCK);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
// 2. 配置串口参数
struct termios tty;
memset(&tty, 0, sizeof(tty));
if (tcgetattr(fd, &tty) != 0) {
perror("tcgetattr");
close(fd);
exit(EXIT_FAILURE);
}
// 设置波特率115200
cfsetispeed(&tty, B115200);
cfsetospeed(&tty, B115200);
// 数据位:8位,无校验,1位停止位
tty.c_cflag &= ~PARENB; // 禁用奇偶校验
tty.c_cflag &= ~CSTOPB; // 1位停止位
tty.c_cflag &= ~CSIZE;
tty.c_cflag |= CS8; // 8位数据位
tty.c_cflag &= ~CRTSCTS; // 禁用硬件流控
tty.c_cflag |= CREAD | CLOCAL; // 启用接收,忽略调制解调器状态
// 非规范模式,禁用回显和信号
tty.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
tty.c_iflag &= ~(IXON | IXOFF | IXANY); // 禁用软件流控
tty.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL);
tty.c_oflag &= ~OPOST; // 原始输出模式
// 非阻塞读取,立即返回可用数据
tty.c_cc[VMIN] = 0;
tty.c_cc[VTIME] = 0;
if (tcsetattr(fd, TCSANOW, &tty) != 0) {
perror("tcsetattr");
close(fd);
exit(EXIT_FAILURE);
}
// 3. 设置信号处理
struct sigaction sa;
sa.sa_handler = sigio_handler;
sigemptyset(&sa.sa_mask);
sigaddset(&sa.sa_mask, SIGIO); // 处理期间阻塞SIGIO
sa.sa_flags = 0;
if (sigaction(SIGIO, &sa, NULL) == -1) {
perror("sigaction");
close(fd);
exit(EXIT_FAILURE);
}
// 4. 设置文件描述符属主和异步模式
if (fcntl(fd, F_SETOWN, getpid()) == -1) {
perror("fcntl F_SETOWN");
close(fd);
exit(EXIT_FAILURE);
}
int flags = fcntl(fd, F_GETFL);
if (flags == -1) {
perror("fcntl F_GETFL");
close(fd);
exit(EXIT_FAILURE);
}
if (fcntl(fd, F_SETFL, flags | O_ASYNC) == -1) {
perror("fcntl F_SETFL");
close(fd);
exit(EXIT_FAILURE);
}
// 5. 主循环等待信号
printf("Listening on %s. Press Ctrl+C to exit.\n", argv[1]);
while (1) {
pause(); // 暂停等待信号
}
close(fd);
return 0;
}
编译与运行
-
编译代码
保存为async_serial.c
,使用以下命令编译:gcc async_serial.c -o async_serial
-
运行程序
需要串口设备路径作为参数:sudo ./async_serial /dev/ttyS0
-
测试
使用另一个终端或虚拟串口发送数据,例如通过echo
命令写入设备文件:echo "Hello, Serial!" > /dev/ttyS0
注意事项
- 权限问题:确保用户有权限访问串口设备(通常需加入
dialout
组或使用sudo
)。 - 信号安全性:信号处理函数中应仅使用异步信号安全函数,避免复杂操作。
- 数据缓冲:实际应用中可能需要更健壮的数据缓冲机制,避免数据丢失。
此示例展示了如何利用信号驱动I/O实现异步串口通信,适用于低至中等数据速率的场景。对于高吞吐量需求,可考虑结合epoll
或专用线程进行优化。