C 语言串口通信技术详解
串口通信是一种历史悠久但至今仍广泛应用的设备通信方式,它以其简单、可靠、易于实现的特点,在工业控制、嵌入式系统、仪器仪表、模块通信(如 GPS、GPRS、蓝牙模块)等领域扮演着重要角色。

本指南将分为以下几个部分:
- 基础概念:理解串口通信的核心原理。
- 核心术语:波特率、数据位、停止位、校验位等。
- Linux 环境下的 C 语言编程:详细讲解如何使用
termios结构体配置和使用串口。 - Windows 环境下的 C 语言编程:讲解如何使用
Windows API操作串口。 - 一个完整的 C 语言示例:一个双向收发的测试程序。
- 高级应用与最佳实践:多线程、非阻塞 I/O、数据帧设计等。
- 常见问题与调试技巧。
基础概念
串口通信,也称为串行通信,是指数据在单条数据线上,一位一位地按顺序进行传输,与并行通信(同时传输多位数据)相比,串口通信的速度较慢,但所需的线路少,成本低,适合长距离通信。
- 全双工:可以同时进行数据的发送和接收。
- 异步通信:通信双方没有共同的时钟信号,通过在数据帧中加入起始位和停止位来同步,这是串口通信最常见的形式。
物理连接:
- DB-9 接口:经典的串口连接器,定义了 9 个引脚,但实际上只使用了其中的 3 个(TXD, RXD, GND)进行基本通信。
TXD(Transmitted Data): 发送数据。RXD(Received Data): 接收数据。GND(Ground): 信号地线,用于提供共同的参考电位。
- 电平标准:
- TTL (0V/5V):用于芯片与芯片之间的短距离通信。
- RS-232 (-12V/+12V):用于设备间的较长距离通信(通常小于 15 米),需要电平转换芯片(如 MAX232)在 TTL 和 RS-232 之间转换。
- RS-485 (差分信号):支持多点通信,传输距离更远(可达 1200 米),抗干扰能力更强。
核心术语
配置串口时,必须正确设置以下几个参数,否则通信将无法进行。

a. 波特率
指每秒传输的比特数,是衡量通信速度的指标,发送方和接收方的波特率必须严格一致。
- 常见波特率:
9600,19200,38400,57600,115200。115200是目前最常用的。
b. 数据位
指每个数据帧中实际包含的数据位数,通常是 8 位,也可以是 5, 6, 7 位,标准字符(如 ASCII)使用 8 位。
c. 停止位
用于表示一个数据帧的结束,可以是 1, 5, 或 2 位,现在大多使用 1 位。
d. 校验位
一种简单的错误检测机制,它根据数据位计算出一个校验位,附加在数据位之后。

- 无校验:不使用校验位。
- 奇校验:确保整个数据帧(数据位 + 校验位)中 '1' 的个数为奇数。
- 偶校验:确保整个数据帧中 '1' 的个数为偶数。
- 标志位/空位:较少使用。
一个典型的串口配置:115200, 8N1,表示波特率为 115200,8 个数据位,无校验,1 个停止位。
Linux 环境下的 C 语言编程
在 Linux 系统中,串口被当作一个普通的设备文件来操作,通常位于 /dev/ 目录下,/dev/ttyS0 (COM1), /dev/ttyUSB0 (USB转串口)。
操作流程与文件 I/O 操作非常相似:open() -> read()/write() -> close()。
关键在于如何使用 termios 结构体来配置串口参数。
步骤详解:
包含头文件
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <errno.h> #include <termios.h> #include <sys/types.h> #include <sys/stat.h> #include <sys/time.h>
打开串口
使用 open() 函数,以非阻塞模式打开是一个好习惯,这样如果串口被占用,程序会立即返回错误,而不是无限等待。
int fd;
fd = open("/dev/ttyUSB0", O_RDWR | O_NOCTTY | O_NDELAY); // O_NOCTTY: 不让串口成为终端 O_NDELAY: 非阻塞
if (fd < 0) {
perror("Failed to open serial port");
return -1;
}
// 恢复为阻塞模式
fcntl(fd, F_SETFL, 0);
配置串口参数
这是最核心的一步,通过修改 termios 结构体来实现。
struct termios options;
tcgetattr(fd, &options); // 获取当前串口配置
// 设置波特率
cfsetispeed(&options, B115200); // 设置输入波特率
cfsetospeed(&options, B115200); // 设置输出波特率
// 设置数据位、校验位、停止位
options.c_cflag &= ~CSIZE; // 清除数据位设置
options.c_cflag |= CS8; // 设置为8位数据位
options.c_cflag &= ~PARENB; // 无校验
options.c_cflag &= ~CSTOPB; // 1位停止位
// 设置控制模式
options.c_cflag |= (CLOCAL | CREAD); // CLOCAL: 忽略调制解调器控制线 CREAD: 启用接收
// 设置原始输入模式
options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); // ICANON: 非规范模式,read()直接读取字节,而不是行
// 设置输出模式
options.c_oflag &= ~OPOST; // 原始输出
// 设置超时
options.c_cc[VTIME] = 10; // 读取每个字符的超时时间 (deciseconds, 1/10秒)
options.c_cc[VMIN] = 0; // 设置为0时,read()立即返回,读取多少字节就返回多少
// 应用设置
if (tcsetattr(fd, TCSANOW, &options) != 0) {
perror("Failed to set serial attributes");
close(fd);
return -1;
}
读写串口
- 读:
read()函数会阻塞,直到有数据到达或超时。char buffer[256]; int n = read(fd, buffer, sizeof(buffer)); if (n > 0) { buffer[n] = '\0'; // 确保字符串以 NULL printf("Received: %s\n", buffer); } - 写:
write()函数将数据写入串口输出缓冲区。char *msg = "Hello from C program!\n"; write(fd, msg, strlen(msg));
关闭串口
close(fd);
Windows 环境下的 C 语言编程
在 Windows 中,串口操作通过 Windows API 完成,流程与 Linux 类似,但函数和结构体完全不同。
包含头文件和链接库
#include <windows.h> #pragma comment(lib, "kernel32.lib") // 通常不需要显式链接,但写上更保险
打开串口
使用 CreateFile() 函数。
HANDLE hSerial;
hSerial = CreateFile("COM1", // 串口名
GENERIC_READ | GENERIC_WRITE, // 读写权限
0, // 不共享
NULL, // 默认安全属性
OPEN_EXISTING, // 必须已存在
FILE_ATTRIBUTE_NORMAL, // 同步I/O
NULL); // 无模板文件
if (hSerial == INVALID_HANDLE_VALUE) {
DWORD error = GetLastError();
printf("Error opening serial port: %d\n", error);
return 1;
}
配置串口参数
使用 DCB (Device Control Block) 结构体。
DCB dcbSerialParams = {0};
dcbSerialParams.DCBlength = sizeof(dcbSerialParams);
// 获取当前DCB配置
if (!GetCommState(hSerial, &dcbSerialParams)) {
printf("Error getting device state\n");
CloseHandle(hSerial);
return 1;
}
// 修改配置
dcbSerialParams.BaudRate = CBR_115200; // 波特率115200
dcbSerialParams.ByteSize = 8; // 8位数据位
dcbSerialParams.StopBits = ONESTOPBIT; // 1位停止位
dcbSerialParams.Parity = NOPARITY; // 无校验
// 应用设置
if (!SetCommState(hSerial, &dcbSerialParams)) {
printf("Error setting device parameters\n");
CloseHandle(hSerial);
return 1;
}
配置I/O缓冲区和超时
使用 COMMTIMEOUTS 结构体。
COMMTIMEOUTS timeouts = {0};
timeouts.ReadIntervalTimeout = 50; // 两字符间最大超时(毫秒)
timeouts.ReadTotalTimeoutConstant = 50; // 总读取超时(毫秒)
timeouts.ReadTotalTimeoutMultiplier = 10; // 每字节超时(毫秒)
timeouts.WriteTotalTimeoutConstant = 50; // 总写入超时(毫秒)
timeouts.WriteTotalTimeoutMultiplier = 10; // 每字节超时(毫秒)
SetCommTimeouts(hSerial, &timeouts);
读写串口
- 读:使用
ReadFile()。char buffer[256]; DWORD bytesRead; if (ReadFile(hSerial, buffer, sizeof(buffer), &bytesRead, NULL)) { if (bytesRead != 0) { buffer[bytesRead] = '\0'; printf("Received: %s\n", buffer); } } - 写:使用
WriteFile()。char *msg = "Hello from Windows C program!\r\n"; DWORD bytesWritten; WriteFile(hSerial, msg, strlen(msg), &bytesWritten, NULL);
关闭串口
CloseHandle(hSerial);
一个完整的 C 语言示例 (Linux 环境)
这个示例实现了一个简单的回环测试程序:发送一个字符串,然后接收并打印从另一端(或自发自收)返回的数据。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <termios.h>
#include <sys/types.h>
#include <sys/stat.h>
// 函数:初始化串口
int init_serial_port(const char *port_name, int baud_rate) {
int fd = open(port_name, O_RDWR | O_NOCTTY | O_NDELAY);
if (fd < 0) {
perror("open serial port failed");
return -1;
}
fcntl(fd, F_SETFL, 0); // 设置为阻塞模式
struct termios options;
tcgetattr(fd, &options);
// 设置波特率
cfsetispeed(&options, baud_rate);
cfsetospeed(&options, baud_rate);
// 8N1
options.c_cflag &= ~CSIZE;
options.c_cflag |= CS8;
options.c_cflag &= ~PARENB;
options.c_cflag &= ~CSTOPB;
options.c_cflag |= (CLOCAL | CREAD);
// 原始输入模式
options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
// 原始输出模式
options.c_oflag &= ~OPOST;
// 超时设置: 读取立即返回,读取0个字节
options.c_cc[VTIME] = 0;
options.c_cc[VMIN] = 0;
if (tcsetattr(fd, TCSANOW, &options) != 0) {
perror("tcsetattr failed");
close(fd);
return -1;
}
return fd;
}
int main() {
const char *port_name = "/dev/ttyUSB0";
int baud_rate = B115200;
int fd;
fd = init_serial_port(port_name, baud_rate);
if (fd < 0) {
fprintf(stderr, "Exiting.\n");
exit(EXIT_FAILURE);
}
printf("Serial port %s opened successfully.\n", port_name);
char *send_msg = "AT\r\n"; // 一个简单的AT指令,用于测试
char buffer[256];
int bytes_sent, bytes_received;
// 发送数据
bytes_sent = write(fd, send_msg, strlen(send_msg));
if (bytes_sent < 0) {
perror("write failed");
} else {
printf("Sent %d bytes: %s", bytes_sent, send_msg);
}
// 等待一小段时间,让设备有时间响应
usleep(500000); // 等待500毫秒
// 接收数据
bytes_received = read(fd, buffer, sizeof(buffer) - 1);
if (bytes_received < 0) {
perror("read failed");
} else if (bytes_received == 0) {
printf("No data received.\n");
} else {
buffer[bytes_received] = '\0'; // 确保字符串结束
printf("Received %d bytes: %s\n", bytes_received, buffer);
}
close(fd);
return 0;
}
高级应用与最佳实践
a. 多线程编程
在许多应用中,串口的发送和接收是两个独立的任务,使用多线程可以简化程序逻辑:
- 主线程:负责业务逻辑,生成要发送的数据。
- 发送线程:从主线程获取数据并发送。
- 接收线程:持续监听串口,一旦有数据到达就读取并通知主线程。
示例框架:
void *send_thread_func(void *arg) {
int fd = *((int *)arg);
while (1) {
// 从队列或共享内存获取数据
// write(fd, data, len);
}
return NULL;
}
void *receive_thread_func(void *arg) {
int fd = *((int *)arg);
char buffer[256];
while (1) {
int n = read(fd, buffer, sizeof(buffer));
if (n > 0) {
// 将接收到的数据放入队列或通知主线程
}
}
return NULL;
}
// 在main函数中
pthread_t send_tid, recv_tid;
pthread_create(&send_tid, NULL, send_thread_func, &fd);
pthread_create(&recv_tid, NULL, receive_thread_func, &fd);
// ...
pthread_join(send_tid, NULL);
pthread_join(recv_tid, NULL);
b. 非阻塞 I/O
在 Linux 中,通过 fcntl(fd, F_SETFL, O_NONBLOCK); 可以将串口设置为非阻塞模式,这样 read() 和 write() 调用会立即返回,而不是等待,这对于需要同时处理多个 I/O 源(如串口、网络、用户输入)的应用非常有用,通常与 select() 或 poll() 函数配合使用。
c. 数据帧设计
直接发送裸文本(如 "Hello")在简单场景下可行,但在复杂环境中容易出错,通常需要设计自定义的数据帧协议。 一个健壮的数据帧通常包含:
- 帧头:用于标识一帧的开始,
0xAA 0x55。 - 长度:数据部分的长度,方便接收方知道要读取多少字节。
- 命令/地址:用于区分不同的指令或设备。
- 数据:有效载荷。
- 校验和:如 CRC16 或简单的和校验,用于验证数据在传输中是否出错。
- 帧尾:用于标识一帧的结束,
0x0D 0x0A(回车换行)。
示例帧:[0xAA, 0x55, 0x04, 0x01, 0x02, 0x03, 0x04, 0xXX, 0x0D, 0x0A]
- 帧头:
0xAA, 0x55 - 长度:
0x04(数据有4个字节) - 命令:
0x01 - 数据:
0x02, 0x03, 0x04 - 校验和:
0xXX(是前面所有字节之和的低8位) - 帧尾:
0x0D, 0x0A
接收方需要编写状态机来解析这样的数据流,确保能正确识别帧头、完整读取数据,并校验数据的有效性。
常见问题与调试技巧
-
数据收发不正常/乱码
- 首要检查:波特率、数据位、停止位、校验位是否完全一致,这是 90% 问题的根源。
- 电平问题:确保设备间使用的是同一种电平标准(TTL vs RS-232),TTL 设备直接连接 RS-232 端口,需要电平转换芯片。
- 交叉连接:确保一方的
TXD连接到另一方的RXD,GND连接到GND。
-
无法接收到数据
- 硬件问题:检查线缆是否插好,是否有损坏。
- 软件问题:
- 在 Linux 下,检查用户是否对该串口设备有读写权限 (
/dev/ttyUSB0可能属于dialout组)。 - 检查
read()函数的超时设置是否过短,导致数据还没来就读取超时了。 - 使用
minicom或screen等工具先手动测试串口,确认硬件和基本配置是通的。
- 在 Linux 下,检查用户是否对该串口设备有读写权限 (
-
调试工具
- Linux:
minicom,screen,cutecom(图形界面)。 - Windows:
超级终端,SecureCRT,PuTTY,串口调试助手。 - 硬件: USB to TTL/RS232/RS485 转换器 是必备工具,可以方便地将电脑的 USB 口转换为串口。示波器或逻辑分析仪是诊断物理层问题的终极武器。
- Linux:
希望这份详尽的指南能帮助你全面掌握 C 语言下的串口通信技术!
