C语言串口编程完全指南

系统编程串口通信

串口编程概述

串口通信(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-BS
  • ISIG:识别信号字符(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校验,可以构建可靠的设备通信系统。建议在实际项目中使用本文的串口通信框架,根据具体协议扩展帧解析和错误处理逻辑。