目录
- 串口通信基础
- 什么是串口?
- 为什么选择串口?
- 串口关键参数
- Linux 下的串口设备
- 设备文件路径
- 查看和配置串口
- 核心编程步骤与函数
- 打开串口
- 配置串口
- 读写串口
- 关闭串口
- 完整 C 语言示例代码
- 发送程序 (
serial_send.c) - 接收程序 (
serial_receive.c) - 如何编译和运行
- 发送程序 (
- 高级主题与最佳实践
- 非阻塞 I/O
- 信号驱动 I/O
- 超时设置
- 线程安全
- 使用
termios库封装
- 常见问题与调试
串口通信基础
什么是串口?
串口(Serial Port),也叫串行端口,是一种用于设备间串行数据通信的接口,它是一位一位地进行数据传输,常见的物理接口有 DB-9(台式机)和 TTL/CMOS 电平(嵌入式板卡,如 Raspberry Pi, Arduino)。

为什么选择串口?
- 简单易用:协议简单,编程接口清晰。
- 广泛支持:几乎所有操作系统和微控制器都支持串口通信。
- 点对点通信:稳定可靠,适合设备与主机之间的直接通信。
- 无需复杂驱动:在 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。
查看和配置串口
查看串口是否存在

ls /dev/ttyS* # 查看物理串口 ls /dev/ttyUSB* # 查看USB转串口 ls /dev/ttyACM* # 查看另一种USB转串口(如Arduino)
查看串口信息
dmesg 命令可以查看内核信息,有助于识别串口设备。
dmesg | grep tty
使用 minicom 或 screen 测试串口
这两个是强大的串口调试工具。
# 安装 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() 函数以只读、只写或读写方式打开串口设备文件。

#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;
}
如何编译和运行
- 保存代码:将上面的代码分别保存为
serial_send.c和serial_receive.c。 - 编译:打开终端,使用
gcc进行编译。gcc serial_send.c -o serial_send gcc serial_receive.c -o serial_receive
- 运行:
- 打开两个终端窗口。
- 在第一个终端中运行接收程序:
./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() 会立即返回 -1,errno 会被设置为 EAGAIN 或 EWOULDBLOCK。
超时设置
除了 VMIN 和 VTIME,select() 或 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() 会影响其他线程,解决方案:
- 加锁:在使用
tcgetattr()和tcsetattr()时,使用互斥锁(pthread_mutex_t)保护。 - 每个线程独立打开:如果可能,让每个线程打开自己的串口描述符(但指向同一个设备文件)。
使用 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()返回-1,errno是EACCES。 - 原因:普通用户没有权限访问
/dev/ttyUSB*设备文件。 - 解决:
- 临时:使用
sudo运行你的程序。 - 永久:将当前用户加入
dialout组,然后注销或重启。sudo usermod -a -G dialout $USER # 然后重启或注销登录
- 临时:使用
- 现象:
-
无法读取或写入数据
- 现象:
read()一直阻塞,或者write()返回错误。 - 原因:
- 线缆问题:TX/RX 线接反了(设备的 TX 要接电脑的 RX,反之亦然)。
- 参数不匹配:双方的波特率、数据位等参数不一致。
- 设备未正确识别:检查
dmesg或ls /dev/ttyUSB*。 - 流控问题:如果启用了硬件流控,但线缆没有连接 RTS/CTS 信号,会导致通信失败。
- 现象:
-
数据丢失或乱码
- 现象:发送的数据,接收端不完整或内容错误。
- 原因:
- 波特率过高:线缆质量不佳或设备处理速度跟不上,导致在高波特率下出错,尝试降低波特率(如从 115200 降到 9600)。
- 程序处理速度慢:接收程序处理数据的速度跟不上数据到达的速度,考虑使用多线程或更大的缓冲区。
- 缺少校验:在嘈杂的电磁环境中,数据可能出错,可以考虑启用校验位或更高级的校验机制(如 CRC)。
希望这份详细的指南能帮助你顺利地在 Linux C 语言环境下进行串口通信!
