Linux C串口通信,如何实现数据收发?

99ANYc3cd6
预计阅读时长 38 分钟
位置: 首页 C语言 正文

目录

  1. 串口通信基础
    • 什么是串口?
    • 为什么选择串口?
    • 串口关键参数
  2. Linux 下的串口设备
    • 设备文件路径
    • 查看和配置串口
  3. 核心编程步骤与函数
    • 打开串口
    • 配置串口
    • 读写串口
    • 关闭串口
  4. 完整 C 语言示例代码
    • 发送程序 (serial_send.c)
    • 接收程序 (serial_receive.c)
    • 如何编译和运行
  5. 高级主题与最佳实践
    • 非阻塞 I/O
    • 信号驱动 I/O
    • 超时设置
    • 线程安全
    • 使用 termios 库封装
  6. 常见问题与调试

串口通信基础

什么是串口?

串口(Serial Port),也叫串行端口,是一种用于设备间串行数据通信的接口,它是一位一位地进行数据传输,常见的物理接口有 DB-9(台式机)和 TTL/CMOS 电平(嵌入式板卡,如 Raspberry Pi, Arduino)。

linux c语言 串口通信
(图片来源网络,侵删)

为什么选择串口?

  • 简单易用:协议简单,编程接口清晰。
  • 广泛支持:几乎所有操作系统和微控制器都支持串口通信。
  • 点对点通信:稳定可靠,适合设备与主机之间的直接通信。
  • 无需复杂驱动:在 Linux 下,它就像一个普通的文件一样操作。

串口关键参数

要成功通信,通信双方必须配置完全一致的串口参数:

  • 波特率:每秒传输的比特数,常见值:9600, 19200, 38400, 57600, 115200,必须两端一致。
  • 数据位:通常为 8 位,表示一个字符用 8 位二进制表示。
  • 停止位:数据位后的空闲位,用于表示字符结束,通常为 1 位。
  • 校验位:用于简单的错误检测,常见的有:
    • N (None):无校验
    • E (Even):偶校验
    • O (Odd):奇校验
    • M (Mark):标记校验 (恒为1)
    • S (Space):空格校验 (恒为0)
  • 流控:控制数据传输的速率,防止数据丢失,常用:
    • None:无流控
    • XON/XOFF:软件流控
    • RTS/CTS:硬件流控

这些参数通常用一个结构体来表示。


Linux 下的串口设备

在 Linux 中,串口被抽象为设备文件

  • 物理串口:通常以 /dev/ttyS 开头,/dev/ttyS0, /dev/ttyS1,这些是主板自带的串口。
  • USB 转串口:当你插入 USB to Serial 转换器(如 CH340, PL2303, FTDI 芯片)后,系统会动态创建一个设备文件,通常以 /dev/ttyUSB/dev/ttyACM 开头,/dev/ttyUSB0, /dev/ttyACM0

查看和配置串口

查看串口是否存在

linux c语言 串口通信
(图片来源网络,侵删)
ls /dev/ttyS*   # 查看物理串口
ls /dev/ttyUSB*  # 查看USB转串口
ls /dev/ttyACM*  # 查看另一种USB转串口(如Arduino)

查看串口信息 dmesg 命令可以查看内核信息,有助于识别串口设备。

dmesg | grep tty

使用 minicomscreen 测试串口 这两个是强大的串口调试工具。

# 安装 minicom
sudo apt-get install minicom
# 配置 minicom
sudo minicom -s
# 选择 "Serial port setup",设置设备路径、波特率等,"Save setup as dfl" 保存为默认配置。
# 退出设置后,按 Ctrl+A, Q 退出 minicom
# 使用 screen (更简单)
# stty 是一个命令行工具,可以临时设置终端参数
# 假设设备是 /dev/ttyUSB0,波特率 115200
stty -F /dev/ttyUSB0 115200 raw -echo
# 然后使用 cat 读取
cat /dev/ttyUSB0
# 或者使用 screen
screen /dev/ttyUSB0 115200
# 退出 screen: Ctrl+A, K

核心编程步骤与函数

在 C 语言中,我们主要使用 <termios.h><unistd.h> 这两个头文件来进行串口编程,操作方式与普通文件 I/O 类似:open, read, write, close

打开串口

使用 open() 函数以只读、只写或读写方式打开串口设备文件。

linux c语言 串口通信
(图片来源网络,侵删)
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <termios.h>
int serial_port = open("/dev/ttyUSB0", O_RDWR); // O_RDWR 表示读写模式
if (serial_port < 0) {
    perror("Error opening serial port");
    return -1;
}

配置串口

这是最关键的一步,我们使用 termios 结构体来配置串口参数。

#include <termios.h>
struct termios tty;
// 1. 获取当前串口配置
if(tcgetattr(serial_port, &tty) != 0) {
    perror("Error getting serial attributes");
    return -1;
}
// 2. 设置波特率
// cfsetispeed 设置输入波特率, cfsetospeed 设置输出波特率
// 也可以使用 cfsetispeed(&tty, B115200); cfsetospeed(&tty, B115200);
cfsetispeed(&tty, B115200);
cfsetospeed(&tty, B115200);
// 3. 设置数据位 (8位)
tty.c_cflag &= ~PARENB; // 清除奇偶校验位
tty.c_cflag &= ~CSTOPB; // 清除停止位,使用1位停止位
tty.c_cflag &= ~CSIZE;  // 清除数据位掩码
tty.c_cflag |= CS8;     // 设置8位数据位
// 4. 设置流控 (无流控)
tty.c_cflag &= ~CRTSCTS; // 禁用硬件流控 (RTS/CTS)
tty.c_cflag |= CREAD | CLOCAL; // 启用接收器,忽略调制解调器控制线
// 5. 设置原始输入模式
// 这是串口通信最常用的模式,所有输入字符都直接传递,不进行处理
tty.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); // 禁用规范模式、回显、信号
// 6. 设置原始输出模式
tty.c_oflag &= ~OPOST; // 禁用输出处理
// 7. 设置超时
// VMIN: 读取时的最小字符数
// VTIME: 读取时的超时时间 (十分之一秒)
// VMIN=0, VTIME=1: 非阻塞读取,只要有数据就返回,等待100ms
tty.c_cc[VMIN] = 0;
tty.c_cc[VTIME] = 1;
// 8. 应用配置
if (tcsetattr(serial_port, TCSANOW, &tty) != 0) {
    perror("Error setting serial attributes");
    return -1;
}

读写串口

配置完成后,就可以像读写文件一样操作串口了。

写入数据

#include <unistd.h>
#include <string.h>
unsigned char msg[] = "Hello from Linux Serial Port!\n";
write(serial_port, msg, sizeof(msg));

读取数据

#include <unistd.h>
#include <string.h>
unsigned char read_buf[256];
int num_bytes = read(serial_port, &read_buf, sizeof(read_buf) - 1);
if (num_bytes < 0) {
    perror("Error reading");
} else if (num_bytes == 0) {
    printf("No data received.\n");
} else {
    read_buf[num_bytes] = '\0'; // 确保字符串以 null 
    printf("Received: %s\n", read_buf);
}

关闭串口

使用 close() 函数关闭文件描述符,释放资源。

close(serial_port);

完整 C 语言示例代码

这里我们提供两个简单的程序:一个发送,一个接收,你可以将它们编译后在两个不同的终端中运行,或者用两台电脑交叉测试。

发送程序 (serial_send.c)

#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <errno.h>
#include <termios.h>
#include <unistd.h>
int main() {
    int serial_port = open("/dev/ttyUSB0", O_WRONLY);
    if (serial_port < 0) {
        printf("Error %i opening serial port: %s\n", errno, strerror(errno));
        return 1;
    }
    // --- 配置串口 ---
    struct termios tty;
    if(tcgetattr(serial_port, &tty) != 0) {
        printf("Error %i from tcgetattr: %s\n", errno, strerror(errno));
        return 1;
    }
    tty.c_cflag &= ~PARENB; // Clear parity bit, disabling parity
    tty.c_cflag &= ~CSTOPB; // Clear stop field, only one stop bit used in communication
    tty.c_cflag &= ~CSIZE;  // Clear data size bits
    tty.c_cflag |= CS8;     // 8 bits per byte
    tty.c_cflag &= ~CRTSCTS; // Disable RTS/CTS hardware flow control
    tty.c_cflag |= CREAD | CLOCAL; // Turn on receiver, ignore modem control lines
    tty.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); // Disable canonical mode, echo, signals
    tty.c_oflag &= ~OPOST; // Prevent special interpretation of output bytes (e.g. newline chars)
    tty.c_oflag &= ~ONLCR; // Prevent conversion of newline to carriage return/line feed
    tty.c_cc[VTIME] = 10;   // Wait for up to 1s (10 deciseconds), returning as soon as data is received.
    tty.c_cc[VMIN] = 0;     // Return as soon as any data is received, even if 0 bytes.
    cfsetispeed(&tty, B115200);
    cfsetospeed(&tty, B115200);
    if (tcsetattr(serial_port, TCSANOW, &tty) != 0) {
        printf("Error %i from tcsetattr: %s\n", errno, strerror(errno));
        return 1;
    }
    // --- 发送数据 ---
    char msg[] = "Hello from sender!\n";
    write(serial_port, msg, strlen(msg));
    close(serial_port);
    return 0;
}

接收程序 (serial_receive.c)

#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <errno.h>
#include <termios.h>
#include <unistd.h>
int main() {
    int serial_port = open("/dev/ttyUSB0", O_RDONLY);
    if (serial_port < 0) {
        printf("Error %i opening serial port: %s\n", errno, strerror(errno));
        return 1;
    }
    // --- 配置串口 (与发送端相同) ---
    struct termios tty;
    if(tcgetattr(serial_port, &tty) != 0) {
        printf("Error %i from tcgetattr: %s\n", errno, strerror(errno));
        return 1;
    }
    tty.c_cflag &= ~PARENB;
    tty.c_cflag &= ~CSTOPB;
    tty.c_cflag &= ~CSIZE;
    tty.c_cflag |= CS8;
    tty.c_cflag &= ~CRTSCTS;
    tty.c_cflag |= CREAD | CLOCAL;
    tty.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
    tty.c_oflag &= ~OPOST;
    tty.c_oflag &= ~ONLCR;
    tty.c_cc[VTIME] = 10;   // Wait for up to 1s
    tty.c_cc[VMIN] = 0;     // Return as soon as any data is received
    cfsetispeed(&tty, B115200);
    cfsetospeed(&tty, B115200);
    if (tcsetattr(serial_port, TCSANOW, &tty) != 0) {
        printf("Error %i from tcsetattr: %s\n", errno, strerror(errno));
        return 1;
    }
    // --- 读取数据 ---
    char read_buf [256];
    memset(&read_buf, '\0', sizeof(read_buf));
    printf("Waiting for data...\n");
    int num_bytes = read(serial_port, &read_buf, sizeof(read_buf) - 1);
    if (num_bytes < 0) {
        printf("Error reading: %s", strerror(errno));
    } else if (num_bytes == 0) {
        printf("No data received.\n");
    } else {
        printf("Received %i bytes: %s\n", num_bytes, read_buf);
    }
    close(serial_port);
    return 0;
}

如何编译和运行

  1. 保存代码:将上面的代码分别保存为 serial_send.cserial_receive.c
  2. 编译:打开终端,使用 gcc 进行编译。
    gcc serial_send.c -o serial_send
    gcc serial_receive.c -o serial_receive
  3. 运行
    • 打开两个终端窗口。
    • 第一个终端中运行接收程序:
      ./serial_receive

      你会看到 "Waiting for data..."。

    • 第二个终端中运行发送程序:
      ./serial_send
    • 切换回第一个终端,你应该能看到 "Received X bytes: Hello from sender!" 的输出。

高级主题与最佳实践

非阻塞 I/O

默认情况下,read() 会阻塞,直到有数据到达或发生错误,如果希望 read() 立即返回,可以使用 fcntl() 设置文件描述符为非阻塞模式。

int flags = fcntl(serial_port, F_GETFL, 0);
fcntl(serial_port, F_SETFL, flags | O_NONBLOCK);

设置后,如果没有数据,read() 会立即返回 -1errno 会被设置为 EAGAINEWOULDBLOCK

超时设置

除了 VMINVTIMEselect()poll() 函数是实现更复杂超时控制的强大工具,它们可以让你等待文件描述符变为“可读”或“可写”,并设置一个超时时间。

#include <sys/select.h>
#include <sys/time.h>
fd_set read_fds;
struct timeval timeout;
FD_ZERO(&read_fds);
FD_SET(serial_port, &read_fds);
timeout.tv_sec = 5; // 5秒超时
timeout.tv_usec = 0;
int ret = select(serial_port + 1, &read_fds, NULL, NULL, &timeout);
if (ret > 0) {
    // 数据可读
    read(serial_port, ...);
} else if (ret == 0) {
    // 超时
    printf("Timeout!\n");
} else {
    // 错误
    perror("select()");
}

线程安全

termios 配置是进程级别的,如果多个线程操作同一个串口端口,一个线程的 tcsetattr() 会影响其他线程,解决方案:

  1. 加锁:在使用 tcgetattr()tcsetattr() 时,使用互斥锁(pthread_mutex_t)保护。
  2. 每个线程独立打开:如果可能,让每个线程打开自己的串口描述符(但指向同一个设备文件)。

使用 termios 库封装

为了方便复用,你可以将串口操作的代码封装成一个库或几个函数,

  • serial_open(const char* path, int baud_rate)
  • serial_write(int fd, const char* data, int len)
  • serial_read(int fd, char* buffer, int len, int timeout_ms)
  • serial_close(int fd)

常见问题与调试

  • 权限被拒绝

    • 现象open() 返回 -1errnoEACCES
    • 原因:普通用户没有权限访问 /dev/ttyUSB* 设备文件。
    • 解决
      1. 临时:使用 sudo 运行你的程序。
      2. 永久:将当前用户加入 dialout 组,然后注销或重启。
        sudo usermod -a -G dialout $USER
        # 然后重启或注销登录
  • 无法读取或写入数据

    • 现象read() 一直阻塞,或者 write() 返回错误。
    • 原因
      1. 线缆问题:TX/RX 线接反了(设备的 TX 要接电脑的 RX,反之亦然)。
      2. 参数不匹配:双方的波特率、数据位等参数不一致。
      3. 设备未正确识别:检查 dmesgls /dev/ttyUSB*
      4. 流控问题:如果启用了硬件流控,但线缆没有连接 RTS/CTS 信号,会导致通信失败。
  • 数据丢失或乱码

    • 现象:发送的数据,接收端不完整或内容错误。
    • 原因
      1. 波特率过高:线缆质量不佳或设备处理速度跟不上,导致在高波特率下出错,尝试降低波特率(如从 115200 降到 9600)。
      2. 程序处理速度慢:接收程序处理数据的速度跟不上数据到达的速度,考虑使用多线程或更大的缓冲区。
      3. 缺少校验:在嘈杂的电磁环境中,数据可能出错,可以考虑启用校验位或更高级的校验机制(如 CRC)。

希望这份详细的指南能帮助你顺利地在 Linux C 语言环境下进行串口通信!

-- 展开阅读全文 --
头像
火车头采集发布dede接口如何配置使用?
« 上一篇 02-08
C语言malloc函数需要包含哪个头文件?
下一篇 » 02-08

相关文章

取消
微信二维码
支付宝二维码

目录[+]