串口编程概述
串口通信(Serial Communication)是最基础、最悠久的通信方式之一,数据按位(bit)顺序传输。在嵌入式开发、工业控制、物联网设备等场景中,串口仍然是设备间通信的首选方式。Linux系统将串口设备抽象为终端设备文件,通过标准的文件I/O接口即可完成串口读写操作。
串口通信原理
串口通信基于UART(Universal Asynchronous Receiver/Transmitter)硬件控制器,数据以帧为单位传输。每一帧包含:起始位(1位,低电平)、数据位(5~8位)、校验位(可选1位)、停止位(1/1.5/2位,高电平)。发送方和接收方必须使用相同的波特率和数据格式才能正确通信。
RS-232与RS-485标准
- RS-232:点对点通信,传输距离约15米,最高速率约1Mbps,共模抑制能力弱,适合短距离设备连接(如调制解调器、GPS模块)
- RS-485:差分信号传输,多点通信(最多32/128/256个节点),传输距离可达1200米,抗干扰能力强,适合工业现场总线
- RS-422:RS-485的全双工版本,四线制,适合需要同时收发的场景
典型应用场景
- 嵌入式设备调试与固件下载
- 工业PLC与传感器数据采集
- GPS模块、蓝牙模块、WiFi模块通信
- Modbus RTU协议通信
- 自定义二进制协议通信
termios结构体详解
Linux串口编程的核心是 termios 结构体,它定义了终端I/O的所有属性。<termios.h> 头文件提供了完整的API。
#include <termios.h>
struct termios {
tcflag_t c_iflag; /* 输入模式标志 */
tcflag_t c_oflag; /* 输出模式标志 */
tcflag_t c_cflag; /* 控制模式标志(串口核心) */
tcflag_t c_lflag; /* 本地模式标志 */
cc_t c_cc[NCCS]; /* 特殊控制字符 */
};
c_iflag - 输入模式标志
c_iflag 控制输入处理方式,串口编程中通常清零大部分标志以获得原始数据流。
IGNBRK:忽略BREAK条件BRKINT:BREAK条件产生SIGINT信号IGNPAR:忽略奇偶校验错误PARMRK:校验错误时标记奇偶错误字节(前缀\377\0)INPCK:启用输入奇偶校验检查ISTRIP:剥离第8位(仅保留7位数据)IXON:启用软件流控(XON/XOFF)IXOFF:启用软件流控发送
/* 原始输入模式:不做任何输入处理 */
options.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP |
INPCK | IXMLK | IXON | IXOFF);
c_oflag - 输出模式标志
c_oflag 控制输出处理方式,串口通信中通常关闭所有输出处理。
OPOST:启用实现定义的输出处理(必须关闭以获得原始输出)ONLCR:将NL映射为CR-NL(行模式有效,原始模式需关闭)
/* 原始输出模式:不做任何输出处理 */
options.c_oflag &= ~OPOST;
options.c_oflag &= ~ONLCR;
c_cflag - 控制模式标志
c_cflag 是串口编程最重要的标志组,控制波特率、数据位、校验位、停止位等核心参数。
CSIZE:数据位掩码(CS5/CS6/CS7/CS8)CSTOPB:设置2个停止位(否则1个)PARENB:启用校验位PARODD:使用奇校验(否则偶校验)CLOCAL:忽略调制解调器控制线CREAD:启用接收器HUPCL:关闭时挂断调制解调器CRTSCTS:启用硬件流控(RTS/CTS)
/* 8N1配置:8数据位、无校验、1停止位 */
options.c_cflag &= ~PARENB; /* 无校验 */
options.c_cflag &= ~CSTOPB; /* 1停止位 */
options.c_cflag &= ~CSIZE; /* 清除数据位掩码 */
options.c_cflag |= CS8; /* 8数据位 */
options.c_cflag |= CLOCAL | CREAD; /* 忽略Modem控制,启用接收 */
options.c_cflag &= ~CRTSCTS; /* 禁用硬件流控 */
c_lflag - 本地模式标志
c_lflag 控制终端的本地处理方式,串口通信中通常设为原始模式。
ICANON:规范模式(行缓冲),关闭后为原始模式ECHO:回显输入字符ECHOE:擦除字符时回显BS-SPACE-BSISIG:识别信号字符(INTR/QUIT/SUSP)
/* 原始模式:禁用规范模式、回显和信号 */
options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
c_cc - 特殊控制字符
在原始模式下,c_cc 数组中的 VMIN 和 VTIME 控制read()的行为。
VMIN:read()返回的最小字节数VTIME:超时时间(单位:0.1秒),两个字符之间的间隔超时
/* 四种 VMIN/VTIME 组合: */
/* 1. 阻塞读取,至少读到1字节才返回 */
options.c_cc[VMIN] = 1;
options.c_cc[VTIME] = 0;
/* 2. 非阻塞读取,有数据立即返回,无数据返回0 */
options.c_cc[VMIN] = 0;
options.c_cc[VTIME] = 0;
/* 3. 带超时的读取,有数据立即返回,无数据等VTIME后返回0 */
options.c_cc[VMIN] = 0;
options.c_cc[VTIME] = 10; /* 1秒超时 */
/* 4. 读取指定数量,带字符间超时 */
options.c_cc[VMIN] = 5; /* 至少读5字节 */
options.c_cc[VTIME] = 1; /* 字符间超时0.1秒 */
推荐使用 VMIN=1/VTIME=0(阻塞等待至少1字节)或 VMIN=0/VTIME=10(1秒超时非阻塞模式)。
打开与配置串口
打开串口设备
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <termios.h>
int serial_open(const char *device) {
/* O_RDWR: 读写模式
O_NOCTTY: 不让此设备成为控制终端
O_NDELAY: 非阻塞打开(之后可改回阻塞) */
int fd = open(device, O_RDWR | O_NOCTTY | O_NDELAY);
if (fd < 0) {
fprintf(stderr, "open %s failed: %s\n", device, strerror(errno));
return -1;
}
/* 恢复为阻塞模式 */
int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags & ~O_NONBLOCK);
return fd;
}
常用串口设备路径:/dev/ttyS0(传统串口)、/dev/ttyUSB0(USB转串口)、/dev/ttyAMA0(树莓派GPIO串口)。
配置串口完整函数
int serial_configure(int fd, int baudrate, int databits,
int stopbits, char parity) {
struct termios options;
/* 获取当前串口配置 */
if (tcgetattr(fd, &options) != 0) {
perror("tcgetattr");
return -1;
}
/* 设置为原始模式 */
options.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP |
INPCK | IXMLK | IXON | IXOFF);
options.c_oflag &= ~OPOST;
options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
/* 禁用软件流控 */
options.c_iflag &= ~(IXON | IXOFF | IXANY);
/* 设置波特率 */
speed_t speed;
switch (baudrate) {
case 9600: speed = B9600; break;
case 19200: speed = B19200; break;
case 38400: speed = B38400; break;
case 57600: speed = B57600; break;
case 115200: speed = B115200; break;
case 230400: speed = B230400; break;
case 460800: speed = B460800; break;
case 921600: speed = B921600; break;
default:
fprintf(stderr, "Unsupported baudrate: %d\n", baudrate);
return -1;
}
cfsetispeed(&options, speed);
cfsetospeed(&options, speed);
/* 设置数据位 */
options.c_cflag &= ~CSIZE;
switch (databits) {
case 5: options.c_cflag |= CS5; break;
case 6: options.c_cflag |= CS6; break;
case 7: options.c_cflag |= CS7; break;
case 8: options.c_cflag |= CS8; break;
default:
fprintf(stderr, "Unsupported databits: %d\n", databits);
return -1;
}
/* 设置校验位 */
switch (parity) {
case 'n': case 'N': /* 无校验 */
options.c_cflag &= ~PARENB;
options.c_iflag &= ~INPCK;
break;
case 'o': case 'O': /* 奇校验 */
options.c_cflag |= PARENB;
options.c_cflag |= PARODD;
options.c_iflag |= INPCK;
break;
case 'e': case 'E': /* 偶校验 */
options.c_cflag |= PARENB;
options.c_cflag &= ~PARODD;
options.c_iflag |= INPCK;
break;
default:
fprintf(stderr, "Unsupported parity: %c\n", parity);
return -1;
}
/* 设置停止位 */
switch (stopbits) {
case 1: options.c_cflag &= ~CSTOPB; break;
case 2: options.c_cflag |= CSTOPB; break;
default:
fprintf(stderr, "Unsupported stopbits: %d\n", stopbits);
return -1;
}
/* 启用接收,忽略Modem控制 */
options.c_cflag |= CLOCAL | CREAD;
/* 设置读取超时:VMIN=1等待至少1字节,VTIME=1字符间超时0.1秒 */
options.c_cc[VMIN] = 1;
options.c_cc[VTIME] = 1;
/* 刷新输入输出缓冲区 */
tcflush(fd, TCIOFLUSH);
/* 应用配置 */
if (tcsetattr(fd, TCSANOW, &options) != 0) {
perror("tcsetattr");
return -1;
}
return 0;
}
tcsetattr 的 when 参数:TCSANOW(立即生效)、TCSADRAIN(等待输出完毕后生效)、TCSAFLUSH(刷新缓冲区后生效)。推荐使用 TCSANOW。
波特率与数据格式
标准波特率
POSIX定义了标准波特率常量,必须使用 cfsetispeed/cfsetospeed 设置:
/* 标准波特率常量 */
B0 /* 挂断连接 */
B50 B75 B110 B134 B150 B200
B300 B600 B1200 B1800 B2400 B4800
B9600 B19200 B38400 B57600 B115200 B230400
B460800 B500000 B576000 B921600 B1000000 B1152000
B1500000 B2000000 B2500000 B3000000 B3500000 B4000000
/* 设置波特率 */
struct termios options;
tcgetattr(fd, &options);
cfsetispeed(&options, B115200); /* 输入波特率 */
cfsetospeed(&options, B115200); /* 输出波特率 */
tcsetattr(fd, TCSANOW, &options);
非标准波特率设置
某些硬件支持非标准波特率,可以通过自定义除数实现:
#include <linux/serial.h>
int set_custom_baudrate(int fd, int baudrate) {
struct serial_struct ser_info;
if (ioctl(fd, TIOCGSERIAL, &ser_info) < 0) {
perror("TIOCGSERIAL");
return -1;
}
ser_info.flags &= ~ASYNC_SPD_MASK;
ser_info.flags |= ASYNC_SPD_CUST;
ser_info.custom_divisor = (ser_info.baud_base + baudrate / 2) / baudrate;
if (ioctl(fd, TIOCSSERIAL, &ser_info) < 0) {
perror("TIOCSSERIAL");
return -1;
}
return 0;
}
数据格式速查
- 8N1:8数据位 + 无校验 + 1停止位,最常用配置
- 7E1:7数据位 + 偶校验 + 1停止位,Modbus RTU标准
- 8O1:8数据位 + 奇校验 + 1停止位,部分工业协议
- 8N2:8数据位 + 无校验 + 2停止位,部分老式设备
读写操作
基本读写
/* 写入数据 */
ssize_t serial_write(int fd, const void *buf, size_t len) {
ssize_t n = write(fd, buf, len);
if (n < 0) {
perror("serial_write");
}
/* 等待数据完全发出 */
tcdrain(fd);
return n;
}
/* 读取数据(VMIN/VTIME控制超时) */
ssize_t serial_read(int fd, void *buf, size_t len) {
ssize_t n = read(fd, buf, len);
if (n < 0) {
perror("serial_read");
}
return n;
}
阻塞与非阻塞模式
/* 设置非阻塞模式 */
int set_nonblocking(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}
/* 设置阻塞模式 */
int set_blocking(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
return fcntl(fd, F_SETFL, flags & ~O_NONBLOCK);
}
/* 非阻塞读取示例 */
void read_nonblocking_example(int fd) {
set_nonblocking(fd);
char buf[256];
ssize_t n = read(fd, buf, sizeof(buf));
if (n > 0) {
printf("Read %zd bytes\n", n);
} else if (n == 0) {
printf("No data available\n");
} else {
if (errno == EAGAIN || errno == EWOULDBLOCK)
printf("No data (EAGAIN)\n");
else
perror("read");
}
}
select多路复用
使用 select 实现带超时的读取,是串口编程中最常用的模式:
#include <sys/select.h>
#include <sys/time.h>
/* 带超时的串口读取
* 返回:>0 读取字节数,0 超时,-1 错误 */
ssize_t serial_read_timeout(int fd, void *buf, size_t len,
int timeout_ms) {
fd_set rfds;
struct timeval tv;
FD_ZERO(&rfds);
FD_SET(fd, &rfds);
tv.tv_sec = timeout_ms / 1000;
tv.tv_usec = (timeout_ms % 1000) * 1000;
int ret = select(fd + 1, &rfds, NULL, NULL, &tv);
if (ret < 0) {
if (errno == EINTR) return 0;
perror("select");
return -1;
}
if (ret == 0) return 0; /* 超时 */
return read(fd, buf, len);
}
/* 精确读取指定字节数(带超时) */
ssize_t serial_read_exact(int fd, void *buf, size_t len,
int timeout_ms) {
size_t total = 0;
unsigned char *p = (unsigned char *)buf;
while (total < len) {
ssize_t n = serial_read_timeout(fd, p + total,
len - total, timeout_ms);
if (n <= 0) return (total > 0) ? (ssize_t)total : n;
total += n;
}
return (ssize_t)total;
}
实战:串口通信框架
以下实现一个完整的串口通信框架,支持打开/关闭串口、配置参数、带超时读写、重试机制和CRC校验。
/* serial_comm.h - 串口通信框架头文件 */
#ifndef SERIAL_COMM_H
#define SERIAL_COMM_H
#include <stddef.h>
#include <stdint.h>
#include <sys/select.h>
#define SERIAL_BUF_SIZE 4096
#define SERIAL_MAX_RETRIES 3
typedef struct {
int fd;
char device[64];
int baudrate;
int databits;
int stopbits;
char parity;
int timeout_ms;
int max_retries;
uint8_t rx_buf[SERIAL_BUF_SIZE];
uint8_t tx_buf[SERIAL_BUF_SIZE];
} serial_t;
/* 初始化/关闭 */
int serial_init(serial_t *s, const char *device, int baudrate,
int databits, int stopbits, char parity);
void serial_close(serial_t *s);
/* 数据收发 */
ssize_t serial_send(serial_t *s, const void *data, size_t len);
ssize_t serial_recv(serial_t *s, void *buf, size_t len);
ssize_t serial_recv_exact(serial_t *s, void *buf, size_t len);
/* 带重试的请求-响应 */
ssize_t serial_request(serial_t *s,
const void *req, size_t req_len,
void *resp, size_t resp_max);
/* CRC校验 */
uint16_t crc16_modbus(const uint8_t *data, size_t len);
#endif
框架实现
/* serial_comm.c - 串口通信框架实现 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <termios.h>
#include <sys/select.h>
#include <sys/time.h>
#include "serial_comm.h"
static int serial_configure(int fd, int baudrate, int databits,
int stopbits, char parity) {
struct termios opt;
if (tcgetattr(fd, &opt) != 0) return -1;
/* 原始模式 */
opt.c_iflag &= ~(IGNBRK|BRKINT|PARMRK|ISTRIP|INPCK|IXMLK|IXON|IXOFF);
opt.c_oflag &= ~OPOST;
opt.c_lflag &= ~(ICANON|ECHO|ECHOE|ISIG);
/* 波特率 */
speed_t spd = B115200;
if (baudrate == 9600) spd = B9600;
if (baudrate == 19200) spd = B19200;
if (baudrate == 38400) spd = B38400;
if (baudrate == 57600) spd = B57600;
cfsetispeed(&opt, spd);
cfsetospeed(&opt, spd);
/* 数据位 */
opt.c_cflag &= ~CSIZE;
if (databits == 7) opt.c_cflag |= CS7;
else opt.c_cflag |= CS8;
/* 校验位 */
if (parity == 'N' || parity == 'n') {
opt.c_cflag &= ~PARENB; opt.c_iflag &= ~INPCK;
} else if (parity == 'E' || parity == 'e') {
opt.c_cflag |= PARENB; opt.c_cflag &= ~PARODD; opt.c_iflag |= INPCK;
} else {
opt.c_cflag |= PARENB | PARODD; opt.c_iflag |= INPCK;
}
/* 停止位 */
if (stopbits == 2) opt.c_cflag |= CSTOPB;
else opt.c_cflag &= ~CSTOPB;
opt.c_cflag |= CLOCAL | CREAD;
opt.c_cc[VMIN] = 0;
opt.c_cc[VTIME] = 1; /* 0.1秒字符间超时 */
tcflush(fd, TCIOFLUSH);
return tcsetattr(fd, TCSANOW, &opt);
}
int serial_init(serial_t *s, const char *device, int baudrate,
int databits, int stopbits, char parity) {
memset(s, 0, sizeof(*s));
strncpy(s->device, device, sizeof(s->device) - 1);
s->baudrate = baudrate;
s->databits = databits;
s->stopbits = stopbits;
s->parity = parity;
s->timeout_ms = 1000;
s->max_retries = SERIAL_MAX_RETRIES;
s->fd = open(device, O_RDWR | O_NOCTTY | O_NDELAY);
if (s->fd < 0) { perror("open"); return -1; }
/* 恢复阻塞模式 */
fcntl(s->fd, F_SETFL, fcntl(s->fd, F_GETFL) & ~O_NONBLOCK);
if (serial_configure(s->fd, baudrate, databits, stopbits, parity) < 0) {
close(s->fd); s->fd = -1;
return -1;
}
return 0;
}
void serial_close(serial_t *s) {
if (s->fd >= 0) {
tcflush(s->fd, TCIOFLUSH);
close(s->fd);
s->fd = -1;
}
}
ssize_t serial_send(serial_t *s, const void *data, size_t len) {
ssize_t n = write(s->fd, data, len);
if (n > 0) tcdrain(s->fd);
return n;
}
ssize_t serial_recv(serial_t *s, void *buf, size_t len) {
fd_set rfds;
struct timeval tv;
FD_ZERO(&rfds);
FD_SET(s->fd, &rfds);
tv.tv_sec = s->timeout_ms / 1000;
tv.tv_usec = (s->timeout_ms % 1000) * 1000;
int ret = select(s->fd + 1, &rfds, NULL, NULL, &tv);
if (ret <= 0) return ret;
return read(s->fd, buf, len);
}
ssize_t serial_recv_exact(serial_t *s, void *buf, size_t len) {
size_t total = 0;
uint8_t *p = (uint8_t *)buf;
while (total < len) {
ssize_t n = serial_recv(s, p + total, len - total);
if (n <= 0) return (total > 0) ? (ssize_t)total : n;
total += n;
}
return (ssize_t)total;
}
uint16_t crc16_modbus(const uint8_t *data, size_t len) {
uint16_t crc = 0xFFFF;
for (size_t i = 0; i < len; i++) {
crc ^= data[i];
for (int j = 0; j < 8; j++) {
if (crc & 1) { crc >>= 1; crc ^= 0xA001; }
else { crc >>= 1; }
}
}
return crc;
}
/* 带重试的请求-响应模式 */
ssize_t serial_request(serial_t *s,
const void *req, size_t req_len,
void *resp, size_t resp_max) {
for (int attempt = 0; attempt < s->max_retries; attempt++) {
tcflush(s->fd, TCIFLUSH); /* 清空输入缓冲区 */
/* 发送请求 */
if (serial_send(s, req, req_len) <= 0) continue;
/* 等待响应 */
ssize_t n = serial_recv(s, resp, resp_max);
if (n > 0) return n;
}
return -1;
}
使用示例
#include <stdio.h>
#include <string.h>
#include "serial_comm.h"
int main(void) {
serial_t serial;
if (serial_init(&serial, "/dev/ttyUSB0", 115200, 8, 1, 'N') < 0) {
fprintf(stderr, "Failed to open serial port\n");
return 1;
}
serial.timeout_ms = 2000;
serial.max_retries = 3;
/* Modbus RTU读保持寄存器请求示例 */
uint8_t req[] = { 0x01, 0x03, 0x00, 0x00, 0x00, 0x0A };
uint16_t crc = crc16_modbus(req, 6);
req[6] = crc & 0xFF;
req[7] = (crc >> 8) & 0xFF;
uint8_t resp[256];
ssize_t n = serial_request(&serial, req, 8, resp, sizeof(resp));
if (n > 0) {
printf("Response (%zd bytes): ", n);
for (ssize_t i = 0; i < n; i++)
printf("%02X ", resp[i]);
printf("\n");
/* 验证CRC */
if (n >= 3) {
uint16_t recv_crc = resp[n-2] | (resp[n-1] << 8);
uint16_t calc_crc = crc16_modbus(resp, n - 2);
if (recv_crc == calc_crc)
printf("CRC OK\n");
else
printf("CRC mismatch!\n");
}
} else {
printf("No response after %d retries\n", serial.max_retries);
}
serial_close(&serial);
return 0;
}
# 编译与测试
gcc -o serial_demo serial_comm.c main.c
./serial_demo
总结
- termios结构体 - 串口配置的核心,c_cflag控制波特率/数据位/校验位/停止位,c_iflag/c_oflag/c_lflag设为原始模式
- VMIN/VTIME - 控制read()的阻塞与超时行为,VMIN=0/VTIME=10是最常用的带超时读取方式
- select多路复用 - 比VMIN/VTIME更灵活的超时控制,支持同时监控多个描述符
- CRC校验 - CRC16-MODBUS是工业通信的标准校验算法,确保数据完整性
- 重试机制 - 串口通信不稳定时自动重发请求,每次重试前清空输入缓冲区
- 实战要点 - 打开串口使用O_NOCTTY|O_NDELAY,配置前先tcgetattr保存原设置,tcflush刷新缓冲区避免残留数据
串口编程是嵌入式和工控开发的基本功。掌握termios各标志位的含义和原始模式的配置方法是关键。在工业场景中,配合Modbus RTU等标准协议和CRC校验,可以构建可靠的设备通信系统。建议在实际项目中使用本文的串口通信框架,根据具体协议扩展帧解析和错误处理逻辑。